Files
Git-Manager-Gui/main.js
2026-02-02 19:00:59 +00:00

1636 lines
59 KiB
JavaScript

// main.js — Main-Process with concurrent folder upload/download, progress events, and temp-dir cleanup
const { app, BrowserWindow, ipcMain, dialog, Menu, nativeImage } = require('electron');
const ppath = require('path');
const fs = require('fs');
const os = require('os');
const crypto = require('crypto');
const { execSync } = require('child_process');
const https = require('https');
const http = require('http');
const Updater = require('./updater.js'); // Auto-Updater
let updater = null;
const {
createRepoGitHub,
createRepoGitea,
listGiteaRepos,
getGiteaRepoContents,
getGiteaFileContent,
uploadGiteaFile,
getGiteaCommits,
getGiteaCommit,
getGiteaCommitDiff,
getGiteaCommitFiles,
searchGiteaCommits,
getGiteaBranches,
listGiteaReleases,
getGiteaRelease,
createGiteaRelease,
editGiteaRelease,
deleteGiteaRelease,
uploadReleaseAsset,
deleteReleaseAsset
} = require('./src/git/apiHandler.js');
const { initRepo, commitAndPush, getBranches, getCommitLogs } = require('./src/git/gitHandler.js');
// NOTE: credentials/data location is computed via getDataDir() to avoid calling app.getPath before ready
function getDataDir() {
try {
return ppath.join(app.getPath('userData'), 'data');
} catch (e) {
// Fallback: use __dirname/data (only if app.getPath not available)
return ppath.join(__dirname, 'data');
}
}
function getCredentialsFilePath() {
return ppath.join(getDataDir(), 'credentials.json');
}
const ALGORITHM = 'aes-256-cbc';
const SECRET_KEY = crypto.scryptSync('SuperSecretKey123!', 'salt', 32);
const IV = Buffer.alloc(16, 0);
// default concurrency for parallel uploads/downloads
const DEFAULT_CONCURRENCY = 4;
// temp drag cleanup delay (ms)
const TMP_CLEANUP_MS = 20_000;
/* -----------------------------
Utilities for safe filesystem ops
----------------------------- */
function ensureDir(dirPath) {
try {
if (fs.existsSync(dirPath)) {
if (!fs.statSync(dirPath).isDirectory()) {
// If a file exists where we expect a directory, remove it
fs.unlinkSync(dirPath);
}
}
fs.mkdirSync(dirPath, { recursive: true });
} catch (e) {
// Re-throw with clearer message
throw new Error(`ensureDir failed for ${dirPath}: ${e && e.message ? e.message : e}`);
}
}
/**
* Create a safe, unique temporary directory under os.tmpdir().
* If an entry exists at that path and it's a file, it will be removed.
* If a directory exists it will be removed and recreated to ensure a clean state.
* Returns the created directory path.
*
* baseName should be a short string (no path separators). We append a timestamp to reduce collisions.
*/
function getSafeTmpDir(baseName) {
const safeBase = (baseName || 'tmp').replace(/[<>:"/\\|?*\x00-\x1F]/g, '_').substring(0, 64);
// Basis-Temp-Ordner für interne Verwaltung
const internalBase = ppath.join(os.tmpdir(), 'gitea-drag');
if (!fs.existsSync(internalBase)) fs.mkdirSync(internalBase, { recursive: true });
// Eindeutiger Unterordner, um Kollisionen zu vermeiden
const uniqueSub = crypto.randomBytes(8).toString('hex');
const internalDir = ppath.join(internalBase, uniqueSub);
fs.mkdirSync(internalDir, { recursive: true });
// Sichtbarer Ordnername = safeBase
const finalDir = ppath.join(internalDir, safeBase);
fs.mkdirSync(finalDir, { recursive: true });
return finalDir;
}
/* -----------------------------
app / window
----------------------------- */
function createWindow() {
// Entfernt das Menü (File, Edit, View...) komplett
Menu.setApplicationMenu(null);
const win = new BrowserWindow({
width: 1200,
height: 820,
webPreferences: {
preload: ppath.join(__dirname, 'preload.js'),
nodeIntegration: false,
contextIsolation: true
}
});
win.loadFile(ppath.join(__dirname, 'renderer', 'index.html'));
// win.webContents.openDevTools();
}
app.whenReady().then(() => {
createWindow();
app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) createWindow(); });
});
app.on('window-all-closed', () => { if (process.platform !== 'darwin') app.quit(); });
/* -----------------------------
Helper: read credentials
----------------------------- */
function readCredentials() {
try {
const CREDENTIALS_FILE = getCredentialsFilePath();
if (!fs.existsSync(CREDENTIALS_FILE)) return null;
const encrypted = fs.readFileSync(CREDENTIALS_FILE);
const decipher = crypto.createDecipheriv(ALGORITHM, SECRET_KEY, IV);
const decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()]);
return JSON.parse(decrypted.toString('utf8'));
} catch (e) {
console.error('readCredentials error', e);
return null;
}
}
/* -----------------------------
Generic concurrency runner (worker pool)
----------------------------- */
async function runLimited(tasks, concurrency = DEFAULT_CONCURRENCY, onProgress = null) {
const results = new Array(tasks.length);
let index = 0;
let processed = 0;
const total = tasks.length;
async function worker() {
while (true) {
const i = index++;
if (i >= tasks.length) return;
try {
const r = await tasks[i]();
results[i] = { ok: true, result: r };
} catch (e) {
results[i] = { ok: false, error: String(e) };
} finally {
processed++;
if (onProgress) {
try { onProgress(processed, total); } catch (_) {}
}
}
}
}
const workers = Array(Math.max(1, Math.min(concurrency, tasks.length))).fill(0).map(() => worker());
await Promise.all(workers);
return results;
}
/* -----------------------------
Basic IPC handlers
----------------------------- */
ipcMain.handle('select-folder', async () => {
const result = await dialog.showOpenDialog({ properties: ['openDirectory'] });
return result.canceled ? null : result.filePaths[0];
});
ipcMain.handle('select-file', async () => {
const result = await dialog.showOpenDialog({ properties: ['openFile', 'multiSelections'] });
if (result.canceled) return { ok: false, files: [] };
return { ok: true, files: result.filePaths };
});
ipcMain.handle('save-credentials', async (event, data) => {
try {
const DATA_DIR = getDataDir();
const CREDENTIALS_FILE = getCredentialsFilePath();
ensureDir(DATA_DIR); // robust gegen ENOTDIR
const json = JSON.stringify(data);
const cipher = crypto.createCipheriv(ALGORITHM, SECRET_KEY, IV);
const encrypted = Buffer.concat([cipher.update(json, 'utf8'), cipher.final()]);
fs.writeFileSync(CREDENTIALS_FILE, encrypted);
return { ok: true };
} catch (e) {
console.error('save-credentials error', e);
return { ok: false, error: String(e) };
}
});
ipcMain.handle('load-credentials', async () => {
try {
return readCredentials();
} catch (e) {
console.error('load-credentials', e);
return null;
}
});
/* -----------------------------
Repo & git handlers
----------------------------- */
ipcMain.handle('create-repo', async (event, data) => {
try {
const credentials = readCredentials();
if (!credentials) return { ok: false, error: 'no-credentials' };
if (data.platform === 'github') {
const repo = await createRepoGitHub({
name: data.name,
token: credentials.githubToken,
auto_init: data.autoInit || true,
license: data.license || '',
private: data.private || false
});
return { ok: true, repo };
} else if (data.platform === 'gitea') {
const repo = await createRepoGitea({
name: data.name,
token: credentials.giteaToken,
url: credentials.giteaURL,
auto_init: data.autoInit || true,
license: data.license || '',
private: data.private || false
});
return { ok: true, repo };
} else return { ok: false, error: 'unknown-platform' };
} catch (e) {
console.error('create-repo error', e);
return { ok: false, error: String(e) };
}
});
ipcMain.handle('push-project', async (event, data) => {
try {
if (!data.folder || !fs.existsSync(data.folder)) return { ok: false, error: 'folder-not-found' };
// Prüfen, ob der lokale Branch 'master' heißt und in 'main' umbenennen
try {
const currentBranch = execSync('git branch --show-current', { cwd: data.folder, stdio: 'pipe', encoding: 'utf-8' }).trim();
console.log('Current local branch:', currentBranch);
if (currentBranch === 'master') {
console.log('Attempting to rename master to main...');
try {
execSync('git branch -m master main', { cwd: data.folder, stdio: 'inherit' });
console.log('Successfully renamed local branch master to main');
} catch (e) {
console.warn('Failed to rename branch (maybe main already exists)', e.message);
}
}
} catch (e) {
console.warn('Could not check local branch (maybe not a git repo yet)', e.message);
}
// 1. Git initialisieren, adden und commiten
await initRepo(data.folder);
// 2. Prüfen, ob ein 'origin' Remote existiert
let remoteUrl = '';
try {
remoteUrl = execSync('git remote get-url origin', { cwd: data.folder, stdio: 'pipe', encoding: 'utf-8' }).trim();
} catch (e) {
if (data.repoName && data.platform) {
const creds = readCredentials();
if (!creds) return { ok: false, error: 'credentials-missing-for-remote' };
const parts = data.repoName.split('/');
if (parts.length === 2) {
const owner = parts[0];
const repo = parts[1];
let constructedUrl = '';
if (data.platform === 'gitea' && creds.giteaURL) {
try {
const urlObj = new URL(creds.giteaURL);
constructedUrl = `${urlObj.protocol}//${creds.giteaToken}@${urlObj.host}/${owner}/${repo}.git`;
} catch (err) { console.error('Invalid Gitea URL', err); }
} else if (data.platform === 'github' && creds.githubToken) {
constructedUrl = `https://${creds.githubToken}@github.com/${owner}/${repo}.git`;
}
if (constructedUrl) {
try {
execSync(`git remote add origin "${constructedUrl}"`, { cwd: data.folder, stdio: 'inherit' });
console.log(`Remote origin added: ${constructedUrl}`);
} catch (err) {
return { ok: false, error: 'Failed to add remote origin: ' + String(err) };
}
} else {
return { ok: false, error: 'Could not construct remote URL.' };
}
} else {
return { ok: false, error: 'Use format Owner/RepoName.' };
}
}
}
// 3. Pushen (nutze 'main')
const progressCb = percent => { try { event.sender.send('push-progress', percent); } catch (_) {} };
await commitAndPush(data.folder, 'main', 'Update from Git Manager GUI', progressCb);
return { ok: true };
} catch (e) {
console.error('push-project error', e);
return { ok: false, error: String(e) };
}
});
ipcMain.handle('getBranches', async (event, data) => {
try {
const branches = await getBranches(data.folder);
// Sortieren, damit 'main' oben steht
branches.sort((a, b) => (a === 'main' ? -1 : b === 'main' ?1 : 0));
return { ok: true, branches };
} catch (e) {
console.error('getBranches error', e);
return { ok: false, error: String(e), branches: [] };
}
});
ipcMain.handle('getCommitLogs', async (event, data) => {
try {
const logs = await getCommitLogs(data.folder, data.count || 50);
return { ok: true, logs };
} catch (e) {
console.error('getCommitLogs error', e);
return { ok: false, error: String(e), logs: [] };
}
});
/* ----------------------------------------------------------------
Neue/kompatible Handler: 'get-commits' (Renderer verwendet ggf. diesen)
- Unterstützt:
1) Lokale Git-Logs via data.folder -> getCommitLogs
2) Gitea Commits via owner+repo -> getGiteaCommits
---------------------------------------------------------------- */
ipcMain.handle('get-commits', async (event, data) => {
try {
// 1) Lokale Git-Logs (folder vorhanden)
if (data && data.folder) {
const logs = await getCommitLogs(data.folder, data.count || 50);
return { ok: true, logs };
}
// 2) Gitea-Commits (owner/repo vorhanden)
if (data && data.owner && data.repo) {
const credentials = readCredentials();
const token = (data && data.token) || (credentials && credentials.giteaToken);
const url = (data && data.url) || (credentials && credentials.giteaURL);
if (!token || !url) return { ok: false, error: 'missing-token-or-url' };
// map optional params
const page = data.page || 1;
const limit = data.limit || 50;
const sha = data.sha; // optional branch/sha filter
const commits = await getGiteaCommits({
token,
url,
owner: data.owner,
repo: data.repo,
page,
limit,
sha
});
return { ok: true, commits };
}
return { ok: false, error: 'invalid-parameters-for-get-commits' };
} catch (e) {
console.error('get-commits error', e);
return { ok: false, error: String(e) };
}
});
/* -----------------------------
Local file tree functions
----------------------------- */
function buildTree(dirPath, options = {}) {
const { exclude = ['node_modules'], maxDepth = 10 } = options;
function walk(currentPath, depth) {
let stat;
try { stat = fs.statSync(currentPath); } catch (e) { return null; }
const name = ppath.basename(currentPath);
const node = { name, path: currentPath, isDirectory: stat.isDirectory(), children: [], depth };
if (node.isDirectory) {
if (exclude.some(ex => currentPath.split(ppath.sep).includes(ex))) return null;
if (depth >= maxDepth) return node;
let items = [];
try { items = fs.readdirSync(currentPath); } catch (e) { return node; }
items.sort((a, b) => a.localeCompare(b, undefined, { sensitivity: 'base' }));
for (const it of items) {
const childPath = ppath.join(currentPath, it);
const child = walk(childPath, depth +1);
if (child) node.children.push(child);
}
}
return node;
}
const roots = [];
const list = fs.readdirSync(dirPath).sort((a, b) => a.localeCompare(b, undefined, { sensitivity: 'base' }));
for (const entry of list) {
const full = ppath.join(dirPath, entry);
const n = walk(full, 0);
if (n) roots.push(n);
}
return roots;
}
ipcMain.handle('getFileTree', async (event, data) => {
try {
const folder = data && data.folder;
if (!folder || !fs.existsSync(folder)) return { ok: false, error: 'folder-not-found' };
const opts = { exclude: (data && data.exclude) || ['node_modules'], maxDepth: (data && data.maxDepth) || 10 };
const tree = buildTree(folder, opts);
return { ok: true, tree };
} catch (e) {
console.error('getFileTree error', e);
return { ok: false, error: String(e) };
}
});
ipcMain.handle('readFile', async (event, data) => {
try {
if (!data || !data.path || !fs.existsSync(data.path)) return { ok: false, error: 'file-not-found' };
const content = fs.readFileSync(data.path, 'utf8');
return { ok: true, content };
} catch (e) {
console.error('readFile error', e);
return { ok: false, error: String(e) };
}
});
ipcMain.handle('writeFile', async (event, data) => {
try {
if (!data || !data.path) return { ok: false, error: 'invalid-path' };
const dir = ppath.dirname(data.path);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
fs.writeFileSync(data.path, data.content || '', 'utf8');
return { ok: true };
} catch (e) {
console.error('writeFile error', e);
return { ok: false, error: String(e) };
}
});
ipcMain.handle('deleteFile', async (event, data) => {
try {
if (!data || !data.path || !fs.existsSync(data.path)) return { ok: false, error: 'file-not-found' };
fs.rmSync(data.path, { recursive: true, force: true });
return { ok: true };
} catch (e) {
console.error('deleteFile error', e);
return { ok: false, error: String(e) };
}
});
/* -----------------------------
Gitea: list repos, contents, file content
----------------------------- */
ipcMain.handle('list-gitea-repos', async (event, data) => {
try {
const credentials = readCredentials();
const token = (data && data.token) || (credentials && credentials.giteaToken);
const url = (data && data.url) || (credentials && credentials.giteaURL);
if (!token || !url) return { ok: false, error: 'missing-token-or-url' };
const repos = await listGiteaRepos({ token, url });
return { ok: true, repos };
} catch (e) {
console.error('list-gitea-repos error', e);
return { ok: false, error: String(e) };
}
});
ipcMain.handle('get-gitea-repo-contents', async (event, data) => {
try {
const credentials = readCredentials();
const token = (data && data.token) || (credentials && credentials.giteaToken);
const url = (data && data.url) || (credentials && credentials.giteaURL);
if (!token || !url) return { ok: false, error: 'missing-token-or-url' };
const owner = data.owner;
const repo = data.repo;
const p = data.path || '';
// FIXED: Pass data.ref directly to apiHandler without forcing 'main'
// This allows apiHandler.js to try ['main', 'master'] if no ref is passed
const ref = data.ref;
const items = await getGiteaRepoContents({ token, url, owner, repo, path: p, ref });
return { ok: true, items };
} catch (e) {
console.error('get-gitea-repo-contents error', e);
return { ok: false, error: String(e) };
}
});
ipcMain.handle('get-gitea-file-content', async (event, data) => {
try {
const credentials = readCredentials();
const token = (data && data.token) || (credentials && credentials.giteaToken);
const url = (data && data.url) || (credentials && credentials.giteaURL);
if (!token || !url) return { ok: false, error: 'missing-token-or-url' };
const owner = data.owner;
const repo = data.repo;
const p = data.path;
// FIXED: Pass data.ref directly
const ref = data.ref;
const content = await getGiteaFileContent({ token, url, owner, repo, path: p, ref });
return { ok: true, content };
} catch (e) {
console.error('get-gitea-file-content error', e);
return { ok: false, error: String(e) };
}
});
// Alias für Editor: read-gitea-file (für Text und Bilder)
ipcMain.handle('read-gitea-file', async (event, data) => {
try {
const credentials = readCredentials();
const token = (data && data.token) || (credentials && credentials.giteaToken);
const url = (data && data.url) || (credentials && credentials.giteaURL);
if (!token || !url) {
console.error('Missing token or URL');
return { ok: false, error: 'missing-token-or-url' };
}
const owner = data.owner;
const repo = data.repo;
const p = data.path;
const ref = data.ref || 'main';
console.log(`read-gitea-file: ${owner}/${repo}/${p} (ref: ${ref})`);
// Prüfe ob es eine Bilddatei ist
const isImage = /\.(png|jpg|jpeg|gif|webp|svg)$/i.test(p);
if (isImage) {
// Für Bilder: Lade als Base64
console.log('Loading as image (Base64)');
const apiUrl = `${url}/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/raw/${encodeURIComponent(p)}?ref=${ref}`;
console.log('Image URL:', apiUrl);
return new Promise((resolve) => {
try {
const protocol = url.startsWith('https') ? https : http;
protocol.get(apiUrl, {
headers: {
'Authorization': `token ${token}`,
'User-Agent': 'Git-Manager-GUI'
}
}, (res) => {
console.log(`Image response status: ${res.statusCode}`);
if (res.statusCode !== 200) {
resolve({ ok: false, error: `HTTP ${res.statusCode}` });
return;
}
const chunks = [];
res.on('data', chunk => chunks.push(chunk));
res.on('end', () => {
try {
const buffer = Buffer.concat(chunks);
const base64 = buffer.toString('base64');
console.log(`Image loaded: ${base64.length} bytes`);
resolve({ ok: true, content: base64 });
} catch (e) {
console.error('Base64 conversion error:', e.message);
resolve({ ok: false, error: String(e) });
}
});
}).on('error', (e) => {
console.error('Image HTTP error:', e.message);
resolve({ ok: false, error: String(e) });
});
} catch (e) {
console.error('Image load try error:', e.message);
resolve({ ok: false, error: String(e) });
}
});
} else {
// Für Text: Nutze normale Funktion
console.log('Loading as text file');
const content = await getGiteaFileContent({ token, url, owner, repo, path: p, ref });
console.log(`Text file loaded: ${content.length} chars`);
return { ok: true, content };
}
} catch (e) {
console.error('read-gitea-file error:', e.message, e.stack);
return { ok: false, error: String(e) };
}
});
ipcMain.handle('upload-gitea-file', async (event, data) => {
try {
const credentials = readCredentials();
const token = (data && data.token) || (credentials && credentials.giteaToken);
const url = (data && data.url) || (credentials && credentials.giteaURL);
if (!token || !url) return { ok: false, error: 'missing-token-or-url' };
const owner = data.owner;
const repo = data.repo;
// destPath is the target folder in the repo
const destPath = (data.destPath || '').replace(/^\//, '').replace(/\/$/, '');
// FIXED: Konvertiere 'master' zu 'main' (Upload should generally target main)
let branch = data.branch || 'main';
if (branch === 'master') branch = 'main';
const message = data.message || 'Upload via Git Manager GUI';
const localFiles = Array.isArray(data.localPath) ? data.localPath : (data.localPath ? [data.localPath] : []);
const results = [];
for (const localFile of localFiles) {
if (!fs.existsSync(localFile)) {
results.push({ file: localFile, ok: false, error: 'local-file-not-found' });
continue;
}
const raw = fs.readFileSync(localFile);
const base64 = raw.toString('base64');
const fileName = ppath.basename(localFile);
// FIXED: Handle destPath correctly. Always combine destPath + filename.
let targetPath;
if (destPath && destPath.length > 0) {
targetPath = `${destPath}/${fileName}`;
} else {
targetPath = fileName;
}
try {
const uploaded = await uploadGiteaFile({
token,
url,
owner,
repo,
path: targetPath,
contentBase64: base64,
message: `${message} - ${fileName}`,
branch
});
results.push({ file: localFile, ok: true, uploaded });
} catch (e) {
console.error('upload error for', localFile, e);
results.push({ file: localFile, ok: false, error: String(e) });
}
}
return { ok: true, results };
} catch (e) {
console.error('upload-gitea-file error', e);
return { ok: false, error: String(e) };
}
});
// Alias für Editor: write-gitea-file
ipcMain.handle('write-gitea-file', async (event, data) => {
try {
const credentials = readCredentials();
const token = (data && data.token) || (credentials && credentials.giteaToken);
const url = (data && data.url) || (credentials && credentials.giteaURL);
if (!token || !url) return { ok: false, error: 'missing-token-or-url' };
const owner = data.owner;
const repo = data.repo;
const path = data.path;
const content = data.content || '';
const ref = data.ref || 'main';
// Konvertiere Content zu Base64
const base64 = Buffer.from(content, 'utf8').toString('base64');
const uploaded = await uploadGiteaFile({
token,
url,
owner,
repo,
path,
contentBase64: base64,
message: `Edit ${path} via Git Manager GUI`,
branch: ref
});
return { ok: true, uploaded };
} catch (e) {
console.error('write-gitea-file error', e);
return { ok: false, error: String(e) };
}
});
ipcMain.handle('upload-local-folder-to-gitea', async (event, data) => {
try {
const credentials = readCredentials();
const token = (data && data.token) || (credentials && credentials.giteaToken);
const url = (data && data.url) || (credentials && credentials.giteaURL);
if (!token || !url) return { ok: false, error: 'missing-token-or-url' };
const localFolder = data.localFolder;
const owner = data.owner;
const repo = data.repo;
// destPath is the target directory in the repo
const destPath = (data.destPath || '').replace(/^\//, '').replace(/\/$/, '');
// FIXED: Konvertiere 'master' zu 'main'
let branch = data.branch || 'main';
if (branch === 'master') branch = 'main';
const messagePrefix = data.messagePrefix || 'Upload folder via GUI';
const concurrency = data.concurrency || DEFAULT_CONCURRENCY;
if (!localFolder || !fs.existsSync(localFolder)) return { ok: false, error: 'local-folder-not-found' };
const items = [];
const folderName = ppath.basename(localFolder);
// FIXED EXCLUDE LIST: Filter out .git, node_modules etc.
const excludeList = ['.git', 'node_modules', '.DS_Store', 'thumbs.db', '.vscode', '.idea'];
(function walk(dir) {
const entries = fs.readdirSync(dir);
for (const entry of entries) {
if (excludeList.includes(entry)) continue; // Skip excluded folders/files
const full = ppath.join(dir, entry);
let stat;
try {
stat = fs.statSync(full);
} catch(e) { continue; } // Skip unreadable files
if (stat.isDirectory()) {
walk(full);
} else if (stat.isFile()) {
const rel = ppath.relative(localFolder, full).split(ppath.sep).join('/');
// FIXED: Respect folder structure. Result: destPath/folderName/rel
let targetPath;
if (destPath && destPath.length > 0) {
targetPath = `${destPath}/${folderName}/${rel}`;
} else {
targetPath = `${folderName}/${rel}`;
}
items.push({ localFile: full, targetPath });
}
}
})(localFolder);
const total = items.length;
if (total === 0) return { ok: true, results: [] };
const tasks = items.map(it => async () => {
const raw = fs.readFileSync(it.localFile);
const base64 = raw.toString('base64');
return uploadGiteaFile({
token,
url,
owner,
repo,
path: it.targetPath,
contentBase64: base64,
message: `${messagePrefix} - ${ppath.basename(it.localFile)}`,
branch
});
});
const onProgress = (processed, t) => {
try { event.sender.send('folder-upload-progress', { processed, total: t, percent: Math.round((processed / t) * 100) }); } catch (_) {}
};
const results = await runLimited(tasks, concurrency, (proc, t) => onProgress(proc, total));
return { ok: true, results };
} catch (e) {
console.error('upload-local-folder-to-gitea error', e);
return { ok: false, error: String(e) };
}
});
ipcMain.handle('download-gitea-file', async (event, data) => {
try {
const credentials = readCredentials();
const token = (data && data.token) || (credentials && credentials.giteaToken);
const url = (data && data.url) || (credentials && credentials.giteaURL);
if (!token || !url) return { ok: false, error: 'missing-token-or-url' };
const owner = data.owner;
const repo = data.repo;
const filePath = data.path;
if (!owner || !repo || !filePath) return { ok: false, error: 'missing-owner-repo-or-path' };
const content = await getGiteaFileContent({ token, url, owner, repo, path: filePath, ref: 'main' });
const save = await dialog.showSaveDialog({ defaultPath: ppath.basename(filePath) });
if (save.canceled || !save.filePath) return { ok: false, error: 'save-canceled' };
fs.writeFileSync(save.filePath, content, 'utf8');
return { ok: true, savedTo: save.filePath };
} catch (e) {
console.error('download-gitea-file error', e);
return { ok: false, error: String(e) };
}
});
ipcMain.handle('download-gitea-folder', async (event, data) => {
try {
const credentials = readCredentials();
const token = (data && data.token) || (credentials && credentials.giteaToken);
const url = (data && data.url) || (credentials && credentials.giteaURL);
if (!token || !url) return { ok: false, error: 'missing-token-or-url' };
const owner = data.owner;
const repo = data.repo;
const remotePath = (data.path || '').replace(/^\/+/, '').replace(/\/+$/, '');
const concurrency = data.concurrency || DEFAULT_CONCURRENCY;
const result = await dialog.showOpenDialog({ properties: ['openDirectory', 'createDirectory'] });
if (result.canceled || !result.filePaths || result.filePaths.length === 0) return { ok: false, error: 'save-canceled' };
const destBase = result.filePaths[0];
const allFiles = [];
async function gather(pathInRepo) {
const items = await getGiteaRepoContents({ token, url, owner, repo, path: pathInRepo, ref: 'main' });
for (const item of items) {
if (item.type === 'dir') await gather(item.path);
else if (item.type === 'file') allFiles.push(item.path);
}
}
await gather(remotePath || '');
const total = allFiles.length;
if (total === 0) return { ok: true, savedTo: destBase, files: [] };
const tasks = allFiles.map(remoteFile => async () => {
const content = await getGiteaFileContent({ token, url, owner, repo, path: remoteFile, ref: 'main' });
const localPath = ppath.join(destBase, remoteFile);
fs.mkdirSync(ppath.dirname(localPath), { recursive: true });
fs.writeFileSync(localPath, content, 'utf8');
return localPath;
});
const onProgress = (processed, t) => {
try { event.sender.send('folder-download-progress', { processed, total: t, percent: Math.round((processed / t) * 100) }); } catch (_) {}
};
const results = await runLimited(tasks, concurrency, (proc, t) => onProgress(proc, total));
const savedFiles = results.filter(r => r.ok).map(r => r.result);
return { ok: true, savedTo: destBase, files: savedFiles };
} catch (e) {
console.error('download-gitea-folder error', e);
return { ok: false, error: String(e) };
}
});
/* -----------------------------
prepare-download-drag (robust)
- stellt sicher, dass alle Dateien komplett geschrieben sind
- erkennt Base64 vs UTF-8 und schreibt als Buffer wenn nötig
- nutzt getSafeTmpDir() (siehe oben in deiner main.js)
----------------------------- */
function isBase64Like(str) {
if (typeof str !== 'string') return false;
// Strip whitespace/newlines
const s = str.replace(/\s+/g, '');
if (s.length === 0) return false;
// Base64 valid chars + padding
if (!/^[A-Za-z0-9+/]*={0,2}$/.test(s)) return false;
// length must be multiple of 4 (except maybe line breaks removed)
if (s.length % 4 !== 0) return false;
try {
// Round-trip check (cheap and practical)
const decoded = Buffer.from(s, 'base64');
return decoded.toString('base64') === s;
} catch (e) {
return false;
}
}
ipcMain.handle('prepare-download-drag', async (event, data) => {
try {
const credentials = readCredentials();
const token = (data && data.token) || (credentials && credentials.giteaToken);
const url = (data && data.url) || (credentials && credentials.giteaURL);
if (!token || !url) return { ok: false, error: 'missing-token-or-url' };
const owner = data.owner;
const repo = data.repo;
const remotePath = (data.path || '').replace(/^\/+/, '').replace(/\/+$/, '');
// Create a unique temp directory (guarantees clean state)
const tmpBase = getSafeTmpDir(repo || 'gitea-repo');
// Gather list of files (recursive)
const allFiles = [];
async function gather(pathInRepo) {
const items = await getGiteaRepoContents({ token, url, owner, repo, path: pathInRepo, ref: data.ref || 'main' });
for (const item of items || []) {
if (item.type === 'dir') await gather(item.path);
else if (item.type === 'file') allFiles.push(item.path);
}
}
await gather(remotePath || '');
// If no files, return early (still provide empty dir)
if (allFiles.length === 0) {
// schedule cleanup
setTimeout(() => { try { if (fs.existsSync(tmpBase)) fs.rmSync(tmpBase, { recursive: true, force: true }); } catch (_) {} }, TMP_CLEANUP_MS);
return { ok: true, tempPath: tmpBase, files: [] };
}
// Download files sequentially or with limited concurrency:
const tasks = allFiles.map(remoteFile => async () => {
const content = await getGiteaFileContent({ token, url, owner, repo, path: remoteFile, ref: data.ref || 'main' });
const localPath = ppath.join(tmpBase, remoteFile);
ensureDir(ppath.dirname(localPath));
// Decide how to write: if Buffer already, write directly. If string, try base64 detection
if (Buffer.isBuffer(content)) {
fs.writeFileSync(localPath, content);
} else if (typeof content === 'string') {
if (isBase64Like(content)) {
const buf = Buffer.from(content, 'base64');
fs.writeFileSync(localPath, buf);
} else {
// treat as utf8 text
fs.writeFileSync(localPath, content, 'utf8');
}
} else {
// fallback: convert to string
fs.writeFileSync(localPath, String(content), 'utf8');
}
return localPath;
});
// runLimited ensures concurrency and waits for all writes to finish
const results = await runLimited(tasks, data.concurrency || DEFAULT_CONCURRENCY);
// verify at least one successful file
const successFiles = results.filter(r => r.ok).map(r => r.result);
if (successFiles.length === 0) {
// cleanup on complete failure
try { if (fs.existsSync(tmpBase)) fs.rmSync(tmpBase, { recursive: true, force: true }); } catch (_) {}
return { ok: false, error: 'no-files-downloaded' };
}
// give renderer the temp dir (renderer should then call 'ondragstart' with the folder path)
// schedule cleanup after delay to keep files available for drag & drop
setTimeout(() => { try { if (fs.existsSync(tmpBase)) fs.rmSync(tmpBase, { recursive: true, force: true }); } catch (_) {} }, TMP_CLEANUP_MS);
return { ok: true, tempPath: tmpBase, files: successFiles };
} catch (e) {
console.error('prepare-download-drag error', e);
return { ok: false, error: String(e) };
}
});
/* -----------------------------
ondragstart (no change to API but more defensive)
- expects renderer to call window.electronAPI.ondragStart(tempPath)
----------------------------- */
ipcMain.on('ondragstart', async (event, filePath) => {
try {
if (!filePath || !fs.existsSync(filePath)) {
console.warn('ondragstart: path missing or not exists:', filePath);
return;
}
// Prefer folder icon when dragging a directory
let icon = nativeImage.createEmpty();
try {
// ask platform for file icon; large size for clearer drag icon
icon = await app.getFileIcon(filePath, { size: 'large' });
} catch (e) {
// ignore, keep empty icon
}
// startDrag accepts { file } where file can be a directory
try {
event.sender.startDrag({ file: filePath, icon });
} catch (e) {
// some platforms may require a single file — if folder fails, try to drop a placeholder file
console.error('startDrag failed for', filePath, e);
}
} catch (e) {
console.error('ondragstart error', e);
}
});
ipcMain.handle('delete-gitea-repo', async (event, data) => {
try {
const credentials = readCredentials();
const token = (data && data.token) || (credentials && credentials.giteaToken);
const urlStr = (data && data.url) || (credentials && credentials.giteaURL);
if (!token || !urlStr) return { ok: false, error: 'missing-token-or-url' };
const owner = data.owner;
const repo = data.repo;
const urlObj = new URL(urlStr);
const protocol = urlObj.protocol === 'https:' ? https : http;
const deletePath = `/api/v1/repos/${owner}/${repo}`;
return new Promise((resolve) => {
const options = {
hostname: urlObj.hostname,
port: urlObj.port || (urlObj.protocol === 'https:' ? 443 : 80),
path: deletePath,
method: 'DELETE',
headers: { 'Authorization': `token ${token}`, 'Content-Type': 'application/json' }
};
const req = protocol.request(options, (res) => {
let body = '';
res.on('data', chunk => body += chunk);
res.on('end', () => {
if (res.statusCode >= 200 && res.statusCode < 300) resolve({ ok: true });
else resolve({ ok: false, error: `HTTP ${res.statusCode}: ${body}` });
});
});
req.on('error', (e) => resolve({ ok: false, error: String(e) }));
req.end();
});
} catch (e) {
console.error('delete-gitea-repo error', e);
return { ok: false, error: String(e) };
}
});
ipcMain.handle('upload-and-push', async (event, data) => {
try {
if (!data || !data.localFolder || !fs.existsSync(data.localFolder)) return { ok: false, error: 'local-folder-not-found' };
const credentials = readCredentials();
const token = (data && data.token) || (credentials && credentials.giteaToken);
const giteaUrl = (data && data.url) || (credentials && credentials.giteaURL);
const owner = data.owner;
const repo = data.repo;
// FIXED: Konvertiere 'master' zu 'main'
let branch = data.branch || 'main';
if (branch === 'master') branch = 'main';
const cloneUrl = data.cloneUrl || null;
const destPath = (data.destPath || '').replace(/^\//, '').replace(/\/$/, '');
if (!owner || !repo) return { ok: false, error: 'missing-owner-or-repo' };
// Prüfen ob es eine Datei oder ein Ordner ist
const stat = fs.statSync(data.localFolder);
const isFile = stat.isFile();
const isDirectory = stat.isDirectory();
// --- FALL 1: Einzelne Datei ---
if (isFile) {
console.log('Detected single file. Attempting API upload...');
const raw = fs.readFileSync(data.localFolder);
const base64 = raw.toString('base64');
const fileName = ppath.basename(data.localFolder);
// Pfad-Logik für Ziel
let targetPath;
if (destPath && destPath.length > 0) {
targetPath = `${destPath}/${fileName}`;
} else {
targetPath = fileName;
}
try {
// VERSUCH 1: API Upload
const result = await uploadGiteaFile({
token,
url: giteaUrl,
owner,
repo,
path: targetPath,
contentBase64: base64,
message: `Upload ${fileName} via GUI`,
branch
});
return { ok: true, usedGit: false, singleFile: true, uploaded: result };
} catch (e) {
console.error('Single file API upload error:', e.message);
// FALLBACK: Wenn API fehlschlägt (z.B. 422 SHA Fehler), nutze Git
// Git löst das "Überschreiben"-Problem zuverlässig.
console.log('Falling back to Git for single file...');
let finalCloneUrl = cloneUrl;
if (!finalCloneUrl && giteaUrl) {
try {
const base = giteaUrl.replace(/\/$/, '');
const urlObj = new URL(base);
finalCloneUrl = `${urlObj.protocol}//${urlObj.host}/${owner}/${repo}.git`;
} catch (err) { console.error('Invalid URL', err); }
}
if (!finalCloneUrl) return { ok: false, error: 'Cannot fallback to Git: No Clone URL' };
// Git-Workflow für einzelne Datei simulieren:
// 1. Temp Ordner erstellen
// 2. Repo klonen
// 3. Datei in Ordner kopieren
// 4. Commit & Push
let authClone = finalCloneUrl;
try {
const urlObj = new URL(finalCloneUrl);
if (token && (urlObj.protocol.startsWith('http'))) {
urlObj.username = encodeURIComponent(token);
authClone = urlObj.toString();
}
} catch (err) {}
const tmpDir = getSafeTmpDir(`git-push-file-${owner}-${repo}`);
try {
execSync(`git clone --depth 1 --branch ${branch} "${authClone}" "${tmpDir}"`, { stdio: 'inherit' });
// Zielort im Repo bestimmen
let destDirInRepo = tmpDir;
if (destPath) {
destDirInRepo = ppath.join(tmpDir, destPath.split('/').join(ppath.sep));
ensureDir(destDirInRepo);
}
const finalFileDest = ppath.join(destDirInRepo, fileName);
// Datei kopieren
fs.copyFileSync(data.localFolder, finalFileDest);
// Git Befehle
execSync(`git -C "${tmpDir}" add .`, { stdio: 'inherit' });
try { execSync(`git -C "${tmpDir}" commit -m "Upload file ${fileName} via GUI"`, { stdio: 'inherit' }); } catch (_) {}
execSync(`git -C "${tmpDir}" push origin ${branch}`, { stdio: 'inherit' });
setTimeout(() => { try { if (fs.existsSync(tmpDir)) fs.rmSync(tmpDir, { recursive: true, force: true }); } catch (_) {} }, 5_000);
return { ok: true, usedGit: true, singleFile: true, msg: 'Uploaded via Git (Fallback)' };
} catch (gitErr) {
console.error('Git Fallback failed:', gitErr);
if (fs.existsSync(tmpDir)) fs.rmSync(tmpDir, { recursive: true, force: true });
return { ok: false, error: `API failed: ${e.message}. Git fallback failed: ${String(gitErr)}` };
}
}
}
// --- FALL 2: Ordner (Normale Git-Logik) ---
if (!isDirectory) {
return { ok: false, error: 'Path is neither file nor directory' };
}
let gitAvailable = true;
try { execSync('git --version', { stdio: 'ignore' }); } catch (e) { gitAvailable = false; }
let finalCloneUrl = cloneUrl;
if (!finalCloneUrl && giteaUrl) {
try {
const base = giteaUrl.replace(/\/$/, '');
const urlObj = new URL(base);
finalCloneUrl = `${urlObj.protocol}//${urlObj.host}/${owner}/${repo}.git`;
} catch (e) {}
}
if (gitAvailable && finalCloneUrl) {
let authClone = finalCloneUrl;
try {
const urlObj = new URL(finalCloneUrl);
if (token && (urlObj.protocol.startsWith('http'))) {
urlObj.username = encodeURIComponent(token);
authClone = urlObj.toString();
}
} catch (e) {}
const tmpDir = getSafeTmpDir(`gitea-push-${owner}-${repo}`);
try {
execSync(`git clone --depth 1 --branch ${branch} "${authClone}" "${tmpDir}"`, { stdio: 'inherit' });
// FIXED: Respektieren von destPath und Ordnernamen im Git-Workflow
const folderName = ppath.basename(data.localFolder);
let targetBaseDir = tmpDir;
if (destPath) {
// Wenn ein Zielpfad angegeben ist (z.B. 'src'), erstelle diesen Ordner im Repo
const osDestPath = destPath.split('/').join(ppath.sep);
targetBaseDir = ppath.join(tmpDir, osDestPath);
ensureDir(targetBaseDir);
}
// Ziel ist immer: targetBaseDir/folderName
const finalDest = ppath.join(targetBaseDir, folderName);
// FIXED: Korrekter Umgang mit fs.cpSync
// Wenn finalDest bereits existiert, muss es entfernt werden, damit cpSync funktioniert
if (fs.existsSync(finalDest)) {
fs.rmSync(finalDest, { recursive: true, force: true });
}
// Kopieren
if (fs.cpSync) {
fs.cpSync(data.localFolder, finalDest, { recursive: true, force: true });
} else {
if (process.platform === 'win32') {
execSync(`robocopy "${data.localFolder}" "${finalDest}" /E /NFL /NDL /NJH /NJS /nc /ns`, { stdio: 'inherit', shell: true });
} else {
execSync(`cp -r "${data.localFolder}/." "${finalDest}"`, { stdio: 'inherit', shell: true });
}
}
try {
execSync(`git -C "${tmpDir}" add .`, { stdio: 'inherit' });
try { execSync(`git -C "${tmpDir}" commit -m "Update from Git Manager GUI"`, { stdio: 'inherit' }); } catch (_) {}
execSync(`git -C "${tmpDir}" push origin ${branch}`, { stdio: 'inherit' });
} catch (e) { throw e; }
setTimeout(() => { try { if (fs.existsSync(tmpDir)) fs.rmSync(tmpDir, { recursive: true, force: true }); } catch (_) {} }, 5_000);
return { ok: true, usedGit: true, msg: 'Uploaded via git push' };
} catch (e) {
console.error('upload-and-push git-flow error', e);
try { if (fs.existsSync(tmpDir)) fs.rmSync(tmpDir, { recursive: true, force: true }); } catch (_) {}
}
}
// Fallback: API Upload (paralleler Upload)
const items = [];
const folderName = ppath.basename(data.localFolder);
// FIXED EXCLUDE LIST: Filter out .git, node_modules etc.
const excludeList = ['.git', 'node_modules', '.DS_Store', 'thumbs.db', '.vscode', '.idea'];
(function walk(dir) {
const entries = fs.readdirSync(dir);
for (const entry of entries) {
if (excludeList.includes(entry)) continue; // Skip excluded folders/files
const full = ppath.join(dir, entry);
let stat;
try {
stat = fs.statSync(full);
} catch(e) { continue; }
if (stat.isDirectory()) {
walk(full);
} else if (stat.isFile()) {
const rel = ppath.relative(data.localFolder, full).split(ppath.sep).join('/');
// FIXED: Pfad-Respektierung: destPath/folderName/rel
let targetPath;
if (destPath && destPath.length > 0) {
targetPath = `${destPath}/${folderName}/${rel}`;
} else {
targetPath = `${folderName}/${rel}`;
}
items.push({ localFile: full, targetPath });
}
}
})(data.localFolder);
const total = items.length;
const concurrency = data.concurrency || DEFAULT_CONCURRENCY;
if (total === 0) return { ok: true, usedGit: false, results: [] };
const tasks = items.map(it => async () => {
const raw = fs.readFileSync(it.localFile);
const base64 = raw.toString('base64');
return uploadGiteaFile({
token,
url: giteaUrl,
owner,
repo,
path: it.targetPath,
contentBase64: base64,
message: `Upload via GUI - ${ppath.basename(it.localFile)}`,
branch
});
});
const onProgress = (processed, t) => {
try { event.sender.send('folder-upload-progress', { processed, total: t, percent: Math.round((processed / t) * 100) }); } catch (_) {}
};
const uploadResults = await runLimited(tasks, concurrency, (proc, t) => onProgress(proc, total));
return { ok: true, usedGit: false, results: uploadResults };
} catch (e) {
console.error('upload-and-push error', e);
return { ok: false, error: String(e) };
}
});
/* ================================
RELEASE MANAGEMENT IPC HANDLERS
================================ */
// List all releases for a repository
ipcMain.handle('list-releases', async (event, data) => {
try {
const credentials = readCredentials();
if (!credentials) return { ok: false, error: 'no-credentials' };
const releases = await listGiteaReleases({
token: credentials.giteaToken,
url: credentials.giteaURL,
owner: data.owner,
repo: data.repo
});
return { ok: true, releases };
} catch (error) {
console.error('list-releases error:', error);
return { ok: false, error: String(error) };
}
});
// Get a specific release by tag
ipcMain.handle('get-release', async (event, data) => {
try {
const credentials = readCredentials();
if (!credentials) return { ok: false, error: 'no-credentials' };
const release = await getGiteaRelease({
token: credentials.giteaToken,
url: credentials.giteaURL,
owner: data.owner,
repo: data.repo,
tag: data.tag
});
return { ok: true, release };
} catch (error) {
console.error('get-release error:', error);
return { ok: false, error: String(error) };
}
});
// Create a new release
ipcMain.handle('create-release', async (event, data) => {
try {
const credentials = readCredentials();
if (!credentials) return { ok: false, error: 'no-credentials' };
const releaseData = {
tag_name: data.tag_name,
name: data.name,
body: data.body,
draft: data.draft,
prerelease: data.prerelease,
target_commitish: data.target_commitish
};
const release = await createGiteaRelease({
token: credentials.giteaToken,
url: credentials.giteaURL,
owner: data.owner,
repo: data.repo,
data: releaseData
});
return { ok: true, release };
} catch (error) {
console.error('create-release error:', error);
return { ok: false, error: String(error) };
}
});
// Edit/update a release
ipcMain.handle('edit-release', async (event, data) => {
try {
const credentials = readCredentials();
if (!credentials) return { ok: false, error: 'no-credentials' };
const updateData = {};
if (data.name !== undefined) updateData.name = data.name;
if (data.body !== undefined) updateData.body = data.body;
if (data.draft !== undefined) updateData.draft = data.draft;
if (data.prerelease !== undefined) updateData.prerelease = data.prerelease;
const release = await editGiteaRelease({
token: credentials.giteaToken,
url: credentials.giteaURL,
owner: data.owner,
repo: data.repo,
releaseId: data.releaseId,
data: updateData
});
return { ok: true, release };
} catch (error) {
console.error('edit-release error:', error);
return { ok: false, error: String(error) };
}
});
// Delete a release
ipcMain.handle('delete-release', async (event, data) => {
try {
const credentials = readCredentials();
if (!credentials) return { ok: false, error: 'no-credentials' };
await deleteGiteaRelease({
token: credentials.giteaToken,
url: credentials.giteaURL,
owner: data.owner,
repo: data.repo,
releaseId: data.releaseId
});
return { ok: true };
} catch (error) {
console.error('delete-release error:', error);
return { ok: false, error: String(error) };
}
});
// Upload a release asset
ipcMain.handle('upload-release-asset', async (event, data) => {
try {
const credentials = readCredentials();
if (!credentials) return { ok: false, error: 'no-credentials' };
const asset = await uploadReleaseAsset({
token: credentials.giteaToken,
url: credentials.giteaURL,
owner: data.owner,
repo: data.repo,
releaseId: data.releaseId,
filePath: data.filePath,
fileName: data.fileName
});
return { ok: true, asset };
} catch (error) {
console.error('upload-release-asset error:', error);
return { ok: false, error: String(error) };
}
});
// Delete a release asset
ipcMain.handle('delete-release-asset', async (event, data) => {
try {
const credentials = readCredentials();
if (!credentials) return { ok: false, error: 'no-credentials' };
await deleteReleaseAsset({
token: credentials.giteaToken,
url: credentials.giteaURL,
owner: data.owner,
repo: data.repo,
assetId: data.assetId
});
return { ok: true };
} catch (error) {
console.error('delete-release-asset error:', error);
return { ok: false, error: String(error) };
}
});
// Download release archive (ZIP/TAR)
ipcMain.handle('download-release-archive', async (event, data) => {
try {
const credentials = readCredentials();
if (!credentials) return { ok: false, error: 'no-credentials' };
const base = credentials.giteaURL.replace(/\/$/, '');
const archiveUrl = `${base}/${data.owner}/${data.repo}/archive/${data.tag}.zip`;
// Ask user where to save
const result = await dialog.showSaveDialog({
defaultPath: `${data.repo}-${data.tag}.zip`,
filters: [{ name: 'ZIP Archive', extensions: ['zip'] }]
});
if (result.canceled) return { ok: false, canceled: true };
const savePath = result.filePath;
// Download archive
const axios = require('axios');
const fs = require('fs');
const writer = fs.createWriteStream(savePath);
const response = await axios({
url: archiveUrl,
method: 'GET',
responseType: 'stream',
headers: { Authorization: `token ${credentials.giteaToken}` }
});
response.data.pipe(writer);
return new Promise((resolve, reject) => {
writer.on('finish', () => resolve({ ok: true, savedTo: savePath }));
writer.on('error', (err) => reject(err));
});
} catch (error) {
console.error('download-release-archive error:', error);
return { ok: false, error: String(error) };
}
});
/* ========================
LOCAL COMMIT HANDLERS
======================== */
ipcMain.handle('get-local-commits', async (event, data) => {
try {
const { getCommitLogs } = require('./src/git/gitHandler.js');
const commits = await getCommitLogs(data.folderPath, data.branch || 'HEAD');
return { ok: true, commits };
} catch (error) {
console.error('get-local-commits error:', error);
return { ok: false, error: String(error) };
}
});
ipcMain.handle('get-local-commit-details', async (event, data) => {
try {
const { getCommitDetails } = require('./src/git/gitHandler.js');
const details = await getCommitDetails(data.folderPath, data.sha);
return { ok: true, ...details };
} catch (error) {
console.error('get-local-commit-details error:', error);
return { ok: false, error: String(error) };
}
});
ipcMain.handle('get-local-commit-diff', async (event, data) => {
try {
const { getCommitDiff } = require('./src/git/gitHandler.js');
const diff = await getCommitDiff(data.folderPath, data.sha);
return { ok: true, diff };
} catch (error) {
console.error('get-local-commit-diff error:', error);
return { ok: false, error: String(error) };
}
});
ipcMain.handle('get-local-commit-files', async (event, data) => {
try {
const { getCommitDetails } = require('./src/git/gitHandler.js');
const details = await getCommitDetails(data.folderPath, data.sha);
return { ok: true, files: details.files || [], stats: { additions: 0, deletions: 0 } };
} catch (error) {
console.error('get-local-commit-files error:', error);
return { ok: false, error: String(error) };
}
});
/* ========================
GITEA COMMIT HANDLERS
======================== */
ipcMain.handle('get-commit-diff', async (event, data) => {
try {
const credentials = readCredentials();
const token = (data && data.token) || (credentials && credentials.giteaToken);
const url = (data && data.url) || (credentials && credentials.giteaURL);
if (!token || !url) {
return { ok: false, error: 'missing-token-or-url' };
}
const diff = await getGiteaCommitDiff({
token,
url,
owner: data.owner,
repo: data.repo,
sha: data.sha
});
return { ok: true, diff };
} catch (error) {
console.error('get-commit-diff error:', error);
return { ok: false, error: String(error) };
}
});
ipcMain.handle('get-commit-files', async (event, data) => {
try {
const credentials = readCredentials();
const token = (data && data.token) || (credentials && credentials.giteaToken);
const url = (data && data.url) || (credentials && credentials.giteaURL);
if (!token || !url) {
return { ok: false, error: 'missing-token-or-url' };
}
const result = await getGiteaCommitFiles({
token,
url,
owner: data.owner,
repo: data.repo,
sha: data.sha
});
return { ok: true, files: result.files, stats: result.stats };
} catch (error) {
console.error('get-commit-files error:', error);
return { ok: false, error: String(error) };
}
});
// main.js - Updater IPC Handlers
// 1. Version abfragen
ipcMain.handle('get-app-version', async () => {
return { ok: true, version: app.getVersion() };
});
// 2. Suche nach Updates (Manuell oder Automatisch)
ipcMain.handle('check-for-updates', async (event) => {
console.log("[Main] Update-Check angefordert...");
try {
if (!updater) {
const win = BrowserWindow.fromWebContents(event.sender);
if (win) updater = new Updater(win);
}
if (updater) await updater.checkForUpdates(false);
return { ok: true };
} catch (error) {
console.error('[Main] Fehler beim Update-Check:', error);
return { ok: false, error: String(error) };
}
});
// 3. Download starten (wird vom "Jetzt installieren" Button gerufen)
ipcMain.handle('start-update-download', async (event, asset) => {
console.log("[Main] Download-Signal erhalten für:", asset ? asset.name : "Unbekannt");
try {
if (!updater) {
const win = BrowserWindow.fromWebContents(event.sender);
updater = new Updater(win);
}
if (asset && asset.browser_download_url) {
await updater.startDownload(asset);
return { ok: true };
}
return { ok: false, error: 'Ungültiges Asset' };
} catch (error) {
console.error('[Main] Download-Fehler:', error);
return { ok: false, error: String(error) };
}
});