From 2da0277a68923175a8589393ff11e13bdf82ccff Mon Sep 17 00:00:00 2001 From: Arunavo Ray Date: Wed, 25 Mar 2026 19:29:46 +0530 Subject: [PATCH] fix sync target resolution for mirrored repos --- src/lib/gitea-enhanced.test.ts | 57 +++++++++++++ src/lib/gitea-enhanced.ts | 142 +++++++++++++++++++++++++++------ src/pages/activity.astro | 1 - src/pages/index.astro | 3 +- 4 files changed, 176 insertions(+), 27 deletions(-) diff --git a/src/lib/gitea-enhanced.test.ts b/src/lib/gitea-enhanced.test.ts index c837cdd..c1cd2e7 100644 --- a/src/lib/gitea-enhanced.test.ts +++ b/src/lib/gitea-enhanced.test.ts @@ -555,6 +555,63 @@ describe("Enhanced Gitea Operations", () => { expect(releaseCall.octokit).toBeDefined(); }); + test("prefers recorded mirroredLocation when owner resolution changes", async () => { + mockGetGiteaRepoOwnerAsync.mockImplementation(() => Promise.resolve("ceph")); + + const config: Partial = { + userId: "user123", + githubConfig: { + username: "testuser", + token: "github-token", + privateRepositories: false, + mirrorStarred: true, + }, + giteaConfig: { + url: "https://gitea.example.com", + token: "encrypted-token", + defaultOwner: "testuser", + mirrorReleases: true, + }, + }; + + const repository: Repository = { + id: "repo789", + name: "test-repo", + fullName: "ceph/test-repo", + owner: "ceph", + cloneUrl: "https://github.com/ceph/test-repo.git", + isPrivate: false, + isStarred: true, + status: repoStatusEnum.parse("mirrored"), + visibility: "public", + userId: "user123", + mirroredLocation: "starred/test-repo", + createdAt: new Date(), + updatedAt: new Date(), + }; + + const result = await syncGiteaRepoEnhanced( + { config, repository }, + { + getGiteaRepoOwnerAsync: mockGetGiteaRepoOwnerAsync, + mirrorGitHubReleasesToGitea: mockMirrorGitHubReleasesToGitea, + mirrorGitRepoIssuesToGitea: mockMirrorGitRepoIssuesToGitea, + mirrorGitRepoPullRequestsToGitea: mockMirrorGitRepoPullRequestsToGitea, + mirrorGitRepoLabelsToGitea: mockMirrorGitRepoLabelsToGitea, + mirrorGitRepoMilestonesToGitea: mockMirrorGitRepoMilestonesToGitea, + } + ); + + expect(result).toEqual({ success: true }); + + const mirrorSyncCalls = mockHttpPost.mock.calls.filter((call) => + String(call[0]).includes("/mirror-sync") + ); + expect(mirrorSyncCalls).toHaveLength(1); + expect(String(mirrorSyncCalls[0][0])).toContain("/api/v1/repos/starred/test-repo/mirror-sync"); + expect(String(mirrorSyncCalls[0][0])).not.toContain("/api/v1/repos/ceph/test-repo/mirror-sync"); + }); + test("blocks sync when pre-sync snapshot fails and blocking is enabled", async () => { mockShouldCreatePreSyncBackup = true; mockShouldBlockSyncOnBackupFailure = true; diff --git a/src/lib/gitea-enhanced.ts b/src/lib/gitea-enhanced.ts index 4a0f76e..2580801 100644 --- a/src/lib/gitea-enhanced.ts +++ b/src/lib/gitea-enhanced.ts @@ -52,6 +52,41 @@ interface GiteaRepoInfo { private: boolean; } +interface SyncTargetCandidate { + owner: string; + repoName: string; +} + +function parseMirroredLocation(location?: string | null): SyncTargetCandidate | null { + if (!location) return null; + + const trimmed = location.trim(); + if (!trimmed) return null; + + const slashIndex = trimmed.indexOf("/"); + if (slashIndex <= 0 || slashIndex === trimmed.length - 1) return null; + + const owner = trimmed.slice(0, slashIndex).trim(); + const repoName = trimmed.slice(slashIndex + 1).trim(); + if (!owner || !repoName) return null; + + return { owner, repoName }; +} + +function dedupeSyncTargets(targets: SyncTargetCandidate[]): SyncTargetCandidate[] { + const seen = new Set(); + const deduped: SyncTargetCandidate[] = []; + + for (const target of targets) { + const key = `${target.owner}/${target.repoName}`.toLowerCase(); + if (seen.has(key)) continue; + seen.add(key); + deduped.push(target); + } + + return deduped; +} + /** * Check if a repository exists in Gitea and return its details */ @@ -285,19 +320,78 @@ export async function syncGiteaRepoEnhanced({ }) .where(eq(repositories.id, repository.id!)); - // Get the expected owner + // Resolve sync target in a backward-compatible order: + // 1) recorded mirroredLocation (actual historical mirror location) + // 2) owner derived from current strategy/config const dependencies = deps ?? (await import("./gitea")); - const repoOwner = await dependencies.getGiteaRepoOwnerAsync({ config, repository }); + const expectedOwner = await dependencies.getGiteaRepoOwnerAsync({ config, repository }); + const recordedTarget = parseMirroredLocation(repository.mirroredLocation); + const candidateTargets = dedupeSyncTargets([ + ...(recordedTarget ? [recordedTarget] : []), + { owner: expectedOwner, repoName: repository.name }, + ]); - // Check if repo exists and get its info - const repoInfo = await getGiteaRepoInfo({ - config, - owner: repoOwner, - repoName: repository.name, - }); + let repoOwner = expectedOwner; + let repoName = repository.name; + let repoInfo: GiteaRepoInfo | null = null; + let firstNonMirrorTarget: SyncTargetCandidate | null = null; + + for (const target of candidateTargets) { + const candidateInfo = await getGiteaRepoInfo({ + config, + owner: target.owner, + repoName: target.repoName, + }); + + if (!candidateInfo) { + continue; + } + + if (!candidateInfo.mirror) { + if (!firstNonMirrorTarget) { + firstNonMirrorTarget = target; + } + continue; + } + + repoOwner = target.owner; + repoName = target.repoName; + repoInfo = candidateInfo; + break; + } if (!repoInfo) { - throw new Error(`Repository ${repository.name} not found in Gitea at ${repoOwner}/${repository.name}`); + if (firstNonMirrorTarget) { + console.warn( + `[Sync] Repository ${repository.name} exists at ${firstNonMirrorTarget.owner}/${firstNonMirrorTarget.repoName} but is not configured as a mirror` + ); + + await db + .update(repositories) + .set({ + status: repoStatusEnum.parse("failed"), + updatedAt: new Date(), + errorMessage: "Repository exists in Gitea but is not configured as a mirror. Manual intervention required.", + }) + .where(eq(repositories.id, repository.id!)); + + await createMirrorJob({ + userId: config.userId, + repositoryId: repository.id, + repositoryName: repository.name, + message: `Cannot sync ${repository.name}: Not a mirror repository`, + details: `Repository ${repository.name} exists in Gitea but is not configured as a mirror. You may need to delete and recreate it as a mirror, or manually configure it as a mirror in Gitea.`, + status: "failed", + }); + + throw new Error(`Repository ${repository.name} is not a mirror. Cannot sync.`); + } + + throw new Error( + `Repository ${repository.name} not found in Gitea. Tried locations: ${candidateTargets + .map((t) => `${t.owner}/${t.repoName}`) + .join(", ")}` + ); } // Check if it's a mirror repository @@ -342,7 +436,7 @@ export async function syncGiteaRepoEnhanced({ giteaUrl: config.giteaConfig.url, giteaToken: decryptedConfig.giteaConfig.token, giteaOwner: repoOwner, - giteaRepo: repository.name, + giteaRepo: repoName, octokit: fpOctokit, githubOwner: repository.owner, githubRepo: repository.name, @@ -407,13 +501,13 @@ export async function syncGiteaRepoEnhanced({ if (shouldBackupForStrategy(backupStrategy, forcePushDetected)) { const cloneUrl = repoInfo.clone_url || - `${config.giteaConfig.url.replace(/\/$/, "")}/${repoOwner}/${repository.name}.git`; + `${config.giteaConfig.url.replace(/\/$/, "")}/${repoOwner}/${repoName}.git`; try { const backupResult = await createPreSyncBundleBackup({ config, owner: repoOwner, - repoName: repository.name, + repoName, cloneUrl, force: true, // Strategy already decided to backup; skip legacy gate }); @@ -464,22 +558,22 @@ export async function syncGiteaRepoEnhanced({ // Update mirror interval if needed if (config.giteaConfig?.mirrorInterval) { try { - console.log(`[Sync] Updating mirror interval for ${repository.name} to ${config.giteaConfig.mirrorInterval}`); - const updateUrl = `${config.giteaConfig.url}/api/v1/repos/${repoOwner}/${repository.name}`; + console.log(`[Sync] Updating mirror interval for ${repoOwner}/${repoName} to ${config.giteaConfig.mirrorInterval}`); + const updateUrl = `${config.giteaConfig.url}/api/v1/repos/${repoOwner}/${repoName}`; await httpPatch(updateUrl, { mirror_interval: config.giteaConfig.mirrorInterval, }, { Authorization: `token ${decryptedConfig.giteaConfig.token}`, }); - console.log(`[Sync] Successfully updated mirror interval for ${repository.name}`); + console.log(`[Sync] Successfully updated mirror interval for ${repoOwner}/${repoName}`); } catch (updateError) { - console.warn(`[Sync] Failed to update mirror interval for ${repository.name}:`, updateError); + console.warn(`[Sync] Failed to update mirror interval for ${repoOwner}/${repoName}:`, updateError); // Continue with sync even if interval update fails } } // Perform the sync - const apiUrl = `${config.giteaConfig.url}/api/v1/repos/${repoOwner}/${repository.name}/mirror-sync`; + const apiUrl = `${config.giteaConfig.url}/api/v1/repos/${repoOwner}/${repoName}/mirror-sync`; try { const response = await httpPost(apiUrl, undefined, { @@ -536,7 +630,7 @@ export async function syncGiteaRepoEnhanced({ octokit, repository, giteaOwner: repoOwner, - giteaRepoName: repository.name, + giteaRepoName: repoName, }); metadataState.components.releases = true; metadataUpdated = true; @@ -568,7 +662,7 @@ export async function syncGiteaRepoEnhanced({ octokit, repository, giteaOwner: repoOwner, - giteaRepoName: repository.name, + giteaRepoName: repoName, }); metadataState.components.issues = true; metadataState.components.labels = true; @@ -601,7 +695,7 @@ export async function syncGiteaRepoEnhanced({ octokit, repository, giteaOwner: repoOwner, - giteaRepoName: repository.name, + giteaRepoName: repoName, }); metadataState.components.pullRequests = true; metadataUpdated = true; @@ -631,7 +725,7 @@ export async function syncGiteaRepoEnhanced({ octokit, repository, giteaOwner: repoOwner, - giteaRepoName: repository.name, + giteaRepoName: repoName, }); metadataState.components.labels = true; metadataUpdated = true; @@ -670,7 +764,7 @@ export async function syncGiteaRepoEnhanced({ octokit, repository, giteaOwner: repoOwner, - giteaRepoName: repository.name, + giteaRepoName: repoName, }); metadataState.components.milestones = true; metadataUpdated = true; @@ -708,7 +802,7 @@ export async function syncGiteaRepoEnhanced({ updatedAt: new Date(), lastMirrored: new Date(), errorMessage: null, - mirroredLocation: `${repoOwner}/${repository.name}`, + mirroredLocation: `${repoOwner}/${repoName}`, metadata: metadataUpdated ? serializeRepositoryMetadataState(metadataState) : repository.metadata ?? null, @@ -720,7 +814,7 @@ export async function syncGiteaRepoEnhanced({ repositoryId: repository.id, repositoryName: repository.name, message: `Sync requested for repository: ${repository.name}`, - details: `Mirror sync was requested for ${repository.name}.`, + details: `Mirror sync was requested for ${repoOwner}/${repoName}.`, status: "synced", }); diff --git a/src/pages/activity.astro b/src/pages/activity.astro index c1e3925..7b36c2a 100644 --- a/src/pages/activity.astro +++ b/src/pages/activity.astro @@ -13,7 +13,6 @@ try { activityData = jobs.flatMap((job: any) => { // Check if log exists before parsing if (!job.log) { - console.warn(`Job ${job.id} has no log data`); return []; } diff --git a/src/pages/index.astro b/src/pages/index.astro index 953d90a..f316b01 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -28,7 +28,6 @@ try { activityData = jobs.flatMap((job: any) => { // Check if log exists before parsing if (!job.log) { - console.warn(`Job ${job.id} has no log data`); return []; } try { @@ -68,4 +67,4 @@ try { - \ No newline at end of file +