1
0
Files
auto-git-gui/main.js
Victor Giers 345e040467 auto-git:
[change] main.js
2025-05-25 03:32:19 +02:00

1122 lines
35 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const { app, BrowserWindow, ipcMain, dialog, Tray, Menu, shell, clipboard, nativeImage } = require('electron');
app.name = 'Auto-Git';
const { exec } = require('child_process');
const { spawn } = require('child_process');
const { spawnSync } = require('child_process');
const path = require('path');
const fs = require('fs');
const os = require('os');
const Store = require('electron-store');
const simpleGit = require('simple-git');
const chokidar = require('chokidar');
const store = new Store({
defaults: {
folders: [],
selected: null,
skymode: true,
skipGitPrompt: true,
intelligentCommitThreshold: 20,
autostart: false,
closeToTray: true,
needsRelocation: true
}
});
let tray = null;
let isQuiting = false;
function updateMissingFolders(win) {
let folders = store.get('folders') || [];
let updatedFolders = [];
let anyChanged = false;
folders = folders.map(f => {
const missing = !fs.existsSync(f.path);
const needsRelocation = f.needsRelocation || false;
if (needsRelocation !== missing) {
anyChanged = true;
updatedFolders.push({ ...f, needsRelocation: missing });
return { ...f, needsRelocation: missing };
}
return f;
});
if (anyChanged) {
store.set('folders', folders);
console.log("change detected");
// Für jeden betroffenen Folder Event schicken:
updatedFolders.forEach(folderObj => {
win.webContents.send('folders-location-updated', folderObj);
});
}
}
updateMissingFolders();
setInterval(updateMissingFolders, 3000);
function createTray(win) {
const iconPath = path.join(__dirname, 'assets/icon/trayicon.png');
const icon = nativeImage.createFromPath(iconPath);
// Standard-Größen je nach OS
let size;
switch (process.platform) {
case 'darwin': // macOS
size = { width: 22, height: 22 };
break;
case 'win32': // Windows
size = { width: 16, height: 16 };
break;
default: // Linux / other
size = { width: 24, height: 24 };
}
const trayImage = icon.resize(size);
const tray = new Tray(trayImage);
tray.setToolTip('Auto-Git läuft im Hintergrund');
tray.on('double-click', () => {
win.show();
win.focus();
});
return tray;
}
let folders = store.get('folders');
if (Array.isArray(folders)) {
folders = folders.map(f => ({
...f,
linesChanged: 0, // zurück auf 0
llmCandidates: [] // leeres Array
}));
store.set('folders', folders);
}
// Map zum Speichern der Watcher pro Ordner
const repoWatchers = new Map();
// Debug Helper
function debug(msg) {
console.log(`[DEBUG ${new Date().toISOString()}] ${msg}`);
}
/**
* Erstellt das BrowserWindow und lädt index.html.
* Gibt das Window-Objekt zurück.
*/
function createWindow() {
const win = new BrowserWindow({
width: 900,
height: 600,
minWidth: 600,
minHeight: 500,
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: 450,
height: 450,
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 shortHash = hash.substring(0, 7);
const diff = await git.diff([`${hash}^!`]);
const msg = (await git.show(['-s', '--format=%B', hash])).trim();
commits.push({ hash: shortHash, message: msg, diff });
}
return commits;
}
// ---- 2. Prompt für LLM bauen ----
async function generateLLMCommitMessages(folderPath, hashes) {
const commits = await getCommitsForLLM(folderPath, hashes);
const prompt = `
Analyze the following git commits. For each commit, generate a concise commit message summarizing the actual change.
- ONLY output a JSON object mapping each commit hash to its new message.
- Do NOT add any explanations, greetings, or extra text.
Example Output:
{
"1a2b3c4": "Fix bug in user registration",
"2b3c4d5": "Refactor login logic"
}
COMMITS (as JSON):
${JSON.stringify(commits, null, 2)}
`;
return prompt;
}
// ---- 3. LLM Streaming Call ----
async function streamLLMCommitMessages(prompt, onDataChunk) {
const selectedModel = store.get('commitModel') || 'qwen2.5-coder:32b';
const response = await fetch('http://localhost:11434/api/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: selectedModel, // ggf. Modell anpassen
prompt: prompt,
stream: true,
options: { temperature: 0.3 }
})
});
if (!response.body) throw new Error('No stream returned');
const reader = response.body.getReader();
const decoder = new TextDecoder();
let fullOutput = '';
let done = false;
while (!done) {
const { value, done: streamDone } = await reader.read();
done = streamDone;
if (value) {
const chunk = decoder.decode(value, { stream: true });
for (const line of chunk.split('\n')) {
if (!line.trim()) continue;
try {
const obj = JSON.parse(line);
if (obj.response) {
fullOutput += obj.response;
if (onDataChunk) onDataChunk(obj.response);
}
if (obj.done) break;
} catch (e) {
// ignore malformed chunk
}
}
}
}
return fullOutput;
}
// ---- 4. JSON Output robust parsen ----
function parseLLMCommitMessages(rawOutput) {
let cleaned = rawOutput.trim();
cleaned = cleaned.replace(/^```(?:json)?|```$/gmi, '');
try {
if (cleaned.trim().startsWith('[')) return JSON.parse(cleaned);
if (cleaned.trim().startsWith('{')) {
const obj = JSON.parse(cleaned);
return Object.entries(obj).map(([commit, newMessage]) => ({ commit, newMessage }));
}
// Zeilenweise Objekte (fuzzy)
if (cleaned.includes('\n')) {
let lines = cleaned.split('\n').map(l => l.trim()).filter(Boolean);
let objs = [];
for (let line of lines) {
try { let o = JSON.parse(line); objs.push(o); } catch {}
}
if (objs.length) return objs;
}
} catch (err) {
// Fallback repair
try {
cleaned = cleaned.replace(/}\s*{/g, '},\n{');
if (!cleaned.startsWith('[')) cleaned = '[' + cleaned;
if (!cleaned.endsWith(']')) cleaned = cleaned + ']';
return JSON.parse(cleaned);
} catch (e) {
throw new Error('Could not parse LLM output:\n' + rawOutput);
}
}
throw new Error('Could not parse LLM output:\n' + rawOutput);
}
// --- Hauptfunktion ---
/**
* Rewords commit messages for each hash (oldest to newest) using git rebase -i in a loop.
* @param {string} repoPath - Path to your repo
* @param {object} commitMessageMap - { fullHash: newMessage }
* @param {string[]} hashes - array of full commit hashes (oldest first!)
*/
async function rewordCommitsSequentially(repoPath, commitMessageMap, hashes) {
const git = simpleGit(repoPath);
// Sort hashes...
const allCommits = (await git.log()).all;
const hashesOrdered = hashes
.map(h => allCommits.find(c => c.hash.startsWith(h)))
.filter(Boolean)
.sort((a, b) =>
allCommits.findIndex(c => c.hash === a.hash) - allCommits.findIndex(c => c.hash === b.hash)
)
.map(c => c.hash);
// EIN Loop!
for (const hash of hashesOrdered) {
// --- Lookup: full hash oder short hash
let msg = commitMessageMap[hash];
if (!msg) msg = commitMessageMap[hash.substring(0, 7)];
if (!msg) {
console.warn('No commit message found for hash', hash, 'or', hash.substring(0, 7));
continue;
}
const commitMsg = msg.replace(/(["$`\\])/g, '\\$1');
const sequenceEditor = `sed -i '' '1s/pick/reword/'`;
// await auf Promise aber OHNE zweiten for!
await new Promise((resolve, reject) => {
const proc = spawn('git', [
'rebase', '-i', `${hash}^`
], {
cwd: repoPath,
env: {
...process.env,
GIT_SEQUENCE_EDITOR: sequenceEditor,
GIT_EDITOR: `echo "${commitMsg}" >`
},
stdio: 'inherit'
});
proc.on('exit', code => code === 0 ? resolve() : reject(new Error(`Failed to reword ${hash}`)));
});
console.log(`[AutoGit] Reworded commit ${hash}`);
}
console.log('[AutoGit] All specified commit messages updated!');
}
//---- 6. Workflow ----
async function runLLMCommitRewrite(folderPath, hashes) {
const prompt = await generateLLMCommitMessages(folderPath, hashes);
const llmRaw = await streamLLMCommitMessages(prompt, chunk => process.stdout.write(chunk));
const commitList = parseLLMCommitMessages(llmRaw);
const messageMap = {};
for (const entry of commitList) messageMap[entry.commit] = entry.newMessage;
await rewordCommitsSequentially(folderPath, messageMap, hashes);
}
/*
// ---- 6. Komplett-Workflow (Random instant messages für debugging) ----
async function runLLMCommitRewrite(folderPath, hashes) {
// Generate a mapping { hash: message }
const messageMap = hashes.reduce((map, hash) => {
map[hash] = getRandomMessage();
return map;
}, {});
console.log(messageMap)
// Call your existing rewrite step with the fake messages
//await cherryPickCommitRewrite(folderPath, messageMap, hashes);
await rewordCommitsSequentially(folderPath, messageMap, hashes);
}
// Helper: returns a “random” placeholder commit message
function getRandomMessage() {
const verbs = [
'Update', 'Refactor', 'Fix', 'Add', 'Remove', 'Improve', 'Optimize',
'Document', 'Cleanup', 'Configure', 'Upgrade', 'Revert'
];
const objects = [
'authentication flow', 'API endpoint', 'styling', 'logging',
'error handling', 'data model', 'build script', 'test suite',
'configuration', 'dependencies', 'README', 'README.md'
];
const details = [
'for better performance',
'to meet new requirements',
'after feedback',
'as per spec',
'to fix typo',
'to improve readability',
'to avoid regressions',
'for consistency'
];
const pick = arr => arr[Math.floor(Math.random() * arr.length)];
return `${pick(verbs)} ${pick(objects)} ${pick(details)}`;
}
*/
// Nutze das Template aus dem Projektordner:
const TEMPLATE_PATH = path.join(__dirname, 'rewrite-commit-msg.js.template');
function createRewriteScript(mapping) {
// Lies das Template
let content = fs.readFileSync(TEMPLATE_PATH, 'utf-8');
// Ersetze __MESSAGE_MAP__ durch dein Mapping
content = content.replace('__MESSAGE_MAP__', JSON.stringify(mapping));
// Speichere als temporäre Datei
const scriptPath = path.join(__dirname, `rewrite-commit-msg.${Date.now()}.js`);
fs.writeFileSync(scriptPath, content, 'utf-8');
return scriptPath;
}
async function autoCommit(folderPath, message) {
const git = simpleGit(folderPath);
const status = await git.status();
if (
status.not_added.length === 0 &&
status.created.length === 0 &&
status.deleted.length === 0 &&
status.modified.length === 0 &&
status.renamed.length === 0
) {
debug('Auto-Commit: Keine Änderungen zum committen.');
return false;
}
let currentBranch = null;
try {
currentBranch = (await git.revparse(['--abbrev-ref', 'HEAD'])).trim();
debug(`[autoCommit] Aktueller Branch: ${currentBranch}`);
} catch {
debug('[autoCommit] HEAD ist detached.');
currentBranch = null;
}
if (!currentBranch || currentBranch === 'HEAD') {
// === Erweiterte Logs ===
const headCommit = (await git.revparse(['HEAD'])).trim();
let masterCommit = null;
let hasMaster = false;
try {
masterCommit = (await git.revparse(['refs/heads/master'])).trim();
hasMaster = true;
} catch (e) {
debug('[autoCommit] master branch existiert nicht.');
masterCommit = null;
hasMaster = false;
}
debug(`[autoCommit] HEAD: ${headCommit}`);
debug(`[autoCommit] master: ${masterCommit}`);
if (hasMaster && headCommit === masterCommit) {
// HEAD ist detached, zeigt aber exakt auf master-Tip → einfach auf master auschecken.
await git.checkout('master');
debug('[autoCommit] HEAD war detached, zeigte aber exakt auf master jetzt zurück auf master.');
// Nach dem Checkout nochmal aktuellen Branch loggen:
currentBranch = (await git.revparse(['--abbrev-ref', 'HEAD'])).trim();
debug(`[autoCommit] Nach checkout: Aktueller Branch: ${currentBranch}`);
// Jetzt **NICHT** weiter zur Umbenenn-Logik!
} else if (hasMaster) {
// HEAD ist detached, zeigt auf einen anderen Commit → backup master und neuen master-Branch
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const backupBranch = `backup-master-${timestamp}`;
await git.branch(['-m', 'master', backupBranch]);
debug(`[autoCommit] Alter master in ${backupBranch} umbenannt.`);
await git.checkout(['-b', 'master']);
debug('[autoCommit] Neuer master-Branch erstellt und ausgecheckt.');
currentBranch = 'master';
} else {
// Kein master vorhanden (erstmalig)
await git.checkout(['-b', 'master']);
debug('[autoCommit] Kein master-Branch vorhanden, neuer master erstellt.');
currentBranch = 'master';
}
}
// --- Zeilenzählung ---
let diffOutput = await git.diff(['--numstat']);
// Zeilensumme berechnen:
let changedLines = 0;
for (let line of diffOutput.split('\n')) {
const match = line.match(/^(\d+|\-)\s+(\d+|\-)\s+(.*)$/);
if (match) {
const added = match[1] === '-' ? 0 : parseInt(match[1], 10);
const deleted = match[2] === '-' ? 0 : parseInt(match[2], 10);
changedLines += added + deleted;
}
}
// Folders aus Store holen
let folders = store.get('folders') || [];
let idx = folders.findIndex(f => f.path === folderPath);
if (idx !== -1) {
folders[idx].linesChanged = (folders[idx].linesChanged || 0) + changedLines;
folders[idx].llmCandidates = folders[idx].llmCandidates || [];
// ===> WICHTIG: Wir commiten ja jetzt, daher merken wir uns gleich die neue Commit-Hash
// Vor git.commit: Merke alten HEAD
const oldHead = (await git.revparse(['HEAD'])).trim();
// Stagen & Committen
await git.add(['-A']);
debug('[autoCommit] Alle Änderungen gestaged.');
await git.commit(message || '[auto]');
debug('[autoCommit] Commit erfolgreich erstellt.');
// Nach Commit: neuen HEAD ermitteln und in llmCandidates speichern
const newHead = (await git.revparse(['HEAD'])).trim();
folders[idx].llmCandidates = folders[idx].llmCandidates || [];
folders[idx].llmCandidates.push(newHead);
console.log(folders[idx].llmCandidates)
// Threshold holen
const threshold = store.get('intelligentCommitThreshold') || 10;
if (folders[idx].linesChanged >= threshold) {
debug('Congratulations! You changed enough lines of code :)');
folders[idx].linesChanged = 0;
const cands = folders[idx].llmCandidates;
folders[idx].llmCandidates = [];
await runLLMCommitRewrite(folderPath, cands);
store.set('folders', folders);
}
store.set('folders', folders);
} else {
// Folder not found! (Debug)
debug(`[autoCommit] Warning: Folder ${folderPath} not found in store`);
}
}
app.whenReady().then(() => {
const win = createWindow();
// Menubar
const menu = Menu.buildFromTemplate([
{
role: 'appMenu',
submenu: [
{ label: 'Settings', click: () => openSettings(win) },
{ label: 'Quit', role: 'quit', click: () => { isQuiting = true; app.quit(); } }
]
},
{ role: 'editMenu' }
]);
Menu.setApplicationMenu(menu);
const tray = createTray(win);
// --- Context Menu bauen ---
function buildTrayMenu() {
const folders = store.get('folders') || [];
const monitoringActive = folders.some(f => f.monitoring);
return Menu.buildFromTemplate([
{ label: 'Auto-Git öffnen', click: () => { win.show(); win.focus(); } },
{ type: 'separator' },
...folders.map(f => ({
label: `${f.monitoring ? '🟢' : '🔴'} ${path.basename(f.path)}`,
submenu: [
{
label: f.monitoring ? 'Monitoring stoppen' : 'Monitoring starten',
click: () => {
win.webContents.send('tray-toggle-monitoring', f.path); // Renderer kann dann toggeln
}
},
{
label: 'Ordner entfernen',
click: () => {
win.webContents.send('tray-remove-folder', f.path);
}
}
]
})),
{ type: 'separator' },
{
label: 'Neuen Ordner hinzufügen',
click: () => { win.webContents.send('tray-add-folder'); }
},
{
label: 'Alle Monitorings starten',
click: () => {
folders.forEach(f => {
if (!f.monitoring) win.webContents.send('tray-toggle-monitoring', f.path);
});
}
},
{
label: 'Alle Monitorings stoppen',
click: () => {
folders.forEach(f => {
if (f.monitoring) win.webContents.send('tray-toggle-monitoring', f.path);
});
}
},
{ type: 'separator' },
{ label: 'Beenden', click: () => { isQuiting = true; app.quit(); } }
]);
}
tray.setToolTip('Auto-Git läuft im Hintergrund');
tray.setContextMenu(buildTrayMenu());
// Menu immer wieder aktualisieren, wenn sich Ordner ändern:
store.onDidChange('folders', () => {
tray.setContextMenu(buildTrayMenu());
});
// Optional: Minimieren auf Tray bei Fenster-Schließen
win.on('close', (e) => {
if (!app.isQuiting) {
e.preventDefault();
win.hide();
}
});
// Doppelklick aufs Tray: Fenster zeigen
tray.on('double-click', () => {
win.show();
});
// 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');
});
// 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;
});
// Zähle Commits
ipcMain.handle('get-commit-count', async (_e, folderObj) => {
try {
if (folderObj.needsRelocation || !fs.existsSync(folderObj.path)) {
return 0;
}
const git = simpleGit(folderObj.path);
const log = await git.log();
return log.total;
} catch (err) {
return 0;
}
});
// 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;
});
// Commits holen
ipcMain.handle('get-commits', async (_e, folderObj) => {
try {
if (folderObj.needsRelocation || !fs.existsSync(folderObj.path)) {
// Folder nicht vorhanden, geben wir leere Liste zurück
return { head: null, commits: [] };
}
const git = simpleGit(folderObj.path);
const log = await git.log(['--all']);
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
}))
};
} catch (err) {
// Im Fehlerfall (z.B. Verzeichnis gelöscht)
return { head: null, commits: [] };
}
});
// 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 };
}
// Ansonsten wie gehabt:
if (hasMaster) {
// HEAD ist detached, zeigt nicht auf master -> backup + neuer master
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const backupBranch = `backup-master-${timestamp}`;
await git.branch(['-m', 'master', backupBranch]);
debug(`[autoCommit] Alter master in ${backupBranch} umbenannt.`);
await git.checkout(['-b', 'master']);
debug('[autoCommit] Neuer master-Branch erstellt und ausgecheckt.');
} else {
// Kein master vorhanden (erstmalig)
await git.checkout(['-b', 'master']);
debug('[autoCommit] Kein master-Branch vorhanden, neuer master erstellt.');
}
}
await git.add(['-A']);
debug('Alle Änderungen gestaged.');
await git.commit(message || 'test');
debug('Commit erfolgreich erstellt.');
// Push hier ggf. noch auskommentiert lassen
return { success: true };
} catch (err) {
debug(`FEHLER beim Commit: ${err.message}`);
return { success: false, error: err.message };
}
});
ipcMain.handle('set-monitoring', async (_e, folderPath, monitoring) => {
let folders = store.get('folders') || [];
folders = folders.map(f =>
f.path === folderPath ? { ...f, monitoring } : f
);
store.set('folders', folders);
debug(`[STORE] Monitoring für ${folderPath}: ${monitoring}`);
// Monitoring-Watcher starten/stoppen
if (monitoring) {
startMonitoringWatcher(folderPath, win);
} else {
stopMonitoringWatcher(folderPath);
}
return monitoring;
});
ipcMain.handle('ollama-list', async () => {
// Versuche erst JSON-Ausgabe
return new Promise(resolve => {
exec('ollama list --json', (err, stdout, stderr) => {
if (err) {
// ENOENT → ollama CLI fehlt
if (err.code === 'ENOENT') {
return resolve({ status: 'no-cli' });
}
// JSON-Modus nicht unterstützt? Dann Fallback auf plain text
return parsePlain();
}
try {
const lines = stdout
.split('\n')
.filter(l => l.trim())
.map(l => JSON.parse(l));
return resolve({ status: 'ok', models: lines });
} catch (_) {
return parsePlain();
}
});
// Fallback-Funktion: parst die tabellarische Ausgabe
function parsePlain() {
exec('ollama list', (err2, out2, stderr2) => {
if (err2) {
if (err2.code === 'ENOENT') return resolve({ status: 'no-cli' });
return resolve({ status: 'error', msg: stderr2 || err2.message });
}
const models = [];
// Jede Zeile nach HEADER ignorieren, Spalten splitten
out2.split('\n').slice(1).forEach(line => {
const cols = line.trim().split(/\s{2,}/);
if (cols[0]) models.push({ name: cols[0] });
});
resolve({ status: 'ok', models });
});
}
});
});
ipcMain.handle('ollama-pull', async (_e, model) => {
return new Promise(resolve => {
exec(`ollama pull ${model}`, (err, stdout, stderr) => {
if (err) return resolve({ status: 'error', msg: stderr || err.message });
resolve({ status: 'ok', msg: stdout });
});
});
});
ipcMain.handle('get-commit-model', () => store.get('commitModel') || 'qwen2.5-coder:7b');
ipcMain.handle('set-commit-model', (_e, val) => store.set('commitModel', val));
ipcMain.handle('get-readme-model', () => store.get('readmeModel') || 'qwen2.5-coder:32b');
ipcMain.handle('set-readme-model', (_e, val) => store.set('readmeModel', val));
ipcMain.handle('get-intelligent-commit-threshold', () => store.get('intelligentCommitThreshold'));
ipcMain.handle('set-intelligent-commit-threshold', (_e, value) => {
store.set('intelligentCommitThreshold', value);
});
ipcMain.handle('get-autostart', () => store.get('autostart'));
ipcMain.handle('set-autostart', (_e, enabled) => {
store.set('autostart', enabled);
app.setLoginItemSettings({
openAtLogin: !!enabled
});
});
ipcMain.handle('get-close-to-tray', () => store.get('closeToTray'));
ipcMain.handle('set-close-to-tray', (_e, val) => store.set('closeToTray', val));
ipcMain.on('close-settings', () => {
if (settingsWin) settingsWin.close();
});
// … Ende der IPC-Handler …
win.webContents.openDevTools({ mode: 'detach' });
// clean up on exit
win.on('close', (e) => {
if (!isQuiting && store.get('closeToTray')) {
e.preventDefault();
win.hide();
}
});
});
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 });
});