1
0

auto-git:

[change] main.js
This commit is contained in:
2025-05-24 15:00:39 +02:00
parent 8702240615
commit d204ab1a54

633
main.js
View File

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