1020 lines
32 KiB
JavaScript
1020 lines
32 KiB
JavaScript
const { app, BrowserWindow, ipcMain, dialog, Menu, shell, clipboard } = require('electron');
|
||
app.name = 'Auto-Git';
|
||
const { exec } = require('child_process');
|
||
const { spawn } = require('child_process');
|
||
const { spawnSync } = require('child_process');
|
||
const path = require('path');
|
||
const fs = require('fs');
|
||
const os = require('os');
|
||
const Store = require('electron-store');
|
||
const simpleGit = require('simple-git');
|
||
//const fetch = require('node-fetch');
|
||
const chokidar = require('chokidar');
|
||
|
||
const store = new Store({
|
||
defaults: {
|
||
folders: [],
|
||
selected: null,
|
||
skymode: true,
|
||
skipGitPrompt: true,
|
||
intelligentCommitThreshold: 100
|
||
}
|
||
});
|
||
|
||
let folders = store.get('folders');
|
||
if (Array.isArray(folders)) {
|
||
folders = folders.map(f => ({
|
||
...f,
|
||
linesChanged: 0, // zurück auf 0
|
||
llmCandidates: [] // leeres Array
|
||
}));
|
||
store.set('folders', folders);
|
||
}
|
||
|
||
// Map zum Speichern der Watcher pro Ordner
|
||
const repoWatchers = new Map();
|
||
|
||
|
||
|
||
// Debug Helper
|
||
function debug(msg) {
|
||
console.log(`[DEBUG ${new Date().toISOString()}] ${msg}`);
|
||
}
|
||
|
||
|
||
/**
|
||
* Erstellt das BrowserWindow und lädt index.html.
|
||
* Gibt das Window-Objekt zurück.
|
||
*/
|
||
function createWindow() {
|
||
const win = new BrowserWindow({
|
||
width: 900,
|
||
height: 600,
|
||
minWidth: 600,
|
||
minHeight: 400,
|
||
title: 'Auto-Git',
|
||
webPreferences: {
|
||
preload: path.join(__dirname, 'preload.js'),
|
||
contextIsolation: true
|
||
}
|
||
});
|
||
win.loadFile('index.html');
|
||
return win;
|
||
}
|
||
|
||
|
||
// Settings-Fenster
|
||
let settingsWin;
|
||
function openSettings(win) {
|
||
if (settingsWin) {
|
||
settingsWin.focus();
|
||
return;
|
||
}
|
||
settingsWin = new BrowserWindow({
|
||
parent: win,
|
||
modal: true,
|
||
width: 400,
|
||
height: 300,
|
||
resizable: false,
|
||
webPreferences: {
|
||
preload: path.join(__dirname, 'preload.js'),
|
||
contextIsolation: true
|
||
}
|
||
});
|
||
settingsWin.removeMenu();
|
||
settingsWin.loadFile('settings.html');
|
||
settingsWin.on('closed', () => settingsWin = null);
|
||
}
|
||
|
||
|
||
/**
|
||
* Startet einen File-Watcher auf .git/refs/heads/master,
|
||
* sendet bei Änderungen 'repo-updated' an den Renderer.
|
||
*/
|
||
function watchRepo(folder, win) {
|
||
const gitHead = path.join(folder, '.git', 'refs', 'heads', 'master');
|
||
const watcher = chokidar.watch(gitHead, { ignoreInitial: true });
|
||
watcher.on('change', () => {
|
||
win.webContents.send('repo-updated', folder);
|
||
});
|
||
repoWatchers.set(folder, watcher);
|
||
}
|
||
|
||
/**
|
||
* Initiiert ein Git-Repo in `folder`, falls noch nicht vorhanden,
|
||
* und erzeugt einen Initial-Commit mit Timestamp.
|
||
*/
|
||
async function initGitRepo(folder) {
|
||
const git = simpleGit(folder);
|
||
const gitDir = path.join(folder, '.git');
|
||
if (!fs.existsSync(gitDir)) {
|
||
await git.init();
|
||
const message = `Initial commit (generated by auto-git)`;
|
||
const readmePath = path.join(folder, 'README.md');
|
||
fs.writeFileSync(readmePath, `# Projekt in ${path.basename(folder)}\n`);
|
||
await git.add('./*');
|
||
await git.commit(message);
|
||
}
|
||
}
|
||
|
||
|
||
// Map für Monitoring-Watcher (nicht repoWatchers!)
|
||
const monitoringWatchers = new Map();
|
||
|
||
// Helper: Baut eine Commit-Message aus git.status()
|
||
function buildCommitMessageFromStatus(status, prefix = '[auto]') {
|
||
const changes = [];
|
||
status.not_added.forEach(f => changes.push(`[add] ${f}`));
|
||
status.created.forEach(f => changes.push(`[add] ${f}`));
|
||
status.modified.forEach(f => changes.push(`[change] ${f}`));
|
||
status.deleted.forEach(f => changes.push(`[unlink] ${f}`));
|
||
status.renamed.forEach(r => changes.push(`[rename] ${r.from} → ${r.to}`));
|
||
return prefix + '\n' + changes.map(l => ` ${l}`).join('\n');
|
||
}
|
||
|
||
function startMonitoringWatcher(folderPath, win) {
|
||
if (monitoringWatchers.has(folderPath)) return;
|
||
const watcher = chokidar.watch(folderPath, {
|
||
ignored: /(^|[\/\\])\..|node_modules|\.git/,
|
||
ignoreInitial: true,
|
||
persistent: true,
|
||
depth: 99,
|
||
awaitWriteFinish: { stabilityThreshold: 300, pollInterval: 100 }
|
||
});
|
||
|
||
// Initialer Commit
|
||
(async () => {
|
||
debug(`[MONITOR] Starte initialen Commit-Check für ${folderPath}`);
|
||
|
||
|
||
|
||
const git = simpleGit(folderPath);
|
||
const status = await git.status();
|
||
if (
|
||
status.not_added.length > 0 ||
|
||
status.created.length > 0 ||
|
||
status.modified.length > 0 ||
|
||
status.deleted.length > 0 ||
|
||
status.renamed.length > 0
|
||
) {
|
||
const msg = buildCommitMessageFromStatus(status, 'auto-git: ');
|
||
const did = await autoCommit(folderPath, msg);
|
||
if (did) {
|
||
win.webContents.send('repo-updated', folderPath);
|
||
debug(`[MONITOR] Initialer Auto-Commit für ${folderPath} durchgeführt:\n${msg}`);
|
||
}
|
||
}
|
||
})();
|
||
|
||
// Bei jedem Event → status neu holen, Message wie beim initialen Check bauen
|
||
watcher.on('all', async () => {
|
||
const git = simpleGit(folderPath);
|
||
const status = await git.status();
|
||
if (
|
||
status.not_added.length > 0 ||
|
||
status.created.length > 0 ||
|
||
status.modified.length > 0 ||
|
||
status.deleted.length > 0 ||
|
||
status.renamed.length > 0
|
||
) {
|
||
const msg = buildCommitMessageFromStatus(status, 'auto-git: ');
|
||
await autoCommit(folderPath, msg);
|
||
win.webContents.send('repo-updated', folderPath);
|
||
}
|
||
});
|
||
|
||
monitoringWatchers.set(folderPath, watcher);
|
||
debug(`[MONITOR] Watcher aktiv für ${folderPath}`);
|
||
}
|
||
|
||
function stopMonitoringWatcher(folderPath) {
|
||
const watcher = monitoringWatchers.get(folderPath);
|
||
if (watcher) {
|
||
watcher.close();
|
||
monitoringWatchers.delete(folderPath);
|
||
debug(`[MONITOR] Watcher gestoppt für ${folderPath}`);
|
||
}
|
||
}
|
||
|
||
|
||
|
||
// ---- Rewrite Git Messages with LLM generated messages ----
|
||
|
||
// ---- 1. Commits & Diffs für LLM sammeln ----
|
||
async function getCommitsForLLM(folderPath, hashes) {
|
||
const git = simpleGit(folderPath);
|
||
const commits = [];
|
||
for (const hash of hashes) {
|
||
const diff = await git.diff([`${hash}^!`]);
|
||
const msg = (await git.show(['-s', '--format=%B', hash])).trim();
|
||
commits.push({ hash, message: msg, diff });
|
||
}
|
||
return commits;
|
||
}
|
||
|
||
// ---- 2. Prompt für LLM bauen ----
|
||
async function generateLLMCommitMessages(folderPath, hashes) {
|
||
const commits = await getCommitsForLLM(folderPath, hashes);
|
||
const prompt = `
|
||
Analyze the following git commits. For each commit, generate a concise commit message summarizing the actual change.
|
||
- ONLY output a JSON object mapping each commit hash to its new message.
|
||
- Do NOT add any explanations, greetings, or extra text.
|
||
|
||
Example Output:
|
||
{
|
||
"1a2b3c4": "Fix bug in user registration",
|
||
"2b3c4d5": "Refactor login logic"
|
||
}
|
||
|
||
COMMITS (as JSON):
|
||
|
||
${JSON.stringify(commits, null, 2)}
|
||
`;
|
||
return prompt;
|
||
}
|
||
|
||
// ---- 3. LLM Streaming Call ----
|
||
async function streamLLMCommitMessages(prompt, onDataChunk) {
|
||
const response = await fetch('http://localhost:11434/api/generate', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
model: 'qwen2.5-coder:32b', // ggf. Modell anpassen
|
||
prompt: prompt,
|
||
stream: true
|
||
})
|
||
});
|
||
|
||
if (!response.body) throw new Error('No stream returned');
|
||
const reader = response.body.getReader();
|
||
const decoder = new TextDecoder();
|
||
|
||
let fullOutput = '';
|
||
let done = false;
|
||
while (!done) {
|
||
const { value, done: streamDone } = await reader.read();
|
||
done = streamDone;
|
||
if (value) {
|
||
const chunk = decoder.decode(value, { stream: true });
|
||
for (const line of chunk.split('\n')) {
|
||
if (!line.trim()) continue;
|
||
try {
|
||
const obj = JSON.parse(line);
|
||
if (obj.response) {
|
||
fullOutput += obj.response;
|
||
if (onDataChunk) onDataChunk(obj.response);
|
||
}
|
||
if (obj.done) break;
|
||
} catch (e) {
|
||
// ignore malformed chunk
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return fullOutput;
|
||
}
|
||
|
||
// ---- 4. JSON Output robust parsen ----
|
||
function parseLLMCommitMessages(rawOutput) {
|
||
let cleaned = rawOutput.trim();
|
||
cleaned = cleaned.replace(/^```(?:json)?|```$/gmi, '');
|
||
|
||
try {
|
||
if (cleaned.trim().startsWith('[')) return JSON.parse(cleaned);
|
||
if (cleaned.trim().startsWith('{')) {
|
||
const obj = JSON.parse(cleaned);
|
||
return Object.entries(obj).map(([commit, newMessage]) => ({ commit, newMessage }));
|
||
}
|
||
// Zeilenweise Objekte (fuzzy)
|
||
if (cleaned.includes('\n')) {
|
||
let lines = cleaned.split('\n').map(l => l.trim()).filter(Boolean);
|
||
let objs = [];
|
||
for (let line of lines) {
|
||
try { let o = JSON.parse(line); objs.push(o); } catch {}
|
||
}
|
||
if (objs.length) return objs;
|
||
}
|
||
} catch (err) {
|
||
// Fallback repair
|
||
try {
|
||
cleaned = cleaned.replace(/}\s*{/g, '},\n{');
|
||
if (!cleaned.startsWith('[')) cleaned = '[' + cleaned;
|
||
if (!cleaned.endsWith(']')) cleaned = cleaned + ']';
|
||
return JSON.parse(cleaned);
|
||
} catch (e) {
|
||
throw new Error('Could not parse LLM output:\n' + rawOutput);
|
||
}
|
||
}
|
||
throw new Error('Could not parse LLM output:\n' + rawOutput);
|
||
}
|
||
|
||
|
||
|
||
|
||
// --- Hauptfunktion ---
|
||
/**
|
||
* Rewords commit messages for each hash (oldest to newest) using git rebase -i in a loop.
|
||
* @param {string} repoPath - Path to your repo
|
||
* @param {object} commitMessageMap - { fullHash: newMessage }
|
||
* @param {string[]} hashes - array of full commit hashes (oldest first!)
|
||
*/
|
||
async function rewordCommitsSequentially(repoPath, commitMessageMap, hashes) {
|
||
const git = simpleGit(repoPath);
|
||
|
||
// Sanity: Sort hashes chronologically by git log order (oldest first)
|
||
const allCommits = (await git.log()).all;
|
||
const hashesOrdered = hashes
|
||
.map(h => allCommits.find(c => c.hash.startsWith(h)))
|
||
.filter(Boolean)
|
||
.sort((a, b) =>
|
||
allCommits.findIndex(c => c.hash === a.hash) - allCommits.findIndex(c => c.hash === b.hash)
|
||
)
|
||
.map(c => c.hash);
|
||
|
||
// Loop over all hashes
|
||
for (const hash of hashesOrdered) {
|
||
// Compose the rebase command: only one commit at a time!
|
||
await new Promise((resolve, reject) => {
|
||
// macOS: sed -i '' ... Linux: sed -i ...
|
||
// Try macOS style, change '' to '' or nothing if you get errors.
|
||
const sequenceEditor = `sed -i '' '1s/pick/reword/'`;
|
||
const commitMsg = commitMessageMap[hash].replace(/(["$`\\])/g, '\\$1'); // Escape quotes etc
|
||
|
||
const proc = spawn('git', [
|
||
'rebase', '-i', `${hash}^`
|
||
], {
|
||
cwd: repoPath,
|
||
env: {
|
||
...process.env,
|
||
GIT_SEQUENCE_EDITOR: sequenceEditor,
|
||
GIT_EDITOR: `echo "${commitMsg}" >`
|
||
},
|
||
stdio: 'inherit'
|
||
});
|
||
proc.on('exit', code => code === 0 ? resolve() : reject(new Error(`Failed to reword ${hash}`)));
|
||
});
|
||
console.log(`[AutoGit] Reworded commit ${hash} ✔`);
|
||
}
|
||
console.log('[AutoGit] All specified commit messages updated!');
|
||
}
|
||
|
||
|
||
|
||
/**
|
||
* Cherry-picks the given commits, amending each one's message, and replaces master.
|
||
* @param {string} repoPath
|
||
* @param {object} commitMessageMap - { [fullHash]: newMessage }
|
||
* @param {string[]} hashes - list of commit hashes, oldest to newest!
|
||
*/
|
||
/*
|
||
async function cherryPickCommitRewrite(repoPath, commitMessageMap, hashes) {
|
||
// 1. Find parent of the OLDEST commit
|
||
//const allCommits = (await git.log()).all;
|
||
//const parentHash = (await git.raw(['rev-parse', `${oldestHash}^`])).trim();
|
||
// 2. Create a new temp branch from the parent
|
||
const git = simpleGit(repoPath);
|
||
if(hashes.length > 1){
|
||
const branchName = "temp_branch" + Date.now();
|
||
console.log(commitMessageMap[hashes[0]]);
|
||
await git.checkout(hashes[0]);
|
||
console.log("checkout " + hashes[0])
|
||
await git.checkoutLocalBranch(branchName);
|
||
console.log("branch " + branchName)
|
||
await git.commit(commitMessageMap[hashes[0]], undefined, { '--amend': null });
|
||
console.log("amend")
|
||
for (let i = 1; i < hashes.length; i++) {
|
||
await git.raw(['cherry-pick', '--no-commit', hashes[i]]);
|
||
console.log("cherry " + hashes[i])
|
||
await git.commit(commitMessageMap[hashes[i]], undefined, { '--amend': null });
|
||
console.log("amend")
|
||
}
|
||
await git.deleteLocalBranch('master', true);
|
||
console.log("branch del")
|
||
await git.branch(['-m', branchName, 'master']);
|
||
|
||
console.log("branch mov")
|
||
} else {
|
||
await git.commit(commitMessageMap[hashes[0]], undefined, { '--amend': null });
|
||
|
||
}
|
||
*/
|
||
//await git.checkoutLocalBranch(NEW_BRANCH);
|
||
/*
|
||
// 3. Cherry-pick and amend each commit in order
|
||
for (const hash of hashes) {
|
||
// Cherry-pick (commit as is)
|
||
let res = spawnSync('git', ['cherry-pick', hash], { cwd: repoPath, stdio: 'inherit' });
|
||
if (res.status !== 0) throw new Error('Cherry-pick failed for ' + hash);
|
||
|
||
// Amend commit message
|
||
const msg = commitMessageMap[hash];
|
||
if (msg) {
|
||
res = spawnSync('git', ['commit', '--amend', '-m', msg], { cwd: repoPath, stdio: 'inherit' });
|
||
if (res.status !== 0) throw new Error('Amend failed for ' + hash);
|
||
}
|
||
}
|
||
|
||
// 4. Move master to rewritten branch (overwrite)
|
||
await git.checkout('master'); // just in case we're not already there
|
||
await git.branch(['-f', 'master', NEW_BRANCH]); // force-move master pointer
|
||
|
||
// 5. Checkout master (HEAD on new history)
|
||
await git.checkout('master');
|
||
|
||
// 6. (Optional) Delete the temp branch
|
||
await git.branch(['-D', NEW_BRANCH]);
|
||
|
||
console.log('\n[AutoGit] Master branch has been overwritten with rewritten commits.');
|
||
*/
|
||
//}
|
||
|
||
// ---- 6. Komplett-Workflow: Von Kandidaten bis Rewrite ----
|
||
/*
|
||
async function runLLMCommitRewrite(folderPath, hashes) {
|
||
const prompt = await generateLLMCommitMessages(folderPath, hashes);
|
||
const llmRaw = await streamLLMCommitMessages(prompt, chunk => process.stdout.write(chunk));
|
||
const commitList = parseLLMCommitMessages(llmRaw);
|
||
const messageMap = {};
|
||
for (const entry of commitList) messageMap[entry.commit] = entry.newMessage;
|
||
await cherryPickCommitRewrite(folderPath, messageMap, hashes);
|
||
}
|
||
*/
|
||
// ---- 6. Komplett-Workflow (Randomized) ----
|
||
async function runLLMCommitRewrite(folderPath, hashes) {
|
||
// Generate a mapping { hash: message }
|
||
const messageMap = hashes.reduce((map, hash) => {
|
||
map[hash] = getRandomMessage();
|
||
return map;
|
||
}, {});
|
||
console.log(messageMap)
|
||
|
||
// Call your existing rewrite step with the fake messages
|
||
//await cherryPickCommitRewrite(folderPath, messageMap, hashes);
|
||
await rewordCommitsSequentially(folderPath, messageMap, hashes);
|
||
}
|
||
|
||
// Helper: returns a “random” placeholder commit message
|
||
function getRandomMessage() {
|
||
const verbs = [
|
||
'Update', 'Refactor', 'Fix', 'Add', 'Remove', 'Improve', 'Optimize',
|
||
'Document', 'Cleanup', 'Configure', 'Upgrade', 'Revert'
|
||
];
|
||
const objects = [
|
||
'authentication flow', 'API endpoint', 'styling', 'logging',
|
||
'error handling', 'data model', 'build script', 'test suite',
|
||
'configuration', 'dependencies', 'README', 'README.md'
|
||
];
|
||
const details = [
|
||
'for better performance',
|
||
'to meet new requirements',
|
||
'after feedback',
|
||
'as per spec',
|
||
'to fix typo',
|
||
'to improve readability',
|
||
'to avoid regressions',
|
||
'for consistency'
|
||
];
|
||
|
||
const pick = arr => arr[Math.floor(Math.random() * arr.length)];
|
||
return `${pick(verbs)} ${pick(objects)} ${pick(details)}`;
|
||
}
|
||
|
||
// Nutze das Template aus dem Projektordner:
|
||
const TEMPLATE_PATH = path.join(__dirname, 'rewrite-commit-msg.js.template');
|
||
|
||
function createRewriteScript(mapping) {
|
||
// Lies das Template
|
||
let content = fs.readFileSync(TEMPLATE_PATH, 'utf-8');
|
||
// Ersetze __MESSAGE_MAP__ durch dein Mapping
|
||
content = content.replace('__MESSAGE_MAP__', JSON.stringify(mapping));
|
||
// Speichere als temporäre Datei
|
||
const scriptPath = path.join(__dirname, `rewrite-commit-msg.${Date.now()}.js`);
|
||
fs.writeFileSync(scriptPath, content, 'utf-8');
|
||
return scriptPath;
|
||
}
|
||
|
||
|
||
|
||
async function autoCommit(folderPath, message) {
|
||
const git = simpleGit(folderPath);
|
||
const status = await git.status();
|
||
if (
|
||
status.not_added.length === 0 &&
|
||
status.created.length === 0 &&
|
||
status.deleted.length === 0 &&
|
||
status.modified.length === 0 &&
|
||
status.renamed.length === 0
|
||
) {
|
||
debug('Auto-Commit: Keine Änderungen zum committen.');
|
||
return false;
|
||
}
|
||
|
||
let currentBranch = null;
|
||
try {
|
||
currentBranch = (await git.revparse(['--abbrev-ref', 'HEAD'])).trim();
|
||
debug(`[autoCommit] Aktueller Branch: ${currentBranch}`);
|
||
} catch {
|
||
debug('[autoCommit] HEAD ist detached.');
|
||
currentBranch = null;
|
||
}
|
||
|
||
if (!currentBranch || currentBranch === 'HEAD') {
|
||
// === Erweiterte Logs ===
|
||
const headCommit = (await git.revparse(['HEAD'])).trim();
|
||
let masterCommit = null;
|
||
let hasMaster = false;
|
||
try {
|
||
masterCommit = (await git.revparse(['refs/heads/master'])).trim();
|
||
hasMaster = true;
|
||
} catch (e) {
|
||
debug('[autoCommit] master branch existiert nicht.');
|
||
masterCommit = null;
|
||
hasMaster = false;
|
||
}
|
||
debug(`[autoCommit] HEAD: ${headCommit}`);
|
||
debug(`[autoCommit] master: ${masterCommit}`);
|
||
|
||
if (hasMaster && headCommit === masterCommit) {
|
||
// HEAD ist detached, zeigt aber exakt auf master-Tip → einfach auf master auschecken.
|
||
await git.checkout('master');
|
||
debug('[autoCommit] HEAD war detached, zeigte aber exakt auf master – jetzt zurück auf master.');
|
||
// Nach dem Checkout nochmal aktuellen Branch loggen:
|
||
currentBranch = (await git.revparse(['--abbrev-ref', 'HEAD'])).trim();
|
||
debug(`[autoCommit] Nach checkout: Aktueller Branch: ${currentBranch}`);
|
||
// Jetzt **NICHT** weiter zur Umbenenn-Logik!
|
||
} else if (hasMaster) {
|
||
// HEAD ist detached, zeigt auf einen anderen Commit → backup master und neuen master-Branch
|
||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||
const backupBranch = `backup-master-${timestamp}`;
|
||
await git.branch(['-m', 'master', backupBranch]);
|
||
debug(`[autoCommit] Alter master in ${backupBranch} umbenannt.`);
|
||
await git.checkout(['-b', 'master']);
|
||
debug('[autoCommit] Neuer master-Branch erstellt und ausgecheckt.');
|
||
currentBranch = 'master';
|
||
} else {
|
||
// Kein master vorhanden (erstmalig)
|
||
await git.checkout(['-b', 'master']);
|
||
debug('[autoCommit] Kein master-Branch vorhanden, neuer master erstellt.');
|
||
currentBranch = 'master';
|
||
}
|
||
}
|
||
|
||
// --- Zeilenzählung ---
|
||
let diffOutput = await git.diff(['--numstat']);
|
||
// Zeilensumme berechnen:
|
||
let changedLines = 0;
|
||
for (let line of diffOutput.split('\n')) {
|
||
const match = line.match(/^(\d+|\-)\s+(\d+|\-)\s+(.*)$/);
|
||
if (match) {
|
||
const added = match[1] === '-' ? 0 : parseInt(match[1], 10);
|
||
const deleted = match[2] === '-' ? 0 : parseInt(match[2], 10);
|
||
changedLines += added + deleted;
|
||
}
|
||
}
|
||
|
||
// Folders aus Store holen
|
||
let folders = store.get('folders') || [];
|
||
let idx = folders.findIndex(f => f.path === folderPath);
|
||
if (idx !== -1) {
|
||
folders[idx].linesChanged = (folders[idx].linesChanged || 0) + changedLines;
|
||
folders[idx].llmCandidates = folders[idx].llmCandidates || [];
|
||
|
||
// ===> WICHTIG: Wir commiten ja jetzt, daher merken wir uns gleich die neue Commit-Hash
|
||
// Vor git.commit: Merke alten HEAD
|
||
const oldHead = (await git.revparse(['HEAD'])).trim();
|
||
|
||
// Stagen & Committen
|
||
await git.add(['-A']);
|
||
debug('[autoCommit] Alle Änderungen gestaged.');
|
||
await git.commit(message || '[auto]');
|
||
debug('[autoCommit] Commit erfolgreich erstellt.');
|
||
|
||
// Nach Commit: neuen HEAD ermitteln und in llmCandidates speichern
|
||
const newHead = (await git.revparse(['HEAD'])).trim();
|
||
folders[idx].llmCandidates = folders[idx].llmCandidates || [];
|
||
folders[idx].llmCandidates.push(newHead);
|
||
console.log(folders[idx].llmCandidates)
|
||
|
||
// Threshold holen
|
||
const threshold = store.get('intelligentCommitThreshold') || 10;
|
||
if (folders[idx].linesChanged >= threshold) {
|
||
debug('Congratulations! You changed enough lines of code :)');
|
||
folders[idx].linesChanged = 0;
|
||
const cands = folders[idx].llmCandidates;
|
||
folders[idx].llmCandidates = [];
|
||
await runLLMCommitRewrite(folderPath, cands);
|
||
store.set('folders', folders);
|
||
}
|
||
store.set('folders', folders);
|
||
} else {
|
||
// Folder not found! (Debug)
|
||
debug(`[autoCommit] Warning: Folder ${folderPath} not found in store`);
|
||
}
|
||
}
|
||
|
||
app.whenReady().then(() => {
|
||
const win = createWindow();
|
||
|
||
// Menüs
|
||
|
||
const menu = Menu.buildFromTemplate([
|
||
{
|
||
role: 'appMenu',
|
||
submenu: [
|
||
{ label: 'Settings', click: () => openSettings(win) },
|
||
{ role: 'quit', label: 'Quit' }
|
||
]
|
||
},
|
||
{ role: 'editMenu' } // <-- hiermit aktivierst du Copy/Paste via Ctrl+C / Cmd+C
|
||
]);
|
||
Menu.setApplicationMenu(menu);
|
||
|
||
|
||
// 1) Beim Start bereits gespeicherte Ordner überwachen und monitoren
|
||
const folders = store.get('folders') || [];
|
||
folders.forEach(folderObj => {
|
||
if (fs.existsSync(path.join(folderObj.path, '.git', 'refs', 'heads', 'master'))) {
|
||
watchRepo(folderObj.path, win);
|
||
}
|
||
if (folderObj.monitoring) {
|
||
startMonitoringWatcher(folderObj.path, win);
|
||
}
|
||
});
|
||
|
||
// 2) IPC-Handler
|
||
ipcMain.handle('get-selected', () => {
|
||
const folders = store.get('folders') || [];
|
||
const selectedPath = store.get('selected');
|
||
return folders.find(f => f.path === selectedPath) || null;
|
||
});
|
||
|
||
ipcMain.handle('set-selected', (_e, folderObjOrPath) => {
|
||
// Akzeptiert sowohl String (legacy) als auch Objekt:
|
||
const folderPath = typeof folderObjOrPath === 'string'
|
||
? folderObjOrPath
|
||
: folderObjOrPath.path;
|
||
store.set('selected', folderPath);
|
||
const folders = store.get('folders') || [];
|
||
return folders.find(f => f.path === folderPath) || null;
|
||
});
|
||
|
||
// Liste aller Folders
|
||
ipcMain.handle('get-folders', () => store.get('folders'));
|
||
|
||
|
||
// Ordner hinzufügen: Open-Dialog, init, Store-Update, watchen, monitoren
|
||
ipcMain.handle('add-folder', async () => {
|
||
const { canceled, filePaths } = await dialog.showOpenDialog({
|
||
properties: ['openDirectory']
|
||
});
|
||
if (canceled || !filePaths[0]) {
|
||
return store.get('folders');
|
||
}
|
||
const newFolder = filePaths[0];
|
||
await initGitRepo(newFolder);
|
||
let folders = store.get('folders') || [];
|
||
let folderObj = folders.find(f => f.path === newFolder);
|
||
if (!folderObj) {
|
||
folderObj = { path: newFolder, monitoring: true, linesChanged: 0, llmCandidates: [] };
|
||
folders.push(folderObj);
|
||
store.set('folders', folders);
|
||
}
|
||
store.set('selected', newFolder);
|
||
watchRepo(newFolder, win);
|
||
startMonitoringWatcher(newFolder, win);
|
||
return store.get('folders');
|
||
});
|
||
/*
|
||
ipcMain.handle('add-folder', async () => {
|
||
const { canceled, filePaths } = await dialog.showOpenDialog({ properties: ['openDirectory'] });
|
||
if (canceled || !filePaths[0]) return store.get('folders');
|
||
const newFolder = filePaths[0];
|
||
|
||
// Repo initialisieren
|
||
await initGitRepo(newFolder);
|
||
|
||
// Im Store ablegen
|
||
const current = store.get('folders');
|
||
if (!current.includes(newFolder)) {
|
||
store.set('folders', [...current, newFolder]);
|
||
}
|
||
store.set('selected', newFolder);
|
||
|
||
// und watchen
|
||
watchRepo(newFolder, win);
|
||
|
||
return store.get('folders');
|
||
});
|
||
*/
|
||
// Ordner entfernen: Watcher schließen, Store-Update
|
||
ipcMain.handle('remove-folder', (_e, folderObj) => {
|
||
const folders = store.get('folders') || [];
|
||
const updated = folders.filter(f => f.path !== folderObj.path);
|
||
store.set('folders', updated);
|
||
if (store.get('selected') === folderObj.path) store.set('selected', null);
|
||
stopMonitoringWatcher(folderObj.path);
|
||
const watcher = repoWatchers.get(folderObj.path);
|
||
if (watcher) watcher.close(), repoWatchers.delete(folderObj.path);
|
||
return updated;
|
||
});
|
||
|
||
/*
|
||
ipcMain.handle('get-selected', () => store.get('selected'));
|
||
ipcMain.handle('set-selected', (_e, folderPath) => {
|
||
store.set('selected', folderPath);
|
||
return folderPath;
|
||
});
|
||
*/
|
||
/*
|
||
ipcMain.handle('remove-folder', (_e, folder) => {
|
||
const watcher = repoWatchers.get(folder);
|
||
if (watcher) {
|
||
watcher.close();
|
||
repoWatchers.delete(folder);
|
||
}
|
||
const updated = store.get('folders').filter(f => f !== folder);
|
||
store.set('folders', updated);
|
||
if (store.get('selected') === folder) {
|
||
store.set('selected', null);
|
||
}
|
||
return updated;
|
||
});
|
||
*/
|
||
|
||
|
||
// Zähle Commits
|
||
ipcMain.handle('get-commit-count', async (_e, folderObj) => {
|
||
const git = simpleGit(folderObj.path);
|
||
const log = await git.log();
|
||
return log.total; // Anzahl der Commits
|
||
});
|
||
|
||
// Prüfe, ob es ungestagte Änderungen gibt
|
||
ipcMain.handle('has-diffs', async (_e, folderObj) => {
|
||
const git = simpleGit(folderObj.path);
|
||
const status = await git.status();
|
||
// modified, not_added, deleted, etc.
|
||
return status.files.length > 0;
|
||
});
|
||
|
||
// Entferne das .git-Verzeichnis
|
||
ipcMain.handle('remove-git-folder', async (_e, folderObj) => {
|
||
const gitDir = path.join(folderObj.path, '.git');
|
||
if (fs.existsSync(gitDir)) {
|
||
await fs.promises.rm(gitDir, { recursive: true, force: true });
|
||
}
|
||
return;
|
||
});
|
||
|
||
/*
|
||
// Selected
|
||
ipcMain.handle('get-selected', () => store.get('selected'));
|
||
ipcMain.handle('set-selected', (_e, folder) => {
|
||
store.set('selected', folder);
|
||
return folder;
|
||
});
|
||
*/
|
||
|
||
// Commits holen
|
||
ipcMain.handle('get-commits', async (_e, folderObj) => {
|
||
const git = simpleGit(folderObj.path);
|
||
// alle Commits holen
|
||
const log = await git.log(['--all']);
|
||
// aktuellen HEAD‐Hash ermitteln
|
||
const fullHead = (await git.revparse(['--verify', 'HEAD'])).trim();
|
||
const head = fullHead.substring(0, 7);
|
||
return {
|
||
head,
|
||
commits: log.all.map(c => ({
|
||
hash: c.hash.substring(0, 7),
|
||
date: c.date,
|
||
message: c.message
|
||
}))
|
||
};
|
||
});
|
||
|
||
// Diff
|
||
ipcMain.handle('diff-commit', async (_e, folderObj, hash) => {
|
||
const git = simpleGit(folderObj.path);
|
||
return git.diff([`${hash}^!`]);
|
||
});
|
||
|
||
// Revert
|
||
ipcMain.handle('revert-commit', async (_e, folderObj, hash) => {
|
||
const git = simpleGit(folderObj.path);
|
||
await git.revert(hash, ['--no-edit']);
|
||
});
|
||
//yo
|
||
/**
|
||
* Checkt das Arbeitsverzeichnis auf exakt den Zustand von `hash` aus.
|
||
*/
|
||
ipcMain.handle('checkout-commit', async (_e, folderObj, hash) => {
|
||
const git = simpleGit(folderObj.path);
|
||
// clean mode: alle lokalen Veränderungen verwerfen
|
||
await git.checkout([hash, '--force']);
|
||
});
|
||
|
||
|
||
// Snapshot
|
||
ipcMain.handle('snapshot-commit', async (_e, folderObj, hash) => {
|
||
const { canceled, filePaths } = await dialog.showOpenDialog({
|
||
title: 'Ordner auswählen zum Speichern des Snapshots',
|
||
properties: ['openDirectory']
|
||
});
|
||
if (canceled || !filePaths[0]) return;
|
||
const outDir = filePaths[0];
|
||
const baseName = path.basename(folderObj.path);
|
||
const filePath = path.join(outDir, `${baseName}-${hash}.zip`);
|
||
return new Promise((resolve, reject) => {
|
||
exec(
|
||
`git -C "${folderObj.path}" archive --format zip --output "${filePath}" ${hash}`,
|
||
err => err ? reject(err) : resolve(filePath)
|
||
);
|
||
});
|
||
});
|
||
|
||
|
||
// IPC für skymode
|
||
ipcMain.handle('get-skymode', () => store.get('skymode'));
|
||
ipcMain.handle('set-skymode', (_e, val) => {
|
||
store.set('skymode', val);
|
||
// sende an alle Fenster
|
||
BrowserWindow.getAllWindows().forEach(win => {
|
||
win.webContents.send('skymode-changed', val);
|
||
});
|
||
});
|
||
ipcMain.handle('get-skip-git-prompt', () => store.get('skipGitPrompt'));
|
||
ipcMain.handle('set-skip-git-prompt', (_e,val) => store.set('skipGitPrompt', val));
|
||
|
||
|
||
// Auto-Verzeichnisstruktur
|
||
const IGNORED_NAMES = [
|
||
'.DS_Store', 'node_modules', '.git', 'dist', 'build',
|
||
'.cache', 'out', '.venv', '.mypy_cache', '__pycache__', 'package-lock.json'
|
||
];
|
||
|
||
function isIgnored(name) {
|
||
return IGNORED_NAMES.includes(name);
|
||
}
|
||
|
||
function walkDir(base, rel = '.') {
|
||
const full = path.join(base, rel);
|
||
let list = [];
|
||
try {
|
||
fs.readdirSync(full, { withFileTypes: true }).forEach(dirent => {
|
||
if (isIgnored(dirent.name)) return;
|
||
const entry = path.join(rel, dirent.name);
|
||
if (dirent.isDirectory()) {
|
||
list.push({ name: dirent.name, type: 'dir', children: walkDir(base, entry) });
|
||
} else {
|
||
list.push({ name: dirent.name, type: 'file' });
|
||
}
|
||
});
|
||
} catch (e) {}
|
||
return list;
|
||
}
|
||
|
||
ipcMain.handle('get-folder-tree', async (_e, folderPath) => {
|
||
try {
|
||
return walkDir(folderPath, '.');
|
||
} catch {
|
||
return [];
|
||
}
|
||
});
|
||
|
||
|
||
|
||
|
||
ipcMain.handle('commit-current-folder', async (_e, folderObj, message) => {
|
||
folder = folderObj.path;
|
||
try {
|
||
debug(`Commit-Vorgang für ${folder} gestartet…`);
|
||
const git = simpleGit(folder);
|
||
|
||
// Prüfe: Gibt es was zu committen?
|
||
const status = await git.status();
|
||
if (
|
||
status.not_added.length === 0 &&
|
||
status.created.length === 0 &&
|
||
status.deleted.length === 0 &&
|
||
status.modified.length === 0 &&
|
||
status.renamed.length === 0
|
||
) {
|
||
debug('Nichts zu committen.');
|
||
return { success: false, error: 'Nichts zu committen.' };
|
||
}
|
||
|
||
// HEAD-Status prüfen
|
||
let currentBranch = null;
|
||
try {
|
||
currentBranch = (await git.revparse(['--abbrev-ref', 'HEAD'])).trim();
|
||
debug(`Aktueller Branch: ${currentBranch}`);
|
||
} catch (err) {
|
||
debug('HEAD ist detached.');
|
||
}
|
||
|
||
// Falls detached, **jetzt erst** alten Branch umbenennen und neuen master erzeugen
|
||
if (!currentBranch || currentBranch === 'HEAD') {
|
||
// HEAD ist detached, prüfe ob HEAD auf dem Tip von master ist!
|
||
const headCommit = (await git.revparse(['HEAD'])).trim();
|
||
let masterCommit = null;
|
||
let hasMaster = false;
|
||
try {
|
||
masterCommit = (await git.revparse(['refs/heads/master'])).trim();
|
||
hasMaster = true;
|
||
} catch (e) {
|
||
masterCommit = null; //wawa
|
||
hasMaster = false;
|
||
}
|
||
|
||
if (hasMaster && headCommit === masterCommit) {
|
||
// HEAD ist detached, aber zeigt exakt auf master!
|
||
await git.checkout('master');
|
||
debug('[autoCommit] HEAD war detached, zeigte aber exakt auf master – jetzt zurück auf master.');
|
||
// **Return nicht vergessen, sonst geht der Branch-Move weiter**
|
||
// ----> Das ist die Zeile die du wahrscheinlich vergessen hast!
|
||
// Beende hier die Branch-Logik
|
||
return { success: true };
|
||
}b
|
||
|
||
// Ansonsten wie gehabt:
|
||
if (hasMaster) {
|
||
// HEAD ist detached, zeigt nicht auf master -> backup + neuer master
|
||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||
const backupBranch = `backup-master-${timestamp}`;
|
||
await git.branch(['-m', 'master', backupBranch]);
|
||
debug(`[autoCommit] Alter master in ${backupBranch} umbenannt.`);
|
||
await git.checkout(['-b', 'master']);
|
||
debug('[autoCommit] Neuer master-Branch erstellt und ausgecheckt.');
|
||
} else {
|
||
// Kein master vorhanden (erstmalig)
|
||
await git.checkout(['-b', 'master']);
|
||
debug('[autoCommit] Kein master-Branch vorhanden, neuer master erstellt.');
|
||
}
|
||
}
|
||
|
||
await git.add(['-A']);
|
||
debug('Alle Änderungen gestaged.');
|
||
await git.commit(message || 'test');
|
||
debug('Commit erfolgreich erstellt.');
|
||
// Push hier ggf. noch auskommentiert lassen
|
||
|
||
return { success: true };
|
||
} catch (err) {
|
||
debug(`FEHLER beim Commit: ${err.message}`);
|
||
return { success: false, error: err.message };
|
||
}
|
||
});
|
||
|
||
ipcMain.handle('set-monitoring', async (_e, folderPath, monitoring) => {
|
||
let folders = store.get('folders') || [];
|
||
folders = folders.map(f =>
|
||
f.path === folderPath ? { ...f, monitoring } : f
|
||
);
|
||
store.set('folders', folders);
|
||
debug(`[STORE] Monitoring für ${folderPath}: ${monitoring}`);
|
||
// Monitoring-Watcher starten/stoppen
|
||
if (monitoring) {
|
||
startMonitoringWatcher(folderPath, win);
|
||
} else {
|
||
stopMonitoringWatcher(folderPath);
|
||
}
|
||
return monitoring;
|
||
});
|
||
|
||
ipcMain.handle('get-intelligent-commit-threshold', () => store.get('intelligentCommitThreshold'));
|
||
ipcMain.handle('set-intelligent-commit-threshold', (_e, value) => {
|
||
store.set('intelligentCommitThreshold', value);
|
||
});
|
||
|
||
|
||
|
||
// … Ende der IPC-Handler …
|
||
});
|
||
|
||
ipcMain.on('show-folder-context-menu', (event, folderPath) => {
|
||
const win = BrowserWindow.fromWebContents(event.sender);
|
||
const template = [
|
||
{
|
||
label: 'Copy Folder Path',
|
||
click: () => {
|
||
clipboard.writeText(folderPath);
|
||
}
|
||
},
|
||
{
|
||
label: 'Open Folder',
|
||
click: () => {
|
||
// öffnet den Ordner in der nativen Dateiansicht
|
||
shell.openPath(folderPath);
|
||
}
|
||
}
|
||
];
|
||
const menu = Menu.buildFromTemplate(template);
|
||
menu.popup({ window: win });
|
||
});
|
||
|
||
// clean up on exit
|
||
app.on('window-all-closed', () => {
|
||
if (process.platform !== 'darwin') app.quit();
|
||
});
|