From 62da5899de4391fecf617a8aa773ef879a385d63 Mon Sep 17 00:00:00 2001 From: Victor Giers Date: Sat, 24 May 2025 14:27:33 +0200 Subject: [PATCH] auto-git: [change] main.js --- main.js | 868 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 868 insertions(+) diff --git a/main.js b/main.js index e69de29..02ece1e 100644 --- a/main.js +++ b/main.js @@ -0,0 +1,868 @@ +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 path = require('path'); +const fs = require('fs'); +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 + } +}); + +// 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. Prompts für LLM bauen +async function getPrompt(folderPath, hashes) { + const commits = await getCommitsForLLM(folderPath, hashes); + + if (commits.length === 1) { + // Only one commit: Prompt LLM for a message just for this diff. + const diff = commits[0].diff; + return `Generate a concise git commit message for these changes: + + ${diff} + + Don't give any feedback on the code, just analyze what changed and write the git commit message. Keep it short! A commit message MUST NOT exceed 70 characters!`; + } else if (commits.length > 1) { + // Multiple commits: Squash them, give all diffs as a big change. + const combinedDiffs = commits.map(c => c.diff).join('\n\n'); + return `Analyze the following code changes (from multiple commits). We will squash them to a single commit and need a concise git commit message for that. + Don't give any feedback on the code, just analyze what changed and write the git commit message. Keep it short! A commit message MUST NOT exceed 70 characters! + Here are the combined diffs: + ${combinedDiffs}`; + } else { + throw new Error('No commits found for LLM 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', + 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; +} + +async function squashCommitMessages(repoPath, commitMessage, hashes) { + const git = simpleGit(repoPath); + + // 1. Find the oldest commit in the sequence + const allCommits = (await git.log()).all; + const hashIdxs = hashes.map(h => allCommits.findIndex(c => c.hash.startsWith(h))); + if (hashIdxs.includes(-1)) throw new Error('Some commit(s) not found.'); + const oldestIdx = Math.min(...hashIdxs); + + // The commit *before* the oldest in the list is the "upstream" for rebase + const rebaseFrom = oldestIdx + 1 < allCommits.length ? allCommits[oldestIdx + 1].hash : '--root'; + + // 2. Create rebase-todo: all commits → squash into first + // Pick the oldest, squash the rest + const toSquash = allCommits.slice(0, oldestIdx + 1).reverse(); + const first = toSquash[0]; + const rest = toSquash.slice(1); + + const todoLines = [ + `pick ${first.hash} ${first.message.split('\n')[0]}`, + ...rest.map(c => `squash ${c.hash} ${c.message.split('\n')[0]}`) + ]; + + // Write the todo file + const tmpdir = require('os').tmpdir(); + const todoPath = require('path').join(tmpdir, `git-rebase-todo-${Date.now()}.txt`); + fs.writeFileSync(todoPath, todoLines.join('\n')); + + // Create a temporary file for the commit message + const msgPath = require('path').join(tmpdir, `llm-squash-msg-${Date.now()}.txt`); + fs.writeFileSync(msgPath, commitMessage); + + // Use GIT_SEQUENCE_EDITOR to inject the todo file, and GIT_EDITOR to inject the commit message + await new Promise((resolve, reject) => { + const proc = spawn('git', ['rebase', '-i', rebaseFrom], { + cwd: repoPath, + env: { + ...process.env, + GIT_SEQUENCE_EDITOR: `cat "${todoPath}" >`, + GIT_EDITOR: `cat "${msgPath}" >` + }, + stdio: 'inherit' + }); + proc.on('exit', code => { + try { + fs.unlinkSync(todoPath); + fs.unlinkSync(msgPath); + } catch {} + if (code === 0) resolve(); + else reject(new Error('git rebase failed with exit code ' + code)); + }); + }); +} + +/** + * Komplett-Workflow: Von Kandidaten bis Rewrite + */ + +async function runLLMCommitPipeline(folderPath, hashes) { + const prompt = await getPrompt(folderPath, hashes); + const llmOutput = (await streamLLMCommitMessages(prompt, chunk => process.stdout.write(chunk))).trim(); + + if (hashes.length === 1) { + // For a single commit: amend it in-place + const git = simpleGit(folderPath); + await git.raw(['commit', '--amend', '-m', llmOutput, '--no-edit', '--allow-empty']); + } else if (hashes.length > 1) { + // For multiple commits: squash & set message + await squashCommitMessages(folderPath, llmOutput, hashes); + } else { + throw new Error('No commits to rewrite.'); + } +} + +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); + + // Threshold holen + const threshold = store.get('intelligentCommitThreshold') || 10; + if (folders[idx].linesChanged >= threshold) { + debug('Congratulations! You changed enough lines of code :)'); + // **Hier: LLM-Workflow starten** + //await runLLMCommitPipeline(folderPath, folders[idx].llmCandidates, win); + await runLLMCommitPipeline(folderPath, folders[idx].llmCandidates); + // Reset danach: + folders[idx].linesChanged = 0; + folders[idx].llmCandidates = []; + //store.set('folders', folders); + } + + // Folder-Objekt speichern + 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 → gleich mehr dazu + 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(); +});