Upload via GUI (7543 Dateien)
This commit is contained in:
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(node -e \"require\\('./src/git/apiHandler.js'\\); console.log\\('apiHandler OK'\\)\")",
|
||||||
|
"Bash(node --check main.js)",
|
||||||
|
"Bash(node --check renderer/renderer.js)",
|
||||||
|
"Bash(node -e \"require\\('./src/git/apiHandler.js'\\)\")",
|
||||||
|
"Bash(node --check preload.js)",
|
||||||
|
"Bash(node --test __tests__/*.test.js)",
|
||||||
|
"Bash(node --check updater.js)",
|
||||||
|
"Bash(node -e \"require\\('./src/git/apiHandler.js'\\); require\\('./src/git/gitHandler.js'\\)\")",
|
||||||
|
"Bash(node -e ' *)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
+1394
-1394
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,116 @@
|
|||||||
|
const test = require('node:test');
|
||||||
|
const assert = require('node:assert');
|
||||||
|
const http = require('node:http');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const os = require('node:os');
|
||||||
|
const path = require('node:path');
|
||||||
|
|
||||||
|
require('../src/git/gitBinary.js').useBundledGit();
|
||||||
|
const { registerFeatureIpc } = require('../src/features/featuresIpc.js');
|
||||||
|
|
||||||
|
// Drei Repos; das ZIP von "langsam" kommt in kleinen Stücken über ~3 s, damit ein Abbruch mitten hineinfällt
|
||||||
|
function fakeGitea() {
|
||||||
|
const server = http.createServer((req, res) => {
|
||||||
|
const u = new URL(req.url, 'http://x');
|
||||||
|
const send = (code, obj) => { res.statusCode = code; res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify(obj)); };
|
||||||
|
if (u.pathname === '/api/v1/user/repos') {
|
||||||
|
if (u.searchParams.get('page') !== '1') return send(200, []);
|
||||||
|
const port = server.address().port;
|
||||||
|
return send(200, ['eins', 'langsam', 'drei'].map(name => ({
|
||||||
|
name, owner: { login: 'M_Viper' }, default_branch: 'main', empty: false,
|
||||||
|
clone_url: `http://127.0.0.1:${port}/M_Viper/${name}.git`
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
const m = u.pathname.match(/^\/api\/v1\/repos\/M_Viper\/(\w+)\/archive\/main\.zip$/);
|
||||||
|
if (m) {
|
||||||
|
const total = 64 * 1024;
|
||||||
|
res.writeHead(200, { 'Content-Type': 'application/zip', 'Content-Length': String(total) });
|
||||||
|
if (m[1] !== 'langsam') return res.end(Buffer.alloc(total, 1));
|
||||||
|
let sent = 0;
|
||||||
|
const t = setInterval(() => {
|
||||||
|
if (res.destroyed) return clearInterval(t);
|
||||||
|
res.write(Buffer.alloc(2048, 1));
|
||||||
|
sent += 2048;
|
||||||
|
if (sent >= total) { clearInterval(t); res.end(); }
|
||||||
|
}, 100);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
send(404, { message: 'not found' });
|
||||||
|
});
|
||||||
|
return new Promise(r => server.listen(0, '127.0.0.1', () => r({ server, url: `http://127.0.0.1:${server.address().port}` })));
|
||||||
|
}
|
||||||
|
|
||||||
|
function setup(url) {
|
||||||
|
const handlers = {};
|
||||||
|
const events = [];
|
||||||
|
registerFeatureIpc({
|
||||||
|
ipcMain: { handle: (name, fn) => { handlers[name] = fn; } },
|
||||||
|
app: { getPath: () => fs.mkdtempSync(path.join(os.tmpdir(), 'gm-ud-')) },
|
||||||
|
readCredentials: () => ({ giteaURL: url, giteaToken: 't' }),
|
||||||
|
buildItemsFromPaths: async () => ({ items: [], skipped: [], ignored: [] }),
|
||||||
|
clearGitTransportBlock: () => false,
|
||||||
|
broadcast: (channel, p) => { if (channel === 'repo-backup-progress') events.push(p); }
|
||||||
|
});
|
||||||
|
return { call: (name, data) => handlers[name]({ sender: { send() {} } }, data), events };
|
||||||
|
}
|
||||||
|
|
||||||
|
test('Backup meldet Fortschritt: Phasen, aktuelles Repo, Bytes und Ergebnis je Repo', async () => {
|
||||||
|
const g = await fakeGitea();
|
||||||
|
try {
|
||||||
|
const { call, events } = setup(g.url);
|
||||||
|
const target = fs.mkdtempSync(path.join(os.tmpdir(), 'gm-bk-'));
|
||||||
|
await call('repo-backup-set-config', { targetDir: target });
|
||||||
|
const r = await call('repo-backup-run-now', {});
|
||||||
|
assert.strictEqual(r.summary.ok, 3, JSON.stringify(r));
|
||||||
|
|
||||||
|
const phases = [...new Set(events.map(e => e.phase))];
|
||||||
|
assert.deepStrictEqual(phases, ['start', 'list', 'running', 'done']);
|
||||||
|
assert.deepStrictEqual(events.find(e => e.phase === 'running').repos, ['M_Viper/eins', 'M_Viper/langsam', 'M_Viper/drei']);
|
||||||
|
assert.ok(events.some(e => e.repo === 'M_Viper/langsam' && e.bytes > 0 && e.bytesTotal === 64 * 1024), 'Download-Fortschritt fehlt');
|
||||||
|
const last = events[events.length - 1];
|
||||||
|
assert.strictEqual(last.results.length, 3);
|
||||||
|
assert.ok(last.results.every(x => x.ok && x.method === 'ZIP'));
|
||||||
|
assert.ok(!events.some(e => 'abort' in e), 'interne Felder dürfen nicht nach außen gehen');
|
||||||
|
|
||||||
|
// Während kein Backup läuft, gibt es auch keinen Zwischenstand
|
||||||
|
const state = await call('repo-backup-get-config', {});
|
||||||
|
assert.strictEqual(state.running, false);
|
||||||
|
assert.strictEqual(state.progress, null);
|
||||||
|
} finally {
|
||||||
|
g.server.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Abbrechen beendet den laufenden ZIP-Download sofort und überspringt den Rest', async () => {
|
||||||
|
const g = await fakeGitea();
|
||||||
|
try {
|
||||||
|
const { call, events } = setup(g.url);
|
||||||
|
const target = fs.mkdtempSync(path.join(os.tmpdir(), 'gm-bk-'));
|
||||||
|
await call('repo-backup-set-config', { targetDir: target });
|
||||||
|
const started = Date.now();
|
||||||
|
const running = call('repo-backup-run-now', {});
|
||||||
|
|
||||||
|
// warten, bis der langsame Download läuft
|
||||||
|
while (!events.some(e => e.repo === 'M_Viper/langsam' && e.bytes > 0)) {
|
||||||
|
if (Date.now() - started > 10000) throw new Error('Download startete nicht');
|
||||||
|
await new Promise(r => setTimeout(r, 50));
|
||||||
|
}
|
||||||
|
const state = await call('repo-backup-get-config', {});
|
||||||
|
assert.strictEqual(state.running, true);
|
||||||
|
assert.strictEqual(state.progress.repo, 'M_Viper/langsam');
|
||||||
|
|
||||||
|
assert.strictEqual((await call('repo-backup-cancel', {})).ok, true);
|
||||||
|
const r = await running;
|
||||||
|
assert.ok(Date.now() - started < 2500, 'Abbruch muss den Download sofort beenden');
|
||||||
|
assert.strictEqual(r.ok, false);
|
||||||
|
assert.strictEqual(r.summary.cancelled, true);
|
||||||
|
assert.strictEqual(r.summary.ok, 1);
|
||||||
|
assert.strictEqual(r.summary.skipped, 2);
|
||||||
|
// keine halbe Datei zurücklassen
|
||||||
|
const archiv = path.join(target, 'M_Viper', 'langsam-archiv');
|
||||||
|
assert.deepStrictEqual(fs.existsSync(archiv) ? fs.readdirSync(archiv) : [], []);
|
||||||
|
assert.strictEqual((await call('repo-backup-cancel', {})).ok, false);
|
||||||
|
} finally {
|
||||||
|
g.server.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
const test = require('node:test');
|
||||||
|
const assert = require('node:assert');
|
||||||
|
const http = require('node:http');
|
||||||
|
const { deleteGiteaPathsBatched } = require('../src/git/apiHandler.js');
|
||||||
|
|
||||||
|
// Minimaler Gitea-Nachbau: Repo-Info, Trees-API (seitenweise) und Mehrfach-Commit-Endpunkt.
|
||||||
|
function fakeGitea(paths, { perPage = 1000, failPath = null } = {}) {
|
||||||
|
const files = new Map(paths.map((p, i) => [p, `sha${i}`]));
|
||||||
|
const commits = [];
|
||||||
|
const server = http.createServer((req, res) => {
|
||||||
|
const u = new URL(req.url, 'http://x');
|
||||||
|
const send = (code, obj) => { res.statusCode = code; res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify(obj)); };
|
||||||
|
if (req.method === 'GET' && u.pathname === '/api/v1/repos/o/r') return send(200, { default_branch: 'main' });
|
||||||
|
if (req.method === 'GET' && u.pathname === '/api/v1/repos/o/r/git/trees/main') {
|
||||||
|
const page = Number(u.searchParams.get('page') || 1);
|
||||||
|
const all = [...files.keys()].map(p => ({ path: p, type: 'blob', sha: files.get(p) }));
|
||||||
|
const slice = all.slice((page - 1) * perPage, page * perPage);
|
||||||
|
return send(200, { tree: slice, truncated: page * perPage < all.length, total_count: all.length, page });
|
||||||
|
}
|
||||||
|
if (req.method === 'POST' && u.pathname === '/api/v1/repos/o/r/contents') {
|
||||||
|
let body = '';
|
||||||
|
req.on('data', c => body += c);
|
||||||
|
req.on('end', () => {
|
||||||
|
const { files: ops } = JSON.parse(body);
|
||||||
|
if (failPath && ops.some(o => o.path === failPath)) return send(422, { message: 'kaputt' });
|
||||||
|
for (const o of ops) {
|
||||||
|
assert.strictEqual(o.operation, 'delete');
|
||||||
|
assert.strictEqual(o.sha, files.get(o.path));
|
||||||
|
files.delete(o.path);
|
||||||
|
}
|
||||||
|
commits.push(ops.length);
|
||||||
|
send(201, {});
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
send(404, { message: 'object does not exist' });
|
||||||
|
});
|
||||||
|
return new Promise(r => server.listen(0, '127.0.0.1', () => r({ server, files, commits, url: `http://127.0.0.1:${server.address().port}` })));
|
||||||
|
}
|
||||||
|
|
||||||
|
test('Ordner wird paketweise gelöscht, Nachbarn bleiben', async () => {
|
||||||
|
const paths = [...Array.from({ length: 2500 }, (_, i) => `mobile/build/f${i}.dex`), 'mobile/build.gradle.kts', 'mobile/src/A.kt'];
|
||||||
|
const g = await fakeGitea(paths, { perPage: 1000 });
|
||||||
|
try {
|
||||||
|
const r = await deleteGiteaPathsBatched({ token: 't', url: g.url, owner: 'o', repo: 'r', prefix: 'mobile/build' });
|
||||||
|
assert.strictEqual(r.deleted, 2500);
|
||||||
|
assert.deepStrictEqual(g.commits, Array(25).fill(100));
|
||||||
|
assert.deepStrictEqual([...g.files.keys()].sort(), ['mobile/build.gradle.kts', 'mobile/src/A.kt']);
|
||||||
|
} finally { g.server.close(); }
|
||||||
|
});
|
||||||
|
|
||||||
|
test('eine fehlerhafte Datei blockiert nicht das ganze Paket', async () => {
|
||||||
|
const paths = Array.from({ length: 10 }, (_, i) => `x/f${i}`);
|
||||||
|
const g = await fakeGitea(paths, { failPath: 'x/f3' });
|
||||||
|
try {
|
||||||
|
const r = await deleteGiteaPathsBatched({ token: 't', url: g.url, owner: 'o', repo: 'r', paths });
|
||||||
|
assert.strictEqual(r.deleted, 9);
|
||||||
|
assert.deepStrictEqual(r.failed.map(f => f.path), ['x/f3']);
|
||||||
|
} finally { g.server.close(); }
|
||||||
|
});
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
const test = require('node:test');
|
||||||
|
const assert = require('node:assert');
|
||||||
|
const http = require('node:http');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const os = require('node:os');
|
||||||
|
const path = require('node:path');
|
||||||
|
const { spawnSync } = require('node:child_process');
|
||||||
|
|
||||||
|
require('../src/git/gitBinary.js').useBundledGit();
|
||||||
|
const { registerFeatureIpc, parsePorcelainZ } = require('../src/features/featuresIpc.js');
|
||||||
|
const { scanForSecrets, checkName } = require('../src/features/secretScan.js');
|
||||||
|
|
||||||
|
// Minimaler Gitea-Nachbau für Diagnose, PRs/Issues und Backup
|
||||||
|
function fakeGitea() {
|
||||||
|
const calls = [];
|
||||||
|
const comments = [];
|
||||||
|
const server = http.createServer((req, res) => {
|
||||||
|
const u = new URL(req.url, 'http://x');
|
||||||
|
calls.push(`${req.method} ${u.pathname}`);
|
||||||
|
const send = (code, obj) => { res.statusCode = code; res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify(obj)); };
|
||||||
|
let body = '';
|
||||||
|
req.on('data', c => body += c);
|
||||||
|
req.on('end', () => {
|
||||||
|
const p = u.pathname;
|
||||||
|
if (p === '/api/v1/version') return send(200, { version: '1.24.0' });
|
||||||
|
if (p === '/api/v1/user') return send(200, { login: 'M_Viper' });
|
||||||
|
if (p === '/api/v1/user/repos') {
|
||||||
|
if (u.searchParams.get('page') && u.searchParams.get('page') !== '1') return send(200, []);
|
||||||
|
return send(200, [{ name: 'deskhub', owner: { login: 'M_Viper' }, default_branch: 'main', empty: false,
|
||||||
|
clone_url: `http://127.0.0.1:${server.address().port}/M_Viper/deskhub.git` }]);
|
||||||
|
}
|
||||||
|
if (p === '/api/v1/repos/M_Viper/deskhub/pulls') return send(200, [{ number: 3, title: 'Fix', state: 'open', user: { login: 'a' }, labels: [], head: { label: 'fix' }, base: { label: 'main' }, mergeable: true }]);
|
||||||
|
if (p === '/api/v1/repos/M_Viper/deskhub/issues' && req.method === 'GET') return send(200, [{ number: 5, title: 'Bug', state: 'open', labels: [{ name: 'bug', color: 'ff0000' }] }]);
|
||||||
|
if (p === '/api/v1/repos/M_Viper/deskhub/issues' && req.method === 'POST') return send(201, { number: 6, ...JSON.parse(body), state: 'open', labels: [] });
|
||||||
|
if (p === '/api/v1/repos/M_Viper/deskhub/issues/5/comments' && req.method === 'POST') { comments.push(JSON.parse(body).body); return send(201, {}); }
|
||||||
|
if (p === '/api/v1/repos/M_Viper/deskhub/issues/5/comments') return send(200, comments.map((b, i) => ({ id: i, body: b, user: { login: 'M_Viper' } })));
|
||||||
|
if (p === '/api/v1/repos/M_Viper/deskhub/issues/5') return send(200, { number: 5, title: 'Bug', state: 'open', labels: [] });
|
||||||
|
if (p === '/api/v1/repos/M_Viper/deskhub/pulls/3/merge') return send(200, {});
|
||||||
|
if (p === '/api/v1/repos/M_Viper/deskhub/archive/main.zip') {
|
||||||
|
res.statusCode = 200; res.setHeader('Content-Type', 'application/zip'); return res.end(Buffer.from('PK\u0003\u0004fake'));
|
||||||
|
}
|
||||||
|
send(404, { message: 'not found' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return new Promise(r => server.listen(0, '127.0.0.1', () => r({ server, calls, comments, url: `http://127.0.0.1:${server.address().port}` })));
|
||||||
|
}
|
||||||
|
|
||||||
|
function setup(url) {
|
||||||
|
const handlers = {};
|
||||||
|
const userData = fs.mkdtempSync(path.join(os.tmpdir(), 'gm-ud-'));
|
||||||
|
registerFeatureIpc({
|
||||||
|
ipcMain: { handle: (name, fn) => { handlers[name] = fn; } },
|
||||||
|
app: { getPath: () => userData },
|
||||||
|
readCredentials: () => ({ giteaURL: url, giteaToken: 'geheim123' }),
|
||||||
|
buildItemsFromPaths: async (paths) => ({
|
||||||
|
items: paths.map(p => ({ localFile: p, targetPath: path.basename(p) })), skipped: [], ignored: []
|
||||||
|
}),
|
||||||
|
clearGitTransportBlock: () => false
|
||||||
|
});
|
||||||
|
const call = (name, data) => handlers[name]({ sender: { send() {} } }, data);
|
||||||
|
return { call, userData };
|
||||||
|
}
|
||||||
|
|
||||||
|
test('Geheimnisse werden an Name und Inhalt erkannt, Vorlagen nicht', async () => {
|
||||||
|
assert.ok(checkName('google-services.json'));
|
||||||
|
assert.ok(checkName('.env'));
|
||||||
|
assert.ok(checkName('release.keystore'));
|
||||||
|
assert.strictEqual(checkName('.env.example'), null);
|
||||||
|
assert.strictEqual(checkName('README.md'), null);
|
||||||
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gm-sec-'));
|
||||||
|
const bot = path.join(dir, 'bot.js');
|
||||||
|
fs.writeFileSync(bot, 'const TOKEN = "123456789:AA' + 'b'.repeat(33) + '";');
|
||||||
|
const clean = path.join(dir, 'app.js');
|
||||||
|
fs.writeFileSync(clean, 'console.log("hallo")');
|
||||||
|
const found = await scanForSecrets([{ localFile: bot, size: fs.statSync(bot).size }, { localFile: clean, size: fs.statSync(clean).size }]);
|
||||||
|
assert.match(found.get(bot), /Telegram/);
|
||||||
|
assert.strictEqual(found.has(clean), false);
|
||||||
|
fs.rmSync(dir, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('git status -z wird korrekt gelesen (inkl. Umbenennung)', () => {
|
||||||
|
const files = parsePorcelainZ(' M src/a.js\0?? neu.txt\0R neu/name.js\0alt/name.js\0D weg.txt\0');
|
||||||
|
assert.deepStrictEqual(files.map(f => [f.code, f.path, f.from]), [['M', 'src/a.js', null], ['?', 'neu.txt', null], ['R', 'neu/name.js', 'alt/name.js'], ['D', 'weg.txt', null]]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Lokale Änderungen: Status, Diff und Commit nur der ausgewählten Dateien', async () => {
|
||||||
|
const { call } = setup('http://127.0.0.1:1');
|
||||||
|
const repo = fs.mkdtempSync(path.join(os.tmpdir(), 'gm-local-'));
|
||||||
|
const git = (...a) => spawnSync('git', ['-C', repo, '-c', 'user.name=t', '-c', 'user.email=t@t', ...a], { encoding: 'utf8' });
|
||||||
|
git('init', '-q');
|
||||||
|
fs.writeFileSync(path.join(repo, 'a.txt'), 'eins\n');
|
||||||
|
git('add', '.'); git('commit', '-qm', 'start');
|
||||||
|
fs.writeFileSync(path.join(repo, 'a.txt'), 'zwei\n');
|
||||||
|
fs.writeFileSync(path.join(repo, 'b.txt'), 'neu\n');
|
||||||
|
|
||||||
|
const st = await call('local-git', { action: 'status', folder: repo });
|
||||||
|
assert.strictEqual(st.ok, true);
|
||||||
|
assert.deepStrictEqual(st.files.map(f => f.path).sort(), ['a.txt', 'b.txt']);
|
||||||
|
|
||||||
|
const diff = await call('local-git', { action: 'diff', folder: repo, file: 'a.txt', code: 'M' });
|
||||||
|
assert.match(diff.diff, /-eins/);
|
||||||
|
assert.match(diff.diff, /\+zwei/);
|
||||||
|
|
||||||
|
const c = await call('local-git', { action: 'commit', folder: repo, files: ['a.txt'], message: 'nur a' });
|
||||||
|
assert.strictEqual(c.ok, true, c.error);
|
||||||
|
const after = await call('local-git', { action: 'status', folder: repo });
|
||||||
|
assert.deepStrictEqual(after.files.map(f => f.path), ['b.txt']);
|
||||||
|
|
||||||
|
const notRepo = await call('local-git', { action: 'status', folder: os.tmpdir() + path.sep + 'gm-gibtsnicht-' + Date.now() });
|
||||||
|
assert.strictEqual(notRepo.ok, false);
|
||||||
|
fs.rmSync(repo, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Upload-Vorschau markiert Geheimnisse', async () => {
|
||||||
|
const { call } = setup('http://127.0.0.1:1');
|
||||||
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gm-prev-'));
|
||||||
|
const a = path.join(dir, 'main.kt'); fs.writeFileSync(a, 'fun main() {}');
|
||||||
|
const g = path.join(dir, 'google-services.json'); fs.writeFileSync(g, '{}');
|
||||||
|
const r = await call('preview-upload-paths', { paths: [a, g], destPath: '' });
|
||||||
|
assert.strictEqual(r.ok, true);
|
||||||
|
assert.strictEqual(r.secretCount, 1);
|
||||||
|
assert.ok(r.files.find(f => f.localFile === g).secret);
|
||||||
|
fs.rmSync(dir, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Diagnose, PRs/Issues und Backup gegen einen nachgebauten Server', async () => {
|
||||||
|
const g = await fakeGitea();
|
||||||
|
const { call } = setup(g.url);
|
||||||
|
try {
|
||||||
|
const d = await call('run-server-diagnosis', {});
|
||||||
|
const byLabel = Object.fromEntries(d.checks.map(c => [c.label, c.status]));
|
||||||
|
assert.strictEqual(byLabel['Gitea-API'], 'ok');
|
||||||
|
assert.strictEqual(byLabel['Token'], 'ok');
|
||||||
|
assert.strictEqual(byLabel['Erreichbar über IPv4'], 'ok');
|
||||||
|
assert.ok(!JSON.stringify(d).includes('geheim123'), 'Token darf nicht in der Diagnose stehen');
|
||||||
|
|
||||||
|
const pulls = await call('gitea-collab', { action: 'listPulls', owner: 'M_Viper', repo: 'deskhub' });
|
||||||
|
assert.strictEqual(pulls.items[0].head, 'fix');
|
||||||
|
const issues = await call('gitea-collab', { action: 'listIssues', owner: 'M_Viper', repo: 'deskhub' });
|
||||||
|
assert.strictEqual(issues.items[0].labels[0].name, 'bug');
|
||||||
|
assert.strictEqual((await call('gitea-collab', { action: 'comment', owner: 'M_Viper', repo: 'deskhub', index: 5, body: 'Danke' })).ok, true);
|
||||||
|
const detail = await call('gitea-collab', { action: 'get', owner: 'M_Viper', repo: 'deskhub', index: 5 });
|
||||||
|
assert.strictEqual(detail.comments[0].body, 'Danke');
|
||||||
|
assert.strictEqual((await call('gitea-collab', { action: 'merge', owner: 'M_Viper', repo: 'deskhub', index: 3, style: 'squash' })).ok, true);
|
||||||
|
assert.strictEqual((await call('gitea-collab', { action: 'createIssue', owner: 'M_Viper', repo: 'deskhub', title: 'Neu' })).item.number, 6);
|
||||||
|
|
||||||
|
const target = fs.mkdtempSync(path.join(os.tmpdir(), 'gm-bk-'));
|
||||||
|
await call('repo-backup-set-config', { targetDir: target, interval: 'daily', keep: 2 });
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
const r = await call('repo-backup-run-now', {});
|
||||||
|
assert.strictEqual(r.summary.ok, 1, JSON.stringify(r));
|
||||||
|
await new Promise(res => setTimeout(res, 61000 / 60)); // eindeutige Zeitstempel
|
||||||
|
}
|
||||||
|
const zips = fs.readdirSync(path.join(target, 'M_Viper', 'deskhub-archiv'));
|
||||||
|
assert.ok(zips.length <= 3 && zips.length >= 1);
|
||||||
|
const state = await call('repo-backup-get-config', {});
|
||||||
|
assert.strictEqual(state.config.lastResult.total, 1);
|
||||||
|
fs.rmSync(target, { recursive: true, force: true });
|
||||||
|
} finally {
|
||||||
|
g.server.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
const test = require('node:test');
|
||||||
|
const assert = require('node:assert');
|
||||||
|
const http = require('node:http');
|
||||||
|
const { bareHostname } = require('../src/utils/helpers.js');
|
||||||
|
const { checkGiteaConnection } = require('../src/git/apiHandler.js');
|
||||||
|
|
||||||
|
function listen(host) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const server = http.createServer((req, res) => {
|
||||||
|
res.setHeader('Content-Type', 'application/json');
|
||||||
|
res.end(JSON.stringify({ version: '1.22.0' }));
|
||||||
|
});
|
||||||
|
server.once('error', reject);
|
||||||
|
server.listen(0, host, () => resolve(server));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
test('bareHostname entfernt Klammern nur bei IPv6-Literalen', () => {
|
||||||
|
assert.strictEqual(bareHostname(new URL('http://[2001:db8::1]:3000').hostname), '2001:db8::1');
|
||||||
|
assert.strictEqual(bareHostname('[::1]'), '::1');
|
||||||
|
assert.strictEqual(bareHostname('127.0.0.1'), '127.0.0.1');
|
||||||
|
assert.strictEqual(bareHostname('git.example.com'), 'git.example.com');
|
||||||
|
assert.strictEqual(bareHostname(undefined), '');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Gitea-Verbindung über IPv4', async () => {
|
||||||
|
const server = await listen('127.0.0.1');
|
||||||
|
try {
|
||||||
|
const r = await checkGiteaConnection({ url: `http://127.0.0.1:${server.address().port}` });
|
||||||
|
assert.strictEqual(r.ok, true);
|
||||||
|
} finally { server.close(); }
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Gitea-Verbindung über IPv6', async (t) => {
|
||||||
|
let server;
|
||||||
|
try { server = await listen('::1'); } catch { return t.skip('IPv6-Loopback nicht verfügbar'); }
|
||||||
|
try {
|
||||||
|
const r = await checkGiteaConnection({ url: `http://[::1]:${server.address().port}` });
|
||||||
|
assert.strictEqual(r.ok, true);
|
||||||
|
} finally { server.close(); }
|
||||||
|
});
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
const test = require('node:test');
|
||||||
|
const assert = require('node:assert');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const os = require('node:os');
|
||||||
|
const path = require('node:path');
|
||||||
|
const { spawnSync } = require('node:child_process');
|
||||||
|
|
||||||
|
const { createPrefs, DEFAULTS } = require('../src/features/appPrefs.js');
|
||||||
|
const { useBundledGit, applyGitIdentity, commitIdentity } = require('../src/git/gitBinary.js');
|
||||||
|
const { createRepoBackup, excludeMatcher } = require('../src/backup/GiteaRepoBackup.js');
|
||||||
|
|
||||||
|
const tmp = (p) => fs.mkdtempSync(path.join(os.tmpdir(), p));
|
||||||
|
|
||||||
|
test('Einstellungen: Standardwerte, Bereinigung und ungültige E-Mail', () => {
|
||||||
|
const file = path.join(tmp('gm-prefs-'), 'app-settings.json');
|
||||||
|
const prefs = createPrefs(file);
|
||||||
|
assert.deepStrictEqual(prefs.get(), DEFAULTS);
|
||||||
|
|
||||||
|
const r = prefs.set({ zoom: 400, theme: 'pink', closeAction: 'tray', gitName: 'Max <x>\n', unbekannt: 1, uploadSecretScan: 0 });
|
||||||
|
assert.strictEqual(r.prefs.zoom, 150);
|
||||||
|
assert.strictEqual(r.prefs.theme, 'dark');
|
||||||
|
assert.strictEqual(r.prefs.closeAction, 'tray');
|
||||||
|
assert.strictEqual(r.prefs.gitName, 'Max x');
|
||||||
|
assert.strictEqual(r.prefs.uploadSecretScan, false);
|
||||||
|
assert.ok(!('unbekannt' in r.prefs));
|
||||||
|
|
||||||
|
const bad = prefs.set({ gitEmail: 'keine-mail' });
|
||||||
|
assert.deepStrictEqual(bad.rejected, ['gitEmail']);
|
||||||
|
assert.strictEqual(bad.prefs.gitEmail, '');
|
||||||
|
|
||||||
|
// nach dem Neuladen noch da
|
||||||
|
assert.strictEqual(createPrefs(file).get().closeAction, 'tray');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Git-Identität greift nur als Rückfallwert und verschwindet beim Leeren', () => {
|
||||||
|
useBundledGit();
|
||||||
|
const userData = tmp('gm-ud-');
|
||||||
|
const home = tmp('gm-home-');
|
||||||
|
const env = { ...process.env, HOME: home, USERPROFILE: home };
|
||||||
|
const ident = () => spawnSync('git', ['var', 'GIT_AUTHOR_IDENT'], { encoding: 'utf8', env: { ...env, GIT_CONFIG_SYSTEM: process.env.GIT_CONFIG_SYSTEM || '' } }).stdout;
|
||||||
|
|
||||||
|
assert.strictEqual(applyGitIdentity({ name: 'Test Name', email: 'test@example.com' }, userData), true);
|
||||||
|
assert.match(ident(), /^Test Name <test@example\.com>/);
|
||||||
|
|
||||||
|
// Eine globale Einstellung hat Vorrang
|
||||||
|
fs.writeFileSync(path.join(home, '.gitconfig'), '[user]\n\tname = Global\n\temail = global@example.com\n');
|
||||||
|
assert.match(ident(), /^Global <global@example\.com>/);
|
||||||
|
|
||||||
|
applyGitIdentity({ name: '', email: '' }, userData);
|
||||||
|
assert.strictEqual(process.env.GIT_CONFIG_SYSTEM, undefined);
|
||||||
|
assert.ok(!fs.existsSync(path.join(userData, 'git-system.gitconfig')));
|
||||||
|
assert.deepStrictEqual(commitIdentity({ gitName: '', gitEmail: '' }), { name: 'Git Manager GUI', email: 'gui@gitmanager.local' });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Backup: Ausschlussmuster und Zeitpunkt für automatische Backups', () => {
|
||||||
|
const m = excludeMatcher('test-*\nM_Viper/Alt?\n# Kommentar\nmy.repo');
|
||||||
|
assert.strictEqual(m('M_Viper/test-1'), true);
|
||||||
|
assert.strictEqual(m('M_Viper/Alt1'), true);
|
||||||
|
assert.strictEqual(m('M_Viper/Alt12'), false);
|
||||||
|
assert.strictEqual(m('x/my.repo'), true);
|
||||||
|
assert.strictEqual(m('x/myXrepo'), false);
|
||||||
|
|
||||||
|
const configPath = path.join(tmp('gm-bk-'), 'repo-backup.json');
|
||||||
|
const backup = createRepoBackup({ configPath, getContext() {}, giteaCall() {}, runGit() {}, isPackFailure() {}, redact: (s) => s });
|
||||||
|
backup.setConfig({ targetDir: 'X:/irgendwo', interval: 'daily', time: '03:00' });
|
||||||
|
const at = (s) => new Date(s);
|
||||||
|
// noch nie gelaufen: vor 03:00 nicht, danach schon
|
||||||
|
assert.strictEqual(backup.isDue(at('2026-10-22T02:59:00')), true, 'Slot von gestern 03:00 ist offen');
|
||||||
|
fs.writeFileSync(configPath, JSON.stringify({ ...JSON.parse(fs.readFileSync(configPath, 'utf8')), lastRun: '2026-10-22T03:05:00' }));
|
||||||
|
assert.strictEqual(backup.isDue(at('2026-10-22T20:00:00')), false, 'heute schon gelaufen');
|
||||||
|
assert.strictEqual(backup.isDue(at('2026-10-23T02:59:00')), false, 'vor 03:00 noch nicht');
|
||||||
|
assert.strictEqual(backup.isDue(at('2026-10-23T03:00:00')), true, 'ab 03:00 wieder fällig');
|
||||||
|
|
||||||
|
backup.setConfig({ time: '25:99' });
|
||||||
|
assert.strictEqual(backup.getState().config.time, '');
|
||||||
|
});
|
||||||
@@ -0,0 +1,201 @@
|
|||||||
|
const test = require('node:test');
|
||||||
|
const assert = require('node:assert');
|
||||||
|
const http = require('node:http');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const os = require('node:os');
|
||||||
|
const path = require('node:path');
|
||||||
|
const { spawnSync } = require('node:child_process');
|
||||||
|
const archiver = require('archiver');
|
||||||
|
|
||||||
|
require('../src/git/gitBinary.js').useBundledGit();
|
||||||
|
const { registerFeatureIpc } = require('../src/features/featuresIpc.js');
|
||||||
|
|
||||||
|
const git = (cwd, ...a) => spawnSync('git', ['-c', 'user.name=t', '-c', 'user.email=t@t', ...a], { cwd, encoding: 'utf8' });
|
||||||
|
const tmp = (p) => fs.mkdtempSync(path.join(os.tmpdir(), p));
|
||||||
|
|
||||||
|
function makeZip(file, entries) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const out = fs.createWriteStream(file);
|
||||||
|
const zip = archiver('zip');
|
||||||
|
out.on('close', resolve);
|
||||||
|
zip.on('error', reject);
|
||||||
|
zip.pipe(out);
|
||||||
|
for (const [name, content] of entries) zip.append(content, { name });
|
||||||
|
zip.finalize();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backup-Ordner wie von GiteaRepoBackup angelegt: Spiegel + ZIP-Stände
|
||||||
|
async function makeBackupDir() {
|
||||||
|
const work = tmp('gm-src-');
|
||||||
|
git(work, 'init', '-q', '-b', 'main');
|
||||||
|
fs.writeFileSync(path.join(work, 'a.txt'), 'hallo');
|
||||||
|
git(work, 'add', '.'); git(work, 'commit', '-qm', 'eins');
|
||||||
|
git(work, 'branch', 'feature'); git(work, 'tag', 'v1.0');
|
||||||
|
const dir = tmp('gm-bk-');
|
||||||
|
fs.mkdirSync(path.join(dir, 'M_Viper'));
|
||||||
|
git(dir, 'clone', '-q', '--mirror', work, path.join(dir, 'M_Viper', 'deskhub.git'));
|
||||||
|
const zipDir = path.join(dir, 'M_Viper', 'deskhub-archiv');
|
||||||
|
fs.mkdirSync(zipDir);
|
||||||
|
await makeZip(path.join(zipDir, 'deskhub-2026-10-01_10-00.zip'), [['deskhub/a.txt', 'alt'], ['deskhub/src/b.kt', 'fun b() {}']]);
|
||||||
|
await makeZip(path.join(zipDir, 'deskhub-2026-10-20_10-00.zip'), [['deskhub/a.txt', 'neu'], ['deskhub/src/b.kt', 'fun b() {}']]);
|
||||||
|
fs.utimesSync(path.join(zipDir, 'deskhub-2026-10-01_10-00.zip'), new Date('2026-10-01'), new Date('2026-10-01'));
|
||||||
|
fs.writeFileSync(path.join(dir, 'backup-protokoll.json'), '{}');
|
||||||
|
return { dir, work };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gitea-Nachbau: neue Repos zeigen auf echte lokale Bare-Repos, damit der Push wirklich läuft
|
||||||
|
function fakeGitea() {
|
||||||
|
const created = [];
|
||||||
|
const patches = [];
|
||||||
|
const repoRoot = tmp('gm-remote-');
|
||||||
|
const server = http.createServer((req, res) => {
|
||||||
|
const u = new URL(req.url, 'http://x');
|
||||||
|
const send = (code, obj) => { res.statusCode = code; res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify(obj)); };
|
||||||
|
let body = '';
|
||||||
|
req.on('data', c => body += c);
|
||||||
|
req.on('end', () => {
|
||||||
|
if (u.pathname === '/api/v1/user') return send(200, { login: 'M_Viper' });
|
||||||
|
if (u.pathname === '/api/v1/user/repos' && req.method === 'POST') {
|
||||||
|
const b = JSON.parse(body);
|
||||||
|
if (b.name === 'deskhub') return send(409, { message: 'repository already exists' });
|
||||||
|
const bare = path.join(repoRoot, `${b.name}.git`);
|
||||||
|
spawnSync('git', ['init', '-q', '--bare', '-b', 'main', bare]);
|
||||||
|
created.push(b);
|
||||||
|
return send(201, { name: b.name, full_name: `M_Viper/${b.name}`, owner: { login: 'M_Viper' }, clone_url: bare, html_url: `http://gitea/M_Viper/${b.name}`, default_branch: 'main' });
|
||||||
|
}
|
||||||
|
if (u.pathname.startsWith('/api/v1/repos/M_Viper/') && req.method === 'PATCH') { patches.push(JSON.parse(body)); return send(200, {}); }
|
||||||
|
send(404, { message: 'not found' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return new Promise(r => server.listen(0, '127.0.0.1', () => r({ server, created, patches, repoRoot, url: `http://127.0.0.1:${server.address().port}` })));
|
||||||
|
}
|
||||||
|
|
||||||
|
function setup(url) {
|
||||||
|
const handlers = {};
|
||||||
|
const userData = tmp('gm-ud-');
|
||||||
|
registerFeatureIpc({
|
||||||
|
ipcMain: { handle: (name, fn) => { handlers[name] = fn; } },
|
||||||
|
app: { getPath: () => userData },
|
||||||
|
readCredentials: () => ({ giteaURL: url, giteaToken: 'geheim123' }),
|
||||||
|
buildItemsFromPaths: async () => ({ items: [], skipped: [], ignored: [] }),
|
||||||
|
clearGitTransportBlock: () => false
|
||||||
|
});
|
||||||
|
return (name, data) => handlers[name]({ sender: { send() {} } }, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
test('Sicherungen werden gefunden: Spiegel und ZIP-Stände, neuester zuerst', async () => {
|
||||||
|
const { dir } = await makeBackupDir();
|
||||||
|
const call = setup('http://127.0.0.1:1');
|
||||||
|
const r = await call('repo-backup-list', { dir });
|
||||||
|
assert.strictEqual(r.ok, true, r.error);
|
||||||
|
assert.strictEqual(r.backups.length, 1);
|
||||||
|
const b = r.backups[0];
|
||||||
|
assert.deepStrictEqual([b.owner, b.repo], ['M_Viper', 'deskhub']);
|
||||||
|
assert.ok(b.mirror);
|
||||||
|
assert.deepStrictEqual(b.zips.map(z => z.file), ['deskhub-2026-10-20_10-00.zip', 'deskhub-2026-10-01_10-00.zip']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Lokal: Spiegel wird zur Arbeitskopie mit allen Branches, ZIP ohne Oberordner entpackt', async () => {
|
||||||
|
const { dir } = await makeBackupDir();
|
||||||
|
const call = setup('http://127.0.0.1:1');
|
||||||
|
const { backups: [b] } = await call('repo-backup-list', { dir });
|
||||||
|
|
||||||
|
const dest1 = tmp('gm-dst-');
|
||||||
|
const m = await call('repo-restore-local', { kind: 'mirror', source: b.mirror.path, destParent: dest1, repo: 'deskhub' });
|
||||||
|
assert.strictEqual(m.ok, true, m.error);
|
||||||
|
assert.strictEqual(fs.readFileSync(path.join(dest1, 'deskhub', 'a.txt'), 'utf8'), 'hallo');
|
||||||
|
assert.match(git(path.join(dest1, 'deskhub'), 'branch', '-r').stdout, /feature/);
|
||||||
|
|
||||||
|
// Zweiter Versuch in denselben Ordner darf nichts überschreiben
|
||||||
|
const again = await call('repo-restore-local', { kind: 'mirror', source: b.mirror.path, destParent: dest1, repo: 'deskhub' });
|
||||||
|
assert.strictEqual(again.ok, false);
|
||||||
|
assert.match(again.error, /existiert bereits/);
|
||||||
|
|
||||||
|
const dest2 = tmp('gm-dst-');
|
||||||
|
const z = await call('repo-restore-local', { kind: 'zip', source: b.zips[0].path, destParent: dest2, repo: 'deskhub' });
|
||||||
|
assert.strictEqual(z.ok, true, z.error);
|
||||||
|
assert.strictEqual(z.files, 2);
|
||||||
|
assert.strictEqual(fs.readFileSync(path.join(dest2, 'deskhub', 'a.txt'), 'utf8'), 'neu');
|
||||||
|
assert.ok(fs.existsSync(path.join(dest2, 'deskhub', 'src', 'b.kt')));
|
||||||
|
});
|
||||||
|
|
||||||
|
// ZIP von Hand bauen: archiver entschärft "../" schon beim Erzeugen, ein echter Angreifer nicht.
|
||||||
|
function rawZip(file, entries) {
|
||||||
|
const zlib = require('node:zlib');
|
||||||
|
const locals = [];
|
||||||
|
const centrals = [];
|
||||||
|
let offset = 0;
|
||||||
|
for (const [name, content] of entries) {
|
||||||
|
const n = Buffer.from(name);
|
||||||
|
const data = Buffer.from(content);
|
||||||
|
const crc = zlib.crc32(data);
|
||||||
|
const lh = Buffer.alloc(30);
|
||||||
|
lh.writeUInt32LE(0x04034b50, 0); lh.writeUInt16LE(20, 4); lh.writeUInt32LE(crc, 14);
|
||||||
|
lh.writeUInt32LE(data.length, 18); lh.writeUInt32LE(data.length, 22); lh.writeUInt16LE(n.length, 26);
|
||||||
|
const ch = Buffer.alloc(46);
|
||||||
|
ch.writeUInt32LE(0x02014b50, 0); ch.writeUInt16LE(20, 4); ch.writeUInt16LE(20, 6); ch.writeUInt32LE(crc, 16);
|
||||||
|
ch.writeUInt32LE(data.length, 20); ch.writeUInt32LE(data.length, 24); ch.writeUInt16LE(n.length, 28); ch.writeUInt32LE(offset, 42);
|
||||||
|
locals.push(lh, n, data);
|
||||||
|
centrals.push(ch, n);
|
||||||
|
offset += lh.length + n.length + data.length;
|
||||||
|
}
|
||||||
|
const cd = Buffer.concat(centrals);
|
||||||
|
const end = Buffer.alloc(22);
|
||||||
|
end.writeUInt32LE(0x06054b50, 0); end.writeUInt16LE(entries.length, 8); end.writeUInt16LE(entries.length, 10);
|
||||||
|
end.writeUInt32LE(cd.length, 12); end.writeUInt32LE(offset, 16);
|
||||||
|
fs.writeFileSync(file, Buffer.concat([...locals, cd, end]));
|
||||||
|
}
|
||||||
|
|
||||||
|
test('Archive mit Pfaden außerhalb des Ziels werden abgewiesen', async () => {
|
||||||
|
const d = tmp('gm-evil-');
|
||||||
|
const zip = path.join(d, 'evil.zip');
|
||||||
|
rawZip(zip, [['deskhub/ok.txt', 'ok'], ['deskhub/../../boese.txt', 'x']]);
|
||||||
|
const unzipper = require('unzipper');
|
||||||
|
const names = (await unzipper.Open.file(zip)).files.map(f => f.path);
|
||||||
|
assert.ok(names.includes('deskhub/../../boese.txt'), 'Testarchiv muss den gefährlichen Pfad wirklich enthalten');
|
||||||
|
const call = setup('http://127.0.0.1:1');
|
||||||
|
const destParent = tmp('gm-dst-');
|
||||||
|
const r = await call('repo-restore-local', { kind: 'zip', source: zip, destParent, repo: 'x' });
|
||||||
|
assert.strictEqual(r.ok, false);
|
||||||
|
assert.match(r.error, /Unsicherer Pfad/);
|
||||||
|
assert.ok(!fs.existsSync(path.join(destParent, 'x', 'ok.txt')), 'auch harmlose Dateien dürfen dann nicht entpackt werden');
|
||||||
|
assert.ok(!fs.existsSync(path.join(destParent, 'boese.txt')));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Gitea: Spiegel und ZIP als neues Repo, Namenskonflikt und ZIP-Überschreiben werden abgefangen', async () => {
|
||||||
|
const { dir } = await makeBackupDir();
|
||||||
|
const g = await fakeGitea();
|
||||||
|
try {
|
||||||
|
const call = setup(g.url);
|
||||||
|
const { backups: [b] } = await call('repo-backup-list', { dir });
|
||||||
|
|
||||||
|
const m = await call('repo-restore-gitea', { kind: 'mirror', source: b.mirror.path, owner: 'M_Viper', repo: 'deskhub', mode: 'new', newName: 'deskhub-wieder', isPrivate: true });
|
||||||
|
assert.strictEqual(m.ok, true, m.error);
|
||||||
|
const bare = path.join(g.repoRoot, 'deskhub-wieder.git');
|
||||||
|
const refs = git(bare, 'for-each-ref', '--format=%(refname)').stdout;
|
||||||
|
assert.match(refs, /refs\/heads\/main/);
|
||||||
|
assert.match(refs, /refs\/heads\/feature/);
|
||||||
|
assert.match(refs, /refs\/tags\/v1\.0/);
|
||||||
|
assert.strictEqual(g.created[0].private, true);
|
||||||
|
|
||||||
|
const z = await call('repo-restore-gitea', { kind: 'zip', source: b.zips[0].path, owner: 'M_Viper', repo: 'deskhub', mode: 'new', newName: 'deskhub-zip' });
|
||||||
|
assert.strictEqual(z.ok, true, z.error);
|
||||||
|
assert.strictEqual(z.method, 'Git');
|
||||||
|
const files = git(path.join(g.repoRoot, 'deskhub-zip.git'), 'ls-tree', '-r', '--name-only', 'main').stdout.trim().split('\n');
|
||||||
|
assert.deepStrictEqual(files.sort(), ['a.txt', 'src/b.kt']);
|
||||||
|
|
||||||
|
const conflict = await call('repo-restore-gitea', { kind: 'mirror', source: b.mirror.path, owner: 'M_Viper', repo: 'deskhub', mode: 'new', newName: 'deskhub' });
|
||||||
|
assert.strictEqual(conflict.ok, false);
|
||||||
|
assert.match(conflict.error, /existiert bereits/);
|
||||||
|
|
||||||
|
const zipOverwrite = await call('repo-restore-gitea', { kind: 'zip', source: b.zips[0].path, owner: 'M_Viper', repo: 'deskhub', mode: 'overwrite' });
|
||||||
|
assert.strictEqual(zipOverwrite.ok, false);
|
||||||
|
assert.match(zipOverwrite.error, /nur mit einem Git-Spiegel/);
|
||||||
|
|
||||||
|
const badName = await call('repo-restore-gitea', { kind: 'zip', source: b.zips[0].path, owner: 'M_Viper', repo: 'deskhub', mode: 'new', newName: 'mit leerzeichen' });
|
||||||
|
assert.strictEqual(badName.ok, false);
|
||||||
|
} finally {
|
||||||
|
g.server.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 55 KiB |
Vendored
BIN
Binary file not shown.
|
After Width: | Height: | Size: 114 KiB |
+665
-665
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,42 @@
|
|||||||
// main.js — Main-Process with concurrent folder upload/download, progress events, and temp-dir cleanup
|
// main.js — Main-Process with concurrent folder upload/download, progress events, and temp-dir cleanup
|
||||||
const { app, BrowserWindow, ipcMain, dialog, Menu, nativeImage, Tray, shell, clipboard, safeStorage } = require('electron');
|
const { app, BrowserWindow, ipcMain, dialog, Menu, nativeImage, Tray, shell, clipboard, safeStorage } = require('electron');
|
||||||
|
// Das Setup startet die frisch installierte EXE einmal mit diesem Schalter, damit Windows Defender
|
||||||
|
// sie schon während der Installation prüft (installer/Installer.cs → Vorwaermen). Sofort beenden.
|
||||||
|
if (process.argv.includes('--gm-vorwaermen')) process.exit(0);
|
||||||
|
// Eingebautes Git aktivieren, bevor irgendein Git-Prozess startet.
|
||||||
|
require('./src/git/gitBinary.js').useBundledGit();
|
||||||
const ppath = require('path');
|
const ppath = require('path');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
|
|
||||||
|
// Allgemeine App-Einstellungen (ohne Geheimnisse) und was sofort davon abhängt
|
||||||
|
const { applyGitIdentity, commitIdentity } = require('./src/git/gitBinary.js');
|
||||||
|
const appPrefs = require('./src/features/appPrefs.js').createPrefs(ppath.join(app.getPath('userData'), 'app-settings.json'));
|
||||||
|
const UPLOAD_EXCLUDES_FILE = ppath.join(app.getPath('userData'), 'upload-excludes.txt');
|
||||||
|
function applyPrefsToProcess(p) {
|
||||||
|
try { applyGitIdentity({ name: p.gitName, email: p.gitEmail }, app.getPath('userData')); } catch (e) { console.warn('Git-Identität:', e.message); }
|
||||||
|
try {
|
||||||
|
if (p.uploadExtraExcludes.trim()) fs.writeFileSync(UPLOAD_EXCLUDES_FILE, p.uploadExtraExcludes + '\n', 'utf8');
|
||||||
|
else if (fs.existsSync(UPLOAD_EXCLUDES_FILE)) fs.rmSync(UPLOAD_EXCLUDES_FILE, { force: true });
|
||||||
|
} catch (e) { console.warn('Upload-Ausschlüsse:', e.message); }
|
||||||
|
}
|
||||||
|
applyPrefsToProcess(appPrefs.get());
|
||||||
|
appPrefs.onChange((p) => {
|
||||||
|
applyPrefsToProcess(p);
|
||||||
|
for (const w of BrowserWindow.getAllWindows()) {
|
||||||
|
try { w.webContents.setZoomFactor(p.zoom / 100); } catch (_) {}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Name/E-Mail für Commits in den temporären Upload-Klonen
|
||||||
|
function gitIdentityConfig() {
|
||||||
|
const id = commitIdentity(appPrefs.get());
|
||||||
|
return [`user.email=${id.email}`, `user.name=${id.name}`];
|
||||||
|
}
|
||||||
|
// Wird beendet (Tray-Menü, Updater, Herunterfahren)? Dann das Fenster nicht in den Tray schicken.
|
||||||
|
let quittingForReal = false;
|
||||||
|
app.on('before-quit', () => { quittingForReal = true; });
|
||||||
const os = require('os');
|
const os = require('os');
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
const { execSync, spawnSync } = require('child_process');
|
const { execSync, spawnSync, spawn } = require('child_process');
|
||||||
const http = require('http');
|
const http = require('http');
|
||||||
const https = require('https');
|
const https = require('https');
|
||||||
|
|
||||||
@@ -16,7 +48,8 @@ const {
|
|||||||
formatErrorForUser,
|
formatErrorForUser,
|
||||||
caches,
|
caches,
|
||||||
runParallel,
|
runParallel,
|
||||||
retryWithBackoff
|
retryWithBackoff,
|
||||||
|
bareHostname
|
||||||
} = require('./src/utils/helpers.js');
|
} = require('./src/utils/helpers.js');
|
||||||
|
|
||||||
// OPTIMIERUNG: Updater wird nicht sofort geladen, sondern verzögert nach Startup
|
// OPTIMIERUNG: Updater wird nicht sofort geladen, sondern verzögert nach Startup
|
||||||
@@ -44,6 +77,8 @@ const {
|
|||||||
getGiteaFileContent,
|
getGiteaFileContent,
|
||||||
uploadGiteaFile,
|
uploadGiteaFile,
|
||||||
uploadItemsViaGiteaApi,
|
uploadItemsViaGiteaApi,
|
||||||
|
listGiteaRepoFiles,
|
||||||
|
deleteGiteaPathsBatched,
|
||||||
getGiteaCurrentUser,
|
getGiteaCurrentUser,
|
||||||
getGiteaCommits,
|
getGiteaCommits,
|
||||||
getGiteaCommit,
|
getGiteaCommit,
|
||||||
@@ -187,6 +222,14 @@ function markGitTransportWorking(authUrl) {
|
|||||||
if (brokenGitTransports.delete(gitTransportKey(authUrl))) saveBrokenGitTransports();
|
if (brokenGitTransports.delete(gitTransportKey(authUrl))) saveBrokenGitTransports();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Hebt die Sperre für einen Host auf, z. B. wenn die Diagnose den Git-Weg als funktionierend erkannt hat.
|
||||||
|
function clearGitTransportBlock(host) {
|
||||||
|
loadBrokenGitTransports();
|
||||||
|
if (!brokenGitTransports.delete(host)) return false;
|
||||||
|
saveBrokenGitTransports();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
function isGitTransportKnownBroken(authUrl) {
|
function isGitTransportKnownBroken(authUrl) {
|
||||||
loadBrokenGitTransports();
|
loadBrokenGitTransports();
|
||||||
const key = gitTransportKey(authUrl);
|
const key = gitTransportKey(authUrl);
|
||||||
@@ -975,6 +1018,9 @@ function createWindow() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
win.loadFile(ppath.join(__dirname, 'renderer', 'index.html'));
|
win.loadFile(ppath.join(__dirname, 'renderer', 'index.html'));
|
||||||
|
win.webContents.on('did-finish-load', () => {
|
||||||
|
try { win.webContents.setZoomFactor(appPrefs.get().zoom / 100); } catch (_) {}
|
||||||
|
});
|
||||||
// win.webContents.openDevTools();
|
// win.webContents.openDevTools();
|
||||||
|
|
||||||
// OPTIMIERUNG: Tray wird verzögert hergestellt (nicht beim Fenster-Create)
|
// OPTIMIERUNG: Tray wird verzögert hergestellt (nicht beim Fenster-Create)
|
||||||
@@ -982,10 +1028,12 @@ function createWindow() {
|
|||||||
createTray(win);
|
createTray(win);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Schließen-Button -> Tray statt Beenden (nur wenn Autostart aktiv)
|
// Schließen-Button -> Tray statt Beenden. Einstellung "Beim Schließen": immer Tray, immer beenden
|
||||||
|
// oder automatisch (Tray nur, wenn die App mit Windows startet).
|
||||||
win.on('close', (e) => {
|
win.on('close', (e) => {
|
||||||
const { enabled } = app.getLoginItemSettings();
|
const action = appPrefs.get().closeAction;
|
||||||
if (enabled) {
|
const toTray = action === 'tray' || (action === 'auto' && app.getLoginItemSettings().enabled);
|
||||||
|
if (toTray && !quittingForReal) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
win.hide();
|
win.hide();
|
||||||
}
|
}
|
||||||
@@ -1140,18 +1188,37 @@ function sanitizeGitRef(ref, fallback = 'main') {
|
|||||||
return normalizeBranch(ref, 'gitea');
|
return normalizeBranch(ref, 'gitea');
|
||||||
}
|
}
|
||||||
|
|
||||||
function runGitSync(args, cwd, options = {}) {
|
// Startet Git ohne den Hauptprozess zu blockieren. Synchrone Aufrufe (spawnSync) froren
|
||||||
const res = spawnSync('git', args, {
|
// die Oberfläche ein, bis Windows "Anwendung reagiert nicht" meldete.
|
||||||
cwd,
|
function execGit(args, { cwd, input, env } = {}) {
|
||||||
encoding: 'utf8',
|
return new Promise((resolve) => {
|
||||||
windowsHide: true,
|
let child;
|
||||||
...options
|
try {
|
||||||
|
child = spawn('git', args, { cwd, env: env || process.env, windowsHide: true });
|
||||||
|
} catch (e) {
|
||||||
|
resolve({ status: -1, stdout: '', stderr: String(e && e.message || e) });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const out = [];
|
||||||
|
const err = [];
|
||||||
|
child.stdout.on('data', c => out.push(c));
|
||||||
|
child.stderr.on('data', c => err.push(c));
|
||||||
|
child.on('error', e => err.push(Buffer.from(String(e && e.message || e))));
|
||||||
|
child.on('close', (code) => resolve({
|
||||||
|
status: code,
|
||||||
|
stdout: Buffer.concat(out).toString('utf8'),
|
||||||
|
stderr: Buffer.concat(err).toString('utf8')
|
||||||
|
}));
|
||||||
|
if (input != null) child.stdin.end(input);
|
||||||
|
else child.stdin.end();
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runGitAsync(args, cwd, options = {}) {
|
||||||
|
const res = await execGit(args, { cwd, env: options.env });
|
||||||
if (res.status !== 0) {
|
if (res.status !== 0) {
|
||||||
throw new Error(String(res.stderr || res.stdout || 'Git-Fehler').trim());
|
throw new Error(String(res.stderr || res.stdout || 'Git-Fehler').trim());
|
||||||
}
|
}
|
||||||
|
|
||||||
return String(res.stdout || '').trim();
|
return String(res.stdout || '').trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1489,7 +1556,7 @@ ipcMain.handle('push-project', async (event, data) => {
|
|||||||
// Aktuellen Branch ermitteln (NICHT umbenennen!)
|
// Aktuellen Branch ermitteln (NICHT umbenennen!)
|
||||||
let currentBranch = data.branch || null;
|
let currentBranch = data.branch || null;
|
||||||
try {
|
try {
|
||||||
const detected = runGitSync(['branch', '--show-current'], data.folder);
|
const detected = await runGitAsync(['branch', '--show-current'], data.folder);
|
||||||
if (detected) {
|
if (detected) {
|
||||||
currentBranch = currentBranch || sanitizeGitRef(detected, 'main');
|
currentBranch = currentBranch || sanitizeGitRef(detected, 'main');
|
||||||
console.log('Current local branch:', detected);
|
console.log('Current local branch:', detected);
|
||||||
@@ -1504,7 +1571,7 @@ ipcMain.handle('push-project', async (event, data) => {
|
|||||||
// 2. Prüfen, ob ein 'origin' Remote existiert
|
// 2. Prüfen, ob ein 'origin' Remote existiert
|
||||||
let remoteUrl = '';
|
let remoteUrl = '';
|
||||||
try {
|
try {
|
||||||
remoteUrl = runGitSync(['remote', 'get-url', 'origin'], data.folder);
|
remoteUrl = await runGitAsync(['remote', 'get-url', 'origin'], data.folder);
|
||||||
|
|
||||||
// Bereits gespeicherte Credential-URLs in sichere Origin-URLs ohne Userinfo konvertieren.
|
// Bereits gespeicherte Credential-URLs in sichere Origin-URLs ohne Userinfo konvertieren.
|
||||||
try {
|
try {
|
||||||
@@ -1513,7 +1580,7 @@ ipcMain.handle('push-project', async (event, data) => {
|
|||||||
remoteObj.username = '';
|
remoteObj.username = '';
|
||||||
remoteObj.password = '';
|
remoteObj.password = '';
|
||||||
const sanitizedRemoteUrl = remoteObj.toString();
|
const sanitizedRemoteUrl = remoteObj.toString();
|
||||||
runGitSync(['remote', 'set-url', 'origin', sanitizedRemoteUrl], data.folder);
|
await runGitAsync(['remote', 'set-url', 'origin', sanitizedRemoteUrl], data.folder);
|
||||||
remoteUrl = sanitizedRemoteUrl;
|
remoteUrl = sanitizedRemoteUrl;
|
||||||
}
|
}
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
@@ -1544,11 +1611,11 @@ ipcMain.handle('push-project', async (event, data) => {
|
|||||||
}
|
}
|
||||||
if (constructedUrl) {
|
if (constructedUrl) {
|
||||||
try {
|
try {
|
||||||
runGitSync(['remote', 'add', 'origin', constructedUrl], data.folder, { stdio: 'inherit' });
|
await runGitAsync(['remote', 'add', 'origin', constructedUrl], data.folder, { stdio: 'inherit' });
|
||||||
|
|
||||||
// Auth nie in der Remote-URL speichern; stattdessen Header scoped auf den Remote-Origin.
|
// Auth nie in der Remote-URL speichern; stattdessen Header scoped auf den Remote-Origin.
|
||||||
if (remoteOrigin && authHeader) {
|
if (remoteOrigin && authHeader) {
|
||||||
runGitSync(['config', `http.${remoteOrigin}.extraheader`, authHeader], data.folder);
|
await runGitAsync(['config', `http.${remoteOrigin}.extraheader`, authHeader], data.folder);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Remote origin added: ${constructedUrl}`);
|
console.log(`Remote origin added: ${constructedUrl}`);
|
||||||
@@ -1583,7 +1650,7 @@ ipcMain.handle('getBranches', async (event, data) => {
|
|||||||
// Aktuellen Branch ermitteln und nach oben sortieren
|
// Aktuellen Branch ermitteln und nach oben sortieren
|
||||||
let currentBranch = null;
|
let currentBranch = null;
|
||||||
try {
|
try {
|
||||||
currentBranch = runGitSync(['branch', '--show-current'], data.folder);
|
currentBranch = await runGitAsync(['branch', '--show-current'], data.folder);
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
branches.sort((a, b) => {
|
branches.sort((a, b) => {
|
||||||
if (a === currentBranch) return -1;
|
if (a === currentBranch) return -1;
|
||||||
@@ -1908,7 +1975,7 @@ ipcMain.handle('deleteFile', async (event, data) => {
|
|||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const refQuery = useBranch ? `?ref=${encodeURIComponent(useBranch)}` : '';
|
const refQuery = useBranch ? `?ref=${encodeURIComponent(useBranch)}` : '';
|
||||||
const req = protocol.request({
|
const req = protocol.request({
|
||||||
hostname: urlObj.hostname, port,
|
hostname: bareHostname(urlObj.hostname), port,
|
||||||
path: `/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/contents/${path.split('/').map(encodeURIComponent).join('/')}${refQuery}`,
|
path: `/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/contents/${path.split('/').map(encodeURIComponent).join('/')}${refQuery}`,
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: { 'Authorization': `token ${token}`, 'Content-Type': 'application/json' }
|
headers: { 'Authorization': `token ${token}`, 'Content-Type': 'application/json' }
|
||||||
@@ -1934,7 +2001,7 @@ ipcMain.handle('deleteFile', async (event, data) => {
|
|||||||
if (useBranch) payload.branch = useBranch;
|
if (useBranch) payload.branch = useBranch;
|
||||||
const body = JSON.stringify(payload);
|
const body = JSON.stringify(payload);
|
||||||
const req = protocol.request({
|
const req = protocol.request({
|
||||||
hostname: urlObj.hostname, port,
|
hostname: bareHostname(urlObj.hostname), port,
|
||||||
path: `/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/contents/${filePath.split('/').map(encodeURIComponent).join('/')}`,
|
path: `/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/contents/${filePath.split('/').map(encodeURIComponent).join('/')}`,
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: {
|
headers: {
|
||||||
@@ -1972,6 +2039,11 @@ ipcMain.handle('deleteFile', async (event, data) => {
|
|||||||
const notFound = contents.errors.some(err => String(err).toLowerCase().includes('does not exist'));
|
const notFound = contents.errors.some(err => String(err).toLowerCase().includes('does not exist'));
|
||||||
if (notFound) return files;
|
if (notFound) return files;
|
||||||
}
|
}
|
||||||
|
// Gitea meldet einen nicht (mehr) vorhandenen Pfad als {"message":"object does not exist ..."}.
|
||||||
|
if (contents && !Array.isArray(contents) && !contents.sha && typeof contents.message === 'string' &&
|
||||||
|
/does not exist|not found/i.test(contents.message)) {
|
||||||
|
return files;
|
||||||
|
}
|
||||||
if (Array.isArray(contents)) {
|
if (Array.isArray(contents)) {
|
||||||
// It's a folder — recurse into it
|
// It's a folder — recurse into it
|
||||||
for (const item of contents) {
|
for (const item of contents) {
|
||||||
@@ -2020,6 +2092,23 @@ ipcMain.handle('deleteFile', async (event, data) => {
|
|||||||
return remaining.length > 0;
|
return remaining.length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Schnellweg: paketweise löschen (ein Commit pro bis zu 100 Dateien).
|
||||||
|
// Nur wenn die Trees-API nicht nutzbar ist, läuft der alte Einzelweg weiter unten.
|
||||||
|
try {
|
||||||
|
const r = await deleteGiteaPathsBatched({
|
||||||
|
token, url: giteaUrl, owner, repo, branch: useBranch || 'HEAD', prefix: filePath
|
||||||
|
});
|
||||||
|
invalidateRepoContentsCache(owner, repo);
|
||||||
|
if (r.failed.length === 0) {
|
||||||
|
logger.info('deleteFile', `Deleted ${r.deleted} files in ${r.commits} commits`);
|
||||||
|
return { ok: true, deleted: r.deleted };
|
||||||
|
}
|
||||||
|
const preview = r.failed.slice(0, 5).map(f => `${f.path}: ${f.error}`).join('; ');
|
||||||
|
return { ok: false, error: `Löschen unvollständig: ${r.failed.length} von ${r.total} Dateien nicht gelöscht (${preview}${r.failed.length > 5 ? ', ...' : ''}).` };
|
||||||
|
} catch (batchErr) {
|
||||||
|
logger.warn('deleteFile', 'Paket-Löschen nicht möglich, nutze Einzelweg', { error: String(batchErr && batchErr.message || batchErr) });
|
||||||
|
}
|
||||||
|
|
||||||
// Collect all files to delete (handles both file and folder)
|
// Collect all files to delete (handles both file and folder)
|
||||||
const filesToDelete = await collectAllFiles(filePath);
|
const filesToDelete = await collectAllFiles(filePath);
|
||||||
|
|
||||||
@@ -2072,6 +2161,70 @@ ipcMain.handle('deleteFile', async (event, data) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/* -----------------------------
|
||||||
|
Gitea: Dateien finden, die laut lokaler .gitignore nicht ins Repo gehören
|
||||||
|
----------------------------- */
|
||||||
|
ipcMain.handle('find-gitignored-in-repo', async (_event, data) => {
|
||||||
|
try {
|
||||||
|
const credentials = readCredentials();
|
||||||
|
const token = credentials && credentials.giteaToken;
|
||||||
|
const url = credentials && credentials.giteaURL;
|
||||||
|
if (!token || !url) return { ok: false, error: 'missing-token-or-url' };
|
||||||
|
const { owner, repo, localFolder } = data || {};
|
||||||
|
const repoPath = String((data && data.repoPath) || '').replace(/^\/+|\/+$/g, '');
|
||||||
|
if (!owner || !repo || !localFolder || !fs.existsSync(localFolder)) return { ok: false, error: 'Lokaler Ordner nicht gefunden' };
|
||||||
|
|
||||||
|
const { branch, files } = await listGiteaRepoFiles({ token, url, owner, repo, branch: data.branch || 'HEAD' });
|
||||||
|
const prefix = repoPath ? `${repoPath}/` : '';
|
||||||
|
const remote = [...files.keys()].filter(p => p.startsWith(prefix));
|
||||||
|
if (remote.length === 0) return { ok: true, branch, files: [], scanned: 0 };
|
||||||
|
|
||||||
|
// Die Regeln des lokalen Ordners auf die Server-Pfade anwenden. check-ignore prüft rein
|
||||||
|
// nach Muster, die Dateien müssen lokal nicht existieren.
|
||||||
|
const tmp = fs.mkdtempSync(ppath.join(os.tmpdir(), 'gm-ignore-'));
|
||||||
|
let ignoredRel = [];
|
||||||
|
try {
|
||||||
|
await execGit(['init', '-q', '--bare', tmp]);
|
||||||
|
const r = await execGit([`--git-dir=${tmp}`, `--work-tree=${localFolder}`, 'check-ignore', '--no-index', '--stdin', '-z'], {
|
||||||
|
cwd: localFolder,
|
||||||
|
input: remote.map(p => p.slice(prefix.length)).join('\0') + '\0'
|
||||||
|
});
|
||||||
|
// Exit-Code 1 heißt: nichts ignoriert.
|
||||||
|
if (r.status !== 0 && r.status !== 1) throw new Error((r.stderr || 'git check-ignore fehlgeschlagen').trim());
|
||||||
|
ignoredRel = r.stdout.split('\0').filter(Boolean);
|
||||||
|
} finally {
|
||||||
|
try { fs.rmSync(tmp, { recursive: true, force: true }); } catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ok: true, branch, files: ignoredRel.map(rel => prefix + rel), scanned: remote.length };
|
||||||
|
} catch (e) {
|
||||||
|
console.error('find-gitignored-in-repo error', e);
|
||||||
|
return { ok: false, error: mapIpcError(e) };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('delete-gitea-paths', async (event, data) => {
|
||||||
|
try {
|
||||||
|
const credentials = readCredentials();
|
||||||
|
const token = credentials && credentials.giteaToken;
|
||||||
|
const url = credentials && credentials.giteaURL;
|
||||||
|
if (!token || !url) return { ok: false, error: 'missing-token-or-url' };
|
||||||
|
const { owner, repo, paths } = data || {};
|
||||||
|
if (!owner || !repo || !Array.isArray(paths) || paths.length === 0) return { ok: false, error: 'Keine Dateien angegeben' };
|
||||||
|
|
||||||
|
const r = await deleteGiteaPathsBatched({
|
||||||
|
token, url, owner, repo, branch: data.branch || 'HEAD', paths,
|
||||||
|
message: data.message,
|
||||||
|
onProgress: (percent) => { try { event.sender.send('folder-upload-progress', { percent }); } catch (_) {} }
|
||||||
|
});
|
||||||
|
invalidateRepoContentsCache(owner, repo);
|
||||||
|
return { ok: r.failed.length === 0, ...r };
|
||||||
|
} catch (e) {
|
||||||
|
console.error('delete-gitea-paths error', e);
|
||||||
|
return { ok: false, error: mapIpcError(e) };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
/* -----------------------------
|
/* -----------------------------
|
||||||
Gitea: list repos, contents, file content
|
Gitea: list repos, contents, file content
|
||||||
----------------------------- */
|
----------------------------- */
|
||||||
@@ -2405,7 +2558,7 @@ ipcMain.handle('upload-gitea-file', async (event, data) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const tmpDir = getSafeTmpDir(`git-file-${owner}-${repo}`);
|
const tmpDir = getSafeTmpDir(`git-file-${owner}-${repo}`);
|
||||||
const gitConfig = ['user.email=gui@gitmanager.local', 'user.name=Git Manager GUI'];
|
const gitConfig = gitIdentityConfig();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { repoGit, isEmptyRepo } = await cloneForUpload({
|
const { repoGit, isEmptyRepo } = await cloneForUpload({
|
||||||
@@ -2617,6 +2770,7 @@ ipcMain.handle('upload-local-folder-to-gitea', async (event, data) => {
|
|||||||
// Exclude-Liste: .git, node_modules etc. überspringen
|
// Exclude-Liste: .git, node_modules etc. überspringen
|
||||||
const excludeList = ['.git', 'node_modules', '.DS_Store', 'thumbs.db', '.vscode', '.idea'];
|
const excludeList = ['.git', 'node_modules', '.DS_Store', 'thumbs.db', '.vscode', '.idea'];
|
||||||
|
|
||||||
|
const isIgnored = await createUploadIgnore(localFolder);
|
||||||
(function walk(dir) {
|
(function walk(dir) {
|
||||||
const entries = fs.readdirSync(dir);
|
const entries = fs.readdirSync(dir);
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
@@ -2624,6 +2778,7 @@ ipcMain.handle('upload-local-folder-to-gitea', async (event, data) => {
|
|||||||
const full = ppath.join(dir, entry);
|
const full = ppath.join(dir, entry);
|
||||||
let stat;
|
let stat;
|
||||||
try { stat = fs.statSync(full); } catch(e) { continue; }
|
try { stat = fs.statSync(full); } catch(e) { continue; }
|
||||||
|
if (isIgnored(full, stat.isDirectory())) continue;
|
||||||
if (stat.isDirectory()) {
|
if (stat.isDirectory()) {
|
||||||
walk(full);
|
walk(full);
|
||||||
} else if (stat.isFile()) {
|
} else if (stat.isFile()) {
|
||||||
@@ -2656,7 +2811,7 @@ ipcMain.handle('upload-local-folder-to-gitea', async (event, data) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const tmpDir = getSafeTmpDir(`git-folder-${owner}-${repo}`);
|
const tmpDir = getSafeTmpDir(`git-folder-${owner}-${repo}`);
|
||||||
const gitConfig = ['user.email=gui@gitmanager.local', 'user.name=Git Manager GUI'];
|
const gitConfig = gitIdentityConfig();
|
||||||
|
|
||||||
// Fortschritt: Start
|
// Fortschritt: Start
|
||||||
try { event.sender.send('folder-upload-progress', { processed: 0, total, percent: 0 }); } catch (_) {}
|
try { event.sender.send('folder-upload-progress', { processed: 0, total, percent: 0 }); } catch (_) {}
|
||||||
@@ -2761,18 +2916,78 @@ ipcMain.handle('upload-local-folder-to-gitea', async (event, data) => {
|
|||||||
Erzeugt EINEN Clone + EINEN Commit + EINEN Push für ALLE Pfade
|
Erzeugt EINEN Clone + EINEN Commit + EINEN Push für ALLE Pfade
|
||||||
zusammen – egal wie viele Dateien/Ordner gedroppt wurden.
|
zusammen – egal wie viele Dateien/Ordner gedroppt wurden.
|
||||||
----------------------------------------------------------------- */
|
----------------------------------------------------------------- */
|
||||||
|
// Liest die .gitignore-Regeln (auch verschachtelte und globale) für einen Upload-Pfad aus
|
||||||
|
// und liefert eine Prüffunktion. Build-Ausgaben wie build/ oder .gradle/ werden so nicht
|
||||||
|
// hochgeladen. Maßgeblich ist das Repo, in dem der Pfad liegt; ohne Repo der Elternordner.
|
||||||
|
// So greift die .gitignore auch, wenn nur einzelne Einträge eines Ordners gezogen werden.
|
||||||
|
// Git bekommt ein leeres Dummy-Repo und den Ordner nur als Arbeitsverzeichnis.
|
||||||
|
//
|
||||||
|
// Läuft asynchron; das Ergebnis pro Arbeitsordner wird im übergebenen Cache gehalten,
|
||||||
|
// damit 19 gezogene Einträge eines Ordners nur eine Git-Abfrage auslösen.
|
||||||
|
async function createUploadIgnore(target, cache = new Map()) {
|
||||||
|
const parent = ppath.dirname(target);
|
||||||
|
let workTree = parent;
|
||||||
|
if (cache.has(`top:${parent}`)) {
|
||||||
|
workTree = cache.get(`top:${parent}`);
|
||||||
|
} else {
|
||||||
|
const top = await execGit(['-C', parent, 'rev-parse', '--show-toplevel']);
|
||||||
|
if (top.status === 0 && top.stdout.trim()) workTree = ppath.resolve(top.stdout.trim());
|
||||||
|
cache.set(`top:${parent}`, workTree);
|
||||||
|
}
|
||||||
|
|
||||||
|
let ignored = cache.get(`ign:${workTree}`);
|
||||||
|
if (!ignored) {
|
||||||
|
ignored = new Set();
|
||||||
|
let tmp = null;
|
||||||
|
try {
|
||||||
|
tmp = fs.mkdtempSync(ppath.join(os.tmpdir(), 'gm-ignore-'));
|
||||||
|
const init = await execGit(['init', '-q', '--bare', tmp]);
|
||||||
|
if (init.status === 0) {
|
||||||
|
const r = await execGit([`--git-dir=${tmp}`, `--work-tree=${workTree}`, 'ls-files', '-z',
|
||||||
|
'--others', '--ignored', '--exclude-standard', '--directory',
|
||||||
|
// zusätzliche Muster aus Einstellungen → Uploads
|
||||||
|
...(fs.existsSync(UPLOAD_EXCLUDES_FILE) ? [`--exclude-from=${UPLOAD_EXCLUDES_FILE}`] : [])], { cwd: workTree });
|
||||||
|
if (r.status === 0) {
|
||||||
|
for (const entry of r.stdout.split('\0')) if (entry) ignored.add(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('createUploadIgnore:', e.message || e);
|
||||||
|
} finally {
|
||||||
|
if (tmp) { try { fs.rmSync(tmp, { recursive: true, force: true }); } catch (_) {} }
|
||||||
|
}
|
||||||
|
cache.set(`ign:${workTree}`, ignored);
|
||||||
|
if (ignored.size) console.log(`[Upload] .gitignore: ${ignored.size} Einträge unter ${workTree} ausgeschlossen`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (fullPath, isDir) => {
|
||||||
|
if (!ignored.size) return false;
|
||||||
|
const rel = ppath.relative(workTree, fullPath).split(ppath.sep).join('/');
|
||||||
|
if (ignored.has(isDir ? `${rel}/` : rel)) return true;
|
||||||
|
// Liegt der Pfad in einem ausgeschlossenen Ordner?
|
||||||
|
for (let i = rel.lastIndexOf('/'); i > 0; i = rel.lastIndexOf('/', i - 1)) {
|
||||||
|
if (ignored.has(rel.slice(0, i + 1))) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const GIT_UPLOAD_EXCLUDE = ['.git', 'node_modules', '.DS_Store', 'thumbs.db', '.vscode', '.idea'];
|
const GIT_UPLOAD_EXCLUDE = ['.git', 'node_modules', '.DS_Store', 'thumbs.db', '.vscode', '.idea'];
|
||||||
|
|
||||||
// Wandelt gedroppte Pfade (Dateien und/oder Ordner) in eine flache Liste
|
// Wandelt gedroppte Pfade (Dateien und/oder Ordner) in eine flache Liste
|
||||||
// { localFile, targetPath } um. Dateien -> destPath/<name>, Ordner ->
|
// { localFile, targetPath } um. Dateien -> destPath/<name>, Ordner ->
|
||||||
// destPath/<ordnername>/<rel> (identisch zum bisherigen Verhalten).
|
// destPath/<ordnername>/<rel> (identisch zum bisherigen Verhalten).
|
||||||
function buildItemsFromPaths(paths, destPath) {
|
async function buildItemsFromPaths(paths, destPath) {
|
||||||
const items = [];
|
const items = [];
|
||||||
|
const ignoreCache = new Map();
|
||||||
const skipped = [];
|
const skipped = [];
|
||||||
|
const ignored = [];
|
||||||
for (const p of paths) {
|
for (const p of paths) {
|
||||||
let stat;
|
let stat;
|
||||||
try { stat = fs.statSync(p); } catch (e) { skipped.push({ path: p, error: 'not-found' }); continue; }
|
try { stat = fs.statSync(p); } catch (e) { skipped.push({ path: p, error: 'not-found' }); continue; }
|
||||||
const baseName = ppath.basename(p);
|
const baseName = ppath.basename(p);
|
||||||
|
const isIgnored = await createUploadIgnore(p, ignoreCache);
|
||||||
|
if (isIgnored(p, stat.isDirectory())) { skipped.push({ path: p, error: 'gitignore' }); continue; }
|
||||||
if (stat.isFile()) {
|
if (stat.isFile()) {
|
||||||
const targetPath = destPath ? `${destPath}/${baseName}` : baseName;
|
const targetPath = destPath ? `${destPath}/${baseName}` : baseName;
|
||||||
items.push({ localFile: p, targetPath });
|
items.push({ localFile: p, targetPath });
|
||||||
@@ -2786,6 +3001,7 @@ function buildItemsFromPaths(paths, destPath) {
|
|||||||
const full = ppath.join(dir, entry);
|
const full = ppath.join(dir, entry);
|
||||||
let st;
|
let st;
|
||||||
try { st = fs.statSync(full); } catch (e) { continue; }
|
try { st = fs.statSync(full); } catch (e) { continue; }
|
||||||
|
if (isIgnored(full, st.isDirectory())) { ignored.push({ path: full, isDir: st.isDirectory() }); continue; }
|
||||||
if (st.isDirectory()) {
|
if (st.isDirectory()) {
|
||||||
walk(full);
|
walk(full);
|
||||||
} else if (st.isFile()) {
|
} else if (st.isFile()) {
|
||||||
@@ -2799,7 +3015,7 @@ function buildItemsFromPaths(paths, destPath) {
|
|||||||
skipped.push({ path: p, error: 'unsupported' });
|
skipped.push({ path: p, error: 'unsupported' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return { items, skipped };
|
return { items, skipped, ignored };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Klont das Repo (shallow), kopiert alle items hinein, commit + push.
|
// Klont das Repo (shallow), kopiert alle items hinein, commit + push.
|
||||||
@@ -2809,7 +3025,7 @@ async function gitPushItemsToGitea({ token, url, owner, repo, branch, items, mes
|
|||||||
const authUrl = `${urlObj.protocol}//${encodeURIComponent(token)}@${urlObj.host}/${owner}/${repo}.git`;
|
const authUrl = `${urlObj.protocol}//${encodeURIComponent(token)}@${urlObj.host}/${owner}/${repo}.git`;
|
||||||
|
|
||||||
const tmpDir = getSafeTmpDir(`git-paths-${owner}-${repo}`);
|
const tmpDir = getSafeTmpDir(`git-paths-${owner}-${repo}`);
|
||||||
const gitConfig = ['user.email=gui@gitmanager.local', 'user.name=Git Manager GUI'];
|
const gitConfig = gitIdentityConfig();
|
||||||
const safeBranch = sanitizeGitRef(branch || 'HEAD', 'HEAD');
|
const safeBranch = sanitizeGitRef(branch || 'HEAD', 'HEAD');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -2891,7 +3107,10 @@ ipcMain.handle('upload-paths-to-gitea', async (event, data) => {
|
|||||||
try { invalidateRepoContentsCache(owner, repo); } catch (_) {}
|
try { invalidateRepoContentsCache(owner, repo); } catch (_) {}
|
||||||
try { caches.repos.invalidate(`${owner}/${repo}`); } catch (_) {}
|
try { caches.repos.invalidate(`${owner}/${repo}`); } catch (_) {}
|
||||||
|
|
||||||
const { items, skipped } = buildItemsFromPaths(paths, destPath);
|
const built = await buildItemsFromPaths(paths, destPath);
|
||||||
|
const skipped = built.skipped;
|
||||||
|
const excluded = new Set(Array.isArray(data.excludeFiles) ? data.excludeFiles : []);
|
||||||
|
const items = excluded.size ? built.items.filter(it => !excluded.has(it.localFile)) : built.items;
|
||||||
if (items.length === 0) {
|
if (items.length === 0) {
|
||||||
return { ok: false, error: 'Keine Dateien zum Hochladen gefunden', skipped, debugId: uploadDebugId };
|
return { ok: false, error: 'Keine Dateien zum Hochladen gefunden', skipped, debugId: uploadDebugId };
|
||||||
}
|
}
|
||||||
@@ -3143,7 +3362,7 @@ ipcMain.handle('delete-gitea-repo', async (event, data) => {
|
|||||||
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const options = {
|
const options = {
|
||||||
hostname: urlObj.hostname,
|
hostname: bareHostname(urlObj.hostname),
|
||||||
port: urlObj.port || (urlObj.protocol === 'https:' ? 443 : 80),
|
port: urlObj.port || (urlObj.protocol === 'https:' ? 443 : 80),
|
||||||
path: deletePath,
|
path: deletePath,
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
@@ -3278,7 +3497,7 @@ ipcMain.handle('upload-and-push', async (event, data) => {
|
|||||||
const cloneArgs = ['clone', '--depth', '1'];
|
const cloneArgs = ['clone', '--depth', '1'];
|
||||||
if (branch !== 'HEAD') cloneArgs.push('--branch', branch);
|
if (branch !== 'HEAD') cloneArgs.push('--branch', branch);
|
||||||
cloneArgs.push(authClone, tmpDir);
|
cloneArgs.push(authClone, tmpDir);
|
||||||
runGitSync(cloneArgs, process.cwd(), gitExecOptions);
|
await runGitAsync(cloneArgs, process.cwd(), gitExecOptions);
|
||||||
|
|
||||||
// Zielort im Repo bestimmen
|
// Zielort im Repo bestimmen
|
||||||
let destDirInRepo = tmpDir;
|
let destDirInRepo = tmpDir;
|
||||||
@@ -3293,17 +3512,17 @@ ipcMain.handle('upload-and-push', async (event, data) => {
|
|||||||
fs.copyFileSync(data.localFolder, finalFileDest);
|
fs.copyFileSync(data.localFolder, finalFileDest);
|
||||||
|
|
||||||
// Git Befehle
|
// Git Befehle
|
||||||
runGitSync(['-C', tmpDir, 'add', '.'], process.cwd(), gitExecOptions);
|
await runGitAsync(['-C', tmpDir, 'add', '.'], process.cwd(), gitExecOptions);
|
||||||
try { runGitSync(['-C', tmpDir, 'commit', '-m', `Upload file ${fileName} via GUI`], process.cwd(), gitExecOptions); } catch (_) {}
|
try { await runGitAsync(['-C', tmpDir, 'commit', '-m', `Upload file ${fileName} via GUI`], process.cwd(), gitExecOptions); } catch (_) {}
|
||||||
let pushBranch = branch;
|
let pushBranch = branch;
|
||||||
if (pushBranch === 'HEAD') {
|
if (pushBranch === 'HEAD') {
|
||||||
try {
|
try {
|
||||||
pushBranch = runGitSync(['-C', tmpDir, 'rev-parse', '--abbrev-ref', 'HEAD'], process.cwd(), gitExecOptions).trim();
|
pushBranch = await runGitAsync(['-C', tmpDir, 'rev-parse', '--abbrev-ref', 'HEAD'], process.cwd(), gitExecOptions);
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
pushBranch = 'main';
|
pushBranch = 'main';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
runGitSync(['-C', tmpDir, 'push', 'origin', pushBranch], process.cwd(), gitExecOptions);
|
await runGitAsync(['-C', tmpDir, 'push', 'origin', pushBranch], process.cwd(), gitExecOptions);
|
||||||
|
|
||||||
setTimeout(() => { try { if (fs.existsSync(tmpDir)) fs.rmSync(tmpDir, { recursive: true, force: true }); } catch (_) {} }, 5_000);
|
setTimeout(() => { try { if (fs.existsSync(tmpDir)) fs.rmSync(tmpDir, { recursive: true, force: true }); } catch (_) {} }, 5_000);
|
||||||
return { ok: true, usedGit: true, singleFile: true, msg: 'Uploaded via Git (Fallback)', debugId: uploadDebugId };
|
return { ok: true, usedGit: true, singleFile: true, msg: 'Uploaded via Git (Fallback)', debugId: uploadDebugId };
|
||||||
@@ -3355,7 +3574,7 @@ ipcMain.handle('upload-and-push', async (event, data) => {
|
|||||||
const cloneArgs = ['clone', '--depth', '1'];
|
const cloneArgs = ['clone', '--depth', '1'];
|
||||||
if (branch !== 'HEAD') cloneArgs.push('--branch', branch);
|
if (branch !== 'HEAD') cloneArgs.push('--branch', branch);
|
||||||
cloneArgs.push(authClone, tmpDir);
|
cloneArgs.push(authClone, tmpDir);
|
||||||
runGitSync(cloneArgs, process.cwd(), gitExecOptions);
|
await runGitAsync(cloneArgs, process.cwd(), gitExecOptions);
|
||||||
|
|
||||||
// Zielverzeichnis im Repo: <destPath>/<folderName>/ (wie im API-Weg)
|
// Zielverzeichnis im Repo: <destPath>/<folderName>/ (wie im API-Weg)
|
||||||
let destDirInRepo = tmpDir;
|
let destDirInRepo = tmpDir;
|
||||||
@@ -3364,14 +3583,20 @@ ipcMain.handle('upload-and-push', async (event, data) => {
|
|||||||
ensureDir(destDirInRepo);
|
ensureDir(destDirInRepo);
|
||||||
|
|
||||||
// Kompletten Ordner rekursiv kopieren, ausgeschlossene Einträge überspringen
|
// Kompletten Ordner rekursiv kopieren, ausgeschlossene Einträge überspringen
|
||||||
|
const isIgnoredLocal = await createUploadIgnore(data.localFolder);
|
||||||
fs.cpSync(data.localFolder, destDirInRepo, {
|
fs.cpSync(data.localFolder, destDirInRepo, {
|
||||||
recursive: true,
|
recursive: true,
|
||||||
filter: (src) => !excludeList.includes(ppath.basename(src))
|
filter: (src) => {
|
||||||
|
if (excludeList.includes(ppath.basename(src))) return false;
|
||||||
|
let isDir = false;
|
||||||
|
try { isDir = fs.statSync(src).isDirectory(); } catch (_) {}
|
||||||
|
return !isIgnoredLocal(src, isDir);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
runGitSync(['-C', tmpDir, 'add', '.'], process.cwd(), gitExecOptions);
|
await runGitAsync(['-C', tmpDir, 'add', '.'], process.cwd(), gitExecOptions);
|
||||||
try {
|
try {
|
||||||
runGitSync(['-C', tmpDir, 'commit', '-m', `Upload ${folderName} via GUI`], process.cwd(), gitExecOptions);
|
await runGitAsync(['-C', tmpDir, 'commit', '-m', `Upload ${folderName} via GUI`], process.cwd(), gitExecOptions);
|
||||||
} catch (commitErr) {
|
} catch (commitErr) {
|
||||||
// "nothing to commit" ist kein echter Fehler
|
// "nothing to commit" ist kein echter Fehler
|
||||||
if (!/nothing to commit/i.test(String(commitErr))) throw commitErr;
|
if (!/nothing to commit/i.test(String(commitErr))) throw commitErr;
|
||||||
@@ -3383,11 +3608,11 @@ ipcMain.handle('upload-and-push', async (event, data) => {
|
|||||||
let pushBranch = branch;
|
let pushBranch = branch;
|
||||||
if (pushBranch === 'HEAD') {
|
if (pushBranch === 'HEAD') {
|
||||||
try {
|
try {
|
||||||
pushBranch = runGitSync(['-C', tmpDir, 'rev-parse', '--abbrev-ref', 'HEAD'], process.cwd(), gitExecOptions).trim();
|
pushBranch = await runGitAsync(['-C', tmpDir, 'rev-parse', '--abbrev-ref', 'HEAD'], process.cwd(), gitExecOptions);
|
||||||
} catch (_) { pushBranch = 'main'; }
|
} catch (_) { pushBranch = 'main'; }
|
||||||
if (!pushBranch || pushBranch === 'HEAD') pushBranch = 'main';
|
if (!pushBranch || pushBranch === 'HEAD') pushBranch = 'main';
|
||||||
}
|
}
|
||||||
runGitSync(['-C', tmpDir, 'push', 'origin', pushBranch], process.cwd(), gitExecOptions);
|
await runGitAsync(['-C', tmpDir, 'push', 'origin', pushBranch], process.cwd(), gitExecOptions);
|
||||||
|
|
||||||
console.log('[UPLOAD_DEBUG][main] directory-upload:git-push-done', { uploadDebugId, pushBranch });
|
console.log('[UPLOAD_DEBUG][main] directory-upload:git-push-done', { uploadDebugId, pushBranch });
|
||||||
setTimeout(() => { try { if (fs.existsSync(tmpDir)) fs.rmSync(tmpDir, { recursive: true, force: true }); } catch (_) {} }, 5_000);
|
setTimeout(() => { try { if (fs.existsSync(tmpDir)) fs.rmSync(tmpDir, { recursive: true, force: true }); } catch (_) {} }, 5_000);
|
||||||
@@ -3407,6 +3632,7 @@ ipcMain.handle('upload-and-push', async (event, data) => {
|
|||||||
const items = [];
|
const items = [];
|
||||||
// excludeList und folderName sind oben (FALL 2) bereits definiert
|
// excludeList und folderName sind oben (FALL 2) bereits definiert
|
||||||
|
|
||||||
|
const isIgnoredApi = await createUploadIgnore(data.localFolder);
|
||||||
(function walk(dir) {
|
(function walk(dir) {
|
||||||
const entries = fs.readdirSync(dir);
|
const entries = fs.readdirSync(dir);
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
@@ -3417,6 +3643,7 @@ ipcMain.handle('upload-and-push', async (event, data) => {
|
|||||||
try {
|
try {
|
||||||
stat = fs.statSync(full);
|
stat = fs.statSync(full);
|
||||||
} catch(e) { continue; }
|
} catch(e) { continue; }
|
||||||
|
if (isIgnoredApi(full, stat.isDirectory())) continue;
|
||||||
|
|
||||||
if (stat.isDirectory()) {
|
if (stat.isDirectory()) {
|
||||||
walk(full);
|
walk(full);
|
||||||
@@ -3550,7 +3777,7 @@ ipcMain.handle('run-batch-repo-action', async (event, data) => {
|
|||||||
}
|
}
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
|
|
||||||
runGitSync(['clone', '--depth', '1', authCloneUrl, repoDir], process.cwd(), { stdio: 'ignore' });
|
await runGitAsync(['clone', '--depth', '1', authCloneUrl, repoDir], process.cwd(), { stdio: 'ignore' });
|
||||||
results.push({ repo: item.id, ok: true, message: `Geklont nach ${repoDir}` });
|
results.push({ repo: item.id, ok: true, message: `Geklont nach ${repoDir}` });
|
||||||
} else if (action === 'create-tag') {
|
} else if (action === 'create-tag') {
|
||||||
const tag = String(options.tag || '').trim();
|
const tag = String(options.tag || '').trim();
|
||||||
@@ -3635,7 +3862,7 @@ ipcMain.handle('rename-gitea-item', async (event, data) => {
|
|||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const bodyStr = body ? JSON.stringify(body) : null;
|
const bodyStr = body ? JSON.stringify(body) : null;
|
||||||
const req = protocol.request({
|
const req = protocol.request({
|
||||||
hostname: urlObj.hostname, port,
|
hostname: bareHostname(urlObj.hostname), port,
|
||||||
path: apiPath, method,
|
path: apiPath, method,
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': `token ${token}`,
|
'Authorization': `token ${token}`,
|
||||||
@@ -3743,7 +3970,7 @@ ipcMain.handle('create-gitea-item', async (event, data) => {
|
|||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const bodyStr = body ? JSON.stringify(body) : null;
|
const bodyStr = body ? JSON.stringify(body) : null;
|
||||||
const req = protocol.request({
|
const req = protocol.request({
|
||||||
hostname: urlObj.hostname,
|
hostname: bareHostname(urlObj.hostname),
|
||||||
port,
|
port,
|
||||||
path: apiPath,
|
path: apiPath,
|
||||||
method,
|
method,
|
||||||
@@ -4646,7 +4873,7 @@ function isAllowedExternalUrl(rawUrl) {
|
|||||||
if (protocol === 'https:' || protocol === 'mailto:') return true;
|
if (protocol === 'https:' || protocol === 'mailto:') return true;
|
||||||
if (protocol === 'http:') {
|
if (protocol === 'http:') {
|
||||||
const host = String(parsed.hostname || '').toLowerCase();
|
const host = String(parsed.hostname || '').toLowerCase();
|
||||||
return host === 'localhost' || host === '127.0.0.1' || host === '::1';
|
return host === 'localhost' || host === '127.0.0.1' || host === '[::1]' || host === '::1';
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
@@ -4929,12 +5156,8 @@ ipcMain.handle('sync-repo-to-github', async (event, data) => {
|
|||||||
: ['clone', '--bare', sourceCloneUrl, mirrorDir];
|
: ['clone', '--bare', sourceCloneUrl, mirrorDir];
|
||||||
|
|
||||||
// runGit: gibt stdout zurück, loggt stderr immer (git push schreibt Status auf stderr)
|
// runGit: gibt stdout zurück, loggt stderr immer (git push schreibt Status auf stderr)
|
||||||
const runGit = (args, cwd) => {
|
const runGit = async (args, cwd) => {
|
||||||
const res = spawnSync('git', args, {
|
const res = await execGit(args, { cwd });
|
||||||
cwd,
|
|
||||||
encoding: 'utf8',
|
|
||||||
windowsHide: true
|
|
||||||
});
|
|
||||||
const out = (res.stdout || '').trim();
|
const out = (res.stdout || '').trim();
|
||||||
const err = (res.stderr || '').trim();
|
const err = (res.stderr || '').trim();
|
||||||
if (err) console.log(`[sync-repo-to-github][git] ${err}`);
|
if (err) console.log(`[sync-repo-to-github][git] ${err}`);
|
||||||
@@ -4945,17 +5168,17 @@ ipcMain.handle('sync-repo-to-github', async (event, data) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
console.log(`[sync-repo-to-github] Klone Quell-Repo (bare): ${sourceCloneUrl}`);
|
console.log(`[sync-repo-to-github] Klone Quell-Repo (bare): ${sourceCloneUrl}`);
|
||||||
runGit(cloneArgs, process.cwd());
|
await runGit(cloneArgs, process.cwd());
|
||||||
console.log('[sync-repo-to-github] Bare-Clone abgeschlossen');
|
console.log('[sync-repo-to-github] Bare-Clone abgeschlossen');
|
||||||
|
|
||||||
// Alle lokalen Branches auflisten
|
// Alle lokalen Branches auflisten
|
||||||
const allBranchesRaw = runGit(['for-each-ref', '--format=%(refname:short)', 'refs/heads/'], mirrorDir);
|
const allBranchesRaw = await runGit(['for-each-ref', '--format=%(refname:short)', 'refs/heads/'], mirrorDir);
|
||||||
const allBranches = (allBranchesRaw || '').split('\n').map(s => s.trim()).filter(s => s && isSafeGitRef(s));
|
const allBranches = (allBranchesRaw || '').split('\n').map(s => s.trim()).filter(s => s && isSafeGitRef(s));
|
||||||
console.log(`[sync-repo-to-github] Lokale Branches: ${allBranches.join(', ') || '(keine)'}`);
|
console.log(`[sync-repo-to-github] Lokale Branches: ${allBranches.join(', ') || '(keine)'}`);
|
||||||
|
|
||||||
// Default-Branch ermitteln
|
// Default-Branch ermitteln
|
||||||
try {
|
try {
|
||||||
const headRef = runGit(['symbolic-ref', '--short', 'HEAD'], mirrorDir);
|
const headRef = await runGit(['symbolic-ref', '--short', 'HEAD'], mirrorDir);
|
||||||
sourceDefaultBranch = String(headRef || '').replace(/^refs\/heads\//, '').trim();
|
sourceDefaultBranch = String(headRef || '').replace(/^refs\/heads\//, '').trim();
|
||||||
if (!isSafeGitRef(sourceDefaultBranch)) sourceDefaultBranch = '';
|
if (!isSafeGitRef(sourceDefaultBranch)) sourceDefaultBranch = '';
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
@@ -4969,19 +5192,19 @@ ipcMain.handle('sync-repo-to-github', async (event, data) => {
|
|||||||
const ghAuthArgs = ['-c', 'credential.helper=', '-c', `http.https://github.com/.extraheader=${githubAuthHeader}`];
|
const ghAuthArgs = ['-c', 'credential.helper=', '-c', `http.https://github.com/.extraheader=${githubAuthHeader}`];
|
||||||
|
|
||||||
// GitHub als Push-Remote setzen
|
// GitHub als Push-Remote setzen
|
||||||
runGit(['remote', 'set-url', 'origin', githubRemoteUrl], mirrorDir);
|
await runGit(['remote', 'set-url', 'origin', githubRemoteUrl], mirrorDir);
|
||||||
|
|
||||||
// Lokale Commit-SHAs loggen (= Gitea-Stand)
|
// Lokale Commit-SHAs loggen (= Gitea-Stand)
|
||||||
for (const branch of allBranches) {
|
for (const branch of allBranches) {
|
||||||
try {
|
try {
|
||||||
const localSha = runGit(['rev-parse', `refs/heads/${branch}`], mirrorDir);
|
const localSha = await runGit(['rev-parse', `refs/heads/${branch}`], mirrorDir);
|
||||||
console.log(`[sync-repo-to-github] Gitea ${branch}: ${localSha}`);
|
console.log(`[sync-repo-to-github] Gitea ${branch}: ${localSha}`);
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remote (GitHub) Commit-SHAs VOR dem Push ermitteln und vergleichen
|
// Remote (GitHub) Commit-SHAs VOR dem Push ermitteln und vergleichen
|
||||||
try {
|
try {
|
||||||
const remoteRefsRaw = runGit([...ghAuthArgs, 'ls-remote', '--heads', 'origin'], mirrorDir);
|
const remoteRefsRaw = await runGit([...ghAuthArgs, 'ls-remote', '--heads', 'origin'], mirrorDir);
|
||||||
const remoteMap = {};
|
const remoteMap = {};
|
||||||
(remoteRefsRaw || '').split('\n').forEach(line => {
|
(remoteRefsRaw || '').split('\n').forEach(line => {
|
||||||
const parts = line.trim().split(/\s+/);
|
const parts = line.trim().split(/\s+/);
|
||||||
@@ -5003,14 +5226,14 @@ ipcMain.handle('sync-repo-to-github', async (event, data) => {
|
|||||||
let pushedCount = 0;
|
let pushedCount = 0;
|
||||||
for (const branch of allBranches) {
|
for (const branch of allBranches) {
|
||||||
console.log(`[sync-repo-to-github] Pushe Branch: ${branch}`);
|
console.log(`[sync-repo-to-github] Pushe Branch: ${branch}`);
|
||||||
runGit([...ghAuthArgs, 'push', '--force', 'origin', `refs/heads/${branch}:refs/heads/${branch}`], mirrorDir);
|
await runGit([...ghAuthArgs, 'push', '--force', 'origin', `refs/heads/${branch}:refs/heads/${branch}`], mirrorDir);
|
||||||
pushedCount++;
|
pushedCount++;
|
||||||
}
|
}
|
||||||
console.log(`[sync-repo-to-github] ${pushedCount} Branch(es) erfolgreich gepusht`);
|
console.log(`[sync-repo-to-github] ${pushedCount} Branch(es) erfolgreich gepusht`);
|
||||||
|
|
||||||
// Auf GitHub nicht mehr vorhandene Branches aufräumen (prune via API, non-fatal)
|
// Auf GitHub nicht mehr vorhandene Branches aufräumen (prune via API, non-fatal)
|
||||||
try {
|
try {
|
||||||
const remoteRefsRaw = runGit([...ghAuthArgs, 'ls-remote', '--heads', 'origin'], mirrorDir);
|
const remoteRefsRaw = await runGit([...ghAuthArgs, 'ls-remote', '--heads', 'origin'], mirrorDir);
|
||||||
const remoteHeads = (remoteRefsRaw || '').split('\n')
|
const remoteHeads = (remoteRefsRaw || '').split('\n')
|
||||||
.map(l => { const m = l.match(/refs\/heads\/(.+)$/); return m ? m[1].trim() : null; })
|
.map(l => { const m = l.match(/refs\/heads\/(.+)$/); return m ? m[1].trim() : null; })
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
@@ -5018,7 +5241,7 @@ ipcMain.handle('sync-repo-to-github', async (event, data) => {
|
|||||||
for (const remoteBranch of remoteHeads) {
|
for (const remoteBranch of remoteHeads) {
|
||||||
if (!localSet.has(remoteBranch) && isSafeGitRef(remoteBranch)) {
|
if (!localSet.has(remoteBranch) && isSafeGitRef(remoteBranch)) {
|
||||||
console.log(`[sync-repo-to-github] Lösche veralteten Remote-Branch: ${remoteBranch}`);
|
console.log(`[sync-repo-to-github] Lösche veralteten Remote-Branch: ${remoteBranch}`);
|
||||||
runGit([...ghAuthArgs, 'push', 'origin', `--delete`, remoteBranch], mirrorDir);
|
await runGit([...ghAuthArgs, 'push', 'origin', `--delete`, remoteBranch], mirrorDir);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (pruneErr) {
|
} catch (pruneErr) {
|
||||||
@@ -5027,7 +5250,7 @@ ipcMain.handle('sync-repo-to-github', async (event, data) => {
|
|||||||
|
|
||||||
// Tags mit Force-Push übertragen (non-fatal)
|
// Tags mit Force-Push übertragen (non-fatal)
|
||||||
try {
|
try {
|
||||||
runGit([...ghAuthArgs, 'push', 'origin', '--tags', '--force'], mirrorDir);
|
await runGit([...ghAuthArgs, 'push', 'origin', '--tags', '--force'], mirrorDir);
|
||||||
console.log('[sync-repo-to-github] Tags erfolgreich gepusht');
|
console.log('[sync-repo-to-github] Tags erfolgreich gepusht');
|
||||||
} catch (tagsErr) {
|
} catch (tagsErr) {
|
||||||
console.warn('[sync-repo-to-github] Tags-Push Warnung:', tagsErr?.message || tagsErr);
|
console.warn('[sync-repo-to-github] Tags-Push Warnung:', tagsErr?.message || tagsErr);
|
||||||
@@ -5105,4 +5328,29 @@ ipcMain.handle('test-github-connection', async (_event, data) => {
|
|||||||
console.error('test-github-connection error', e);
|
console.error('test-github-connection error', e);
|
||||||
return { ok: false, error: mapIpcError(e) };
|
return { ok: false, error: mapIpcError(e) };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/* -----------------------------
|
||||||
|
Neue Funktionen: Upload-Vorschau, Diagnose, lokale Änderungen, PRs/Issues, Repo-Backup
|
||||||
|
----------------------------- */
|
||||||
|
require('./src/features/featuresIpc.js').registerFeatureIpc({
|
||||||
|
ipcMain,
|
||||||
|
app,
|
||||||
|
readCredentials,
|
||||||
|
buildItemsFromPaths,
|
||||||
|
clearGitTransportBlock,
|
||||||
|
getGitTransportBlocks: () => { loadBrokenGitTransports(); return Object.fromEntries(brokenGitTransports); },
|
||||||
|
prefs: appPrefs,
|
||||||
|
openPath: (dir) => shell.openPath(dir),
|
||||||
|
notify: (title, body) => {
|
||||||
|
try {
|
||||||
|
const { Notification } = require('electron');
|
||||||
|
if (Notification.isSupported()) new Notification({ title, body, icon: ppath.join(__dirname, 'assets', 'icon.png') }).show();
|
||||||
|
} catch (e) { console.warn('Benachrichtigung:', e.message); }
|
||||||
|
},
|
||||||
|
broadcast: (channel, payload) => {
|
||||||
|
for (const win of BrowserWindow.getAllWindows()) {
|
||||||
|
try { if (!win.isDestroyed()) win.webContents.send(channel, payload); } catch (_) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
Generated
+3885
-5275
File diff suppressed because it is too large
Load Diff
+74
-73
@@ -1,73 +1,74 @@
|
|||||||
{
|
{
|
||||||
"name": "git-manager-gui",
|
"name": "git-manager-gui",
|
||||||
"version": "2.1.7",
|
"version": "2.2.0",
|
||||||
"description": "Git Manager GUI - Verwaltung von Git Repositories",
|
"description": "Git Manager GUI - Verwaltung von Git Repositories",
|
||||||
"author": "M_Viper",
|
"author": "M_Viper",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "electron .",
|
"start": "electron .",
|
||||||
"build": "electron-builder",
|
"build": "electron-builder --win dir && node scripts/build-setup.js",
|
||||||
"test": "node --test __tests__/*.test.js",
|
"test": "node --test __tests__/*.test.js",
|
||||||
"pdf": "electron generate-pdf.js"
|
"pdf": "electron generate-pdf.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.13.5",
|
"archiver": "^6.0.0",
|
||||||
"form-data": "^4.0.5",
|
"axios": "^1.13.5",
|
||||||
"simple-git": "^3.32.3",
|
"form-data": "^4.0.5",
|
||||||
"archiver": "^6.0.0",
|
"simple-git": "^3.32.3",
|
||||||
"unzipper": "^0.11.0"
|
"unzipper": "^0.11.0"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"minimatch": "^3.1.4"
|
"minimatch": "^3.1.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"electron": "^41.0.4",
|
"electron": "^44.6.0",
|
||||||
"electron-builder": "^26.8.1"
|
"electron-builder": "^26.17.0"
|
||||||
},
|
},
|
||||||
"build": {
|
"build": {
|
||||||
"asar": true,
|
"asar": true,
|
||||||
"appId": "com.viper.gitmanager",
|
"appId": "com.viper.gitmanager",
|
||||||
"productName": "Git Manager GUI",
|
"productName": "Git Manager GUI",
|
||||||
"files": [
|
"files": [
|
||||||
"**/*",
|
"**/*",
|
||||||
"!node_modules/.cache",
|
"!node_modules/.cache",
|
||||||
"!**/*.map",
|
"!**/*.map",
|
||||||
"!**/.git",
|
"!**/.git",
|
||||||
"!**/.git/**",
|
"!**/.git/**",
|
||||||
"!repos/**",
|
"!repos/**",
|
||||||
"!data/**",
|
"!data/**",
|
||||||
"!backup/**",
|
"!backup/**",
|
||||||
"!*.zip",
|
"!*.zip",
|
||||||
"!*.rar",
|
"!*.rar",
|
||||||
"!*.log"
|
"!*.log",
|
||||||
],
|
"!vendor/**",
|
||||||
"extraResources": [
|
"!installer/**",
|
||||||
"repos/",
|
"!scripts/**",
|
||||||
{
|
"!__tests__/**",
|
||||||
"from": "data",
|
"!assets/icon.ico"
|
||||||
"to": "data",
|
],
|
||||||
"filter": [
|
"extraResources": [
|
||||||
"**/*",
|
"repos/",
|
||||||
"!credentials.json"
|
{
|
||||||
]
|
"from": "data",
|
||||||
}
|
"to": "data",
|
||||||
],
|
"filter": [
|
||||||
"directories": {
|
"**/*",
|
||||||
"buildResources": "assets"
|
"!credentials.json"
|
||||||
},
|
]
|
||||||
"win": {
|
},
|
||||||
"icon": "assets/icon.png",
|
{
|
||||||
"target": [
|
"from": "vendor/mingit",
|
||||||
"nsis"
|
"to": "mingit"
|
||||||
]
|
}
|
||||||
},
|
],
|
||||||
"nsis": {
|
"directories": {
|
||||||
"oneClick": false,
|
"buildResources": "assets"
|
||||||
"perMachine": false,
|
},
|
||||||
"allowToChangeInstallationDirectory": true,
|
"win": {
|
||||||
"createDesktopShortcut": true,
|
"icon": "assets/icon.png",
|
||||||
"createStartMenuShortcut": true,
|
"target": [
|
||||||
"shortcutName": "Git Manager GUI"
|
"dir"
|
||||||
}
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
+30
@@ -104,6 +104,9 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
// Local Repository Commits
|
// Local Repository Commits
|
||||||
getLocalCommits: (data) => ipcRenderer.invoke('get-local-commits', data),
|
getLocalCommits: (data) => ipcRenderer.invoke('get-local-commits', data),
|
||||||
getLocalCommitDiff: (data) => ipcRenderer.invoke('get-local-commit-diff', data),
|
getLocalCommitDiff: (data) => ipcRenderer.invoke('get-local-commit-diff', data),
|
||||||
|
findGitignoredInRepo: (data) => ipcRenderer.invoke('find-gitignored-in-repo', data),
|
||||||
|
deleteGiteaPaths: (data) => ipcRenderer.invoke('delete-gitea-paths', data),
|
||||||
|
getLocalCommitFiles: (data) => ipcRenderer.invoke('get-local-commit-files', data),
|
||||||
getLocalCommitDetails: (data) => ipcRenderer.invoke('get-local-commit-details', data),
|
getLocalCommitDetails: (data) => ipcRenderer.invoke('get-local-commit-details', data),
|
||||||
|
|
||||||
// === FAVORITEN & ZULETZT GEÖFFNET ===
|
// === FAVORITEN & ZULETZT GEÖFFNET ===
|
||||||
@@ -151,6 +154,33 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
return () => ipcRenderer.removeListener('push-progress', listener);
|
return () => ipcRenderer.removeListener('push-progress', listener);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Neue Funktionen
|
||||||
|
prefsGet: () => ipcRenderer.invoke('prefs-get'),
|
||||||
|
prefsSet: (data) => ipcRenderer.invoke('prefs-set', data),
|
||||||
|
openKnownFolder: (which) => ipcRenderer.invoke('open-known-folder', { which }),
|
||||||
|
gitTransportState: () => ipcRenderer.invoke('git-transport-state'),
|
||||||
|
gitTransportClear: (host) => ipcRenderer.invoke('git-transport-clear', { host }),
|
||||||
|
previewUploadPaths: (data) => ipcRenderer.invoke('preview-upload-paths', data),
|
||||||
|
runServerDiagnosis: (data) => ipcRenderer.invoke('run-server-diagnosis', data),
|
||||||
|
localGit: (data) => ipcRenderer.invoke('local-git', data),
|
||||||
|
giteaCollab: (data) => ipcRenderer.invoke('gitea-collab', data),
|
||||||
|
repoBackupGetConfig: () => ipcRenderer.invoke('repo-backup-get-config'),
|
||||||
|
repoBackupSetConfig: (data) => ipcRenderer.invoke('repo-backup-set-config', data),
|
||||||
|
repoBackupRunNow: () => ipcRenderer.invoke('repo-backup-run-now'),
|
||||||
|
repoBackupCancel: () => ipcRenderer.invoke('repo-backup-cancel'),
|
||||||
|
repoBackupList: (data) => ipcRenderer.invoke('repo-backup-list', data),
|
||||||
|
repoRestoreLocal: (data) => ipcRenderer.invoke('repo-restore-local', data),
|
||||||
|
repoRestoreGitea: (data) => ipcRenderer.invoke('repo-restore-gitea', data),
|
||||||
|
onRepoRestoreProgress: (cb) => {
|
||||||
|
const listener = (event, payload) => { try { cb(payload); } catch (_) {} };
|
||||||
|
ipcRenderer.on('repo-restore-progress', listener);
|
||||||
|
return () => ipcRenderer.removeListener('repo-restore-progress', listener);
|
||||||
|
},
|
||||||
|
onRepoBackupProgress: (cb) => {
|
||||||
|
const listener = (event, payload) => { try { cb(payload); } catch (_) {} };
|
||||||
|
ipcRenderer.on('repo-backup-progress', listener);
|
||||||
|
return () => ipcRenderer.removeListener('repo-backup-progress', listener);
|
||||||
|
},
|
||||||
onFolderUploadProgress: (cb) => {
|
onFolderUploadProgress: (cb) => {
|
||||||
const listener = (event, payload) => { try { cb(payload); } catch (_) {} };
|
const listener = (event, payload) => { try { cb(payload); } catch (_) {} };
|
||||||
ipcRenderer.on('folder-upload-progress', listener);
|
ipcRenderer.on('folder-upload-progress', listener);
|
||||||
|
|||||||
+4
-1
@@ -358,7 +358,10 @@ class Updater {
|
|||||||
|
|
||||||
if (process.platform === 'win32') {
|
if (process.platform === 'win32') {
|
||||||
try {
|
try {
|
||||||
const child = spawn(filePath, [], {
|
// Das eigene Setup (ab 2.1.8) aktualisiert mit --update ohne Rückfragen und startet die App neu.
|
||||||
|
// Ältere NSIS-Setups kennen den Schalter nicht und bekommen ihn deshalb nicht.
|
||||||
|
const isOwnSetup = /^git-manager-gui-setup/i.test(path.basename(filePath));
|
||||||
|
const child = spawn(filePath, isOwnSetup ? ['--update'] : [], {
|
||||||
detached: true,
|
detached: true,
|
||||||
stdio: 'ignore',
|
stdio: 'ignore',
|
||||||
shell: false
|
shell: false
|
||||||
|
|||||||
Reference in New Issue
Block a user