901 lines
29 KiB
JavaScript
901 lines
29 KiB
JavaScript
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}`);
|
||
}
|
||
}
|
||
}
|
||
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
const os = require('os');
|
||
const { spawn } = require('child_process');
|
||
const simpleGit = require('simple-git');
|
||
|
||
function cleanRebaseDirs(repoPath) {
|
||
const dirs = ['rebase-merge', 'rebase-apply'];
|
||
for (const d of dirs) {
|
||
const p = path.join(repoPath, '.git', d);
|
||
if (fs.existsSync(p)) fs.rmSync(p, { recursive: true, force: true });
|
||
}
|
||
}
|
||
|
||
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. Collect all commits to squash (in reverse = chronological)
|
||
const toSquash = allCommits.slice(0, oldestIdx + 1).reverse();
|
||
if (toSquash.length < 2) throw new Error("Need at least 2 commits to squash.");
|
||
|
||
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 = os.tmpdir();
|
||
const todoPath = path.join(tmpdir, `git-rebase-todo-${Date.now()}.txt`);
|
||
fs.writeFileSync(todoPath, todoLines.join('\n'));
|
||
|
||
// Create a temporary script for SEQUENCE_EDITOR
|
||
const seqScript = path.join(tmpdir, `llm-seq-edit-${Date.now()}.sh`);
|
||
fs.writeFileSync(seqScript, `#!/bin/sh\ncp "${todoPath}" "$1"\n`);
|
||
fs.chmodSync(seqScript, 0o755);
|
||
|
||
// Create a temporary script for GIT_EDITOR (sets commit message)
|
||
const msgScript = path.join(tmpdir, `llm-msg-edit-${Date.now()}.sh`);
|
||
fs.writeFileSync(msgScript, `#!/bin/sh\necho "${commitMessage.replace(/"/g, '\\"')}" > "$1"\n`);
|
||
fs.chmodSync(msgScript, 0o755);
|
||
|
||
// Launch rebase
|
||
await new Promise((resolve, reject) => {
|
||
const proc = spawn('git', ['rebase', '-i', rebaseFrom], {
|
||
cwd: repoPath,
|
||
env: {
|
||
...process.env,
|
||
GIT_SEQUENCE_EDITOR: seqScript,
|
||
GIT_EDITOR: msgScript
|
||
},
|
||
stdio: 'inherit'
|
||
});
|
||
proc.on('exit', code => {
|
||
try {
|
||
fs.unlinkSync(todoPath);
|
||
fs.unlinkSync(seqScript);
|
||
fs.unlinkSync(msgScript);
|
||
} catch {}
|
||
if (code === 0) resolve();
|
||
else reject(new Error('git rebase failed with exit code ' + code));
|
||
});
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Komplett-Workflow: Von Kandidaten bis 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();
|
||
});
|