diff --git a/packages/server/src/services/gitea-service.test.ts b/packages/server/src/services/gitea-service.test.ts index 768f1a7f05..b0484b3cb5 100644 --- a/packages/server/src/services/gitea-service.test.ts +++ b/packages/server/src/services/gitea-service.test.ts @@ -2330,10 +2330,7 @@ describe("createGiteaService", () => { number: 5, baseRefName: "main", headRefName: "feat/sample-change", - checkoutRefs: [ - { remoteName: "origin", remoteRef: "refs/pull/5/head" }, - { remoteName: "origin", remoteRef: "refs/heads/feat/sample-change" }, - ], + checkoutRefs: [{ remoteName: "origin", remoteRef: "refs/pull/5/head" }], headOwnerLogin: "contributor", headRepositorySshUrl: "git@gitea.com:contributor/sample-repo.git", headRepositoryUrl: "https://gitea.com/contributor/sample-repo", @@ -2343,6 +2340,128 @@ describe("createGiteaService", () => { expect(calls).toContainEqual(["api", "repos/example-user/sample-repo/pulls/5"]); }); + it("prefixes the local branch name with the fork owner for a cross-repository checkout", () => { + const { service } = makeService(() => ok("")); + + const localBranchName = service.buildPrLocalBranchName?.({ + headRef: "patch-1", + checkoutTarget: { + number: 5, + baseRefName: "main", + headRefName: "patch-1", + headOwnerLogin: "contributor", + headRepositorySshUrl: null, + headRepositoryUrl: null, + isCrossRepository: true, + }, + }); + + expect(localBranchName).toBe("contributor/patch-1"); + }); + + it("keeps the head ref name as-is for a same-repository checkout", () => { + const { service } = makeService(() => ok("")); + + const localBranchName = service.buildPrLocalBranchName?.({ + headRef: "patch-1", + checkoutTarget: { + number: 5, + baseRefName: "main", + headRefName: "patch-1", + headOwnerLogin: null, + headRepositorySshUrl: null, + headRepositoryUrl: null, + isCrossRepository: false, + }, + }); + + expect(localBranchName).toBe("patch-1"); + }); + + it("falls back to a PR-number prefix when the fork owner login is not a safe branch path segment", () => { + const { service } = makeService(() => ok("")); + + const localBranchName = service.buildPrLocalBranchName?.({ + headRef: "patch-1", + checkoutTarget: { + number: 5, + baseRefName: "main", + headRefName: "patch-1", + headOwnerLogin: "../evil", + headRepositorySshUrl: null, + headRepositoryUrl: null, + isCrossRepository: true, + }, + }); + + expect(localBranchName).toBe("pr-5/patch-1"); + }); + + it("falls back to a PR-number prefix when the fork owner login would produce an invalid ref", () => { + const { service } = makeService(() => ok("")); + + const localBranchName = service.buildPrLocalBranchName?.({ + headRef: "patch-1", + checkoutTarget: { + number: 5, + baseRefName: "main", + headRefName: "patch-1", + headOwnerLogin: "contributor.lock", + headRepositorySshUrl: null, + headRepositoryUrl: null, + isCrossRepository: true, + }, + }); + + expect(localBranchName).toBe("pr-5/patch-1"); + }); + + it("falls back to a PR-number prefix when the fork owner login is unknown", () => { + const { service } = makeService(() => ok("")); + + const localBranchName = service.buildPrLocalBranchName?.({ + headRef: "patch-1", + checkoutTarget: { + number: 5, + baseRefName: "main", + headRefName: "patch-1", + headOwnerLogin: null, + headRepositorySshUrl: null, + headRepositoryUrl: null, + isCrossRepository: true, + }, + }); + + expect(localBranchName).toBe("pr-5/patch-1"); + }); + + it("lowercases the fork owner login for the branch prefix", () => { + const { service } = makeService(() => ok("")); + + const localBranchName = service.buildPrLocalBranchName?.({ + headRef: "patch-1", + checkoutTarget: { + number: 5, + baseRefName: "main", + headRefName: "patch-1", + headOwnerLogin: "Contributor", + headRepositorySshUrl: null, + headRepositoryUrl: null, + isCrossRepository: true, + }, + }); + + expect(localBranchName).toBe("contributor/patch-1"); + }); + + it("returns the pull head ref for default checkout refs", () => { + const { service } = makeService(() => ok("")); + + expect(service.defaultCheckoutRefs?.({ changeRequestNumber: 5, headRef: "patch-1" })).toEqual([ + { remoteName: "origin", remoteRef: "refs/pull/5/head" }, + ]); + }); + it("creates a pull request and parses the resulting URL and index", async () => { const { service, calls } = makeService((args) => { if (args[0] === "pr" && args[1] === "create") { diff --git a/packages/server/src/services/gitea-service.ts b/packages/server/src/services/gitea-service.ts index 88b5caa004..281b5357d8 100644 --- a/packages/server/src/services/gitea-service.ts +++ b/packages/server/src/services/gitea-service.ts @@ -1377,6 +1377,19 @@ function throwFirstNonTeaAuthSearchRejection(results: PromiseSettledResult { const summary = await this.getPullRequest(input); + const fork = await resolveForkCheckoutFacts(input.cwd, summary); const checkoutRefs = [ { remoteName: "origin", remoteRef: `refs/pull/${summary.number}/head` }, ]; - if (summary.headRefName) { + // on a fork PR thats the base repos branch + if (summary.headRefName && !fork.isCrossRepository) { checkoutRefs.push({ remoteName: "origin", remoteRef: `refs/heads/${summary.headRefName}` }); } - const fork = await resolveForkCheckoutFacts(input.cwd, summary); return { number: summary.number, baseRefName: summary.baseRefName,