Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| eece161e58 | |||
| a35d6cf0cf | |||
| 594f12e927 | |||
| 8d70b71d66 | |||
| 2c39ae4651 | |||
| 9c14a1a7e0 | |||
| 677b983706 | |||
| 20a9d601ff | |||
| df9790177d | |||
| 1621a100af | |||
| 3f2e1d1e30 | |||
| e7b3c9c447 | |||
| e8474957e8 | |||
| 94e8c21e9b | |||
| 7d18161141 | |||
| d88eade5a0 | |||
| 6895a042d1 | |||
| e379f6a096 | |||
| fca9d9c66f | |||
| fd868ea238 | |||
| 1a76dc4e64 | |||
| a0c964e05e | |||
| e109082d56 | |||
| 2c93a139d5 | |||
| 7bd8668e79 | |||
| 19be614284 | |||
| 0272756be7 |
BIN
assets/Thumbs.db
Normal file
BIN
assets/Thumbs.db
Normal file
Binary file not shown.
BIN
assets/icon.png
Normal file
BIN
assets/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 67 KiB |
689
main.js
689
main.js
@@ -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));
|
||||||
|
|
||||||
|
// 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');
|
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
13
package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
100
preload.js
100
preload.js
@@ -2,43 +2,99 @@
|
|||||||
const { contextBridge, ipcRenderer } = require('electron');
|
const { contextBridge, ipcRenderer } = require('electron');
|
||||||
|
|
||||||
contextBridge.exposeInMainWorld('electronAPI', {
|
contextBridge.exposeInMainWorld('electronAPI', {
|
||||||
|
// Lokale Datei-Operationen
|
||||||
selectFolder: () => ipcRenderer.invoke('select-folder'),
|
selectFolder: () => ipcRenderer.invoke('select-folder'),
|
||||||
selectFile: () => ipcRenderer.invoke('select-file'),
|
selectFile: () => ipcRenderer.invoke('select-file'),
|
||||||
|
getFileTree: (data) => ipcRenderer.invoke('getFileTree', data),
|
||||||
|
readFile: (data) => ipcRenderer.invoke('readFile', data),
|
||||||
|
writeFile: (data) => ipcRenderer.invoke('writeFile', data),
|
||||||
|
deleteFile: (data) => ipcRenderer.invoke('deleteFile', data),
|
||||||
|
|
||||||
|
// Gitea Datei-Operationen
|
||||||
|
listGiteaRepos: (data) => ipcRenderer.invoke('list-gitea-repos', data),
|
||||||
|
getGiteaRepoContents: (data) => ipcRenderer.invoke('get-gitea-repo-contents', data),
|
||||||
|
getGiteaFileContent: (data) => ipcRenderer.invoke('get-gitea-file-content', data),
|
||||||
|
readGiteaFile: (data) => ipcRenderer.invoke('read-gitea-file', data),
|
||||||
|
writeGiteaFile: (data) => ipcRenderer.invoke('write-gitea-file', data),
|
||||||
|
uploadGiteaFile: (data) => ipcRenderer.invoke('upload-gitea-file', data),
|
||||||
|
downloadGiteaFolder: (data) => ipcRenderer.invoke('download-gitea-folder', data),
|
||||||
|
downloadGiteaFile: (data) => ipcRenderer.invoke('download-gitea-file', data),
|
||||||
|
uploadLocalFolderToGitea: (data) => ipcRenderer.invoke('upload-local-folder-to-gitea', data),
|
||||||
|
|
||||||
|
// Repository & Git Management
|
||||||
saveCredentials: (data) => ipcRenderer.invoke('save-credentials', data),
|
saveCredentials: (data) => ipcRenderer.invoke('save-credentials', data),
|
||||||
loadCredentials: () => ipcRenderer.invoke('load-credentials'),
|
loadCredentials: () => ipcRenderer.invoke('load-credentials'),
|
||||||
createRepo: (data) => ipcRenderer.invoke('create-repo', data),
|
createRepo: (data) => ipcRenderer.invoke('create-repo', data),
|
||||||
pushProject: (data) => ipcRenderer.invoke('push-project', data),
|
pushProject: (data) => ipcRenderer.invoke('push-project', data),
|
||||||
getBranches: (data) => ipcRenderer.invoke('getBranches', data),
|
getBranches: (data) => ipcRenderer.invoke('getBranches', data),
|
||||||
getCommitLogs: (data) => ipcRenderer.invoke('getCommitLogs', data),
|
getCommitLogs: (data) => ipcRenderer.invoke('getCommitLogs', data),
|
||||||
getFileTree: (data) => ipcRenderer.invoke('getFileTree', data),
|
|
||||||
readFile: (data) => ipcRenderer.invoke('readFile', data),
|
|
||||||
deleteFile: (data) => ipcRenderer.invoke('deleteFile', data),
|
|
||||||
listGiteaRepos: (data) => ipcRenderer.invoke('list-gitea-repos', data),
|
|
||||||
getGiteaRepoContents: (data) => ipcRenderer.invoke('get-gitea-repo-contents', data),
|
|
||||||
getGiteaFileContent: (data) => ipcRenderer.invoke('get-gitea-file-content', 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),
|
|
||||||
downloadGiteaFile: (data) => ipcRenderer.invoke('download-gitea-file', data),
|
|
||||||
prepareDownloadDrag: (data) => ipcRenderer.invoke('prepare-download-drag', data),
|
|
||||||
startNativeDrag: (filePath) => ipcRenderer.send('ondragstart', filePath),
|
|
||||||
uploadAndPush: (data) => ipcRenderer.invoke('upload-and-push', data),
|
uploadAndPush: (data) => ipcRenderer.invoke('upload-and-push', data),
|
||||||
deleteGiteaRepo: (data) => ipcRenderer.invoke('delete-gitea-repo', data),
|
deleteGiteaRepo: (data) => ipcRenderer.invoke('delete-gitea-repo', data),
|
||||||
|
|
||||||
// progress subscriptions
|
// Drag & Drop
|
||||||
|
prepareDownloadDrag: (data) => ipcRenderer.invoke('prepare-download-drag', data),
|
||||||
|
startNativeDrag: (filePath) => ipcRenderer.send('ondragstart', filePath),
|
||||||
|
|
||||||
|
// Release Management
|
||||||
|
listReleases: (data) => ipcRenderer.invoke('list-releases', data),
|
||||||
|
getRelease: (data) => ipcRenderer.invoke('get-release', data),
|
||||||
|
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) => {
|
onPushProgress: (cb) => {
|
||||||
ipcRenderer.on('push-progress', (event, percent) => {
|
const listener = (event, percent) => { try { cb(percent); } catch (_) {} };
|
||||||
try { cb(percent); } catch (_) {}
|
ipcRenderer.on('push-progress', listener);
|
||||||
});
|
return () => ipcRenderer.removeListener('push-progress', listener);
|
||||||
},
|
},
|
||||||
|
|
||||||
onFolderUploadProgress: (cb) => {
|
onFolderUploadProgress: (cb) => {
|
||||||
ipcRenderer.on('folder-upload-progress', (event, payload) => {
|
const listener = (event, payload) => { try { cb(payload); } catch (_) {} };
|
||||||
try { cb(payload); } catch (_) {}
|
ipcRenderer.on('folder-upload-progress', listener);
|
||||||
});
|
return () => ipcRenderer.removeListener('folder-upload-progress', listener);
|
||||||
},
|
},
|
||||||
|
|
||||||
onFolderDownloadProgress: (cb) => {
|
onFolderDownloadProgress: (cb) => {
|
||||||
ipcRenderer.on('folder-download-progress', (event, payload) => {
|
const listener = (event, payload) => { try { cb(payload); } catch (_) {} };
|
||||||
try { cb(payload); } catch (_) {}
|
ipcRenderer.on('folder-download-progress', listener);
|
||||||
});
|
return () => ipcRenderer.removeListener('folder-download-progress', listener);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -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>
|
||||||
|
|
||||||
<div id="main">
|
<div class="tool-group">
|
||||||
<aside id="explorerPanel" class="card">
|
<select id="platform" title="Plattform auswählen">
|
||||||
<h3>Project Files</h3>
|
<option value="gitea" selected>Gitea</option>
|
||||||
<div id="fileTree" class="file-tree"></div>
|
<option value="github">GitHub</option>
|
||||||
</aside>
|
</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>
|
||||||
|
|
||||||
<section id="detailsPanel">
|
<span id="status" class="status">Bereit</span>
|
||||||
<div id="repoDetails" class="card">
|
</div>
|
||||||
<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 -->
|
<main id="main">
|
||||||
<div style="margin-bottom: 5px;">
|
<div id="explorerGrid" class="explorer-grid">
|
||||||
<label for="licenseSelect">License:</label>
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<div id="settingsModal" class="modal hidden">
|
||||||
|
<div class="modalContent card">
|
||||||
|
<h2>⚙️ Einstellungen</h2>
|
||||||
|
|
||||||
|
<div class="input-group">
|
||||||
|
<label>GitHub Token</label>
|
||||||
|
<input id="githubToken" type="password" placeholder="ghp_...">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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 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">
|
<select id="licenseSelect">
|
||||||
<option value="">No License</option>
|
<option value="">Keine Lizenz</option>
|
||||||
<option value="MIT">MIT License</option>
|
<option value="mit">MIT License</option>
|
||||||
<option value="Apache-2.0">Apache License 2.0</option>
|
<option value="apache-2.0">Apache 2.0</option>
|
||||||
<option value="GPL-3.0">GNU General Public License v3.0</option>
|
<option value="gpl-3.0">GPL v3</option>
|
||||||
<option value="BSD-3-Clause">BSD 3-Clause "New" or "Revised" License</option>
|
<option value="bsd-3-clause">BSD 3-Clause</option>
|
||||||
<option value="BSD-2-Clause">BSD 2-Clause "Simplified" License</option>
|
<option value="agpl-3.0">AGPL v3</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>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- NEU: README Checkbox -->
|
<div class="input-group">
|
||||||
<div style="margin-bottom: 10px;">
|
<label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
|
||||||
<label>
|
|
||||||
<input type="checkbox" id="createReadme" checked>
|
<input type="checkbox" id="createReadme" checked>
|
||||||
Initialize Repository with a README
|
<span style="text-transform: none; letter-spacing: normal;">README.md initialisieren</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div><button id="btnCreateRepo">Create Repo</button></div>
|
<div class="input-group">
|
||||||
<div style="margin-top:10px;"><button id="btnLoadGiteaRepos">Load My Gitea Repos</button>
|
<label>Lokaler Push-Branch</label>
|
||||||
<div id="giteaRepoContainer"></div>
|
<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>
|
</div>
|
||||||
|
|
||||||
<div id="previewArea" class="card">
|
<div id="fileEditorModal" class="modal hidden">
|
||||||
<h3 id="previewTitle">Preview</h3>
|
<div class="file-editor-card">
|
||||||
<pre id="previewContent" class="preview-content">Select a file to preview</pre>
|
<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>
|
||||||
|
|
||||||
<div id="commitLogs" class="card">
|
<div id="fileEditorTabs" class="file-editor-tabs"></div>
|
||||||
<h3>Commit Logs</h3>
|
|
||||||
<div id="logs">No commits yet.</div>
|
<div id="searchBar" class="search-bar hidden">
|
||||||
</div>
|
<input id="searchInput" type="text" class="search-input" placeholder="Suchen...">
|
||||||
</section>
|
<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>
|
||||||
|
|
||||||
<!-- Settings Modal -->
|
<div class="file-editor-container">
|
||||||
<div id="settingsModal" class="modal hidden">
|
<div id="lineNumbers" class="line-numbers"></div>
|
||||||
<div class="modalContent card">
|
<textarea id="fileEditorContent" class="file-editor-textarea" placeholder="Dateiinhalt..."></textarea>
|
||||||
<h2>Settings</h2>
|
<div id="imagePreview" class="image-preview hidden"></div>
|
||||||
<label>GitHub Token</label><input id="githubToken" type="password">
|
</div>
|
||||||
<label>Gitea Token</label><input id="giteaToken" type="password">
|
|
||||||
<label>Gitea URL</label><input id="giteaURL" type="text" placeholder="https://gitea.example.com">
|
<div class="file-editor-footer">
|
||||||
<div style="margin-top:10px;">
|
<div class="file-editor-info">
|
||||||
<button id="btnSaveSettings">Save</button>
|
<span id="fileEditorPath" class="file-path">Pfad: /path/to/file</span>
|
||||||
<button id="btnCloseSettings">Close</button>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="./renderer.js"></script>
|
<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>
|
||||||
3706
renderer/renderer.js
3706
renderer/renderer.js
File diff suppressed because it is too large
Load Diff
1428
renderer/style.css
1428
renderer/style.css
File diff suppressed because it is too large
Load Diff
@@ -39,10 +39,36 @@ async function createRepoGitHub({ name, token, auto_init = true, license = '', p
|
|||||||
body.license_template = license;
|
body.license_template = license;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
const response = await axios.post('https://api.github.com/user/repos', body, {
|
const response = await axios.post('https://api.github.com/user/repos', body, {
|
||||||
headers: { Authorization: `token ${token}` }
|
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
|
||||||
};
|
};
|
||||||
@@ -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
164
updater.js
Normal 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;
|
||||||
Reference in New Issue
Block a user