Upload via GUI (31 Dateien)

This commit is contained in:
Git Manager GUI
2026-07-16 08:01:04 +02:00
parent 3ed561fe78
commit 565835418e
7 changed files with 373 additions and 116 deletions

1
data/credentials.json Normal file

File diff suppressed because one or more lines are too long

View File

@@ -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">

256
main.js
View File

@@ -2444,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();
@@ -2841,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);

View File

@@ -1,6 +1,6 @@
{ {
"name": "git-manager-gui", "name": "git-manager-gui",
"version": "2.1.5", "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",

View File

@@ -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'),

View File

@@ -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) {
@@ -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;

View File

@@ -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) {
@@ -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;