diff --git a/packages/server/src/server/resolve-worktree-creation-intent.test.ts b/packages/server/src/server/resolve-worktree-creation-intent.test.ts index ff0c9ddb0b..8d44b9ba80 100644 --- a/packages/server/src/server/resolve-worktree-creation-intent.test.ts +++ b/packages/server/src/server/resolve-worktree-creation-intent.test.ts @@ -187,7 +187,7 @@ describe("resolveWorktreeCreationIntent", () => { headRepositorySshUrl: null, headRepositoryUrl: null, isCrossRepository: false, - hasOriginHeadBranch: false, + headRefKind: "pull-ref", }); const intent = await resolveWorktreeCreationIntent( @@ -351,8 +351,7 @@ describe("resolveWorktreeCreationIntent", () => { headRepositorySshUrl: null, headRepositoryUrl: null, isCrossRepository: false, - hasOriginHeadBranch: false, - hasHeadBranch: false, + headRefKind: "pull-ref", }), }, }); @@ -386,7 +385,7 @@ describe("resolveWorktreeCreationIntent", () => { headRepositorySshUrl: "git@gitea.test:contributor/repo.git", headRepositoryUrl: "https://gitea.test/contributor/repo", isCrossRepository: true, - hasHeadBranch: false, + headRefKind: "pull-ref", }), }, }); @@ -419,7 +418,7 @@ describe("resolveWorktreeCreationIntent", () => { headRepositorySshUrl: null, headRepositoryUrl: null, isCrossRepository: true, - hasHeadBranch: false, + headRefKind: "pull-ref", }), }, }); diff --git a/packages/server/src/server/resolve-worktree-creation-intent.ts b/packages/server/src/server/resolve-worktree-creation-intent.ts index 15aba9479a..5eddc0ef7b 100644 --- a/packages/server/src/server/resolve-worktree-creation-intent.ts +++ b/packages/server/src/server/resolve-worktree-creation-intent.ts @@ -192,7 +192,7 @@ async function resolvePrCheckoutIntent( const localBranchName = service.buildPrLocalBranchName?.({ headRef, checkoutTarget }); const crossRepository = resolveCrossRepositoryFields(checkoutTarget); const trackOriginHead = - !checkoutTarget.isCrossRepository && checkoutTarget.hasOriginHeadBranch !== false; + !checkoutTarget.isCrossRepository && checkoutTarget.headRefKind !== "pull-ref"; return { kind: "checkout-change-request", diff --git a/packages/server/src/services/forge-service.ts b/packages/server/src/services/forge-service.ts index be12acdef5..8499cc8756 100644 --- a/packages/server/src/services/forge-service.ts +++ b/packages/server/src/services/forge-service.ts @@ -39,10 +39,8 @@ export interface PullRequestCheckoutTarget { headRepositorySshUrl: string | null; headRepositoryUrl: string | null; isCrossRepository: boolean; - /** false when origin has no head branch to track */ - hasOriginHeadBranch?: boolean; - /** false when headRefName is a synthetic pull ref, not a real branch name (agit PRs, deleted branches) */ - hasHeadBranch?: boolean; + /** "pull-ref" when headRefName is a synthetic pull ref, not a real branch name (agit PRs, deleted branches); undefined means "branch" */ + headRefKind?: "branch" | "pull-ref"; } export interface PullRequestCheckoutRef { diff --git a/packages/server/src/services/gitea-service.test.ts b/packages/server/src/services/gitea-service.test.ts index 94edc2f970..9e3a3ecc62 100644 --- a/packages/server/src/services/gitea-service.test.ts +++ b/packages/server/src/services/gitea-service.test.ts @@ -2303,8 +2303,7 @@ describe("createGiteaService", () => { headRepositorySshUrl: null, headRepositoryUrl: null, isCrossRepository: false, - hasOriginHeadBranch: true, - hasHeadBranch: true, + headRefKind: "branch", }); }); @@ -2341,8 +2340,7 @@ describe("createGiteaService", () => { headRepositorySshUrl: "git@gitea.com:contributor/sample-repo.git", headRepositoryUrl: "https://gitea.com/contributor/sample-repo", isCrossRepository: true, - hasOriginHeadBranch: false, - hasHeadBranch: true, + headRefKind: "branch", }); expect(calls).toContainEqual(["api", "repos/example-user/sample-repo/pulls/5"]); @@ -2369,8 +2367,7 @@ describe("createGiteaService", () => { { remoteName: "upstream", remoteRef: "refs/pull/5/head" }, ], isCrossRepository: false, - hasOriginHeadBranch: false, - hasHeadBranch: false, + headRefKind: "pull-ref", }); }); @@ -2418,8 +2415,7 @@ describe("createGiteaService", () => { { remoteName: "upstream", remoteRef: "refs/pull/5/head" }, ], isCrossRepository: false, - hasOriginHeadBranch: false, - hasHeadBranch: false, + headRefKind: "pull-ref", }); }); @@ -2439,8 +2435,7 @@ describe("createGiteaService", () => { ], headOwnerLogin: null, isCrossRepository: true, - hasOriginHeadBranch: false, - hasHeadBranch: true, + headRefKind: "branch", }); }); @@ -2457,7 +2452,7 @@ describe("createGiteaService", () => { remoteName: "origin", remoteRef: "refs/heads/feat/sample-change", }); - expect(target.hasOriginHeadBranch).toBe(true); + expect(target.headRefKind).toBe("branch"); }); it("prefixes the local branch name with the fork owner for a cross-repository checkout", () => { @@ -2492,7 +2487,7 @@ describe("createGiteaService", () => { headRepositorySshUrl: null, headRepositoryUrl: null, isCrossRepository: false, - hasHeadBranch: false, + headRefKind: "pull-ref", }, }); @@ -2512,7 +2507,7 @@ describe("createGiteaService", () => { headRepositorySshUrl: null, headRepositoryUrl: null, isCrossRepository: true, - hasHeadBranch: false, + headRefKind: "pull-ref", }, }); @@ -2532,7 +2527,7 @@ describe("createGiteaService", () => { headRepositorySshUrl: null, headRepositoryUrl: null, isCrossRepository: true, - hasHeadBranch: false, + headRefKind: "pull-ref", }, }); diff --git a/packages/server/src/services/gitea-service.ts b/packages/server/src/services/gitea-service.ts index 80a9228ac4..892f9cc33a 100644 --- a/packages/server/src/services/gitea-service.ts +++ b/packages/server/src/services/gitea-service.ts @@ -1445,19 +1445,30 @@ export function createGiteaService(options: CreateGiteaServiceOptions = {}): For return runJsonParse(args, runOptions, stdout, schema); } + function resolveHeadRefKind( + pr: z.infer, + number: number, + ): "branch" | "pull-ref" { + // head.ref only becomes the branch name when the branch still exists, + // agit PRs and deleted branches keep the pull ref + const isPullRefOnly = + pr.flow === GITEA_PULL_REQUEST_FLOW_AGIT || pr.head?.ref === `refs/pull/${number}/head`; + return isPullRefOnly ? "pull-ref" : "branch"; + } + async function resolveForkCheckoutFacts( cwd: string, summary: PullRequestSummary, ): Promise<{ isCrossRepository: boolean; - hasHeadBranch: boolean; + headRefKind: "branch" | "pull-ref"; headOwnerLogin: string | null; headRepositorySshUrl: string | null; headRepositoryUrl: string | null; }> { const sameRepo = { isCrossRepository: false, - hasHeadBranch: true, + headRefKind: "branch" as const, headOwnerLogin: null, headRepositorySshUrl: null, headRepositoryUrl: null, @@ -1476,18 +1487,14 @@ export function createGiteaService(options: CreateGiteaServiceOptions = {}): For ); const headRepo = pr.head?.repo ?? null; const baseRepo = pr.base?.repo ?? null; - // head.ref only becomes the branch name when the branch still exists, - // agit PRs and deleted branches keep the pull ref - const hasHeadBranch = - pr.flow !== GITEA_PULL_REQUEST_FLOW_AGIT && - pr.head?.ref !== `refs/pull/${summary.number}/head`; + const headRefKind = resolveHeadRefKind(pr, summary.number); if (baseRepo?.id == null || headRepo?.id === baseRepo.id) { - return { ...sameRepo, hasHeadBranch }; + return { ...sameRepo, headRefKind }; } // headRepo is null when the fork was deleted return { isCrossRepository: true, - hasHeadBranch, + headRefKind, headOwnerLogin: headRepo?.owner?.login ?? null, headRepositorySshUrl: headRepo?.ssh_url ?? null, headRepositoryUrl: headRepo?.html_url ?? null, @@ -2033,7 +2040,7 @@ export function createGiteaService(options: CreateGiteaServiceOptions = {}): For const checkoutRefs = [originPullRef]; // for a fork PR a branch with that name on origin belongs to the base repo const headBranchOnOrigin = - !fork.isCrossRepository && fork.hasHeadBranch && Boolean(summary.headRefName); + !fork.isCrossRepository && fork.headRefKind === "branch" && Boolean(summary.headRefName); if (headBranchOnOrigin) { checkoutRefs.push({ remoteName: "origin", remoteRef: `refs/heads/${summary.headRefName}` }); } @@ -2048,8 +2055,7 @@ export function createGiteaService(options: CreateGiteaServiceOptions = {}): For headRepositorySshUrl: fork.headRepositorySshUrl, headRepositoryUrl: fork.headRepositoryUrl, isCrossRepository: fork.isCrossRepository, - hasOriginHeadBranch: headBranchOnOrigin, - hasHeadBranch: fork.hasHeadBranch, + headRefKind: fork.headRefKind, }; }, diff --git a/packages/server/src/services/github-service.test.ts b/packages/server/src/services/github-service.test.ts index ab71115f36..3109bcc63c 100644 --- a/packages/server/src/services/github-service.test.ts +++ b/packages/server/src/services/github-service.test.ts @@ -797,6 +797,7 @@ describe("ForgeService", () => { headRepositorySshUrl: "git@github.com:therainisme/paseo.git", headRepositoryUrl: "https://github.com/therainisme/paseo", isCrossRepository: true, + headRefKind: "branch", }); expect(runner.calls).toHaveLength(2); diff --git a/packages/server/src/services/github-service.ts b/packages/server/src/services/github-service.ts index 0ea4a8bf14..2dd3fdc269 100644 --- a/packages/server/src/services/github-service.ts +++ b/packages/server/src/services/github-service.ts @@ -3522,6 +3522,7 @@ function toPullRequestCheckoutTarget( headRepositorySshUrl: pullRequest.headRepository?.sshUrl || null, headRepositoryUrl: pullRequest.headRepository?.url || null, isCrossRepository: pullRequest.isCrossRepository, + headRefKind: "branch", }; } diff --git a/packages/server/src/services/gitlab-service.test.ts b/packages/server/src/services/gitlab-service.test.ts index 371198e447..b9b6d447d4 100644 --- a/packages/server/src/services/gitlab-service.test.ts +++ b/packages/server/src/services/gitlab-service.test.ts @@ -477,6 +477,7 @@ describe("createGitLabService", () => { headRepositorySshUrl: null, headRepositoryUrl: null, isCrossRepository: false, + headRefKind: "branch", }); expect(calls[0]).toEqual(["mr", "view", "14", "-F", "json"]); }); diff --git a/packages/server/src/services/gitlab-service.ts b/packages/server/src/services/gitlab-service.ts index 1fff0e2a02..c5f54be2cb 100644 --- a/packages/server/src/services/gitlab-service.ts +++ b/packages/server/src/services/gitlab-service.ts @@ -1204,6 +1204,7 @@ export function createGitLabService(options: CreateGitLabServiceOptions = {}): F mr.target_project_id !== undefined && mr.target_project_id !== null && mr.source_project_id !== mr.target_project_id, + headRefKind: "branch", }; }, diff --git a/packages/server/src/utils/change-request-checkout.test.ts b/packages/server/src/utils/change-request-checkout.test.ts index dbd9f55a0d..74a57c2445 100644 --- a/packages/server/src/utils/change-request-checkout.test.ts +++ b/packages/server/src/utils/change-request-checkout.test.ts @@ -70,20 +70,20 @@ describe("buildForkLocalBranchName", () => { ...fork, isCrossRepository: false, headOwnerLogin: "arthur", - hasHeadBranch: false, + headRefKind: "pull-ref", }), ).toBe("pr-42"); }); it("prefixes pr- with the owner for a fork pull-ref-only head", () => { expect( - buildForkLocalBranchName({ ...fork, headOwnerLogin: "Arthur", hasHeadBranch: false }), + buildForkLocalBranchName({ ...fork, headOwnerLogin: "Arthur", headRefKind: "pull-ref" }), ).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", - ); + expect( + buildForkLocalBranchName({ ...fork, headOwnerLogin: null, headRefKind: "pull-ref" }), + ).toBe("pr-42"); }); }); diff --git a/packages/server/src/utils/change-request-checkout.ts b/packages/server/src/utils/change-request-checkout.ts index 7fc6e2190b..12c779571b 100644 --- a/packages/server/src/utils/change-request-checkout.ts +++ b/packages/server/src/utils/change-request-checkout.ts @@ -26,15 +26,15 @@ 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; + /** "pull-ref" when headRef is a synthetic pull ref, not a real branch name (agit PRs, deleted branches); undefined means "branch" */ + headRefKind?: "branch" | "pull-ref"; } 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; + const headSegment = params.headRefKind === "pull-ref" ? pullRefFallback : params.headRef; if (!params.isCrossRepository) { return headSegment; }