1
0
Files
auto-git-gui/main.js

1020 lines
32 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, Menu, shell, clipboard } = 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 fetch = require('node-fetch');
const chokidar = require('chokidar');
const store = new Store({
defaults: {
folders: [],
selected: null,
skymode: true,
skipGitPrompt: true,
intelligentCommitThreshold: 100
}
});
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: 400,
title: 'Auto-Git',
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true
}
});
win.loadFile('index.html');
return win;
}
// Settings-Fenster
let settingsWin;
function openSettings(win) {
if (settingsWin) {
settingsWin.focus();
return;
}
settingsWin = new BrowserWindow({
parent: win,
modal: true,
width: 400,
height: 300,
resizable: false,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true
}
});
settingsWin.removeMenu();
settingsWin.loadFile('settings.html');
settingsWin.on('closed', () => settingsWin = null);
}
/**
* Startet einen File-Watcher auf .git/refs/heads/master,
* sendet bei Änderungen 'repo-updated' an den Renderer.
*/
function watchRepo(folder, win) {
const gitHead = path.join(folder, '.git', 'refs', 'heads', 'master');
const watcher = chokidar.watch(gitHead, { ignoreInitial: true });
watcher.on('change', () => {
win.webContents.send('repo-updated', folder);
});
repoWatchers.set(folder, watcher);
}
/**
* Initiiert ein Git-Repo in `folder`, falls noch nicht vorhanden,
* und erzeugt einen Initial-Commit mit Timestamp.
*/
async function initGitRepo(folder) {
const git = simpleGit(folder);
const gitDir = path.join(folder, '.git');
if (!fs.existsSync(gitDir)) {
await git.init();
const message = `Initial commit (generated by auto-git)`;
const readmePath = path.join(folder, 'README.md');
fs.writeFileSync(readmePath, `# Projekt in ${path.basename(folder)}\n`);
await git.add('./*');
await git.commit(message);
}
}
// Map für Monitoring-Watcher (nicht repoWatchers!)
const monitoringWatchers = new Map();
// Helper: Baut eine Commit-Message aus git.status()
function buildCommitMessageFromStatus(status, prefix = '[auto]') {
const changes = [];
status.not_added.forEach(f => changes.push(`[add] ${f}`));
status.created.forEach(f => changes.push(`[add] ${f}`));
status.modified.forEach(f => changes.push(`[change] ${f}`));
status.deleted.forEach(f => changes.push(`[unlink] ${f}`));
status.renamed.forEach(r => changes.push(`[rename] ${r.from}${r.to}`));
return prefix + '\n' + changes.map(l => ` ${l}`).join('\n');
}
function startMonitoringWatcher(folderPath, win) {
if (monitoringWatchers.has(folderPath)) return;
const watcher = chokidar.watch(folderPath, {
ignored: /(^|[\/\\])\..|node_modules|\.git/,
ignoreInitial: true,
persistent: true,
depth: 99,
awaitWriteFinish: { stabilityThreshold: 300, pollInterval: 100 }
});
// Initialer Commit
(async () => {
debug(`[MONITOR] Starte initialen Commit-Check für ${folderPath}`);
const git = simpleGit(folderPath);
const status = await git.status();
if (
status.not_added.length > 0 ||
status.created.length > 0 ||
status.modified.length > 0 ||
status.deleted.length > 0 ||
status.renamed.length > 0
) {
const msg = buildCommitMessageFromStatus(status, 'auto-git: ');
const did = await autoCommit(folderPath, msg);
if (did) {
win.webContents.send('repo-updated', folderPath);
debug(`[MONITOR] Initialer Auto-Commit für ${folderPath} durchgeführt:\n${msg}`);
}
}
})();
// Bei jedem Event → status neu holen, Message wie beim initialen Check bauen
watcher.on('all', async () => {
const git = simpleGit(folderPath);
const status = await git.status();
if (
status.not_added.length > 0 ||
status.created.length > 0 ||
status.modified.length > 0 ||
status.deleted.length > 0 ||
status.renamed.length > 0
) {
const msg = buildCommitMessageFromStatus(status, 'auto-git: ');
await autoCommit(folderPath, msg);
win.webContents.send('repo-updated', folderPath);
}
});
monitoringWatchers.set(folderPath, watcher);
debug(`[MONITOR] Watcher aktiv für ${folderPath}`);
}
function stopMonitoringWatcher(folderPath) {
const watcher = monitoringWatchers.get(folderPath);
if (watcher) {
watcher.close();
monitoringWatchers.delete(folderPath);
debug(`[MONITOR] Watcher gestoppt für ${folderPath}`);
}
}
// ---- Rewrite Git Messages with LLM generated messages ----
// ---- 1. Commits & Diffs für LLM sammeln ----
async function getCommitsForLLM(folderPath, hashes) {
const git = simpleGit(folderPath);
const commits = [];
for (const hash of hashes) {
const diff = await git.diff([`${hash}^!`]);
const msg = (await git.show(['-s', '--format=%B', hash])).trim();
commits.push({ hash, message: msg, diff });
}
return commits;
}
// ---- 2. 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 response = await fetch('http://localhost:11434/api/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: 'qwen2.5-coder:32b', // ggf. Modell anpassen
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;
}
// ---- 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);
// Sanity: Sort hashes chronologically by git log order (oldest first)
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);
// Loop over all hashes
for (const hash of hashesOrdered) {
// Compose the rebase command: only one commit at a time!
await new Promise((resolve, reject) => {
// macOS: sed -i '' ... Linux: sed -i ...
// Try macOS style, change '' to '' or nothing if you get errors.
const sequenceEditor = `sed -i '' '1s/pick/reword/'`;
const commitMsg = commitMessageMap[hash].replace(/(["$`\\])/g, '\\$1'); // Escape quotes etc
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!');
}
/**
* Cherry-picks the given commits, amending each one's message, and replaces master.
* @param {string} repoPath
* @param {object} commitMessageMap - { [fullHash]: newMessage }
* @param {string[]} hashes - list of commit hashes, oldest to newest!
*/
/*
async function cherryPickCommitRewrite(repoPath, commitMessageMap, hashes) {
// 1. Find parent of the OLDEST commit
//const allCommits = (await git.log()).all;
//const parentHash = (await git.raw(['rev-parse', `${oldestHash}^`])).trim();
// 2. Create a new temp branch from the parent
const git = simpleGit(repoPath);
if(hashes.length > 1){
const branchName = "temp_branch" + Date.now();
console.log(commitMessageMap[hashes[0]]);
await git.checkout(hashes[0]);
console.log("checkout " + hashes[0])
await git.checkoutLocalBranch(branchName);
console.log("branch " + branchName)
await git.commit(commitMessageMap[hashes[0]], undefined, { '--amend': null });
console.log("amend")
for (let i = 1; i < hashes.length; i++) {
await git.raw(['cherry-pick', '--no-commit', hashes[i]]);
console.log("cherry " + hashes[i])
await git.commit(commitMessageMap[hashes[i]], undefined, { '--amend': null });
console.log("amend")
}
await git.deleteLocalBranch('master', true);
console.log("branch del")
await git.branch(['-m', branchName, 'master']);
console.log("branch mov")
} else {
await git.commit(commitMessageMap[hashes[0]], undefined, { '--amend': null });
}
*/
//await git.checkoutLocalBranch(NEW_BRANCH);
/*
// 3. Cherry-pick and amend each commit in order
for (const hash of hashes) {
// Cherry-pick (commit as is)
let res = spawnSync('git', ['cherry-pick', hash], { cwd: repoPath, stdio: 'inherit' });
if (res.status !== 0) throw new Error('Cherry-pick failed for ' + hash);
// Amend commit message
const msg = commitMessageMap[hash];
if (msg) {
res = spawnSync('git', ['commit', '--amend', '-m', msg], { cwd: repoPath, stdio: 'inherit' });
if (res.status !== 0) throw new Error('Amend failed for ' + hash);
}
}
// 4. Move master to rewritten branch (overwrite)
await git.checkout('master'); // just in case we're not already there
await git.branch(['-f', 'master', NEW_BRANCH]); // force-move master pointer
// 5. Checkout master (HEAD on new history)
await git.checkout('master');
// 6. (Optional) Delete the temp branch
await git.branch(['-D', NEW_BRANCH]);
console.log('\n[AutoGit] Master branch has been overwritten with rewritten commits.');
*/
//}
// ---- 6. Komplett-Workflow: Von Kandidaten bis Rewrite ----
/*
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 cherryPickCommitRewrite(folderPath, messageMap, hashes);
}
*/
// ---- 6. Komplett-Workflow (Randomized) ----
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();
// 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
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();
});