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(); });