diff --git a/packages/server/src/utils/change-request-checkout.test.ts b/packages/server/src/utils/change-request-checkout.test.ts index b630ee15af..dbd9f55a0d 100644 --- a/packages/server/src/utils/change-request-checkout.test.ts +++ b/packages/server/src/utils/change-request-checkout.test.ts @@ -61,4 +61,29 @@ describe("buildForkLocalBranchName", () => { it("falls back to the pr number when the owner is not a safe ref segment", () => { expect(buildForkLocalBranchName({ ...fork, headOwnerLogin: "-zaphod" })).toBe("pr-42/patch-1"); }); + + // pull-ref-only heads (agit PRs, deleted branches) report headRef as the + // literal pull ref, e.g. refs/pull/42/head, not a real branch name + it("collapses a same-repository pull-ref-only head to pr-", () => { + expect( + buildForkLocalBranchName({ + ...fork, + isCrossRepository: false, + headOwnerLogin: "arthur", + hasHeadBranch: false, + }), + ).toBe("pr-42"); + }); + + it("prefixes pr- with the owner for a fork pull-ref-only head", () => { + expect( + buildForkLocalBranchName({ ...fork, headOwnerLogin: "Arthur", hasHeadBranch: false }), + ).toBe("arthur/pr-42"); + }); + + it("does not double the pr number when the fork owner is unknown and there is no head branch", () => { + expect(buildForkLocalBranchName({ ...fork, headOwnerLogin: null, hasHeadBranch: false })).toBe( + "pr-42", + ); + }); }); diff --git a/packages/server/src/utils/change-request-checkout.ts b/packages/server/src/utils/change-request-checkout.ts index f3f0b3feeb..7fc6e2190b 100644 --- a/packages/server/src/utils/change-request-checkout.ts +++ b/packages/server/src/utils/change-request-checkout.ts @@ -26,12 +26,22 @@ export interface ForkLocalBranchNameInput { number: number; isCrossRepository: boolean; headOwnerLogin: string | null; + /** false when headRef is a synthetic pull ref, not a real branch name (agit PRs, deleted branches) */ + hasHeadBranch?: boolean; } export function buildForkLocalBranchName(params: ForkLocalBranchNameInput): string { + const pullRefFallback = `pr-${params.number}`; + // headRef is the literal pull ref (e.g. refs/pull/42/head) when there is no + // real branch to name after, so use the pr number instead + const headSegment = params.hasHeadBranch === false ? pullRefFallback : params.headRef; if (!params.isCrossRepository) { - return params.headRef; + return headSegment; } const owner = normalizeForgeOwnerForBranch(params.headOwnerLogin); - return `${owner ?? `pr-${params.number}`}/${params.headRef}`; + if (!owner) { + // don't double up pr-/pr- when the owner is also unknown + return headSegment === pullRefFallback ? headSegment : `${pullRefFallback}/${headSegment}`; + } + return `${owner}/${headSegment}`; }