1
0
Files
auto-git-gui/main.js
2025-05-23 20:52:25 +02:00

356 lines
9.8 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.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 });
// main.js
const simpleGit = require('simple-git');
const path = require('path');
// Debug Helper
function debug(msg) {
console.log(`[DEBUG ${new Date().toISOString()}] ${msg}`);
}
ipcMain.handle('commit-current-folder', async (_e, folder, message) => {
try {
debug(`Commit-Vorgang für ${folder} gestartet…`);
const git = simpleGit(folder);
// 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.');
}
// Wenn detached, alten Branch umbenennen und neuen master erzeugen
if (!currentBranch || currentBranch === 'HEAD') {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const backupBranch = `backup-master-${timestamp}`;
// Alten master umbenennen (nur falls vorhanden!)
const branches = await git.branchLocal();
if (branches.all.includes('master')) {
await git.branch(['-m', 'master', backupBranch]);
debug(`Alter master-Branch wurde in ${backupBranch} umbenannt.`);
}
// Neuer master-Branch
await git.checkout(['-b', 'master']);
debug('Neuer master-Branch erstellt und ausgecheckt.');
}
await git.add(['-A']);
debug('Alle Änderungen gestaged.');
await git.commit(message || 'test');
debug('Commit erfolgreich erstellt.');
await git.push(['-u', 'origin', 'master']);
debug('Push auf origin/master erfolgreich.');
return { success: true };
} catch (err) {
debug(`FEHLER beim Commit: ${err.message}`);
return { success: false, error: err.message };
}
});
});
// clean up on exit
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit();
});