Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
565835418e | ||
|
|
3ed561fe78 | ||
|
|
fe6c31793e | ||
|
|
4bb948d665 | ||
|
|
5c792eb1a8 | ||
|
|
309babf849 |
File diff suppressed because one or more lines are too long
+7
-7
@@ -490,13 +490,13 @@
|
|||||||
<!-- Features -->
|
<!-- Features -->
|
||||||
<section class="section" id="features">
|
<section class="section" id="features">
|
||||||
<h2 class="section-title">Warum Git Manager Pro?</h2>
|
<h2 class="section-title">Warum Git Manager Pro?</h2>
|
||||||
<p class="section-desc">Alles was du brauchst um deine Repositories effizient zu verwalten.</p>
|
<p class="section-desc">Alles, was du brauchst, um deine Repositories effizient zu verwalten.</p>
|
||||||
|
|
||||||
<div class="features-grid">
|
<div class="features-grid">
|
||||||
<div class="feature-card">
|
<div class="feature-card">
|
||||||
<div class="feature-icon">🌐</div>
|
<div class="feature-icon">🌐</div>
|
||||||
<h3>Gitea & GitHub</h3>
|
<h3>Gitea & GitHub</h3>
|
||||||
<p>Unterstützt beide Plattformen seamless — alle Repositories an einem Ort.</p>
|
<p>Unterstützt beide Plattformen nahtlos — alle Repositories an einem Ort.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="feature-card">
|
<div class="feature-card">
|
||||||
<div class="feature-icon">📂</div>
|
<div class="feature-icon">📂</div>
|
||||||
@@ -535,16 +535,16 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="feature-card">
|
<div class="feature-card">
|
||||||
<div class="feature-icon">📥</div>
|
<div class="feature-icon">📥</div>
|
||||||
<h3>Ein-Klick Migration</h3>
|
<h3>Ein-Klick-Migration</h3>
|
||||||
<p>Migriere Repos zwischen Services — mit kompletter History.</p>
|
<p>Migriere Repos zwischen Services — mit kompletter History.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="demo-section">
|
<div class="demo-section">
|
||||||
<h3 style="color: var(--accent2); margin-bottom: 16px;">🎥 Demo (baldiger Update)</h3>
|
<h3 style="color: var(--accent2); margin-bottom: 16px;">🎥 Demo (baldiges Update)</h3>
|
||||||
<div class="demo-mockup">
|
<div class="demo-mockup">
|
||||||
Screenshot-Bereich — Demo und UI-Preview kommen bald!<br><br>
|
Screenshot-Bereich — Demo und UI-Vorschau kommen bald!<br><br>
|
||||||
<em>Starte die App mit npm start um einen Live-Demo zu sehen</em>
|
<em>Starte die App mit npm start, um eine Live-Demo zu sehen</em>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -603,7 +603,7 @@
|
|||||||
<div class="links-section">
|
<div class="links-section">
|
||||||
<div class="link-card">
|
<div class="link-card">
|
||||||
<h3>📖 Benutzerhandbuch</h3>
|
<h3>📖 Benutzerhandbuch</h3>
|
||||||
<p>Vollständiges Handbuch mit Screenshots und Schritt-für-Schritt Anleitungen.</p>
|
<p>Vollständiges Handbuch mit Screenshots und Schritt-für-Schritt-Anleitungen.</p>
|
||||||
<a href="HANDBUCH.html" target="_blank">Zum Handbuch →</a>
|
<a href="HANDBUCH.html" target="_blank">Zum Handbuch →</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="link-card">
|
<div class="link-card">
|
||||||
|
|||||||
@@ -283,7 +283,7 @@ function tryReadWrappedCredentials(raw) {
|
|||||||
credentials: null,
|
credentials: null,
|
||||||
needsRewrite: false,
|
needsRewrite: false,
|
||||||
reason: 'safeStorage-unavailable',
|
reason: 'safeStorage-unavailable',
|
||||||
message: 'Gespeicherte Zugangsdaten koennen in dieser Sitzung nicht gelesen werden. Bitte neu anmelden.'
|
message: 'Gespeicherte Zugangsdaten können in dieser Sitzung nicht gelesen werden. Bitte neu anmelden.'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -299,7 +299,7 @@ function tryReadWrappedCredentials(raw) {
|
|||||||
credentials: null,
|
credentials: null,
|
||||||
needsRewrite: false,
|
needsRewrite: false,
|
||||||
reason: 'safeStorage-decrypt-failed',
|
reason: 'safeStorage-decrypt-failed',
|
||||||
message: 'Gespeicherte Zugangsdaten konnten nicht entschluesselt werden. Bitte Token neu eingeben.'
|
message: 'Gespeicherte Zugangsdaten konnten nicht entschlüsselt werden. Bitte Token neu eingeben.'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -349,7 +349,7 @@ function readCredentialsFromFile(filePath) {
|
|||||||
credentials: null,
|
credentials: null,
|
||||||
needsRewrite: false,
|
needsRewrite: false,
|
||||||
reason: 'legacy-decrypt-failed',
|
reason: 'legacy-decrypt-failed',
|
||||||
message: 'Gespeicherte Zugangsdaten waren unlesbar und wurden zurueckgesetzt. Bitte Zugangsdaten neu eingeben.',
|
message: 'Gespeicherte Zugangsdaten waren unlesbar und wurden zurückgesetzt. Bitte Zugangsdaten neu eingeben.',
|
||||||
filePath
|
filePath
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -678,7 +678,7 @@ function createWindow() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Externe Fenster immer blockieren und nur erlaubte URLs explizit im System-Browser oeffnen.
|
// Externe Fenster immer blockieren und nur erlaubte URLs explizit im System-Browser öffnen.
|
||||||
win.webContents.setWindowOpenHandler(({ url }) => {
|
win.webContents.setWindowOpenHandler(({ url }) => {
|
||||||
if (isAllowedExternalUrl(url)) {
|
if (isAllowedExternalUrl(url)) {
|
||||||
shell.openExternal(url).catch(() => {});
|
shell.openExternal(url).catch(() => {});
|
||||||
@@ -833,7 +833,7 @@ function mapIpcError(errorLike) {
|
|||||||
if (raw.includes('timeout') || raw.includes('econnaborted')) {
|
if (raw.includes('timeout') || raw.includes('econnaborted')) {
|
||||||
return 'Zeitüberschreitung bei der Verbindung. Bitte Netzwerk oder Server prüfen.';
|
return 'Zeitüberschreitung bei der Verbindung. Bitte Netzwerk oder Server prüfen.';
|
||||||
}
|
}
|
||||||
if (raw.includes('http://') || raw.includes('https://') || raw.includes('ungueltige gitea url') || raw.includes('ungültige gitea url') || raw.includes('invalid')) {
|
if (raw.includes('http://') || raw.includes('https://') || raw.includes('ungültige gitea url') || raw.includes('ungültige gitea url') || raw.includes('invalid')) {
|
||||||
return 'Ungültige URL. Beispiel für IPv6: http://[2001:db8::1]:3000';
|
return 'Ungültige URL. Beispiel für IPv6: http://[2001:db8::1]:3000';
|
||||||
}
|
}
|
||||||
return String(errorLike && errorLike.message ? errorLike.message : errorLike);
|
return String(errorLike && errorLike.message ? errorLike.message : errorLike);
|
||||||
@@ -1714,7 +1714,7 @@ ipcMain.handle('deleteFile', async (event, data) => {
|
|||||||
return files;
|
return files;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function executeDeleteBatch(files) {
|
async function executeDeleteBatch(files, concurrency = 2) {
|
||||||
const deleteOps = files.map(f =>
|
const deleteOps = files.map(f =>
|
||||||
async () => {
|
async () => {
|
||||||
const res = await giteaDeleteFile(f.path, f.sha);
|
const res = await giteaDeleteFile(f.path, f.sha);
|
||||||
@@ -1722,8 +1722,8 @@ ipcMain.handle('deleteFile', async (event, data) => {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// Gitea: mit Concurrency=2 + Retry-Logik bei 409-Konflikten für bessere Performance
|
// Concurrency kann dynamisch reduziert werden, um API-Konflikte bei Nachloeschungen zu vermeiden
|
||||||
const deleteResults = await runParallel(deleteOps, 2);
|
const deleteResults = await runParallel(deleteOps, concurrency);
|
||||||
const failedEntries = deleteResults.filter(r => !r.ok || !(r.result && r.result.ok));
|
const failedEntries = deleteResults.filter(r => !r.ok || !(r.result && r.result.ok));
|
||||||
return { deleteResults, failedEntries };
|
return { deleteResults, failedEntries };
|
||||||
}
|
}
|
||||||
@@ -1741,38 +1741,31 @@ ipcMain.handle('deleteFile', async (event, data) => {
|
|||||||
return { ok: true, deleted: 0 };
|
return { ok: true, deleted: 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete attempt
|
// Delete attempt (schnell) + sequenzieller Fallback bei Konflikten
|
||||||
let { failedEntries } = await executeDeleteBatch(filesToDelete);
|
let { failedEntries } = await executeDeleteBatch(filesToDelete, 2);
|
||||||
|
|
||||||
|
// Bei Teilfehlern sofort sequenziellen Retry fahren (stabiler bei parallelen Commits)
|
||||||
if (failedEntries.length > 0) {
|
if (failedEntries.length > 0) {
|
||||||
const failedFiles = failedEntries
|
const failedFiles = failedEntries
|
||||||
.map(r => (r.result && r.result.path) ? r.result.path : 'unknown')
|
.map(r => (r.result && r.result.path) ? r.result.path : 'unknown')
|
||||||
.join(', ');
|
.join(', ');
|
||||||
logger.error('deleteFile', `Failed to delete ${failedEntries.length} files`, { files: failedFiles });
|
logger.warn('deleteFile', `Initial delete had ${failedEntries.length} failures, retrying sequentially`, { files: failedFiles });
|
||||||
return { ok: false, error: `${failedEntries.length} von ${filesToDelete.length} Dateien konnten nicht gelöscht werden` };
|
}
|
||||||
|
|
||||||
|
// Verification + bis zu zwei sequenzielle Nachlöschversuche auf verbleibende Dateien
|
||||||
|
for (let attempt = 0; attempt < 2; attempt++) {
|
||||||
|
if (!(await pathStillExists(filePath))) break;
|
||||||
|
const remaining = await collectAllFiles(filePath);
|
||||||
|
if (remaining.length === 0) break;
|
||||||
|
({ failedEntries } = await executeDeleteBatch(remaining, 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verification + one automatic retry on leftovers
|
|
||||||
if (await pathStillExists(filePath)) {
|
if (await pathStillExists(filePath)) {
|
||||||
const remaining = await collectAllFiles(filePath);
|
const remaining = await collectAllFiles(filePath);
|
||||||
if (remaining.length > 0) {
|
const remainingPreview = remaining.slice(0, 10).map(f => f.path).join(', ');
|
||||||
({ failedEntries } = await executeDeleteBatch(remaining));
|
|
||||||
if (failedEntries.length > 0) {
|
|
||||||
const failedFiles = failedEntries
|
|
||||||
.map(r => (r.result && r.result.path) ? r.result.path : 'unknown')
|
|
||||||
.join(', ');
|
|
||||||
return {
|
|
||||||
ok: false,
|
|
||||||
error: `Nachloeschung fehlgeschlagen. ${failedEntries.length} Dateien konnten nicht gelöscht werden (${failedFiles}).`
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (await pathStillExists(filePath)) {
|
|
||||||
return {
|
return {
|
||||||
ok: false,
|
ok: false,
|
||||||
error: 'Löschen unvollständig: Der Ordnerpfad ist nach zwei Versuchen noch vorhanden.'
|
error: `Löschen unvollständig: ${remaining.length} Dateien sind noch vorhanden (${remainingPreview}${remaining.length > 10 ? ', ...' : ''}).`
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2451,6 +2444,174 @@ ipcMain.handle('upload-local-folder-to-gitea', async (event, data) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/* -----------------------------------------------------------------
|
||||||
|
Gemeinsamer Git-Upload für einen kompletten Drop (Dateien + Ordner).
|
||||||
|
Erzeugt EINEN Clone + EINEN Commit + EINEN Push für ALLE Pfade
|
||||||
|
zusammen – egal wie viele Dateien/Ordner gedroppt wurden.
|
||||||
|
----------------------------------------------------------------- */
|
||||||
|
const GIT_UPLOAD_EXCLUDE = ['.git', 'node_modules', '.DS_Store', 'thumbs.db', '.vscode', '.idea'];
|
||||||
|
|
||||||
|
// Wandelt gedroppte Pfade (Dateien und/oder Ordner) in eine flache Liste
|
||||||
|
// { localFile, targetPath } um. Dateien -> destPath/<name>, Ordner ->
|
||||||
|
// destPath/<ordnername>/<rel> (identisch zum bisherigen Verhalten).
|
||||||
|
function buildItemsFromPaths(paths, destPath) {
|
||||||
|
const items = [];
|
||||||
|
const skipped = [];
|
||||||
|
for (const p of paths) {
|
||||||
|
let stat;
|
||||||
|
try { stat = fs.statSync(p); } catch (e) { skipped.push({ path: p, error: 'not-found' }); continue; }
|
||||||
|
const baseName = ppath.basename(p);
|
||||||
|
if (stat.isFile()) {
|
||||||
|
const targetPath = destPath ? `${destPath}/${baseName}` : baseName;
|
||||||
|
items.push({ localFile: p, targetPath });
|
||||||
|
} else if (stat.isDirectory()) {
|
||||||
|
const folderName = baseName;
|
||||||
|
(function walk(dir) {
|
||||||
|
let entries;
|
||||||
|
try { entries = fs.readdirSync(dir); } catch (e) { return; }
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (GIT_UPLOAD_EXCLUDE.includes(entry)) continue;
|
||||||
|
const full = ppath.join(dir, entry);
|
||||||
|
let st;
|
||||||
|
try { st = fs.statSync(full); } catch (e) { continue; }
|
||||||
|
if (st.isDirectory()) {
|
||||||
|
walk(full);
|
||||||
|
} else if (st.isFile()) {
|
||||||
|
const rel = ppath.relative(p, full).split(ppath.sep).join('/');
|
||||||
|
const targetPath = destPath ? `${destPath}/${folderName}/${rel}` : `${folderName}/${rel}`;
|
||||||
|
items.push({ localFile: full, targetPath });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})(p);
|
||||||
|
} else {
|
||||||
|
skipped.push({ path: p, error: 'unsupported' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { items, skipped };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Klont das Repo (shallow), kopiert alle items hinein, commit + push.
|
||||||
|
async function gitPushItemsToGitea({ token, url, owner, repo, branch, items, message, onProgress }) {
|
||||||
|
const simpleGit = require('simple-git');
|
||||||
|
|
||||||
|
const rawUrl = url.startsWith('http') ? url : `https://${url}`;
|
||||||
|
const urlObj = new URL(rawUrl.replace(/\/$/, ''));
|
||||||
|
const authUrl = `${urlObj.protocol}//${encodeURIComponent(token)}@${urlObj.host}/${owner}/${repo}.git`;
|
||||||
|
|
||||||
|
const tmpDir = getSafeTmpDir(`git-paths-${owner}-${repo}`);
|
||||||
|
const gitConfig = ['user.email=gui@gitmanager.local', 'user.name=Git Manager GUI'];
|
||||||
|
const safeBranch = sanitizeGitRef(branch || 'HEAD', 'HEAD');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const git = simpleGit({ config: gitConfig });
|
||||||
|
let repoGit;
|
||||||
|
let isEmptyRepo = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const cloneArgs = ['--depth', '1', '--no-single-branch'];
|
||||||
|
if (safeBranch !== 'HEAD') cloneArgs.push('--branch', safeBranch);
|
||||||
|
console.log('[UploadPaths] Clone-Versuch:', { cloneArgs, tmpDir });
|
||||||
|
await git.clone(authUrl, tmpDir, cloneArgs);
|
||||||
|
repoGit = simpleGit({ baseDir: tmpDir, config: gitConfig });
|
||||||
|
} catch (cloneErr) {
|
||||||
|
const cloneMsg = String(cloneErr);
|
||||||
|
if (cloneMsg.includes('empty') || cloneMsg.includes('nothing to clone') ||
|
||||||
|
cloneMsg.includes('did not match') || cloneMsg.includes('Remote branch')) {
|
||||||
|
isEmptyRepo = true;
|
||||||
|
repoGit = simpleGit({ baseDir: tmpDir, config: gitConfig });
|
||||||
|
await repoGit.init();
|
||||||
|
await repoGit.addRemote('origin', authUrl);
|
||||||
|
} else {
|
||||||
|
throw cloneErr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (onProgress) onProgress(30);
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
const destFile = ppath.join(tmpDir, ...item.targetPath.split('/'));
|
||||||
|
fs.mkdirSync(ppath.dirname(destFile), { recursive: true });
|
||||||
|
fs.copyFileSync(item.localFile, destFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (onProgress) onProgress(60);
|
||||||
|
|
||||||
|
await repoGit.add('.');
|
||||||
|
|
||||||
|
let hasChanges = true;
|
||||||
|
try {
|
||||||
|
await repoGit.commit(message);
|
||||||
|
} catch (commitErr) {
|
||||||
|
if (String(commitErr).includes('nothing to commit')) hasChanges = false;
|
||||||
|
else throw commitErr;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasChanges) {
|
||||||
|
let pushBranch = safeBranch;
|
||||||
|
if (pushBranch === 'HEAD' || isEmptyRepo) {
|
||||||
|
try { const bs = await repoGit.branch(); pushBranch = bs.current || 'main'; } catch (_) { pushBranch = 'main'; }
|
||||||
|
}
|
||||||
|
if (isEmptyRepo) await repoGit.push(['-u', 'origin', pushBranch]);
|
||||||
|
else await repoGit.push('origin', pushBranch);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (onProgress) onProgress(100);
|
||||||
|
setTimeout(() => { try { if (fs.existsSync(tmpDir)) fs.rmSync(tmpDir, { recursive: true, force: true }); } catch (_) {} }, 5000);
|
||||||
|
return { hasChanges };
|
||||||
|
} catch (gitErr) {
|
||||||
|
try { if (fs.existsSync(tmpDir)) fs.rmSync(tmpDir, { recursive: true, force: true }); } catch (_) {}
|
||||||
|
throw gitErr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ipcMain.handle('upload-paths-to-gitea', async (event, data) => {
|
||||||
|
const uploadDebugId = `p-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
||||||
|
try {
|
||||||
|
const credentials = readCredentials();
|
||||||
|
const token = (data && data.token) || (credentials && credentials.giteaToken);
|
||||||
|
const url = (data && data.url) || (credentials && credentials.giteaURL);
|
||||||
|
if (!token || !url) return { ok: false, error: 'Zugangsdaten fehlen', debugId: uploadDebugId };
|
||||||
|
|
||||||
|
const owner = data.owner;
|
||||||
|
const repo = data.repo;
|
||||||
|
if (!owner || !repo) return { ok: false, error: 'missing-owner-or-repo', debugId: uploadDebugId };
|
||||||
|
|
||||||
|
const destPath = (data.destPath || '').replace(/^\//, '').replace(/\/$/, '');
|
||||||
|
const branch = sanitizeGitRef(data.branch || 'HEAD', 'HEAD');
|
||||||
|
const messagePrefix = data.messagePrefix || 'Upload via GUI';
|
||||||
|
const paths = Array.isArray(data.paths) ? data.paths.filter(Boolean) : [];
|
||||||
|
if (paths.length === 0) return { ok: false, error: 'Keine gültigen Pfade übergeben', debugId: uploadDebugId };
|
||||||
|
|
||||||
|
try { invalidateRepoContentsCache(owner, repo); } catch (_) {}
|
||||||
|
try { caches.repos.invalidate(`${owner}/${repo}`); } catch (_) {}
|
||||||
|
|
||||||
|
const { items, skipped } = buildItemsFromPaths(paths, destPath);
|
||||||
|
if (items.length === 0) {
|
||||||
|
return { ok: false, error: 'Keine Dateien zum Hochladen gefunden', skipped, debugId: uploadDebugId };
|
||||||
|
}
|
||||||
|
|
||||||
|
const total = items.length;
|
||||||
|
const onProgress = (percent) => {
|
||||||
|
try { event.sender.send('folder-upload-progress', { processed: Math.floor(total * percent / 100), total, percent }); } catch (_) {}
|
||||||
|
};
|
||||||
|
onProgress(0);
|
||||||
|
|
||||||
|
console.log('[UploadPaths] start', { uploadDebugId, owner, repo, branch, total, destPath, pathCount: paths.length });
|
||||||
|
await gitPushItemsToGitea({
|
||||||
|
token, url, owner, repo, branch, items,
|
||||||
|
message: `${messagePrefix} (${total} Datei${total === 1 ? '' : 'en'})`,
|
||||||
|
onProgress
|
||||||
|
});
|
||||||
|
console.log('[UploadPaths] done', { uploadDebugId, total });
|
||||||
|
|
||||||
|
const results = items.map(it => ({ ok: true, localFile: it.localFile, targetPath: it.targetPath }));
|
||||||
|
return { ok: true, results, failedCount: 0, skipped, debugId: uploadDebugId };
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[UploadPaths] fatal', { uploadDebugId, error: String(e) });
|
||||||
|
return { ok: false, error: String(e && e.message ? e.message : e), debugId: uploadDebugId };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
ipcMain.handle('download-gitea-file', async (event, data) => {
|
ipcMain.handle('download-gitea-file', async (event, data) => {
|
||||||
try {
|
try {
|
||||||
const credentials = readCredentials();
|
const credentials = readCredentials();
|
||||||
@@ -2848,19 +3009,97 @@ ipcMain.handle('upload-and-push', async (event, data) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- FALL 2: Ordner (Normale Git-Logik) ---
|
// --- FALL 2: Ordner ---
|
||||||
if (!isDirectory) {
|
if (!isDirectory) {
|
||||||
return { ok: false, error: `Path is neither file nor directory (${uploadDebugId})` };
|
return { ok: false, error: `Path is neither file nor directory (${uploadDebugId})` };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const folderName = ppath.basename(data.localFolder);
|
||||||
|
// Dateien/Ordner, die niemals hochgeladen werden sollen
|
||||||
|
const excludeList = ['.git', 'node_modules', '.DS_Store', 'thumbs.db', '.vscode', '.idea'];
|
||||||
|
|
||||||
|
// === PRIMÄRWEG: git push =========================================
|
||||||
|
// Ein einziger komprimierter Transfer + genau EIN Commit statt
|
||||||
|
// hunderter API-Requests. Das behebt sowohl die Langsamkeit als auch
|
||||||
|
// die "cannot lock ref"/Timeout-Fehler durch parallele Einzel-Commits.
|
||||||
|
// Bei Fehler (z.B. git nicht installiert) fällt der Code auf den
|
||||||
|
// API-Upload weiter unten zurück.
|
||||||
|
try {
|
||||||
|
let finalCloneUrl = cloneUrl;
|
||||||
|
if (!finalCloneUrl && giteaUrl) {
|
||||||
|
const base = giteaUrl.replace(/\/$/, '');
|
||||||
|
const urlObj = new URL(base);
|
||||||
|
finalCloneUrl = `${urlObj.protocol}//${urlObj.host}/${owner}/${repo}.git`;
|
||||||
|
}
|
||||||
|
if (!finalCloneUrl) throw new Error('no-clone-url');
|
||||||
|
|
||||||
|
// Token in die Clone-URL einbetten (für Authentifizierung beim Push)
|
||||||
|
let authClone = finalCloneUrl;
|
||||||
|
try {
|
||||||
|
const urlObj = new URL(finalCloneUrl);
|
||||||
|
if (token && urlObj.protocol.startsWith('http')) {
|
||||||
|
urlObj.username = encodeURIComponent(token);
|
||||||
|
authClone = urlObj.toString();
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
|
||||||
|
const tmpDir = getSafeTmpDir(`git-push-folder-${owner}-${repo}`);
|
||||||
|
try {
|
||||||
|
console.log('[UPLOAD_DEBUG][main] directory-upload:git-clone', { uploadDebugId, branch, destPath });
|
||||||
|
const cloneArgs = ['clone', '--depth', '1'];
|
||||||
|
if (branch !== 'HEAD') cloneArgs.push('--branch', branch);
|
||||||
|
cloneArgs.push(authClone, tmpDir);
|
||||||
|
runGitSync(cloneArgs, process.cwd(), gitExecOptions);
|
||||||
|
|
||||||
|
// Zielverzeichnis im Repo: <destPath>/<folderName>/ (wie im API-Weg)
|
||||||
|
let destDirInRepo = tmpDir;
|
||||||
|
if (destPath) destDirInRepo = ppath.join(tmpDir, destPath.split('/').join(ppath.sep));
|
||||||
|
destDirInRepo = ppath.join(destDirInRepo, folderName);
|
||||||
|
ensureDir(destDirInRepo);
|
||||||
|
|
||||||
|
// Kompletten Ordner rekursiv kopieren, ausgeschlossene Einträge überspringen
|
||||||
|
fs.cpSync(data.localFolder, destDirInRepo, {
|
||||||
|
recursive: true,
|
||||||
|
filter: (src) => !excludeList.includes(ppath.basename(src))
|
||||||
|
});
|
||||||
|
|
||||||
|
runGitSync(['-C', tmpDir, 'add', '.'], process.cwd(), gitExecOptions);
|
||||||
|
try {
|
||||||
|
runGitSync(['-C', tmpDir, 'commit', '-m', `Upload ${folderName} via GUI`], process.cwd(), gitExecOptions);
|
||||||
|
} catch (commitErr) {
|
||||||
|
// "nothing to commit" ist kein echter Fehler
|
||||||
|
if (!/nothing to commit/i.test(String(commitErr))) throw commitErr;
|
||||||
|
console.log('[UPLOAD_DEBUG][main] directory-upload:git-nothing-to-commit', { uploadDebugId });
|
||||||
|
setTimeout(() => { try { if (fs.existsSync(tmpDir)) fs.rmSync(tmpDir, { recursive: true, force: true }); } catch (_) {} }, 5_000);
|
||||||
|
return { ok: true, usedGit: true, results: [], debugId: uploadDebugId, msg: 'Keine Änderungen zum Hochladen' };
|
||||||
|
}
|
||||||
|
|
||||||
|
let pushBranch = branch;
|
||||||
|
if (pushBranch === 'HEAD') {
|
||||||
|
try {
|
||||||
|
pushBranch = runGitSync(['-C', tmpDir, 'rev-parse', '--abbrev-ref', 'HEAD'], process.cwd(), gitExecOptions).trim();
|
||||||
|
} catch (_) { pushBranch = 'main'; }
|
||||||
|
if (!pushBranch || pushBranch === 'HEAD') pushBranch = 'main';
|
||||||
|
}
|
||||||
|
runGitSync(['-C', tmpDir, 'push', 'origin', pushBranch], process.cwd(), gitExecOptions);
|
||||||
|
|
||||||
|
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);
|
||||||
|
return { ok: true, usedGit: true, results: [], debugId: uploadDebugId };
|
||||||
|
} catch (gitErr) {
|
||||||
|
try { if (fs.existsSync(tmpDir)) fs.rmSync(tmpDir, { recursive: true, force: true }); } catch (_) {}
|
||||||
|
throw gitErr;
|
||||||
|
}
|
||||||
|
} catch (gitErr) {
|
||||||
|
console.warn('[UPLOAD_DEBUG][main] directory-upload:git-failed-fallback-to-api', { uploadDebugId, error: String(gitErr && gitErr.message ? gitErr.message : gitErr) });
|
||||||
|
// -> weiter zum API-Fallback unten
|
||||||
|
}
|
||||||
|
|
||||||
console.log('[UPLOAD_DEBUG][main] directory-upload:using-api-path', { uploadDebugId, branch, destPath });
|
console.log('[UPLOAD_DEBUG][main] directory-upload:using-api-path', { uploadDebugId, branch, destPath });
|
||||||
|
|
||||||
// Fallback: API Upload (paralleler Upload)
|
// Fallback: API Upload (paralleler Upload)
|
||||||
const items = [];
|
const items = [];
|
||||||
const folderName = ppath.basename(data.localFolder);
|
// excludeList und folderName sind oben (FALL 2) bereits definiert
|
||||||
|
|
||||||
// FIXED EXCLUDE LIST: Filter out .git, node_modules etc.
|
|
||||||
const excludeList = ['.git', 'node_modules', '.DS_Store', 'thumbs.db', '.vscode', '.idea'];
|
|
||||||
|
|
||||||
(function walk(dir) {
|
(function walk(dir) {
|
||||||
const entries = fs.readdirSync(dir);
|
const entries = fs.readdirSync(dir);
|
||||||
@@ -3844,7 +4083,7 @@ ipcMain.handle('import-settings-bundle', async () => {
|
|||||||
const parsed = JSON.parse(raw);
|
const parsed = JSON.parse(raw);
|
||||||
|
|
||||||
if (!parsed || parsed.bundleType !== 'git-manager-settings-backup') {
|
if (!parsed || parsed.bundleType !== 'git-manager-settings-backup') {
|
||||||
return { ok: false, error: 'Ungueltige Backup-Datei.' };
|
return { ok: false, error: 'Ungültige Backup-Datei.' };
|
||||||
}
|
}
|
||||||
|
|
||||||
const existing = readCredentials() || {};
|
const existing = readCredentials() || {};
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "git-manager-gui",
|
"name": "git-manager-gui",
|
||||||
"version": "2.1.3",
|
"version": "2.1.6",
|
||||||
"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",
|
||||||
|
|||||||
+1
-2
@@ -38,6 +38,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
downloadGiteaFolder: (data) => ipcRenderer.invoke('download-gitea-folder', data),
|
downloadGiteaFolder: (data) => ipcRenderer.invoke('download-gitea-folder', data),
|
||||||
downloadGiteaFile: (data) => ipcRenderer.invoke('download-gitea-file', data),
|
downloadGiteaFile: (data) => ipcRenderer.invoke('download-gitea-file', data),
|
||||||
uploadLocalFolderToGitea: (data) => ipcRenderer.invoke('upload-local-folder-to-gitea', data),
|
uploadLocalFolderToGitea: (data) => ipcRenderer.invoke('upload-local-folder-to-gitea', data),
|
||||||
|
uploadPathsToGitea: (data) => ipcRenderer.invoke('upload-paths-to-gitea', data),
|
||||||
|
|
||||||
// Repository & Git Management
|
// Repository & Git Management
|
||||||
saveCredentials: (data) => ipcRenderer.invoke('save-credentials', data),
|
saveCredentials: (data) => ipcRenderer.invoke('save-credentials', data),
|
||||||
@@ -95,7 +96,6 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
|
|
||||||
// Commit History & Visualization
|
// Commit History & Visualization
|
||||||
getCommits: (data) => ipcRenderer.invoke('get-commits', data),
|
getCommits: (data) => ipcRenderer.invoke('get-commits', data),
|
||||||
getCommitDetails: (data) => ipcRenderer.invoke('get-commit-details', data),
|
|
||||||
getCommitDiff: (data) => ipcRenderer.invoke('get-commit-diff', data),
|
getCommitDiff: (data) => ipcRenderer.invoke('get-commit-diff', data),
|
||||||
getCommitFiles: (data) => ipcRenderer.invoke('get-commit-files', data),
|
getCommitFiles: (data) => ipcRenderer.invoke('get-commit-files', data),
|
||||||
searchCommits: (data) => ipcRenderer.invoke('search-commits', data),
|
searchCommits: (data) => ipcRenderer.invoke('search-commits', data),
|
||||||
@@ -105,7 +105,6 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
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),
|
||||||
getLocalCommitDetails: (data) => ipcRenderer.invoke('get-local-commit-details', data),
|
getLocalCommitDetails: (data) => ipcRenderer.invoke('get-local-commit-details', data),
|
||||||
searchLocalCommits: (data) => ipcRenderer.invoke('search-local-commits', data),
|
|
||||||
|
|
||||||
// === FAVORITEN & ZULETZT GEÖFFNET ===
|
// === FAVORITEN & ZULETZT GEÖFFNET ===
|
||||||
loadFavorites: () => ipcRenderer.invoke('load-favorites'),
|
loadFavorites: () => ipcRenderer.invoke('load-favorites'),
|
||||||
|
|||||||
+112
-135
@@ -360,6 +360,23 @@ function loadRepos() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Gravur-/Projekttitel zurücksetzen, wenn zur Übersicht gewechselt wird.
|
||||||
|
// Einmalig auf Modulebene definiert, damit die Funktion sofort ab Programmstart
|
||||||
|
// verfügbar ist (früher versehentlich in loadRepoContents definiert -> beim
|
||||||
|
// Klick vor dem ersten Repo-Laden "resetProjectGravurTitle is not a function").
|
||||||
|
window.resetProjectGravurTitle = function() {
|
||||||
|
const gravurTitle = document.getElementById('project-gravur-title');
|
||||||
|
if (gravurTitle) gravurTitle.textContent = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
// loadRepos EINMAL umhüllen, sodass beim Laden der Repo-Liste der Projekttitel
|
||||||
|
// zurückgesetzt wird (früher wurde bei jedem Repo-Aufruf neu umhüllt).
|
||||||
|
const origLoadRepos = loadRepos;
|
||||||
|
loadRepos = function(...args) {
|
||||||
|
window.resetProjectGravurTitle();
|
||||||
|
return origLoadRepos.apply(this, args);
|
||||||
|
};
|
||||||
|
|
||||||
/** Load repositories from GitHub and render the grid using loadGiteaRepos's renderer */
|
/** Load repositories from GitHub and render the grid using loadGiteaRepos's renderer */
|
||||||
async function loadGithubRepos(requestId = null) {
|
async function loadGithubRepos(requestId = null) {
|
||||||
const activeRequestId = requestId || ++repoLoadRequestId;
|
const activeRequestId = requestId || ++repoLoadRequestId;
|
||||||
@@ -831,10 +848,6 @@ async function uploadDroppedPaths({ paths, owner, repo, destPath = '', cloneUrl
|
|||||||
return { ok: false, error: 'Drag-and-Drop Upload ist aktuell nur für Gitea aktiv.' };
|
return { ok: false, error: 'Drag-and-Drop Upload ist aktuell nur für Gitea aktiv.' };
|
||||||
}
|
}
|
||||||
|
|
||||||
let successCount = 0;
|
|
||||||
let failedCount = 0;
|
|
||||||
const errors = [];
|
|
||||||
|
|
||||||
const withTimeout = (promise, ms, label) => {
|
const withTimeout = (promise, ms, label) => {
|
||||||
return Promise.race([
|
return Promise.race([
|
||||||
promise,
|
promise,
|
||||||
@@ -842,79 +855,57 @@ async function uploadDroppedPaths({ paths, owner, repo, destPath = '', cloneUrl
|
|||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
|
|
||||||
for (let i = 0; i < safePaths.length; i++) {
|
// Der KOMPLETTE Drop (alle Dateien UND Ordner zusammen) wird in EINEM
|
||||||
const p = safePaths[i];
|
// Aufruf hochgeladen: ein Repo-Clone, ein Commit, ein Push – egal wie
|
||||||
const baseName = p.split(/[\\/]/).pop() || p;
|
// viele Objekte gedroppt wurden. Das war die Ursache für die langen
|
||||||
showProgress(Math.round(((i + 1) / safePaths.length) * 100), `Upload ${i + 1}/${safePaths.length}: ${baseName}`);
|
// Wartezeiten und Zeitüberschreitungen (vorher: ein kompletter Clone+Push
|
||||||
try {
|
// pro einzelner Datei bzw. pro Ordner).
|
||||||
let res = null;
|
showProgress(15, `Bereite Upload von ${safePaths.length} Objekt(en) vor ...`);
|
||||||
|
try {
|
||||||
|
const res = await withTimeout(window.electronAPI.uploadPathsToGitea({
|
||||||
|
paths: safePaths,
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
destPath,
|
||||||
|
branch,
|
||||||
|
messagePrefix: 'Upload via GUI'
|
||||||
|
}), 900000, 'upload-paths-to-gitea'); // 15 Min Sicherheits-Timeout
|
||||||
|
showProgress(100, 'Upload abgeschlossen');
|
||||||
|
console.log('[UPLOAD_DEBUG][renderer] uploadDroppedPaths:result', res);
|
||||||
|
try { window.electronAPI.debugToMain('log', 'uploadDroppedPaths:result', { count: safePaths.length, ok: res?.ok, failedCount: res?.failedCount }); } catch (_) {}
|
||||||
|
|
||||||
const pathType = await withTimeout(window.electronAPI.getPathType(p), 5000, 'get-path-type');
|
if (!res?.ok) {
|
||||||
console.log('[UPLOAD_DEBUG][renderer] uploadDroppedPaths:pathType', { path: p, pathType });
|
return {
|
||||||
try { window.electronAPI.debugToMain('log', 'uploadDroppedPaths:pathType', { path: p, pathType }); } catch (_) {}
|
ok: false,
|
||||||
|
error: res?.error || 'Upload fehlgeschlagen',
|
||||||
if (!pathType?.ok) {
|
failedFiles: Array.isArray(res?.results) ? res.results.filter(r => !r.ok) : [],
|
||||||
throw new Error(pathType?.error || 'path-type-failed');
|
successCount: 0,
|
||||||
}
|
failedCount: safePaths.length
|
||||||
|
};
|
||||||
if (pathType.type === 'file') {
|
|
||||||
res = await withTimeout(window.electronAPI.uploadGiteaFile({
|
|
||||||
owner,
|
|
||||||
repo,
|
|
||||||
localPath: [p],
|
|
||||||
destPath,
|
|
||||||
branch,
|
|
||||||
platform: currentState.platform
|
|
||||||
}), 15000, 'upload-gitea-file');
|
|
||||||
} else if (pathType.type === 'dir') {
|
|
||||||
res = await withTimeout(window.electronAPI.uploadLocalFolderToGitea({
|
|
||||||
localFolder: p,
|
|
||||||
owner,
|
|
||||||
repo,
|
|
||||||
destPath,
|
|
||||||
branch,
|
|
||||||
messagePrefix: 'Upload folder via GUI'
|
|
||||||
}), 600000, 'upload-local-folder-to-gitea');
|
|
||||||
} else {
|
|
||||||
throw new Error(`Nicht unterstuetzter Pfadtyp: ${pathType.type || 'unknown'}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('[UPLOAD_DEBUG][renderer] uploadDroppedPaths:itemResult', { path: p, res });
|
|
||||||
try { window.electronAPI.debugToMain('log', 'uploadDroppedPaths:itemResult', { path: p, res }); } catch (_) {}
|
|
||||||
|
|
||||||
if (!res?.ok) {
|
|
||||||
failedCount++;
|
|
||||||
const detailedFailure = Array.isArray(res?.failedFiles) && res.failedFiles.length > 0
|
|
||||||
? `${baseName}: ${res.failedFiles[0].targetPath} - ${res.failedFiles[0].error || 'Unbekannter Fehler'}`
|
|
||||||
: `${baseName}: ${res?.error || 'Unbekannter Fehler'}`;
|
|
||||||
errors.push(detailedFailure);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const failedEntries = Array.isArray(res.results) ? res.results.filter(r => !r.ok) : [];
|
|
||||||
if (failedEntries.length > 0) {
|
|
||||||
failedCount++;
|
|
||||||
errors.push(`${baseName}: ${failedEntries[0]?.error || 'Teilweise fehlgeschlagen'}`);
|
|
||||||
} else {
|
|
||||||
successCount++;
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
failedCount++;
|
|
||||||
const errMsg = String(err && err.message ? err.message : err);
|
|
||||||
errors.push(`${baseName}: ${errMsg}`);
|
|
||||||
console.error('[UPLOAD_DEBUG][renderer] uploadDroppedPaths:itemError', { path: p, err: errMsg });
|
|
||||||
try { window.electronAPI.debugToMain('error', 'uploadDroppedPaths:itemError', { path: p, err: errMsg }); } catch (_) {}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (failedCount > 0) {
|
const failedEntries = Array.isArray(res.results) ? res.results.filter(r => !r.ok) : [];
|
||||||
return { ok: false, error: errors[0] || `${failedCount} Upload(s) fehlgeschlagen`, successCount, failedCount };
|
const uploaded = (Array.isArray(res.results) ? res.results.length : 0) - failedEntries.length;
|
||||||
}
|
if (failedEntries.length > 0) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
error: failedEntries[0]?.error || 'Teilweise fehlgeschlagen',
|
||||||
|
failedFiles: failedEntries,
|
||||||
|
successCount: uploaded,
|
||||||
|
failedCount: failedEntries.length
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const result = { ok: true, uploadedFiles: successCount, uploadedDirs: 0 };
|
const result = { ok: true, uploadedFiles: uploaded, uploadedDirs: 0 };
|
||||||
try { window.electronAPI.debugToMain('log', 'uploadDroppedPaths:done', result); } catch (_) {}
|
try { window.electronAPI.debugToMain('log', 'uploadDroppedPaths:done', result); } catch (_) {}
|
||||||
console.log('[UPLOAD_DEBUG][renderer] uploadDroppedPaths:done', result);
|
console.log('[UPLOAD_DEBUG][renderer] uploadDroppedPaths:done', result);
|
||||||
return result;
|
return result;
|
||||||
|
} catch (err) {
|
||||||
|
const errMsg = String(err && err.message ? err.message : err);
|
||||||
|
console.error('[UPLOAD_DEBUG][renderer] uploadDroppedPaths:error', { err: errMsg });
|
||||||
|
try { window.electronAPI.debugToMain('error', 'uploadDroppedPaths:error', { err: errMsg }); } catch (_) {}
|
||||||
|
return { ok: false, error: errMsg, successCount: 0, failedCount: safePaths.length };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractDroppedPaths(files) {
|
function extractDroppedPaths(files) {
|
||||||
@@ -944,7 +935,7 @@ async function toggleRepoVisibility(owner, repoName, currentPrivate) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const targetPrivate = !currentPrivate;
|
const targetPrivate = !currentPrivate;
|
||||||
const actionText = targetPrivate ? 'privat' : 'oeffentlich';
|
const actionText = targetPrivate ? 'privat' : 'öffentlich';
|
||||||
showProgress(35, `Repository wird ${actionText} gesetzt...`);
|
showProgress(35, `Repository wird ${actionText} gesetzt...`);
|
||||||
const result = await window.electronAPI.updateGiteaRepoVisibility({
|
const result = await window.electronAPI.updateGiteaRepoVisibility({
|
||||||
token: creds.giteaToken,
|
token: creds.giteaToken,
|
||||||
@@ -1048,7 +1039,7 @@ function showTagsEditorModal(owner, repoName, seed, knownTopics) {
|
|||||||
|
|
||||||
const hint = document.createElement('div');
|
const hint = document.createElement('div');
|
||||||
hint.className = 'settings-inline-hint';
|
hint.className = 'settings-inline-hint';
|
||||||
hint.textContent = 'Vorschlaege kommen live von deiner Gitea-Seite. Neue Tags sind ebenfalls erlaubt.';
|
hint.textContent = 'Vorschläge kommen live von deiner Gitea-Seite. Neue Tags sind ebenfalls erlaubt.';
|
||||||
|
|
||||||
group.appendChild(repoLabel);
|
group.appendChild(repoLabel);
|
||||||
group.appendChild(selectedHostEl);
|
group.appendChild(selectedHostEl);
|
||||||
@@ -1130,7 +1121,7 @@ function showTagsEditorModal(owner, repoName, seed, knownTopics) {
|
|||||||
}
|
}
|
||||||
list = list.filter(t => !selected.some(s => s.toLowerCase() === t.toLowerCase())).slice(0, 50);
|
list = list.filter(t => !selected.some(s => s.toLowerCase() === t.toLowerCase())).slice(0, 50);
|
||||||
if (list.length === 0) {
|
if (list.length === 0) {
|
||||||
suggestionsHost.innerHTML = '<div style="padding:10px 12px;color:var(--text-muted);font-size:12px;">Keine Vorschlaege</div>';
|
suggestionsHost.innerHTML = '<div style="padding:10px 12px;color:var(--text-muted);font-size:12px;">Keine Vorschläge</div>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
suggestionsHost.innerHTML = '';
|
suggestionsHost.innerHTML = '';
|
||||||
@@ -1521,7 +1512,7 @@ async function buildGithubHeatmapFromRepoCommits(monthsBack) {
|
|||||||
dayMap.set(key, (dayMap.get(key) || 0) + 1);
|
dayMap.set(key, (dayMap.get(key) || 0) + 1);
|
||||||
}
|
}
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
// einzelne Repos koennen fehlschlagen ohne den Gesamtprozess zu blockieren
|
// einzelne Repos können fehlschlagen, ohne den Gesamtprozess zu blockieren
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -2742,9 +2733,9 @@ function renderTabs() {
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (tab.dirty) {
|
if (tab.dirty) {
|
||||||
const ok = await showActionConfirmModal({
|
const ok = await showActionConfirmModal({
|
||||||
title: 'Ungespeicherte Aenderungen',
|
title: 'Ungespeicherte Änderungen',
|
||||||
message: `${tab.name} hat ungespeicherte Aenderungen. Wirklich schliessen?`,
|
message: `${tab.name} hat ungespeicherte Änderungen. Wirklich schließen?`,
|
||||||
confirmText: 'Schliessen',
|
confirmText: 'Schließen',
|
||||||
danger: true
|
danger: true
|
||||||
});
|
});
|
||||||
if (!ok) return;
|
if (!ok) return;
|
||||||
@@ -3009,9 +3000,9 @@ async function closeFileEditor() {
|
|||||||
|
|
||||||
if (unsaved.length > 0) {
|
if (unsaved.length > 0) {
|
||||||
const ok = await showActionConfirmModal({
|
const ok = await showActionConfirmModal({
|
||||||
title: 'Ungespeicherte Aenderungen',
|
title: 'Ungespeicherte Änderungen',
|
||||||
message: `${unsaved.length} Datei(en) haben ungespeicherte Aenderungen. Wirklich schliessen?`,
|
message: `${unsaved.length} Datei(en) haben ungespeicherte Änderungen. Wirklich schließen?`,
|
||||||
confirmText: 'Schliessen',
|
confirmText: 'Schließen',
|
||||||
danger: true
|
danger: true
|
||||||
});
|
});
|
||||||
if (!ok) return;
|
if (!ok) return;
|
||||||
@@ -3049,7 +3040,7 @@ async function openFileEditor(filePath, fileName) {
|
|||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
addTab(filePath, fileName, response.content);
|
addTab(filePath, fileName, response.content);
|
||||||
} else {
|
} else {
|
||||||
await showInfoModal('Datei konnte nicht geoeffnet werden', `Fehler: ${response.error || 'Unbekannter Fehler'}`, true);
|
await showInfoModal('Datei konnte nicht geöffnet werden', `Fehler: ${response.error || 'Unbekannter Fehler'}`, true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3065,7 +3056,7 @@ async function openFileEditor(filePath, fileName) {
|
|||||||
console.log('✅ File opened');
|
console.log('✅ File opened');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error opening file:', error);
|
console.error('Error opening file:', error);
|
||||||
await showInfoModal('Datei konnte nicht geoeffnet werden', 'Fehler beim Oeffnen der Datei.', true);
|
await showInfoModal('Datei konnte nicht geöffnet werden', 'Fehler beim Öffnen der Datei.', true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3110,7 +3101,7 @@ async function openGiteaFileInEditor(owner, repo, filePath, fileName) {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error opening Gitea file:', error);
|
console.error('Error opening Gitea file:', error);
|
||||||
await showInfoModal('Datei konnte nicht geoeffnet werden', 'Fehler beim Oeffnen der Datei.', true);
|
await showInfoModal('Datei konnte nicht geöffnet werden', 'Fehler beim Öffnen der Datei.', true);
|
||||||
showError('Fehler');
|
showError('Fehler');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3277,7 +3268,7 @@ async function saveCurrentFile(isAutoSave = false) {
|
|||||||
|
|
||||||
// Prüfe ob es eine Bilddatei ist
|
// Prüfe ob es eine Bilddatei ist
|
||||||
if (/\.(png|jpg|jpeg|gif|webp|svg)$/i.test(currentActiveTab)) {
|
if (/\.(png|jpg|jpeg|gif|webp|svg)$/i.test(currentActiveTab)) {
|
||||||
await showInfoModal('Nicht bearbeitbar', 'Bilder koennen nicht bearbeitet werden.');
|
await showInfoModal('Nicht bearbeitbar', 'Bilder können nicht bearbeitet werden.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3941,20 +3932,6 @@ async function loadRepoContents(owner, repo, path) {
|
|||||||
showError('Error: ' + (res.error || 'Unknown error'));
|
showError('Error: ' + (res.error || 'Unknown error'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Wenn zur Übersicht gewechselt wird, Gravur zurücksetzen
|
|
||||||
window.resetProjectGravurTitle = function() {
|
|
||||||
const gravurTitle = document.getElementById('project-gravur-title');
|
|
||||||
if (gravurTitle) gravurTitle.textContent = '';
|
|
||||||
};
|
|
||||||
|
|
||||||
// ...existing code...
|
|
||||||
|
|
||||||
// Nach dem Laden der Repo-Liste oder beim Klick auf "Zurück" rufe resetProjectGravurTitle() auf
|
|
||||||
const origLoadRepos = loadRepos;
|
|
||||||
loadRepos = function(...args) {
|
|
||||||
resetProjectGravurTitle();
|
|
||||||
return origLoadRepos.apply(this, args);
|
|
||||||
};
|
|
||||||
|
|
||||||
const grid = $('explorerGrid');
|
const grid = $('explorerGrid');
|
||||||
if (!grid) return;
|
if (!grid) return;
|
||||||
@@ -4374,7 +4351,7 @@ function showSyncConfirmModal({ title = 'Bestaetigen', message = '', confirmText
|
|||||||
|
|
||||||
const detailTitle = document.createElement('div');
|
const detailTitle = document.createElement('div');
|
||||||
detailTitle.style.cssText = 'font-size:12px;font-weight:700;letter-spacing:0.06em;text-transform:uppercase;color:#b9c9e8;margin-bottom:4px;';
|
detailTitle.style.cssText = 'font-size:12px;font-weight:700;letter-spacing:0.06em;text-transform:uppercase;color:#b9c9e8;margin-bottom:4px;';
|
||||||
detailTitle.textContent = 'Was wird uebernommen';
|
detailTitle.textContent = 'Was wird übernommen';
|
||||||
detailBlock.appendChild(detailTitle);
|
detailBlock.appendChild(detailTitle);
|
||||||
|
|
||||||
rows.forEach((d) => {
|
rows.forEach((d) => {
|
||||||
@@ -4600,7 +4577,7 @@ function showRepoContextMenu(ev, owner, repoName, cloneUrl, element, isPrivate =
|
|||||||
details: [
|
details: [
|
||||||
{ label: 'Richtung', value: 'Gitea -> GitHub' },
|
{ label: 'Richtung', value: 'Gitea -> GitHub' },
|
||||||
{ label: 'Repository', value: `${owner}/${repoName}` },
|
{ label: 'Repository', value: `${owner}/${repoName}` },
|
||||||
{ label: 'Sichtbarkeit', value: isPrivate ? 'Privat' : 'Oeffentlich' },
|
{ label: 'Sichtbarkeit', value: isPrivate ? 'Privat' : 'Öffentlich' },
|
||||||
{ label: 'Beschreibung', value: (repoMeta?.description || '-').slice(0, 160) },
|
{ label: 'Beschreibung', value: (repoMeta?.description || '-').slice(0, 160) },
|
||||||
{ label: 'Topics', value: (Array.isArray(repoMeta?.topics) && repoMeta.topics.length > 0) ? repoMeta.topics.join(', ') : '-' }
|
{ label: 'Topics', value: (Array.isArray(repoMeta?.topics) && repoMeta.topics.length > 0) ? repoMeta.topics.join(', ') : '-' }
|
||||||
]
|
]
|
||||||
@@ -4633,7 +4610,7 @@ function showRepoContextMenu(ev, owner, repoName, cloneUrl, element, isPrivate =
|
|||||||
details: [
|
details: [
|
||||||
{ label: 'Richtung', value: 'GitHub -> Gitea' },
|
{ label: 'Richtung', value: 'GitHub -> Gitea' },
|
||||||
{ label: 'Repository', value: `${owner}/${repoName}` },
|
{ label: 'Repository', value: `${owner}/${repoName}` },
|
||||||
{ label: 'Sichtbarkeit', value: isPrivate ? 'Privat' : 'Oeffentlich' },
|
{ label: 'Sichtbarkeit', value: isPrivate ? 'Privat' : 'Öffentlich' },
|
||||||
{ label: 'Beschreibung', value: (repoMeta?.description || '-').slice(0, 160) },
|
{ label: 'Beschreibung', value: (repoMeta?.description || '-').slice(0, 160) },
|
||||||
{ label: 'Topics', value: (Array.isArray(repoMeta?.topics) && repoMeta.topics.length > 0) ? repoMeta.topics.join(', ') : '-' }
|
{ label: 'Topics', value: (Array.isArray(repoMeta?.topics) && repoMeta.topics.length > 0) ? repoMeta.topics.join(', ') : '-' }
|
||||||
]
|
]
|
||||||
@@ -4757,9 +4734,9 @@ function showRepoContextMenu(ev, owner, repoName, cloneUrl, element, isPrivate =
|
|||||||
const deleteItem = createMenuItem('🗑️', 'Repo löschen', async () => {
|
const deleteItem = createMenuItem('🗑️', 'Repo löschen', async () => {
|
||||||
menu.remove();
|
menu.remove();
|
||||||
const ok = await showActionConfirmModal({
|
const ok = await showActionConfirmModal({
|
||||||
title: 'Repository loeschen',
|
title: 'Repository löschen',
|
||||||
message: `Delete ${repoName}?`,
|
message: `Repository ${repoName} wirklich löschen?`,
|
||||||
confirmText: 'Loeschen',
|
confirmText: 'Löschen',
|
||||||
danger: true
|
danger: true
|
||||||
});
|
});
|
||||||
if (ok) {
|
if (ok) {
|
||||||
@@ -4767,13 +4744,13 @@ function showRepoContextMenu(ev, owner, repoName, cloneUrl, element, isPrivate =
|
|||||||
const res = await window.electronAPI.deleteGiteaRepo({ owner, repo: repoName, platform: currentState.platform });
|
const res = await window.electronAPI.deleteGiteaRepo({ owner, repo: repoName, platform: currentState.platform });
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
element.remove();
|
element.remove();
|
||||||
showSuccess('Repository deleted');
|
showSuccess('Repository gelöscht');
|
||||||
} else {
|
} else {
|
||||||
showError('Delete failed: ' + res.error);
|
showError('Löschen fehlgeschlagen: ' + res.error);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Delete error:', error);
|
console.error('Delete error:', error);
|
||||||
showError('Delete failed');
|
showError('Löschen fehlgeschlagen');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, '#ef4444');
|
}, '#ef4444');
|
||||||
@@ -4832,9 +4809,9 @@ function showGiteaItemContextMenu(ev, item, owner, repo) {
|
|||||||
|
|
||||||
addItem('🗑️', `Alle ${selectedItems.size} löschen`, async () => {
|
addItem('🗑️', `Alle ${selectedItems.size} löschen`, async () => {
|
||||||
const ok = await showActionConfirmModal({
|
const ok = await showActionConfirmModal({
|
||||||
title: 'Mehrfach-Loeschen',
|
title: 'Mehrfach-Löschen',
|
||||||
message: `${selectedItems.size} Elemente wirklich loeschen?`,
|
message: `${selectedItems.size} Elemente wirklich löschen?`,
|
||||||
confirmText: 'Loeschen',
|
confirmText: 'Löschen',
|
||||||
danger: true
|
danger: true
|
||||||
});
|
});
|
||||||
if (!ok) return;
|
if (!ok) return;
|
||||||
@@ -4911,10 +4888,10 @@ function showGiteaItemContextMenu(ev, item, owner, repo) {
|
|||||||
|
|
||||||
// --- LÖSCHEN ---
|
// --- LÖSCHEN ---
|
||||||
addItem('🗑️', 'Löschen', async () => {
|
addItem('🗑️', 'Löschen', async () => {
|
||||||
const ok = await showActionConfirmModal({
|
const ok = await showActionConfirmModal({
|
||||||
title: 'Element loeschen',
|
title: 'Element löschen',
|
||||||
message: `"${item.name}" wirklich loeschen?`,
|
message: `"${item.name}" wirklich löschen?`,
|
||||||
confirmText: 'Loeschen',
|
confirmText: 'Löschen',
|
||||||
danger: true
|
danger: true
|
||||||
});
|
});
|
||||||
if (!ok) return;
|
if (!ok) return;
|
||||||
@@ -4927,7 +4904,7 @@ function showGiteaItemContextMenu(ev, item, owner, repo) {
|
|||||||
loadRepoContents(owner, repo, currentState.path);
|
loadRepoContents(owner, repo, currentState.path);
|
||||||
} else {
|
} else {
|
||||||
showError('Löschen fehlgeschlagen: ' + (res?.error || ''));
|
showError('Löschen fehlgeschlagen: ' + (res?.error || ''));
|
||||||
await showInfoModal('Loeschen fehlgeschlagen', 'Loeschen fehlgeschlagen:\n' + (res?.error || 'Unbekannter Fehler'), true);
|
await showInfoModal('Löschen fehlgeschlagen', 'Löschen fehlgeschlagen:\n' + (res?.error || 'Unbekannter Fehler'), true);
|
||||||
}
|
}
|
||||||
}, '#ef4444');
|
}, '#ef4444');
|
||||||
|
|
||||||
@@ -4980,9 +4957,9 @@ function showLocalItemContextMenu(ev, node) {
|
|||||||
|
|
||||||
addItem('🗑️', `Alle ${selectedItems.size} löschen`, async () => {
|
addItem('🗑️', `Alle ${selectedItems.size} löschen`, async () => {
|
||||||
const ok = await showActionConfirmModal({
|
const ok = await showActionConfirmModal({
|
||||||
title: 'Mehrfach-Loeschen',
|
title: 'Mehrfach-Löschen',
|
||||||
message: `${selectedItems.size} Elemente wirklich loeschen?`,
|
message: `${selectedItems.size} Elemente wirklich löschen?`,
|
||||||
confirmText: 'Loeschen',
|
confirmText: 'Löschen',
|
||||||
danger: true
|
danger: true
|
||||||
});
|
});
|
||||||
if (!ok) return;
|
if (!ok) return;
|
||||||
@@ -5041,10 +5018,10 @@ function showLocalItemContextMenu(ev, node) {
|
|||||||
|
|
||||||
// --- LÖSCHEN ---
|
// --- LÖSCHEN ---
|
||||||
addItem('🗑️', 'Löschen', async () => {
|
addItem('🗑️', 'Löschen', async () => {
|
||||||
const ok = await showActionConfirmModal({
|
const ok = await showActionConfirmModal({
|
||||||
title: 'Element loeschen',
|
title: 'Element löschen',
|
||||||
message: `"${node.name}" wirklich loeschen?`,
|
message: `"${node.name}" wirklich löschen?`,
|
||||||
confirmText: 'Loeschen',
|
confirmText: 'Löschen',
|
||||||
danger: true
|
danger: true
|
||||||
});
|
});
|
||||||
if (!ok) return;
|
if (!ok) return;
|
||||||
@@ -6807,9 +6784,9 @@ function createReleaseCard(release, isLatest) {
|
|||||||
`;
|
`;
|
||||||
deleteAssetBtn.onclick = async () => {
|
deleteAssetBtn.onclick = async () => {
|
||||||
const ok = await showActionConfirmModal({
|
const ok = await showActionConfirmModal({
|
||||||
title: 'Asset loeschen',
|
title: 'Asset löschen',
|
||||||
message: `Delete asset "${asset.name}"?`,
|
message: `Asset "${asset.name}" wirklich löschen?`,
|
||||||
confirmText: 'Loeschen',
|
confirmText: 'Löschen',
|
||||||
danger: true
|
danger: true
|
||||||
});
|
});
|
||||||
if (ok) {
|
if (ok) {
|
||||||
@@ -6820,7 +6797,7 @@ function createReleaseCard(release, isLatest) {
|
|||||||
});
|
});
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
assetItem.remove();
|
assetItem.remove();
|
||||||
setStatus('Asset deleted');
|
setStatus('Asset gelöscht');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -6910,7 +6887,7 @@ function createReleaseCard(release, isLatest) {
|
|||||||
addAssetBtn.onclick = () => showUploadAssetDialog(release);
|
addAssetBtn.onclick = () => showUploadAssetDialog(release);
|
||||||
|
|
||||||
const deleteBtn = document.createElement('button');
|
const deleteBtn = document.createElement('button');
|
||||||
deleteBtn.textContent = '🗑️ Delete';
|
deleteBtn.textContent = '🗑️ Löschen';
|
||||||
deleteBtn.style.cssText = `
|
deleteBtn.style.cssText = `
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--danger);
|
color: var(--danger);
|
||||||
@@ -6923,9 +6900,9 @@ function createReleaseCard(release, isLatest) {
|
|||||||
`;
|
`;
|
||||||
deleteBtn.onclick = async () => {
|
deleteBtn.onclick = async () => {
|
||||||
const ok = await showActionConfirmModal({
|
const ok = await showActionConfirmModal({
|
||||||
title: 'Release loeschen',
|
title: 'Release löschen',
|
||||||
message: `Delete release "${release.name || release.tag_name}"?`,
|
message: `Release "${release.name || release.tag_name}" wirklich löschen?`,
|
||||||
confirmText: 'Loeschen',
|
confirmText: 'Löschen',
|
||||||
danger: true
|
danger: true
|
||||||
});
|
});
|
||||||
if (ok) {
|
if (ok) {
|
||||||
@@ -7093,7 +7070,7 @@ function showCreateReleaseModal(owner, repo) {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Fehler beim Auswählen der Datei:', error);
|
console.error('Fehler beim Auswählen der Datei:', error);
|
||||||
await showInfoModal('Dateidialog', 'Konnte Dateidialog nicht oeffnen.', true);
|
await showInfoModal('Dateidialog', 'Konnte Dateidialog nicht öffnen.', true);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -8103,7 +8080,7 @@ async function initUpdater() {
|
|||||||
$('btnImportSettingsBundle').onclick = async () => {
|
$('btnImportSettingsBundle').onclick = async () => {
|
||||||
const ok = await showActionConfirmModal({
|
const ok = await showActionConfirmModal({
|
||||||
title: 'Backup importieren',
|
title: 'Backup importieren',
|
||||||
message: 'Vorhandene Einstellungen koennen ueberschrieben werden. Fortfahren?',
|
message: 'Vorhandene Einstellungen können überschrieben werden. Fortfahren?',
|
||||||
confirmText: 'Import starten',
|
confirmText: 'Import starten',
|
||||||
danger: false
|
danger: false
|
||||||
});
|
});
|
||||||
|
|||||||
+50
-16
@@ -16,6 +16,12 @@ const axiosInstance = axios.create({
|
|||||||
httpsAgent: ipv4HttpsAgent,
|
httpsAgent: ipv4HttpsAgent,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Kurzlebiger Cache für die Default-Branch-Auflösung (HEAD -> z.B. "main").
|
||||||
|
// Verhindert, dass bei einem Ordner-Upload für jede einzelne Datei erneut
|
||||||
|
// die Repo-Info vom Server geladen wird.
|
||||||
|
const defaultBranchCache = new Map();
|
||||||
|
const DEFAULT_BRANCH_TTL_MS = 60_000;
|
||||||
|
|
||||||
function normalizeAndValidateBaseUrl(rawUrl) {
|
function normalizeAndValidateBaseUrl(rawUrl) {
|
||||||
const value = (rawUrl || '').trim();
|
const value = (rawUrl || '').trim();
|
||||||
if (!value) {
|
if (!value) {
|
||||||
@@ -26,7 +32,7 @@ function normalizeAndValidateBaseUrl(rawUrl) {
|
|||||||
try {
|
try {
|
||||||
parsed = new URL(value);
|
parsed = new URL(value);
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
throw new Error('Ungueltige Gitea URL. Beispiel fuer IPv6: http://[2001:db8::1]:3000');
|
throw new Error('Ungültige Gitea-URL. Beispiel für IPv6: http://[2001:db8::1]:3000');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
||||||
@@ -685,23 +691,32 @@ async function uploadGiteaFile({ token, url, owner, repo, path, contentBase64, m
|
|||||||
// Behalte den branch so wie übergeben - aber 'HEAD' muss zum echten Branch aufgelöst werden
|
// Behalte den branch so wie übergeben - aber 'HEAD' muss zum echten Branch aufgelöst werden
|
||||||
let branchName = branch || 'HEAD';
|
let branchName = branch || 'HEAD';
|
||||||
|
|
||||||
// HEAD-Auflösung: Wenn branch === 'HEAD', den Default-Branch des Repos abrufen
|
// HEAD-Auflösung: Wenn branch === 'HEAD', den Default-Branch des Repos abrufen.
|
||||||
|
// Ergebnis kurz cachen, damit bei einem Ordner-Upload nicht für JEDE Datei
|
||||||
|
// erneut die Repo-Info geladen wird (das war eine Hauptursache der Langsamkeit).
|
||||||
if (branchName === 'HEAD') {
|
if (branchName === 'HEAD') {
|
||||||
try {
|
const cacheKey = `${base}::${owner}/${repo}`;
|
||||||
const repoInfoUrl = `${base}/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`;
|
const cached = defaultBranchCache.get(cacheKey);
|
||||||
const repoInfo = await tryRequest(repoInfoUrl, token);
|
if (cached && (Date.now() - cached.ts) < DEFAULT_BRANCH_TTL_MS) {
|
||||||
if (repoInfo.ok && repoInfo.data.default_branch) {
|
branchName = cached.branch;
|
||||||
branchName = repoInfo.data.default_branch;
|
} else {
|
||||||
console.log(`[Upload Debug] HEAD aufgelöst zu: ${branchName}`);
|
try {
|
||||||
} else {
|
const repoInfoUrl = `${base}/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`;
|
||||||
// Fallback auf 'main' wenn Auflösung fehlschlägt
|
const repoInfo = await tryRequest(repoInfoUrl, token);
|
||||||
|
if (repoInfo.ok && repoInfo.data.default_branch) {
|
||||||
|
branchName = repoInfo.data.default_branch;
|
||||||
|
console.log(`[Upload Debug] HEAD aufgelöst zu: ${branchName}`);
|
||||||
|
} else {
|
||||||
|
// Fallback auf 'main' wenn Auflösung fehlschlägt
|
||||||
|
branchName = 'main';
|
||||||
|
console.warn(`[Upload Debug] HEAD-Auflösung fehlgeschlagen, verwende Fallback: ${branchName}`);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Fallback auf 'main' wenn Fehler
|
||||||
branchName = 'main';
|
branchName = 'main';
|
||||||
console.warn(`[Upload Debug] HEAD-Auflösung fehlgeschlagen, verwende Fallback: ${branchName}`);
|
console.warn(`[Upload Debug] HEAD-Auflösung fehlgeschlagen (${e.message}), verwende Fallback: ${branchName}`);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
defaultBranchCache.set(cacheKey, { branch: branchName, ts: Date.now() });
|
||||||
// Fallback auf 'main' wenn Fehler
|
|
||||||
branchName = 'main';
|
|
||||||
console.warn(`[Upload Debug] HEAD-Auflösung fehlgeschlagen (${e.message}), verwende Fallback: ${branchName}`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -767,6 +782,25 @@ async function uploadGiteaFile({ token, url, owner, repo, path, contentBase64, m
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`Upload Attempt ${retryCount + 1} for ${path}:`, err.response ? err.response.data : err.message);
|
console.error(`Upload Attempt ${retryCount + 1} for ${path}:`, err.response ? err.response.data : err.message);
|
||||||
|
|
||||||
|
// Netzwerk-Timeout / abgebrochene Verbindung: mit Backoff erneut versuchen,
|
||||||
|
// statt sofort abzubrechen. Deckt ECONNABORTED (axios-Timeout), ETIMEDOUT,
|
||||||
|
// ECONNRESET und EPIPE ab.
|
||||||
|
const netCode = err.code || '';
|
||||||
|
const isNetworkTimeout = !err.response &&
|
||||||
|
(netCode === 'ECONNABORTED' || netCode === 'ETIMEDOUT' ||
|
||||||
|
netCode === 'ECONNRESET' || netCode === 'EPIPE' ||
|
||||||
|
/timeout/i.test(String(err.message || '')));
|
||||||
|
if (isNetworkTimeout) {
|
||||||
|
if (retryCount < MAX_RETRIES) {
|
||||||
|
retryCount++;
|
||||||
|
const wait = 1000 * retryCount; // 1s, 2s, 3s ...
|
||||||
|
console.warn(`-> Netzwerk-Timeout (${netCode || 'timeout'}). Erneuter Versuch in ${wait}ms... (Retry ${retryCount}/${MAX_RETRIES})`);
|
||||||
|
await sleep(wait);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
throw new Error(`Zeitüberschreitung beim Hochladen von "${path}". Bitte Netzwerk/Server prüfen und erneut versuchen.`);
|
||||||
|
}
|
||||||
|
|
||||||
// Behandle 500 Server-Fehler speziell
|
// Behandle 500 Server-Fehler speziell
|
||||||
if (err.response && err.response.status === 500) {
|
if (err.response && err.response.status === 500) {
|
||||||
const errorMsg = err.response.data?.message || err.message;
|
const errorMsg = err.response.data?.message || err.message;
|
||||||
@@ -1293,7 +1327,7 @@ async function updateGiteaRepoTopics({ token, url, owner, repo, topics }) {
|
|||||||
);
|
);
|
||||||
return res.data;
|
return res.data;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Fallback fuer aeltere/abweichende Gitea-Versionen
|
// Fallback für ältere/abweichende Gitea-Versionen
|
||||||
const res = await axiosInstance.patch(
|
const res = await axiosInstance.patch(
|
||||||
`${base}/api/v1/repos/${repoPath}`,
|
`${base}/api/v1/repos/${repoPath}`,
|
||||||
{ topics: safeTopics },
|
{ topics: safeTopics },
|
||||||
|
|||||||
+2
-2
@@ -74,7 +74,7 @@ class Updater {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[Updater] Fehler beim Update-Check:', error);
|
console.error('[Updater] Fehler beim Update-Check:', error);
|
||||||
this.emitUpdateError('Update-Pruefung fehlgeschlagen.', {
|
this.emitUpdateError('Update-Prüfung fehlgeschlagen.', {
|
||||||
phase: 'check',
|
phase: 'check',
|
||||||
silent: !!silent,
|
silent: !!silent,
|
||||||
error: String(error?.message || error)
|
error: String(error?.message || error)
|
||||||
@@ -325,7 +325,7 @@ class Updater {
|
|||||||
} catch (verifyErr) {
|
} catch (verifyErr) {
|
||||||
console.error('[Updater] Checksum-Validierung konnte nicht ausgeführt werden:', verifyErr?.message || verifyErr);
|
console.error('[Updater] Checksum-Validierung konnte nicht ausgeführt werden:', verifyErr?.message || verifyErr);
|
||||||
fs.unlink(tempPath, () => {});
|
fs.unlink(tempPath, () => {});
|
||||||
this.emitUpdateError('Update-Download fehlgeschlagen: Checksum-Validierung nicht moeglich.', {
|
this.emitUpdateError('Update-Download fehlgeschlagen: Checksum-Validierung nicht möglich.', {
|
||||||
phase: 'download',
|
phase: 'download',
|
||||||
reason: 'checksum-verify-failed',
|
reason: 'checksum-verify-failed',
|
||||||
error: String(verifyErr?.message || verifyErr)
|
error: String(verifyErr?.message || verifyErr)
|
||||||
|
|||||||
Reference in New Issue
Block a user