27 Commits
1.2.0 ... 2.0.3

Author SHA1 Message Date
eece161e58 Upload README.md via GUI 2026-02-03 21:52:41 +00:00
a35d6cf0cf Upload preload.js via GUI 2026-02-03 21:52:40 +00:00
594f12e927 Upload package-lock.json via GUI 2026-02-03 21:52:40 +00:00
8d70b71d66 Upload package.json via GUI 2026-02-03 21:52:39 +00:00
2c39ae4651 Upload main.js via GUI 2026-02-03 21:52:38 +00:00
9c14a1a7e0 Upload LICENSE via GUI 2026-02-03 21:52:37 +00:00
677b983706 Update from Git Manager GUI 2026-02-03 22:52:37 +01:00
20a9d601ff Update from Git Manager GUI 2026-02-03 22:52:35 +01:00
df9790177d Upload updater.js via GUI 2026-02-03 21:52:33 +00:00
1621a100af Update from Git Manager GUI 2026-02-03 22:51:03 +01:00
3f2e1d1e30 Upload preload.js via GUI 2026-02-02 19:01:00 +00:00
e7b3c9c447 Upload package.json via GUI 2026-02-02 19:01:00 +00:00
e8474957e8 Upload main.js via GUI 2026-02-02 19:00:59 +00:00
94e8c21e9b Update from Git Manager GUI 2026-02-02 20:00:57 +01:00
7d18161141 Update from Git Manager GUI 2026-02-02 20:00:55 +01:00
d88eade5a0 Upload file updater.js via GUI 2026-02-02 20:00:53 +01:00
6895a042d1 Upload package.json via GUI 2026-02-02 16:55:34 +00:00
e379f6a096 Upload main.js via GUI 2026-02-02 16:55:33 +00:00
fca9d9c66f Update from Git Manager GUI 2026-02-02 17:55:33 +01:00
fd868ea238 Update from Git Manager GUI 2026-02-02 17:55:31 +01:00
1a76dc4e64 Update from Git Manager GUI 2026-02-02 17:55:29 +01:00
a0c964e05e Upload preload.js via GUI 2026-02-02 16:55:27 +00:00
e109082d56 Upload package-lock.json via GUI 2026-02-02 16:55:26 +00:00
2c93a139d5 Upload preload.js via GUI 2025-12-28 21:18:09 +00:00
7bd8668e79 Upload package-lock.json via GUI 2025-12-28 21:18:08 +00:00
19be614284 Upload package.json via GUI 2025-12-28 21:18:08 +00:00
0272756be7 Upload main.js via GUI 2025-12-28 21:18:07 +00:00
12 changed files with 6494 additions and 748 deletions

BIN
assets/Thumbs.db Normal file

Binary file not shown.

BIN
assets/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

691
main.js
View File

@@ -7,6 +7,8 @@ const crypto = require('crypto');
const { execSync } = require('child_process'); const { execSync } = require('child_process');
const https = require('https'); const https = require('https');
const http = require('http'); const http = require('http');
const Updater = require('./updater.js'); // Auto-Updater
let updater = null;
const { const {
createRepoGitHub, createRepoGitHub,
@@ -14,7 +16,20 @@ const {
listGiteaRepos, listGiteaRepos,
getGiteaRepoContents, getGiteaRepoContents,
getGiteaFileContent, getGiteaFileContent,
uploadGiteaFile uploadGiteaFile,
getGiteaCommits,
getGiteaCommit,
getGiteaCommitDiff,
getGiteaCommitFiles,
searchGiteaCommits,
getGiteaBranches,
listGiteaReleases,
getGiteaRelease,
createGiteaRelease,
editGiteaRelease,
deleteGiteaRelease,
uploadReleaseAsset,
deleteReleaseAsset
} = require('./src/git/apiHandler.js'); } = require('./src/git/apiHandler.js');
const { initRepo, commitAndPush, getBranches, getCommitLogs } = require('./src/git/gitHandler.js'); const { initRepo, commitAndPush, getBranches, getCommitLogs } = require('./src/git/gitHandler.js');
@@ -69,19 +84,24 @@ function ensureDir(dirPath) {
*/ */
function getSafeTmpDir(baseName) { function getSafeTmpDir(baseName) {
const safeBase = (baseName || 'tmp').replace(/[<>:"/\\|?*\x00-\x1F]/g, '_').substring(0, 64); const safeBase = (baseName || 'tmp').replace(/[<>:"/\\|?*\x00-\x1F]/g, '_').substring(0, 64);
const dir = ppath.join(os.tmpdir(), `${safeBase}-${Date.now()}`);
try { // Basis-Temp-Ordner für interne Verwaltung
if (fs.existsSync(dir)) { const internalBase = ppath.join(os.tmpdir(), 'gitea-drag');
if (!fs.statSync(dir).isDirectory()) fs.unlinkSync(dir); if (!fs.existsSync(internalBase)) fs.mkdirSync(internalBase, { recursive: true });
else fs.rmSync(dir, { recursive: true, force: true });
} // Eindeutiger Unterordner, um Kollisionen zu vermeiden
fs.mkdirSync(dir, { recursive: true }); const uniqueSub = crypto.randomBytes(8).toString('hex');
return dir; const internalDir = ppath.join(internalBase, uniqueSub);
} catch (e) { fs.mkdirSync(internalDir, { recursive: true });
throw new Error(`getSafeTmpDir failed for ${dir}: ${e && e.message ? e.message : e}`);
} // Sichtbarer Ordnername = safeBase
const finalDir = ppath.join(internalDir, safeBase);
fs.mkdirSync(finalDir, { recursive: true });
return finalDir;
} }
/* ----------------------------- /* -----------------------------
app / window app / window
----------------------------- */ ----------------------------- */
@@ -168,7 +188,14 @@ ipcMain.handle('select-folder', async () => {
ipcMain.handle('select-file', async () => { ipcMain.handle('select-file', async () => {
const result = await dialog.showOpenDialog({ properties: ['openFile', 'multiSelections'] }); const result = await dialog.showOpenDialog({ properties: ['openFile', 'multiSelections'] });
if (result.canceled) return { ok: false, files: [] }; if (result.canceled) return { ok: false, files: [] };
return { ok: true, files: result.filePaths };
// Rückgabe mit path und name für jede Datei
const files = result.filePaths.map(filePath => ({
path: filePath,
name: ppath.basename(filePath)
}));
return { ok: true, files };
}); });
ipcMain.handle('save-credentials', async (event, data) => { ipcMain.handle('save-credentials', async (event, data) => {
@@ -324,6 +351,52 @@ ipcMain.handle('getCommitLogs', async (event, data) => {
} }
}); });
/* ----------------------------------------------------------------
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 Local file tree functions
----------------------------- */ ----------------------------- */
@@ -382,6 +455,21 @@ ipcMain.handle('readFile', async (event, data) => {
} }
}); });
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) => { ipcMain.handle('deleteFile', async (event, data) => {
try { try {
if (!data || !data.path || !fs.existsSync(data.path)) return { ok: false, error: 'file-not-found' }; if (!data || !data.path || !fs.existsSync(data.path)) return { ok: false, error: 'file-not-found' };
@@ -453,6 +541,83 @@ ipcMain.handle('get-gitea-file-content', async (event, data) => {
} }
}); });
// 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) => { ipcMain.handle('upload-gitea-file', async (event, data) => {
try { try {
const credentials = readCredentials(); const credentials = readCredentials();
@@ -510,6 +675,41 @@ ipcMain.handle('upload-gitea-file', async (event, data) => {
} }
}); });
// 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) => { ipcMain.handle('upload-local-folder-to-gitea', async (event, data) => {
try { try {
const credentials = readCredentials(); const credentials = readCredentials();
@@ -661,6 +861,30 @@ ipcMain.handle('download-gitea-folder', async (event, data) => {
} }
}); });
/* -----------------------------
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) => { ipcMain.handle('prepare-download-drag', async (event, data) => {
try { try {
const credentials = readCredentials(); const credentials = readCredentials();
@@ -671,46 +895,105 @@ ipcMain.handle('prepare-download-drag', async (event, data) => {
const repo = data.repo; const repo = data.repo;
const remotePath = (data.path || '').replace(/^\/+/, '').replace(/\/+$/, ''); const remotePath = (data.path || '').replace(/^\/+/, '').replace(/\/+$/, '');
// Use safe tmp dir (unique) // Create a unique temp directory (guarantees clean state)
const tmpBase = getSafeTmpDir(repo || 'gitea-repo'); const tmpBase = getSafeTmpDir(repo || 'gitea-repo');
// Gather list of files (recursive)
const allFiles = []; const allFiles = [];
async function gather(pathInRepo) { async function gather(pathInRepo) {
const items = await getGiteaRepoContents({ token, url, owner, repo, path: pathInRepo, ref: 'main' }); const items = await getGiteaRepoContents({ token, url, owner, repo, path: pathInRepo, ref: data.ref || 'main' });
for (const item of items) { for (const item of items || []) {
if (item.type === 'dir') await gather(item.path); if (item.type === 'dir') await gather(item.path);
else if (item.type === 'file') allFiles.push(item.path); else if (item.type === 'file') allFiles.push(item.path);
} }
} }
await gather(remotePath || ''); 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 tasks = allFiles.map(remoteFile => async () => {
const content = await getGiteaFileContent({ token, url, owner, repo, path: remoteFile, ref: 'main' }); const content = await getGiteaFileContent({ token, url, owner, repo, path: remoteFile, ref: data.ref || 'main' });
const localPath = ppath.join(tmpBase, remoteFile); const localPath = ppath.join(tmpBase, remoteFile);
fs.mkdirSync(ppath.dirname(localPath), { recursive: true }); ensureDir(ppath.dirname(localPath));
fs.writeFileSync(localPath, content, 'utf8');
// 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; return localPath;
}); });
await runLimited(tasks, DEFAULT_CONCURRENCY); // 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); setTimeout(() => { try { if (fs.existsSync(tmpBase)) fs.rmSync(tmpBase, { recursive: true, force: true }); } catch (_) {} }, TMP_CLEANUP_MS);
return { ok: true, tempPath: tmpBase };
return { ok: true, tempPath: tmpBase, files: successFiles };
} catch (e) { } catch (e) {
console.error('prepare-download-drag error', e); console.error('prepare-download-drag error', e);
return { ok: false, error: String(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) => { ipcMain.on('ondragstart', async (event, filePath) => {
try { try {
if (!filePath || !fs.existsSync(filePath)) return; 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(); let icon = nativeImage.createEmpty();
try { icon = await app.getFileIcon(filePath); } catch (e) {} try {
try { event.sender.startDrag({ file: filePath, icon: icon }); } catch (e) { console.error('startDrag failed', e); } // ask platform for file icon; large size for clearer drag icon
} catch (e) { console.error('ondragstart error', e); } 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) => { ipcMain.handle('delete-gitea-repo', async (event, data) => {
try { try {
const credentials = readCredentials(); const credentials = readCredentials();
@@ -1001,3 +1284,361 @@ ipcMain.handle('upload-and-push', async (event, data) => {
return { ok: false, error: String(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);
const errorMsg = error.message || String(error);
return { ok: false, error: errorMsg };
}
});
// 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) };
}
});

13
package-lock.json generated
View File

@@ -1,14 +1,15 @@
{ {
"name": "git-manager-gui", "name": "git-manager-gui",
"version": "1.2.0", "version": "2.0.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "git-manager-gui", "name": "git-manager-gui",
"version": "1.2.0", "version": "2.0.0",
"dependencies": { "dependencies": {
"axios": "^1.5.0", "axios": "^1.13.4",
"form-data": "^4.0.5",
"simple-git": "^3.19.1" "simple-git": "^3.19.1"
}, },
"devDependencies": { "devDependencies": {
@@ -1286,9 +1287,9 @@
} }
}, },
"node_modules/axios": { "node_modules/axios": {
"version": "1.13.2", "version": "1.13.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.4.tgz",
"integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", "integrity": "sha512-1wVkUaAO6WyaYtCkcYCOx12ZgpGf9Zif+qXa4n+oYzK558YryKqiL6UWwd5DqiH3VRW0GYhTZQ/vlgJrCoNQlg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"follow-redirects": "^1.15.6", "follow-redirects": "^1.15.6",

View File

@@ -1,13 +1,14 @@
{ {
"name": "git-manager-gui", "name": "git-manager-gui",
"version": "1.2.0", "version": "2.0.1",
"main": "main.js", "main": "main.js",
"scripts": { "scripts": {
"start": "electron .", "start": "electron .",
"build": "electron-builder" "build": "electron-builder"
}, },
"dependencies": { "dependencies": {
"axios": "^1.5.0", "axios": "^1.13.4",
"form-data": "^4.0.5",
"simple-git": "^3.19.1" "simple-git": "^3.19.1"
}, },
"devDependencies": { "devDependencies": {

View File

@@ -1,44 +1,100 @@
// preload.js — expose IPC to renderer // preload.js — expose IPC to renderer
const { contextBridge, ipcRenderer } = require('electron'); const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('electronAPI', { contextBridge.exposeInMainWorld('electronAPI', {
selectFolder: () => ipcRenderer.invoke('select-folder'), // Lokale Datei-Operationen
selectFile: () => ipcRenderer.invoke('select-file'), selectFolder: () => ipcRenderer.invoke('select-folder'),
saveCredentials: (data) => ipcRenderer.invoke('save-credentials', data), selectFile: () => ipcRenderer.invoke('select-file'),
loadCredentials: () => ipcRenderer.invoke('load-credentials'), getFileTree: (data) => ipcRenderer.invoke('getFileTree', data),
createRepo: (data) => ipcRenderer.invoke('create-repo', data), readFile: (data) => ipcRenderer.invoke('readFile', data),
pushProject: (data) => ipcRenderer.invoke('push-project', data), writeFile: (data) => ipcRenderer.invoke('writeFile', data),
getBranches: (data) => ipcRenderer.invoke('getBranches', data), deleteFile: (data) => ipcRenderer.invoke('deleteFile', data),
getCommitLogs: (data) => ipcRenderer.invoke('getCommitLogs', data),
getFileTree: (data) => ipcRenderer.invoke('getFileTree', data), // Gitea Datei-Operationen
readFile: (data) => ipcRenderer.invoke('readFile', data), listGiteaRepos: (data) => ipcRenderer.invoke('list-gitea-repos', data),
deleteFile: (data) => ipcRenderer.invoke('deleteFile', data), getGiteaRepoContents: (data) => ipcRenderer.invoke('get-gitea-repo-contents', data),
listGiteaRepos: (data) => ipcRenderer.invoke('list-gitea-repos', data), getGiteaFileContent: (data) => ipcRenderer.invoke('get-gitea-file-content', data),
getGiteaRepoContents: (data) => ipcRenderer.invoke('get-gitea-repo-contents', data), readGiteaFile: (data) => ipcRenderer.invoke('read-gitea-file', data),
getGiteaFileContent: (data) => ipcRenderer.invoke('get-gitea-file-content', data), writeGiteaFile: (data) => ipcRenderer.invoke('write-gitea-file', data),
uploadGiteaFile: (data) => ipcRenderer.invoke('upload-gitea-file', data), uploadGiteaFile: (data) => ipcRenderer.invoke('upload-gitea-file', data),
uploadLocalFolderToGitea: (data) => ipcRenderer.invoke('upload-local-folder-to-gitea', data), downloadGiteaFolder: (data) => ipcRenderer.invoke('download-gitea-folder', data),
downloadGiteaFolder: (data) => ipcRenderer.invoke('download-gitea-folder', data), downloadGiteaFile: (data) => ipcRenderer.invoke('download-gitea-file', data),
downloadGiteaFile: (data) => ipcRenderer.invoke('download-gitea-file', data), uploadLocalFolderToGitea: (data) => ipcRenderer.invoke('upload-local-folder-to-gitea', data),
prepareDownloadDrag: (data) => ipcRenderer.invoke('prepare-download-drag', data),
startNativeDrag: (filePath) => ipcRenderer.send('ondragstart', filePath), // Repository & Git Management
uploadAndPush: (data) => ipcRenderer.invoke('upload-and-push', data), saveCredentials: (data) => ipcRenderer.invoke('save-credentials', data),
deleteGiteaRepo: (data) => ipcRenderer.invoke('delete-gitea-repo', data), loadCredentials: () => ipcRenderer.invoke('load-credentials'),
createRepo: (data) => ipcRenderer.invoke('create-repo', data),
// progress subscriptions pushProject: (data) => ipcRenderer.invoke('push-project', data),
onPushProgress: (cb) => { getBranches: (data) => ipcRenderer.invoke('getBranches', data),
ipcRenderer.on('push-progress', (event, percent) => { getCommitLogs: (data) => ipcRenderer.invoke('getCommitLogs', data),
try { cb(percent); } catch (_) {} uploadAndPush: (data) => ipcRenderer.invoke('upload-and-push', data),
}); deleteGiteaRepo: (data) => ipcRenderer.invoke('delete-gitea-repo', data),
},
onFolderUploadProgress: (cb) => { // Drag & Drop
ipcRenderer.on('folder-upload-progress', (event, payload) => { prepareDownloadDrag: (data) => ipcRenderer.invoke('prepare-download-drag', data),
try { cb(payload); } catch (_) {} startNativeDrag: (filePath) => ipcRenderer.send('ondragstart', filePath),
});
}, // Release Management
onFolderDownloadProgress: (cb) => { listReleases: (data) => ipcRenderer.invoke('list-releases', data),
ipcRenderer.on('folder-download-progress', (event, payload) => { getRelease: (data) => ipcRenderer.invoke('get-release', data),
try { cb(payload); } catch (_) {} createRelease: (data) => ipcRenderer.invoke('create-release', data),
}); editRelease: (data) => ipcRenderer.invoke('edit-release', data),
} deleteRelease: (data) => ipcRenderer.invoke('delete-release', data),
uploadReleaseAsset: (data) => ipcRenderer.invoke('upload-release-asset', data),
deleteReleaseAsset: (data) => ipcRenderer.invoke('delete-release-asset', data),
downloadReleaseArchive: (data) => ipcRenderer.invoke('download-release-archive', data),
// Commit History & Visualization
getCommits: (data) => ipcRenderer.invoke('get-commits', data),
getCommitDetails: (data) => ipcRenderer.invoke('get-commit-details', data),
getCommitDiff: (data) => ipcRenderer.invoke('get-commit-diff', data),
getCommitFiles: (data) => ipcRenderer.invoke('get-commit-files', data),
searchCommits: (data) => ipcRenderer.invoke('search-commits', data),
getRepoBranches: (data) => ipcRenderer.invoke('get-repo-branches', data),
// Local Repository Commits
getLocalCommits: (data) => ipcRenderer.invoke('get-local-commits', data),
getLocalCommitDiff: (data) => ipcRenderer.invoke('get-local-commit-diff', data),
getLocalCommitDetails: (data) => ipcRenderer.invoke('get-local-commit-details', data),
searchLocalCommits: (data) => ipcRenderer.invoke('search-local-commits', data),
// === UPDATER APIs ===
checkForUpdates: () => ipcRenderer.invoke('check-for-updates'),
getAppVersion: () => ipcRenderer.invoke('get-app-version'),
// Triggert den tatsächlichen Download des Assets
// WICHTIG: Muss in renderer.js als window.electronAPI.startUpdateDownload(asset) aufgerufen werden
startUpdateDownload: (asset) => ipcRenderer.invoke('start-update-download', asset),
// === Progress & Update Subscriptions ===
onUpdateAvailable: (cb) => {
const listener = (event, info) => cb(info);
ipcRenderer.on('update-available', listener);
return () => ipcRenderer.removeListener('update-available', listener);
},
onUpdateProgress: (cb) => {
const listener = (event, percent) => cb(percent);
ipcRenderer.on('update-progress', listener);
return () => ipcRenderer.removeListener('update-progress', listener);
},
onPushProgress: (cb) => {
const listener = (event, percent) => { try { cb(percent); } catch (_) {} };
ipcRenderer.on('push-progress', listener);
return () => ipcRenderer.removeListener('push-progress', listener);
},
onFolderUploadProgress: (cb) => {
const listener = (event, payload) => { try { cb(payload); } catch (_) {} };
ipcRenderer.on('folder-upload-progress', listener);
return () => ipcRenderer.removeListener('folder-upload-progress', listener);
},
onFolderDownloadProgress: (cb) => {
const listener = (event, payload) => { try { cb(payload); } catch (_) {} };
ipcRenderer.on('folder-download-progress', listener);
return () => ipcRenderer.removeListener('folder-download-progress', listener);
}
}); });

View File

@@ -1,89 +1,208 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="de">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<title>Git Manager Explorer</title> <title>Git Manager Explorer Pro</title>
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="style.css"> <link rel="stylesheet" href="style.css">
</head> </head>
<body> <body>
<div id="app"> <div id="app">
<div id="toolbar"> <div id="toolbar">
<button id="btnSettings">Settings</button> <div class="tool-group">
<button id="btnSelectFolder">Select Folder</button> <button id="btnSettings" title="Einstellungen">⚙️ Settings</button>
<button id="btnPush">Push / Update</button> <button id="btnBack" class="secondary hidden" title="Zurück">⬅️ Zurück</button>
<select id="platform"><option value="github">GitHub</option><option value="gitea">Gitea</option></select> <button id="btnSelectFolder" class="accent-btn" title="Lokalen Ordner öffnen">📂 Open Local</button>
<span id="status" class="status"></span> <button id="btnLoadGiteaRepos" class="accent-btn" title="Gitea Repositories laden">🌐 Load Gitea</button>
</div>
<div class="tool-group">
<select id="platform" title="Plattform auswählen">
<option value="gitea" selected>Gitea</option>
<option value="github">GitHub</option>
</select>
<button id="btnOpenRepoActions" title="Neues Repository erstellen">🚀 New Repo</button>
<button id="btnPush" title="Projekt pushen">⬆️ Push</button>
<button id="btnCommits" class="hidden" title="Commit History anzeigen">📊 Commits</button>
<button id="btnReleases" class="hidden" title="Releases anzeigen">📦 Releases</button>
</div>
<span id="status" class="status">Bereit</span>
</div> </div>
<div id="main"> <main id="main">
<aside id="explorerPanel" class="card"> <div id="explorerGrid" class="explorer-grid">
<h3>Project Files</h3>
<div id="fileTree" class="file-tree"></div>
</aside>
<section id="detailsPanel">
<div id="repoDetails" class="card">
<div><label>Branch:</label><select id="branchSelect"></select></div>
<div><label>Repository Name:</label><input id="repoName" type="text" placeholder="repo-name"></div>
<!-- NEU: License Auswahl -->
<div style="margin-bottom: 5px;">
<label for="licenseSelect">License:</label>
<select id="licenseSelect">
<option value="">No License</option>
<option value="MIT">MIT License</option>
<option value="Apache-2.0">Apache License 2.0</option>
<option value="GPL-3.0">GNU General Public License v3.0</option>
<option value="BSD-3-Clause">BSD 3-Clause "New" or "Revised" License</option>
<option value="BSD-2-Clause">BSD 2-Clause "Simplified" License</option>
<option value="LGPL-3.0">GNU Lesser General Public License v3.0</option>
<option value="MPL-2.0">Mozilla Public License 2.0</option>
<option value="Unlicense">The Unlicense</option>
</select>
</div>
<!-- NEU: README Checkbox -->
<div style="margin-bottom: 10px;">
<label>
<input type="checkbox" id="createReadme" checked>
Initialize Repository with a README
</label>
</div>
<div><button id="btnCreateRepo">Create Repo</button></div>
<div style="margin-top:10px;"><button id="btnLoadGiteaRepos">Load My Gitea Repos</button>
<div id="giteaRepoContainer"></div>
</div>
</div> </div>
</main>
<div id="previewArea" class="card">
<h3 id="previewTitle">Preview</h3>
<pre id="previewContent" class="preview-content">Select a file to preview</pre>
</div>
<div id="commitLogs" class="card">
<h3>Commit Logs</h3>
<div id="logs">No commits yet.</div>
</div>
</section>
</div>
<!-- Settings Modal -->
<div id="settingsModal" class="modal hidden"> <div id="settingsModal" class="modal hidden">
<div class="modalContent card"> <div class="modalContent card">
<h2>Settings</h2> <h2>⚙️ Einstellungen</h2>
<label>GitHub Token</label><input id="githubToken" type="password">
<label>Gitea Token</label><input id="giteaToken" type="password"> <div class="input-group">
<label>Gitea URL</label><input id="giteaURL" type="text" placeholder="https://gitea.example.com"> <label>GitHub Token</label>
<div style="margin-top:10px;"> <input id="githubToken" type="password" placeholder="ghp_...">
<button id="btnSaveSettings">Save</button> </div>
<button id="btnCloseSettings">Close</button>
<div class="input-group">
<label>Gitea Token</label>
<input id="giteaToken" type="password" placeholder="Token hier einfügen">
</div>
<div class="input-group">
<label>Gitea URL</label>
<input id="giteaURL" type="text" placeholder="https://gitea.example.com">
</div>
<div class="input-group" style="margin-top: 30px; padding-top: 20px; border-top: 1px solid rgba(255,255,255,0.1);">
<label>App Version</label>
<div style="display: flex; gap: 12px; align-items: center;">
<input id="appVersion" type="text" readonly style="flex: 1; background: rgba(255,255,255,0.05); cursor: not-allowed;">
<button id="btnCheckUpdates" style="
background: linear-gradient(135deg, #00d4ff, #8b5cf6);
color: #000;
border: none;
padding: 10px 20px;
border-radius: 8px;
font-weight: 600;
cursor: pointer;
white-space: nowrap;
">🔄 Nach Updates suchen</button>
</div>
</div>
<div class="modal-buttons">
<button id="btnSaveSettings">Speichern</button>
<button id="btnCloseSettings" class="secondary">Abbrechen</button>
</div> </div>
</div> </div>
</div> </div>
</div>
<script src="./renderer.js"></script> <div id="repoActionModal" class="modal hidden">
<div class="modalContent card">
<h2>🚀 Neues Repository erstellen</h2>
<div class="input-group">
<label>Repository Name</label>
<input id="repoName" type="text" placeholder="mein-projekt">
</div>
<div class="input-group">
<label>Target Branch</label>
<input id="targetBranch" type="text" value="main" placeholder="main">
</div>
<div class="input-group">
<label>Lizenz</label>
<select id="licenseSelect">
<option value="">Keine Lizenz</option>
<option value="mit">MIT License</option>
<option value="apache-2.0">Apache 2.0</option>
<option value="gpl-3.0">GPL v3</option>
<option value="bsd-3-clause">BSD 3-Clause</option>
<option value="agpl-3.0">AGPL v3</option>
</select>
</div>
<div class="input-group">
<label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
<input type="checkbox" id="createReadme" checked>
<span style="text-transform: none; letter-spacing: normal;">README.md initialisieren</span>
</label>
</div>
<div class="input-group">
<label>Lokaler Push-Branch</label>
<select id="branchSelect">
<option value="main">main</option>
</select>
</div>
<div class="modal-buttons">
<button id="btnCreateRepo" class="accent-btn">Erstellen</button>
<button id="btnCloseRepoActions" class="secondary">Abbrechen</button>
</div>
</div>
</div>
<div id="fileEditorModal" class="modal hidden">
<div class="file-editor-card">
<div class="file-editor-header">
<div class="file-editor-title">
<span id="fileEditorIcon">📄</span>
<span id="fileEditorName">file.txt</span>
</div>
<div class="file-editor-toolbar">
<button id="btnEditorSearch" class="editor-tool-btn" title="Suchen (Ctrl+F)">🔍</button>
<button id="btnEditorSave" class="editor-tool-btn" title="Speichern">💾</button>
<button id="btnCloseEditor" class="editor-tool-btn" title="Schließen"></button>
</div>
</div>
<div id="fileEditorTabs" class="file-editor-tabs"></div>
<div id="searchBar" class="search-bar hidden">
<input id="searchInput" type="text" class="search-input" placeholder="Suchen...">
<input id="replaceInput" type="text" class="search-input" placeholder="Ersetzen...">
<button id="btnReplace" class="search-btn">Ersetzen</button>
<button id="btnReplaceAll" class="search-btn">Alle</button>
<button id="btnCloseSearch" class="search-btn"></button>
<span id="searchInfo" class="search-info">0/0</span>
</div>
<div class="file-editor-container">
<div id="lineNumbers" class="line-numbers"></div>
<textarea id="fileEditorContent" class="file-editor-textarea" placeholder="Dateiinhalt..."></textarea>
<div id="imagePreview" class="image-preview hidden"></div>
</div>
<div class="file-editor-footer">
<div class="file-editor-info">
<span id="fileEditorPath" class="file-path">Pfad: /path/to/file</span>
<span id="fileEditorStats" class="file-stats"></span>
<span id="fileEditorCursor" class="file-cursor">Zeile 1, Spalte 1</span>
<span id="autoSaveStatus" class="auto-save-status" style="display:none;">✓ Gespeichert</span>
</div>
<div class="modal-buttons">
<button id="btnDiscardEdit" class="secondary">Verwerfen</button>
</div>
</div>
</div>
</div>
<div id="updateModal" class="modal hidden">
<div class="modalContent card" style="max-width: 450px; border: 1px solid var(--accent-primary); box-shadow: 0 0 30px rgba(0, 212, 255, 0.2);">
<div style="display: flex; align-items: center; gap: 20px; margin-bottom: 20px;">
<div style="font-size: 3rem; filter: drop-shadow(0 0 10px var(--accent-primary));">🚀</div>
<div>
<h2 style="margin: 0; background: var(--accent-gradient); -webkit-background-clip: text; -webkit-text-fill-color: transparent;">Update verfügbar!</h2>
<p id="updateVersionInfo" style="color: var(--text-secondary); margin: 5px 0 0 0; font-family: monospace;"></p>
</div>
</div>
<div class="input-group">
<label style="color: var(--accent-primary); font-size: 0.8rem; letter-spacing: 1px;">RELEASE NOTES</label>
<div id="updateChangelog" style="
background: rgba(0,0,0,0.25);
padding: 15px;
border-radius: 8px;
max-height: 180px;
overflow-y: auto;
font-size: 0.9rem;
line-height: 1.6;
color: var(--text-primary);
border: 1px solid rgba(255,255,255,0.05);
white-space: pre-wrap;
"></div>
</div>
<div class="modal-buttons" style="margin-top: 25px; gap: 12px;">
<button id="btnStartUpdate" class="accent-btn" style="flex: 2; height: 45px; font-weight: bold;">🚀 Jetzt installieren</button>
<button id="btnIgnoreUpdate" class="secondary" style="flex: 1; height: 45px;">Später</button>
</div>
</div>
</div>
</div> <script src="renderer.js"></script>
</body> </body>
</html> </html>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -39,10 +39,36 @@ async function createRepoGitHub({ name, token, auto_init = true, license = '', p
body.license_template = license; body.license_template = license;
} }
const response = await axios.post('https://api.github.com/user/repos', body, { try {
headers: { Authorization: `token ${token}` } const response = await axios.post('https://api.github.com/user/repos', body, {
}); headers: { Authorization: `token ${token}` }
return response.data; });
return response.data;
} catch (error) {
// Benutzerfreundliche Fehlerbehandlung
if (error.response) {
const status = error.response.status;
const data = error.response.data;
if (status === 401) {
throw new Error('Authentifizierung fehlgeschlagen. Bitte überprüfen Sie Ihren GitHub-Token.');
} else if (status === 422) {
const msg = data?.message || 'Repository konnte nicht erstellt werden';
if (msg.includes('name already exists')) {
throw new Error(`Ein Repository mit dem Namen "${name}" existiert bereits.`);
}
throw new Error(`GitHub-Fehler: ${msg}`);
} else if (status === 403) {
throw new Error('Zugriff verweigert. Bitte überprüfen Sie Ihre Token-Berechtigungen.');
} else {
throw new Error(`GitHub-Fehler (${status}): ${data?.message || error.message}`);
}
} else if (error.request) {
throw new Error('Keine Antwort von GitHub. Bitte überprüfen Sie Ihre Internetverbindung.');
} else {
throw new Error(`Fehler beim Erstellen des Repositories: ${error.message}`);
}
}
} }
async function createRepoGitea({ name, token, url, auto_init = true, license = '', private: isPrivate = false }) { async function createRepoGitea({ name, token, url, auto_init = true, license = '', private: isPrivate = false }) {
@@ -54,6 +80,10 @@ async function createRepoGitea({ name, token, url, auto_init = true, license = '
console.log('Token length:', token ? token.length : 0); console.log('Token length:', token ? token.length : 0);
console.log('Name:', name); console.log('Name:', name);
console.log('auto_init:', auto_init); console.log('auto_init:', auto_init);
console.log('License:', license);
// Normalisiere Lizenz zu Großbuchstaben, wenn vorhanden
const normalizedLicense = license ? license.toUpperCase() : '';
const body = { const body = {
name, name,
@@ -62,21 +92,77 @@ async function createRepoGitea({ name, token, url, auto_init = true, license = '
default_branch: 'main' default_branch: 'main'
}; };
if (license) { if (normalizedLicense) {
body.license = license; body.license = normalizedLicense;
} }
console.log('Request body:', JSON.stringify(body, null, 2)); console.log('Request body:', JSON.stringify(body, null, 2));
try { try {
const response = await axios.post(endpoint, body, { const response = await axios.post(endpoint, body, {
headers: { Authorization: `token ${token}` } headers: { Authorization: `token ${token}` },
timeout: 15000 // 15 Sekunden Timeout
}); });
console.log('Success! Status:', response.status); console.log('Success! Status:', response.status);
return response.data; return response.data;
} catch (error) { } catch (error) {
console.error('Error creating repo:', error.response?.status, error.response?.data); console.error('Error creating repo:', error.response?.status, error.response?.data);
throw error;
// Wenn Lizenz-Fehler auftritt (500 mit Lizenz-Meldung), versuche ohne Lizenz
if (error.response?.status === 500 &&
error.response?.data?.message?.includes('getLicense') &&
normalizedLicense) {
console.warn(`Lizenz "${normalizedLicense}" wird vom Server nicht unterstützt. Versuche ohne Lizenz...`);
const bodyWithoutLicense = {
name,
private: isPrivate,
auto_init: auto_init,
default_branch: 'main'
};
try {
const retryResponse = await axios.post(endpoint, bodyWithoutLicense, {
headers: { Authorization: `token ${token}` },
timeout: 15000
});
console.log('Success without license! Status:', retryResponse.status);
console.warn(`Hinweis: Repository wurde ohne Lizenz erstellt, da "${normalizedLicense}" nicht verfügbar ist.`);
return retryResponse.data;
} catch (retryError) {
// Falls auch ohne Lizenz fehlschlägt, behandle den neuen Fehler
error = retryError;
}
}
// Benutzerfreundliche Fehlerbehandlung
if (error.response) {
const status = error.response.status;
const data = error.response.data;
if (status === 401) {
throw new Error('Authentifizierung fehlgeschlagen. Bitte überprüfen Sie Ihren Gitea-Token.');
} else if (status === 409 || (status === 422 && data?.message?.includes('already exists'))) {
throw new Error(`Ein Repository mit dem Namen "${name}" existiert bereits auf Gitea.`);
} else if (status === 403) {
throw new Error('Zugriff verweigert. Bitte überprüfen Sie Ihre Token-Berechtigungen.');
} else if (status === 404) {
throw new Error('Gitea-Server nicht gefunden. Bitte überprüfen Sie die URL.');
} else if (status === 422) {
const msg = data?.message || 'Repository konnte nicht erstellt werden';
throw new Error(`Gitea-Fehler: ${msg}`);
} else if (status === 500 && data?.message?.includes('getLicense')) {
throw new Error(`Die Lizenz "${normalizedLicense}" wird von Ihrem Gitea-Server nicht unterstützt. Bitte wählen Sie eine andere Lizenz oder lassen Sie das Feld leer.`);
} else {
throw new Error(`Gitea-Fehler (${status}): ${data?.message || error.message}`);
}
} else if (error.code === 'ECONNABORTED') {
throw new Error('Zeitüberschreitung beim Verbinden mit Gitea. Bitte überprüfen Sie Ihre Internetverbindung oder Server-URL.');
} else if (error.request) {
throw new Error('Keine Antwort von Gitea-Server. Bitte überprüfen Sie die URL und Ihre Internetverbindung.');
} else {
throw new Error(`Fehler beim Erstellen des Repositories: ${error.message}`);
}
} }
} }
@@ -260,7 +346,7 @@ async function uploadGiteaFile({ token, url, owner, repo, path, contentBase64, m
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms)); const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
let retryCount = 0; let retryCount = 0;
const MAX_RETRIES = 3; const MAX_RETRIES = 2; // Reduziert auf 2 Retries für schnelleren Fallback
while (retryCount <= MAX_RETRIES) { while (retryCount <= MAX_RETRIES) {
let sha = await fetchSha(); let sha = await fetchSha();
@@ -275,14 +361,48 @@ async function uploadGiteaFile({ token, url, owner, repo, path, contentBase64, m
}; };
if (sha) body.sha = sha; if (sha) body.sha = sha;
console.log(`[Upload Debug] Datei: ${path}, Branch: ${branchName}, SHA: ${sha ? sha.substring(0, 8) : 'keine'}`);
try { try {
const res = await axios.put(endpoint, body, { const res = await axios.put(endpoint, body, {
headers: { Authorization: `token ${token}` } headers: { Authorization: `token ${token}` },
timeout: 30000 // 30 Sekunden Timeout für größere Dateien
}); });
console.log(`[Upload Success] ${path} erfolgreich gespeichert`);
return res.data; return res.data;
} catch (err) { } catch (err) {
console.error(`Upload Attempt ${retryCount + 1} for ${path}:`, err.response ? err.response.data : err.message); console.error(`Upload Attempt ${retryCount + 1} for ${path}:`, err.response ? err.response.data : err.message);
// Behandle 500 Server-Fehler speziell
if (err.response && err.response.status === 500) {
const errorMsg = err.response.data?.message || err.message;
// Git-Referenz-Konflikt: Branch hat sich geändert, SHA ist veraltet
if (errorMsg.includes('cannot lock ref') || errorMsg.includes('failed to update ref') || errorMsg.includes('but expected')) {
if (retryCount < MAX_RETRIES) {
retryCount++;
console.warn(`-> Git-Referenz-Konflikt erkannt. Branch hat sich geändert. Aktualisiere SHA und versuche erneut... (Retry ${retryCount}/${MAX_RETRIES})`);
await sleep(500); // Kurze Pause
continue; // SHA wird in der nächsten Iteration neu geholt
} else {
throw new Error(`Git-Referenz-Konflikt: Der Branch "${branchName}" wurde während des Uploads geändert. Bitte versuchen Sie es erneut.`);
}
}
// Wenn es ein Lizenz-Fehler ist (sollte nicht passieren, aber als Fallback)
if (errorMsg.includes('getLicense')) {
throw new Error(`Server-Fehler beim Speichern: Lizenz-Problem. Versuchen Sie es erneut.`);
}
// Wenn es ein anderes Branch-Problem ist
if (errorMsg.includes('branch') || errorMsg.includes('ref')) {
throw new Error(`Server-Fehler: Problem mit Branch "${branchName}". Details: ${errorMsg}`);
}
// Allgemeiner 500-Fehler
throw new Error(`Server-Fehler (500) beim Speichern der Datei. Details: ${errorMsg}`);
}
const isShaRequired = err.response && const isShaRequired = err.response &&
err.response.status === 422 && err.response.status === 422 &&
err.response.data && err.response.data &&
@@ -291,25 +411,457 @@ async function uploadGiteaFile({ token, url, owner, repo, path, contentBase64, m
if (isShaRequired && retryCount < MAX_RETRIES) { if (isShaRequired && retryCount < MAX_RETRIES) {
retryCount++; retryCount++;
console.warn(`-> 422 SHA Required. Waiting 2 seconds for server index update... (Retry ${retryCount}/${MAX_RETRIES})`); console.warn(`-> 422 SHA Required. Waiting 1.5 seconds for server index update... (Retry ${retryCount}/${MAX_RETRIES})`);
await sleep(2000); // Wartezeit geben await sleep(1500); // Reduzierte Wartezeit für schnelleren Fallback
// Schleife wird neu gestartet, SHA wird erneut gesucht // Schleife wird neu gestartet, SHA wird erneut gesucht
continue; continue;
} else if (isShaRequired && retryCount >= MAX_RETRIES) { } else if (isShaRequired && retryCount >= MAX_RETRIES) {
throw new Error(`Upload failed after ${MAX_RETRIES} retries. Server insists file exists but we cannot find its SHA. Check the repository manually.`); // Verbesserte Fehlermeldung mit Hinweis auf Git-Fallback
const error = new Error(`API-Upload fehlgeschlagen: Repository wurde gerade erstellt, Index noch nicht bereit. Verwende Git-Fallback.`);
error.code = 'SHA_NOT_FOUND';
throw error;
}
// Andere Fehler mit besserer Meldung werfen
if (err.response) {
const status = err.response.status;
const data = err.response.data;
if (status === 401) {
throw new Error('Authentifizierung fehlgeschlagen. Bitte überprüfen Sie Ihren Token.');
} else if (status === 403) {
throw new Error('Zugriff verweigert. Keine Berechtigung zum Schreiben in dieses Repository.');
} else if (status === 404) {
throw new Error(`Datei oder Repository nicht gefunden. Bitte überprüfen Sie den Pfad: ${path}`);
} else {
throw new Error(`Fehler beim Speichern (${status}): ${data?.message || err.message}`);
}
} }
// Andere Fehler sofort werfen
throw err; throw err;
} }
} }
} }
/* ================================
COMMIT HISTORY FUNCTIONS (GITEA)
================================ */
/**
* Get commit history from Gitea repository
*/
async function getGiteaCommits({ token, url, owner, repo, branch = 'main', page = 1, limit = 50 }) {
const base = normalizeBase(url);
if (!base) throw new Error('Invalid Gitea base URL');
const endpoint = `${base}/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/commits`;
try {
const response = await axios.get(endpoint, {
headers: { Authorization: `token ${token}` },
params: {
sha: branch,
page,
limit
}
});
return response.data;
} catch (err) {
console.error('getGiteaCommits error:', err.response?.data || err.message);
throw err;
}
}
/**
* Get a specific commit with diff
*/
async function getGiteaCommit({ token, url, owner, repo, sha }) {
const base = normalizeBase(url);
if (!base) throw new Error('Invalid Gitea base URL');
const endpoint = `${base}/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/git/commits/${sha}`;
try {
const response = await axios.get(endpoint, {
headers: { Authorization: `token ${token}` }
});
return response.data;
} catch (err) {
console.error('getGiteaCommit error:', err.response?.data || err.message);
throw err;
}
}
/**
* Get commit diff/patch
*/
async function getGiteaCommitDiff({ token, url, owner, repo, sha }) {
const base = normalizeBase(url);
if (!base) throw new Error('Invalid Gitea base URL');
// Gitea returns diff in the commit endpoint with .diff extension
const endpoint = `${base}/${owner}/${repo}/commit/${sha}.diff`;
try {
const response = await axios.get(endpoint, {
headers: { Authorization: `token ${token}` }
});
return response.data;
} catch (err) {
console.error('getGiteaCommitDiff error:', err.response?.data || err.message);
throw err;
}
}
/**
* Get commit file changes/stats
*/
async function getGiteaCommitFiles({ token, url, owner, repo, sha }) {
const base = normalizeBase(url);
if (!base) throw new Error('Invalid Gitea base URL');
const endpoint = `${base}/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/git/commits/${sha}`;
try {
const response = await axios.get(endpoint, {
headers: { Authorization: `token ${token}` }
});
// Gitea commit response includes stats
const commit = response.data;
// Normalize files to match local git format
const files = (commit.files || []).map(f => ({
file: f.filename || f.file || '',
changes: (f.additions || 0) + (f.deletions || 0),
insertions: f.additions || 0,
deletions: f.deletions || 0,
binary: f.binary || false
}));
return {
files,
stats: commit.stats || { additions: 0, deletions: 0, total: 0 }
};
} catch (err) {
console.error('getGiteaCommitFiles error:', err.response?.data || err.message);
throw err;
}
}
/**
* Search commits in Gitea repository
*/
async function searchGiteaCommits({ token, url, owner, repo, query, branch = 'main' }) {
const base = normalizeBase(url);
if (!base) throw new Error('Invalid Gitea base URL');
// Gitea doesn't have direct commit search, so we get commits and filter
const endpoint = `${base}/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/commits`;
try {
const response = await axios.get(endpoint, {
headers: { Authorization: `token ${token}` },
params: {
sha: branch,
limit: 100 // Get more for searching
}
});
// Filter commits by query
const lowerQuery = query.toLowerCase();
const filtered = response.data.filter(commit => {
const message = (commit.commit?.message || '').toLowerCase();
const author = (commit.commit?.author?.name || '').toLowerCase();
return message.includes(lowerQuery) || author.includes(lowerQuery);
});
return filtered;
} catch (err) {
console.error('searchGiteaCommits error:', err.response?.data || err.message);
throw err;
}
}
/**
* Get branches for branch graph visualization
*/
async function getGiteaBranches({ token, url, owner, repo }) {
const base = normalizeBase(url);
if (!base) throw new Error('Invalid Gitea base URL');
const endpoint = `${base}/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/branches`;
try {
const response = await axios.get(endpoint, {
headers: { Authorization: `token ${token}` }
});
return response.data;
} catch (err) {
console.error('getGiteaBranches error:', err.response?.data || err.message);
throw err;
}
}
/* ================================
RELEASE MANAGEMENT FUNCTIONS
================================ */
/**
* List all releases for a repository
*/
async function listGiteaReleases({ token, url, owner, repo }) {
const base = normalizeBase(url);
if (!base) throw new Error('Invalid Gitea base URL');
const endpoint = `${base}/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/releases`;
try {
const response = await axios.get(endpoint, {
headers: { Authorization: `token ${token}` }
});
return response.data;
} catch (err) {
console.error('listGiteaReleases error:', err.response?.data || err.message);
throw err;
}
}
/**
* Get a specific release by tag
*/
async function getGiteaRelease({ token, url, owner, repo, tag }) {
const base = normalizeBase(url);
if (!base) throw new Error('Invalid Gitea base URL');
const endpoint = `${base}/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/releases/tags/${encodeURIComponent(tag)}`;
try {
const response = await axios.get(endpoint, {
headers: { Authorization: `token ${token}` }
});
return response.data;
} catch (err) {
console.error('getGiteaRelease error:', err.response?.data || err.message);
throw err;
}
}
/**
* Create a new release
*/
async function createGiteaRelease({ token, url, owner, repo, data }) {
const base = normalizeBase(url);
if (!base) throw new Error('Invalid Gitea base URL');
const endpoint = `${base}/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/releases`;
const body = {
tag_name: data.tag_name,
name: data.name || data.tag_name,
body: data.body || '',
draft: data.draft || false,
prerelease: data.prerelease || false,
target_commitish: data.target_commitish || 'main'
};
try {
const response = await axios.post(endpoint, body, {
headers: {
Authorization: `token ${token}`,
'Content-Type': 'application/json'
},
timeout: 15000
});
return response.data;
} catch (err) {
console.error('createGiteaRelease error:', err.response?.data || err.message);
// Benutzerfreundliche Fehlerbehandlung
if (err.response) {
const status = err.response.status;
const data = err.response.data;
if (status === 401) {
throw new Error('Authentifizierung fehlgeschlagen. Bitte überprüfen Sie Ihren Token.');
} else if (status === 403) {
throw new Error('Zugriff verweigert. Keine Berechtigung zum Erstellen von Releases.');
} else if (status === 404) {
throw new Error(`Repository "${owner}/${repo}" nicht gefunden.`);
} else if (status === 409 || (status === 422 && data?.message?.includes('already exists'))) {
throw new Error(`Ein Release mit dem Tag "${data.tag_name}" existiert bereits.`);
} else if (status === 422) {
const msg = data?.message || 'Release konnte nicht erstellt werden';
throw new Error(`Gitea-Fehler: ${msg}`);
} else if (status === 500) {
const msg = data?.message || err.message;
throw new Error(`Server-Fehler (500) beim Erstellen des Release. Details: ${msg}`);
} else {
throw new Error(`Fehler beim Erstellen des Release (${status}): ${data?.message || err.message}`);
}
} else if (err.code === 'ECONNABORTED') {
throw new Error('Zeitüberschreitung. Bitte versuchen Sie es erneut.');
} else if (err.request) {
throw new Error('Keine Antwort vom Server. Bitte überprüfen Sie Ihre Internetverbindung.');
} else {
throw new Error(`Fehler beim Erstellen des Release: ${err.message}`);
}
}
}
/**
* Edit/update an existing release
*/
async function editGiteaRelease({ token, url, owner, repo, releaseId, data }) {
const base = normalizeBase(url);
if (!base) throw new Error('Invalid Gitea base URL');
const endpoint = `${base}/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/releases/${releaseId}`;
const body = {};
if (data.name !== undefined) body.name = data.name;
if (data.body !== undefined) body.body = data.body;
if (data.draft !== undefined) body.draft = data.draft;
if (data.prerelease !== undefined) body.prerelease = data.prerelease;
if (data.tag_name !== undefined) body.tag_name = data.tag_name;
try {
const response = await axios.patch(endpoint, body, {
headers: {
Authorization: `token ${token}`,
'Content-Type': 'application/json'
}
});
return response.data;
} catch (err) {
console.error('editGiteaRelease error:', err.response?.data || err.message);
throw err;
}
}
/**
* Delete a release
*/
async function deleteGiteaRelease({ token, url, owner, repo, releaseId }) {
const base = normalizeBase(url);
if (!base) throw new Error('Invalid Gitea base URL');
const endpoint = `${base}/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/releases/${releaseId}`;
try {
await axios.delete(endpoint, {
headers: { Authorization: `token ${token}` }
});
return { ok: true };
} catch (err) {
console.error('deleteGiteaRelease error:', err.response?.data || err.message);
throw err;
}
}
/**
* Upload a release asset (attachment)
* Note: Gitea uses multipart/form-data for asset uploads
*/
async function uploadReleaseAsset({ token, url, owner, repo, releaseId, filePath, fileName }) {
const base = normalizeBase(url);
if (!base) throw new Error('Invalid Gitea base URL');
const FormData = require('form-data');
const fs = require('fs');
const endpoint = `${base}/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/releases/${releaseId}/assets`;
const formData = new FormData();
formData.append('attachment', fs.createReadStream(filePath), {
filename: fileName || require('path').basename(filePath)
});
try {
const response = await axios.post(endpoint, formData, {
headers: {
Authorization: `token ${token}`,
...formData.getHeaders()
},
maxContentLength: Infinity,
maxBodyLength: Infinity,
timeout: 300000 // 5 Minuten für große Dateien
});
return response.data;
} catch (err) {
console.error('uploadReleaseAsset error:', err.response?.data || err.message);
// Benutzerfreundliche Fehlerbehandlung
if (err.response) {
const status = err.response.status;
const data = err.response.data;
if (status === 401) {
throw new Error('Authentifizierung fehlgeschlagen beim Upload.');
} else if (status === 403) {
throw new Error('Zugriff verweigert. Keine Berechtigung zum Hochladen von Assets.');
} else if (status === 404) {
throw new Error(`Release mit ID ${releaseId} nicht gefunden.`);
} else if (status === 413) {
throw new Error('Datei ist zu groß. Maximale Größe überschritten.');
} else if (status === 500) {
const msg = data?.message || err.message;
throw new Error(`Server-Fehler beim Upload: ${msg}`);
} else {
throw new Error(`Fehler beim Upload (${status}): ${data?.message || err.message}`);
}
} else if (err.code === 'ECONNABORTED') {
throw new Error('Upload-Zeitüberschreitung. Datei ist möglicherweise zu groß.');
} else if (err.code === 'ENOENT') {
throw new Error(`Datei nicht gefunden: ${filePath}`);
} else if (err.request) {
throw new Error('Keine Antwort vom Server beim Upload.');
} else {
throw new Error(`Upload fehlgeschlagen: ${err.message}`);
}
}
}
/**
* Delete a release asset
*/
async function deleteReleaseAsset({ token, url, owner, repo, assetId }) {
const base = normalizeBase(url);
if (!base) throw new Error('Invalid Gitea base URL');
const endpoint = `${base}/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/releases/assets/${assetId}`;
try {
await axios.delete(endpoint, {
headers: { Authorization: `token ${token}` }
});
return { ok: true };
} catch (err) {
console.error('deleteReleaseAsset error:', err.response?.data || err.message);
throw err;
}
}
module.exports = { module.exports = {
createRepoGitHub, createRepoGitHub,
createRepoGitea, createRepoGitea,
listGiteaRepos, listGiteaRepos,
getGiteaRepoContents, getGiteaRepoContents,
getGiteaFileContent, getGiteaFileContent,
uploadGiteaFile uploadGiteaFile,
// Commit History
getGiteaCommits,
getGiteaCommit,
getGiteaCommitDiff,
getGiteaCommitFiles,
searchGiteaCommits,
getGiteaBranches,
// Release Management
listGiteaReleases,
getGiteaRelease,
createGiteaRelease,
editGiteaRelease,
deleteGiteaRelease,
uploadReleaseAsset,
deleteReleaseAsset
}; };

View File

@@ -54,4 +54,282 @@ async function getCommitLogs(folderPath, count = 50) {
return log.all.map(c => `${c.hash.substring(0,7)} - ${c.message}`); return log.all.map(c => `${c.hash.substring(0,7)} - ${c.message}`);
} }
module.exports = { initRepo, commitAndPush, getBranches, getCommitLogs }; /* ================================
EXTENDED COMMIT HISTORY FUNCTIONS
================================ */
/**
* Get detailed commit history with full information
*/
async function getDetailedCommitHistory(folderPath, options = {}) {
const git = gitFor(folderPath);
const logOptions = {
maxCount: options.limit || 100,
file: options.file || undefined
};
if (options.author) {
logOptions['--author'] = options.author;
}
if (options.since) {
logOptions['--since'] = options.since;
}
if (options.until) {
logOptions['--until'] = options.until;
}
if (options.grep) {
logOptions['--grep'] = options.grep;
}
const log = await git.log(logOptions);
return log.all.map(commit => ({
hash: commit.hash,
shortHash: commit.hash.substring(0, 7),
author: commit.author_name,
authorEmail: commit.author_email,
date: commit.date,
message: commit.message,
body: commit.body,
refs: commit.refs || '',
parentHashes: commit.parent || []
}));
}
/**
* Get commit diff for a specific commit
*/
async function getCommitDiff(folderPath, commitHash) {
const git = gitFor(folderPath);
try {
// Get diff from parent to this commit
const diff = await git.diff([`${commitHash}~1`, commitHash]);
return diff;
} catch (error) {
// If no parent (first commit), show diff from empty tree
try {
const diff = await git.diff(['--root', commitHash]);
return diff;
} catch (err) {
console.error('Error getting diff:', err);
return '';
}
}
}
/**
* Get file changes for a specific commit
*/
async function getCommitFileChanges(folderPath, commitHash) {
const git = gitFor(folderPath);
try {
const diff = await git.diffSummary([`${commitHash}~1`, commitHash]);
return {
files: diff.files.map(file => ({
file: file.file,
changes: file.changes,
insertions: file.insertions,
deletions: file.deletions,
binary: file.binary || false
})),
insertions: diff.insertions,
deletions: diff.deletions,
changed: diff.changed
};
} catch (error) {
// First commit case
try {
const diff = await git.diffSummary(['--root', commitHash]);
return {
files: diff.files.map(file => ({
file: file.file,
changes: file.changes,
insertions: file.insertions,
deletions: file.deletions,
binary: file.binary || false
})),
insertions: diff.insertions,
deletions: diff.deletions,
changed: diff.changed
};
} catch (err) {
console.error('Error getting file changes:', err);
return { files: [], insertions: 0, deletions: 0, changed: 0 };
}
}
}
/**
* Get detailed commit info including stats
*/
async function getCommitDetails(folderPath, commitHash) {
const git = gitFor(folderPath);
try {
const log = await git.log({ from: commitHash, to: commitHash, maxCount: 1 });
if (log.all.length === 0) {
throw new Error('Commit not found');
}
const commit = log.all[0];
const fileChanges = await getCommitFileChanges(folderPath, commitHash);
const diff = await getCommitDiff(folderPath, commitHash);
return {
hash: commit.hash,
shortHash: commit.hash.substring(0, 7),
author: commit.author_name,
authorEmail: commit.author_email,
date: commit.date,
message: commit.message,
body: commit.body,
refs: commit.refs || '',
parentHashes: commit.parent || [],
fileChanges,
diff
};
} catch (error) {
console.error('Error getting commit details:', error);
throw error;
}
}
/**
* Get branch graph data for visualization
*/
async function getBranchGraph(folderPath, limit = 50) {
const git = gitFor(folderPath);
try {
// Get all branches
const branches = await git.branchLocal();
const allBranches = branches.all;
// Get graph structure
const log = await git.log({
maxCount: limit,
'--all': null,
'--graph': null,
'--decorate': null,
'--oneline': null
});
return {
branches: allBranches,
current: branches.current,
commits: log.all
};
} catch (error) {
console.error('Error getting branch graph:', error);
return { branches: [], current: '', commits: [] };
}
}
/**
* Search commits by message, author, or content
*/
async function searchCommits(folderPath, query, options = {}) {
const git = gitFor(folderPath);
const logOptions = {
maxCount: options.limit || 100,
'--all': null
};
// Search in commit message
if (options.searchMessage !== false) {
logOptions['--grep'] = query;
}
// Search by author
if (options.author) {
logOptions['--author'] = options.author;
}
// Case insensitive
if (options.caseInsensitive !== false) {
logOptions['--regexp-ignore-case'] = null;
}
try {
const log = await git.log(logOptions);
return log.all.map(commit => ({
hash: commit.hash,
shortHash: commit.hash.substring(0, 7),
author: commit.author_name,
authorEmail: commit.author_email,
date: commit.date,
message: commit.message,
body: commit.body,
refs: commit.refs || ''
}));
} catch (error) {
console.error('Error searching commits:', error);
return [];
}
}
/**
* Get commit statistics
*/
async function getCommitStats(folderPath, since = '1 month ago') {
const git = gitFor(folderPath);
try {
const log = await git.log({
'--since': since,
'--all': null
});
const authorStats = {};
const dailyStats = {};
log.all.forEach(commit => {
// Author stats
const author = commit.author_name;
if (!authorStats[author]) {
authorStats[author] = { commits: 0, email: commit.author_email };
}
authorStats[author].commits++;
// Daily stats
const date = new Date(commit.date).toISOString().split('T')[0];
if (!dailyStats[date]) {
dailyStats[date] = 0;
}
dailyStats[date]++;
});
return {
totalCommits: log.all.length,
authors: authorStats,
daily: dailyStats
};
} catch (error) {
console.error('Error getting commit stats:', error);
return { totalCommits: 0, authors: {}, daily: {} };
}
}
module.exports = {
initRepo,
commitAndPush,
getBranches,
getCommitLogs,
getDetailedCommitHistory,
getCommitDiff,
getCommitFileChanges,
getCommitDetails,
getBranchGraph,
searchCommits,
getCommitStats
};

164
updater.js Normal file
View File

@@ -0,0 +1,164 @@
// updater.js - Moderner Auto-Updater für Git Manager GUI
const { app, shell } = require('electron');
const https = require('https');
const fs = require('fs');
const path = require('path');
const { spawn } = require('child_process');
const GITEA_API_URL = 'https://git.viper.ipv64.net/api/v1/repos/M_Viper/Git-Manager-Gui/releases';
class Updater {
constructor(mainWindow) {
this.mainWindow = mainWindow;
this.checkingForUpdates = false;
}
/**
* Hauptfunktion zur Prüfung auf Updates
*/
async checkForUpdates(silent = false) {
if (this.checkingForUpdates) return;
this.checkingForUpdates = true;
try {
const latestRelease = await this.getLatestRelease();
if (!latestRelease) return;
// Versionen säubern (nur Zahlen und Punkte)
const serverVer = latestRelease.tag_name.replace(/[^\d.]/g, '');
const localVer = app.getVersion().replace(/[^\d.]/g, '');
console.log(`[Updater] Version-Check: Server(${serverVer}) vs Lokal(${localVer})`);
if (this.compareVersions(serverVer, localVer) > 0) {
console.log("[Updater] Update verfügbar. Sende Daten an Renderer...");
this.mainWindow.webContents.send('update-available', {
version: serverVer,
body: latestRelease.body,
url: latestRelease.html_url,
asset: this.findAsset(latestRelease.assets)
});
} else {
console.log("[Updater] Anwendung ist auf dem neuesten Stand.");
}
} catch (error) {
console.error('[Updater] Fehler beim Update-Check:', error);
} finally {
this.checkingForUpdates = false;
}
}
async getLatestRelease() {
return new Promise((resolve, reject) => {
const options = { headers: { 'User-Agent': 'GitManager-GUI-Updater' } };
https.get(GITEA_API_URL, options, (res) => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => {
try {
const releases = JSON.parse(data);
resolve(Array.isArray(releases) ? releases[0] : null);
} catch (e) { reject(e); }
});
}).on('error', reject);
});
}
compareVersions(v1, v2) {
const a = v1.split('.').map(Number);
const b = v2.split('.').map(Number);
for (let i = 0; i < 3; i++) {
if ((a[i] || 0) > (b[i] || 0)) return 1;
if ((a[i] || 0) < (b[i] || 0)) return -1;
}
return 0;
}
findAsset(assets) {
if (!assets) return null;
const ext = process.platform === 'win32' ? '.exe' : '.AppImage';
return assets.find(a => a.name.toLowerCase().endsWith(ext));
}
/**
* Startet den Download und führt die Installation aus
*/
async startDownload(asset) {
if (!asset || !asset.browser_download_url) {
console.error("[Updater] Kein gültiges Asset gefunden!");
return;
}
const tempPath = path.join(app.getPath('temp'), asset.name);
console.log(`[Updater] Download gestartet: ${asset.name} -> ${tempPath}`);
const file = fs.createWriteStream(tempPath);
const download = (url) => {
https.get(url, { headers: { 'User-Agent': 'Electron-Updater' } }, (res) => {
// Handle Redirects
if (res.statusCode === 301 || res.statusCode === 302) {
return download(res.headers.location);
}
if (res.statusCode !== 200) {
console.error(`[Updater] Download-Fehler: Status ${res.statusCode}`);
return;
}
res.pipe(file);
file.on('finish', () => {
file.close();
console.log("[Updater] Download abgeschlossen. Initialisiere entkoppelten Installer...");
this.installAndQuit(tempPath);
});
}).on('error', (err) => {
fs.unlink(tempPath, () => {});
console.error("[Updater] Netzwerkfehler beim Download:", err);
});
};
download(asset.browser_download_url);
}
/**
* Führt den Installer entkoppelt aus und schließt die App mit Verzögerung
*/
installAndQuit(filePath) {
console.log(`[Updater] Bereite Installation vor: ${filePath}`);
if (process.platform === 'win32') {
// Wir nutzen spawn mit detached: true, damit der Installer weiterläuft,
// wenn der Hauptprozess (Electron) beendet wird.
try {
const child = spawn('cmd.exe', ['/c', 'start', '""', filePath], {
detached: true,
stdio: 'ignore'
});
child.unref(); // Trennt die Referenz zum Installer
console.log("[Updater] Installer-Prozess entkoppelt gestartet. App schließt in 2 Sek...");
// WICHTIG: Wir geben Windows Zeit, den Installer stabil zu laden,
// bevor wir die App schließen und die Dateisperren aufheben.
setTimeout(() => {
app.quit();
}, 2000);
} catch (err) {
console.error("[Updater] Kritischer Fehler beim Installer-Start:", err);
// Notfall-Versuch über shell
shell.openPath(filePath).then(() => app.quit());
}
} else {
// Linux/AppImage
shell.openPath(filePath).then(() => {
setTimeout(() => app.quit(), 1000);
});
}
}
}
module.exports = Updater;