1
0
Files
auto-git-gui/main.js
2025-05-23 21:03:08 +02:00

352 lines
9.5 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 path = require('path');
const fs = require('fs');
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
}
});
// 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);
}
}
app.whenReady().then(() => {
const win = createWindow();
// Menüs
const menu = Menu.buildFromTemplate([
{
role: 'appMenu',
submenu: [
{
label: 'Settings', click: () => openSettings(win)
},
{ role: 'quit', label: 'Quit' }
]
}, // mehr menüs hier
]);
Menu.setApplicationMenu(menu);
// 1) Beim Start bereits gespeicherte Ordner überwachen
const folders = store.get('folders');
folders.forEach(folder => {
// nur watchen, wenn .git existiert
if (fs.existsSync(path.join(folder, '.git', 'refs', 'heads', 'master'))) {
watchRepo(folder, win);
}
});
// 2) IPC-Handler
// Liste aller Folders
ipcMain.handle('get-folders', () => store.get('folders'));
// Ordner hinzufügen: Open-Dialog, init, Store-Update, watchen
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, 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, folder) => {
const git = simpleGit(folder);
const log = await git.log();
return log.total; // Anzahl der Commits
});
// Prüfe, ob es ungestagte Änderungen gibt
ipcMain.handle('has-diffs', async (_e, folder) => {
const git = simpleGit(folder);
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, folder) => {
const gitDir = path.join(folder, '.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, folder) => {
const git = simpleGit(folder);
// 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, folder, hash) => {
const git = simpleGit(folder);
return git.diff([`${hash}^!`]);
});
// Revert
ipcMain.handle('revert-commit', async (_e, folder, hash) => {
const git = simpleGit(folder);
await git.revert(hash, ['--no-edit']);
});
/**
* Checkt das Arbeitsverzeichnis auf exakt den Zustand von `hash` aus.
*/
ipcMain.handle('checkout-commit', async (_e, folder, hash) => {
const git = simpleGit(folder);
// clean mode: alle lokalen Veränderungen verwerfen
await git.checkout([hash, '--force']);
});
// Snapshot
ipcMain.handle('snapshot-commit', async (_e, folder, 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(folder);
const filePath = path.join(outDir, `${baseName}-${hash}.zip`);
return new Promise((resolve, reject) => {
exec(
`git -C "${folder}" 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));
ipcMain.handle('commit-current-folder', async (_e, folder, message) => {
try {
debug(`Commit-Vorgang für ${folder} gestartet…`);
const git = simpleGit(folder);
// 1) BranchStatus prüfen
let currentBranch = null;
try {
currentBranch = (await git.revparse(['--abbrev-ref', 'HEAD'])).trim();
debug(`Aktueller Branch: ${currentBranch}`);
} catch {
debug('HEAD ist detached.');
}
// 2) Falls detached, alten master sichern und neuen anlegen
if (!currentBranch || currentBranch === 'HEAD') {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const backupBranch = `backup-master-${timestamp}`;
const branches = await git.branchLocal();
if (branches.all.includes('master')) {
await git.branch(['-m', 'master', backupBranch]);
debug(`Alter master → ${backupBranch}`);
}
await git.checkout(['-b', 'master']);
debug('Neuer master-Branch erstellt.');
}
// 3) Commit & Push
await git.add(['-A']); debug('git add -A');
await git.commit(message || 'test'); debug('git commit');
await git.push(['-u','origin','master']); debug('git push');
return { success: true };
} catch (err) {
debug(`FEHLER beim Commit: ${err.message}`);
return { success: false, error: err.message };
}
});
// … 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();
});