diff --git a/main.js b/main.js index d2a17d2..d3aec4e 100644 --- a/main.js +++ b/main.js @@ -41,6 +41,7 @@ const { // GitHub listGithubRepos, getGithubCurrentUser, + githubRepoExists, getGithubUserHeatmap, getGithubRepoContents, getGithubFileContent, @@ -56,6 +57,7 @@ const { editGithubRelease, deleteGithubRelease, updateGithubRepoVisibility, + updateGithubRepoDefaultBranch, updateGithubRepoTopics, deleteGithubRepo } = require('./src/git/apiHandler.js'); @@ -3507,6 +3509,7 @@ ipcMain.handle('check-clone-target-collisions', async (_event, data) => { ipcMain.handle('sync-repo-to-github', async (event, data) => { let mirrorDir = null; + let sourceDefaultBranch = ''; try { const creds = readCredentials(); if (!creds?.githubToken) { @@ -3542,8 +3545,16 @@ ipcMain.handle('sync-repo-to-github', async (event, data) => { repoCreated = true; } catch (e) { const msg = String(e?.message || e || ''); - if (!/already exists|existiert bereits|name already exists/i.test(msg)) { - throw e; + if (!/already exists|already_exists|existiert bereits|name already exists/i.test(msg)) { + // Falls GitHub mit einer unscharfen Meldung antwortet, explizit auf Existenz pruefen. + const exists = await githubRepoExists({ + token: creds.githubToken, + owner: targetOwner, + repo + }).catch(() => false); + if (!exists) { + throw e; + } } } @@ -3575,29 +3586,133 @@ ipcMain.handle('sync-repo-to-github', async (event, data) => { } catch (_) {} mirrorDir = fs.mkdtempSync(ppath.join(os.tmpdir(), 'git-manager-sync-')); - const cloneArgs = cloneNeedsGiteaHeader - ? ['-c', `http.${sourceOrigin}.extraheader=AUTHORIZATION: token ${creds.giteaToken}`, 'clone', '--mirror', sourceCloneUrl, mirrorDir] - : ['clone', '--mirror', sourceCloneUrl, mirrorDir]; + // --bare statt --mirror: lädt alle Branches/Tags ohne mirror-Flag zu setzen, + // damit anschließende Refspec-Pushes (Force) funktionieren. + const cloneArgs = cloneNeedsGiteaHeader + ? ['-c', `http.${sourceOrigin}.extraheader=AUTHORIZATION: token ${creds.giteaToken}`, 'clone', '--bare', sourceCloneUrl, mirrorDir] + : ['clone', '--bare', sourceCloneUrl, mirrorDir]; + + // runGit: gibt stdout zurück, loggt stderr immer (git push schreibt Status auf stderr) const runGit = (args, cwd) => { const res = spawnSync('git', args, { cwd, encoding: 'utf8', windowsHide: true }); + const out = (res.stdout || '').trim(); + const err = (res.stderr || '').trim(); + if (err) console.log(`[sync-repo-to-github][git] ${err}`); if (res.status !== 0) { - throw new Error((res.stderr || res.stdout || 'Git-Fehler').trim()); + throw new Error((err || out || 'Git-Fehler').trim()); } - return (res.stdout || '').trim(); + return out; }; + console.log(`[sync-repo-to-github] Klone Quell-Repo (bare): ${sourceCloneUrl}`); runGit(cloneArgs, process.cwd()); + console.log('[sync-repo-to-github] Bare-Clone abgeschlossen'); + + // Alle lokalen Branches auflisten + const allBranchesRaw = runGit(['for-each-ref', '--format=%(refname:short)', 'refs/heads/'], mirrorDir); + const allBranches = (allBranchesRaw || '').split('\n').map(s => s.trim()).filter(s => s && isSafeGitRef(s)); + console.log(`[sync-repo-to-github] Lokale Branches: ${allBranches.join(', ') || '(keine)'}`); + + // Default-Branch ermitteln + try { + const headRef = runGit(['symbolic-ref', '--short', 'HEAD'], mirrorDir); + sourceDefaultBranch = String(headRef || '').replace(/^refs\/heads\//, '').trim(); + if (!isSafeGitRef(sourceDefaultBranch)) sourceDefaultBranch = ''; + } catch (_) { + sourceDefaultBranch = allBranches[0] || ''; + } + if (!sourceDefaultBranch && allBranches.length > 0) sourceDefaultBranch = allBranches[0]; + console.log(`[sync-repo-to-github] Quell-Default-Branch: "${sourceDefaultBranch || '(unbekannt)'}"`); const githubRemoteUrl = `https://github.com/${targetOwner}/${repo}.git`; const githubAuthHeader = `AUTHORIZATION: basic ${Buffer.from(`x-access-token:${creds.githubToken}`, 'utf8').toString('base64')}`; + const ghAuthArgs = ['-c', 'credential.helper=', '-c', `http.https://github.com/.extraheader=${githubAuthHeader}`]; - runGit(['remote', 'set-url', '--push', 'origin', githubRemoteUrl], mirrorDir); - runGit(['-c', 'credential.helper=', '-c', `http.https://github.com/.extraheader=${githubAuthHeader}`, 'push', '--mirror'], mirrorDir); + // GitHub als Push-Remote setzen + runGit(['remote', 'set-url', 'origin', githubRemoteUrl], mirrorDir); + + // Lokale Commit-SHAs loggen (= Gitea-Stand) + for (const branch of allBranches) { + try { + const localSha = runGit(['rev-parse', `refs/heads/${branch}`], mirrorDir); + console.log(`[sync-repo-to-github] Gitea ${branch}: ${localSha}`); + } catch (_) {} + } + + // Remote (GitHub) Commit-SHAs VOR dem Push ermitteln und vergleichen + try { + const remoteRefsRaw = runGit([...ghAuthArgs, 'ls-remote', '--heads', 'origin'], mirrorDir); + const remoteMap = {}; + (remoteRefsRaw || '').split('\n').forEach(line => { + const parts = line.trim().split(/\s+/); + if (parts.length === 2) { + const branch = parts[1].replace('refs/heads/', ''); + remoteMap[branch] = parts[0]; + } + }); + for (const branch of allBranches) { + const remoteSha = remoteMap[branch] || '(nicht vorhanden)'; + console.log(`[sync-repo-to-github] GitHub ${branch}: ${remoteSha}`); + } + } catch (lsErr) { + console.warn('[sync-repo-to-github] ls-remote Warnung:', lsErr?.message || lsErr); + } + + // Jeden Branch einzeln force-pushen für maximale Zuverlässigkeit + console.log(`[sync-repo-to-github] Force-Push ${allBranches.length} Branch(es) nach GitHub: ${githubRemoteUrl}`); + let pushedCount = 0; + for (const branch of allBranches) { + console.log(`[sync-repo-to-github] Pushe Branch: ${branch}`); + runGit([...ghAuthArgs, 'push', '--force', 'origin', `refs/heads/${branch}:refs/heads/${branch}`], mirrorDir); + pushedCount++; + } + console.log(`[sync-repo-to-github] ${pushedCount} Branch(es) erfolgreich gepusht`); + + // Auf GitHub nicht mehr vorhandene Branches aufräumen (prune via API, non-fatal) + try { + const remoteRefsRaw = runGit([...ghAuthArgs, 'ls-remote', '--heads', 'origin'], mirrorDir); + const remoteHeads = (remoteRefsRaw || '').split('\n') + .map(l => { const m = l.match(/refs\/heads\/(.+)$/); return m ? m[1].trim() : null; }) + .filter(Boolean); + const localSet = new Set(allBranches); + for (const remoteBranch of remoteHeads) { + if (!localSet.has(remoteBranch) && isSafeGitRef(remoteBranch)) { + console.log(`[sync-repo-to-github] Lösche veralteten Remote-Branch: ${remoteBranch}`); + runGit([...ghAuthArgs, 'push', 'origin', `--delete`, remoteBranch], mirrorDir); + } + } + } catch (pruneErr) { + console.warn('[sync-repo-to-github] Prune-Warnung:', pruneErr?.message || pruneErr); + } + + // Tags mit Force-Push übertragen (non-fatal) + try { + runGit([...ghAuthArgs, 'push', 'origin', '--tags', '--force'], mirrorDir); + console.log('[sync-repo-to-github] Tags erfolgreich gepusht'); + } catch (tagsErr) { + console.warn('[sync-repo-to-github] Tags-Push Warnung:', tagsErr?.message || tagsErr); + } + + // Default-Branch auf GitHub per API angleichen + if (sourceDefaultBranch) { + console.log(`[sync-repo-to-github] Setze GitHub Default-Branch auf: ${sourceDefaultBranch}`); + try { + await updateGithubRepoDefaultBranch({ + token: creds.githubToken, + owner: targetOwner, + repo, + defaultBranch: sourceDefaultBranch + }); + console.log('[sync-repo-to-github] Default-Branch aktualisiert'); + } catch (defaultBranchErr) { + console.warn('sync-repo-to-github: default-branch update warning', defaultBranchErr?.message || defaultBranchErr); + } + } // Optional: Topics auf GitHub angleichen (nur falls übergeben) const topics = Array.isArray(data?.topics) ? data.topics.filter(Boolean) : []; @@ -3617,7 +3732,8 @@ ipcMain.handle('sync-repo-to-github', async (event, data) => { return { ok: true, repoCreated, - githubRepo: `${targetOwner}/${repo}` + githubRepo: `${targetOwner}/${repo}`, + sourceDefaultBranch: sourceDefaultBranch || null }; } catch (e) { console.error('sync-repo-to-github error', e);