From d204ab1a546dd594d21e4e6e0973d318faa96f12 Mon Sep 17 00:00:00 2001 From: Victor Giers Date: Sat, 24 May 2025 15:00:39 +0200 Subject: [PATCH] auto-git: [change] main.js --- main.js | 633 -------------------------------------------------------- 1 file changed, 633 deletions(-) diff --git a/main.js b/main.js index a43dadc..e69de29 100644 --- a/main.js +++ b/main.js @@ -1,633 +0,0 @@ -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}`); -} - - - -async function squashCommitMessages(repoPath, commitMessage, hashes) { - cleanRebaseDirs(repoPath); - 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') || 150; - if (folders[idx].linesChanged >= threshold) { - debug('Congratulations! You changed enough lines of code :)'); - // **Hier: LLM-Workflow starten** - //await runLLMCommitPipeline(folderPath, folders[idx].llmCandidates, win); - folders[idx].linesChanged = 0; - const candidates = folders[idx].llmCandidates; - folders[idx].llmCandidates = []; - await runLLMCommitPipeline(folderPath, candidates); - // Reset danach: - //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(); -});