Refactored folder monitoring logic and added intelligent commit functionality
This commit is contained in:
892
main.js
892
main.js
@@ -0,0 +1,892 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 `You're a professional programmer who writes git commit messages all day.
|
||||||
|
Generate a concise git commit message for these changes:
|
||||||
|
|
||||||
|
${diff}
|
||||||
|
|
||||||
|
--------------------------------------
|
||||||
|
|
||||||
|
Answer VERY BRIEFLY. 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 BE STRAIGHT TO THE POINT!
|
||||||
|
Also reply to my message, just give me the commit message.`;
|
||||||
|
} 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 `You're a professional programmer who writes git commit messages all day.
|
||||||
|
Analyze the following code changes (from multiple commits). To squash them into a single commit I need a concise git commit message from you describing the changes in a single sentence.
|
||||||
|
Here are the combined diffs:
|
||||||
|
${combinedDiffs}
|
||||||
|
|
||||||
|
--------------------------------------
|
||||||
|
|
||||||
|
Even if this might seem like a lot of code, I need you to answer VERY BRIEFLY. A git commit message to be precise is what I need from you, to protocol these changes.
|
||||||
|
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 BE STRAIGHT TO THE POINT!
|
||||||
|
Don't reply to my message, just give me the commit message.`;
|
||||||
|
} else {
|
||||||
|
throw new Error('No commits found for LLM prompt.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 3. LLM Streaming Call
|
||||||
|
async function streamLLMCommitMessages(prompt, onDataChunk) {
|
||||||
|
console.log(prompt);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanRebaseDirs(repoPath) {
|
||||||
|
const gitDir = path.join(repoPath, '.git');
|
||||||
|
const rebaseDirs = ['rebase-merge', 'rebase-apply'];
|
||||||
|
for (const dir of rebaseDirs) {
|
||||||
|
const fullPath = path.join(gitDir, dir);
|
||||||
|
if (fs.existsSync(fullPath)) {
|
||||||
|
fs.rmSync(fullPath, { recursive: true, force: true });
|
||||||
|
console.log(`[AutoGit] Entfernt alte ${dir}-Direktory: ${fullPath}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user