From 1dd3dea2319fe2e131ebb1088fda525c16d0a868 Mon Sep 17 00:00:00 2001 From: ARUNAVO RAY Date: Fri, 6 Mar 2026 10:15:47 +0530 Subject: [PATCH] fix preserve strategy fork owner routing (#215) --- src/lib/gitea.test.ts | 50 +++++++++++++++++++++++++++++++++++++++++-- src/lib/gitea.ts | 49 ++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 95 insertions(+), 4 deletions(-) diff --git a/src/lib/gitea.test.ts b/src/lib/gitea.test.ts index 2b0c9e3..c207acc 100644 --- a/src/lib/gitea.test.ts +++ b/src/lib/gitea.test.ts @@ -72,10 +72,21 @@ mock.module("./gitea", () => { const mirrorStrategy = config?.githubConfig?.mirrorStrategy || (config?.giteaConfig?.preserveOrgStructure ? "preserve" : "flat-user"); + const configuredGitHubOwner = + (config?.githubConfig?.owner || config?.githubConfig?.username || "") + .trim() + .toLowerCase(); + const repoOwner = repository?.owner?.trim().toLowerCase(); switch (mirrorStrategy) { case "preserve": - return repository?.organization || config?.giteaConfig?.defaultOwner || "giteauser"; + if (repository?.organization) { + return repository.organization; + } + if (configuredGitHubOwner && repoOwner && repoOwner !== configuredGitHubOwner) { + return repository.owner; + } + return config?.giteaConfig?.defaultOwner || "giteauser"; case "single-org": return config?.giteaConfig?.organization || config?.giteaConfig?.defaultOwner || "giteauser"; case "mixed": @@ -99,7 +110,7 @@ mock.module("./gitea", () => { return mockDbSelectResult[0].destinationOrg; } - return config?.giteaConfig?.defaultOwner || "giteauser"; + return mockGetGiteaRepoOwner({ config, repository }); }); return { isRepoPresentInGitea: mockIsRepoPresentInGitea, @@ -376,6 +387,7 @@ describe("Gitea Repository Mirroring", () => { describe("getGiteaRepoOwner - Organization Override Tests", () => { const baseConfig: Partial = { githubConfig: { + owner: "testuser", username: "testuser", token: "token", preserveOrgStructure: false, @@ -484,6 +496,18 @@ describe("getGiteaRepoOwner - Organization Override Tests", () => { expect(result).toBe("giteauser"); }); + test("preserve strategy: personal repos owned by another user keep source owner namespace", () => { + const repo = { + ...baseRepo, + owner: "nice-user", + fullName: "nice-user/test-repo", + organization: undefined, + isForked: true, + }; + const result = getGiteaRepoOwner({ config: baseConfig, repository: repo }); + expect(result).toBe("nice-user"); + }); + test("preserve strategy: org repos go to same org name", () => { const repo = { ...baseRepo, organization: "myorg" }; const result = getGiteaRepoOwner({ config: baseConfig, repository: repo }); @@ -589,4 +613,26 @@ describe("getGiteaRepoOwner - Organization Override Tests", () => { expect(result).toBe("FOO"); }); + + test("getGiteaRepoOwnerAsync preserves external personal owner for preserve strategy", async () => { + const configWithUser: Partial = { + ...baseConfig, + userId: "user-id", + }; + + const repo = { + ...baseRepo, + owner: "nice-user", + fullName: "nice-user/test-repo", + organization: undefined, + isForked: true, + }; + + const result = await getGiteaRepoOwnerAsync({ + config: configWithUser, + repository: repo, + }); + + expect(result).toBe("nice-user"); + }); }); diff --git a/src/lib/gitea.ts b/src/lib/gitea.ts index d05c8cf..0a6e127 100644 --- a/src/lib/gitea.ts +++ b/src/lib/gitea.ts @@ -138,14 +138,35 @@ export const getGiteaRepoOwner = ({ // Get the mirror strategy - use preserveOrgStructure for backward compatibility const mirrorStrategy = config.githubConfig.mirrorStrategy || (config.giteaConfig.preserveOrgStructure ? "preserve" : "flat-user"); + const configuredGitHubOwner = + ( + config.githubConfig.owner || + (config.githubConfig as typeof config.githubConfig & { username?: string }).username || + "" + ) + .trim() + .toLowerCase(); switch (mirrorStrategy) { case "preserve": - // Keep GitHub structure - org repos go to same org, personal repos to user (or override) + // Keep GitHub structure: + // - org repos stay in the same org + // - personal repos owned by other users keep their source owner namespace + // - personal repos owned by the configured account go to defaultOwner if (repository.organization) { return repository.organization; } - // Use personal repos override if configured, otherwise use username + + const normalizedRepoOwner = repository.owner.trim().toLowerCase(); + if ( + normalizedRepoOwner && + configuredGitHubOwner && + normalizedRepoOwner !== configuredGitHubOwner + ) { + return repository.owner; + } + + // Personal repos from the configured GitHub account go to the configured default owner return config.giteaConfig.defaultOwner; case "single-org": @@ -376,6 +397,23 @@ export const mirrorGithubRepoToGitea = async ({ // Get the correct owner based on the strategy (with organization overrides) let repoOwner = await getGiteaRepoOwnerAsync({ config, repository }); + const mirrorStrategy = config.githubConfig.mirrorStrategy || + (config.giteaConfig.preserveOrgStructure ? "preserve" : "flat-user"); + const configuredGitHubOwner = ( + config.githubConfig.owner || + (config.githubConfig as typeof config.githubConfig & { username?: string }).username || + "" + ) + .trim() + .toLowerCase(); + const normalizedRepoOwner = repository.owner.trim().toLowerCase(); + const isExternalPersonalRepoInPreserveMode = + mirrorStrategy === "preserve" && + !repository.organization && + !repository.isStarred && + normalizedRepoOwner !== "" && + configuredGitHubOwner !== "" && + normalizedRepoOwner !== configuredGitHubOwner; // Determine the actual repository name to use (handle duplicates for starred repos) let targetRepoName = repository.name; @@ -520,6 +558,13 @@ export const mirrorGithubRepoToGitea = async ({ (orgError.message.includes('Permission denied') || orgError.message.includes('Authentication failed') || orgError.message.includes('does not have permission'))) { + if (isExternalPersonalRepoInPreserveMode) { + throw new Error( + `Cannot create/access namespace "${repoOwner}" for ${repository.fullName}. ` + + `Refusing fallback to "${config.giteaConfig.defaultOwner}" in preserve mode to avoid cross-owner overwrite.` + ); + } + console.warn(`[Fallback] Organization creation/access failed. Attempting to mirror to user account instead.`); // Update the repository owner to use the user account