diff --git a/apps/server/src/git/GitManager.test.ts b/apps/server/src/git/GitManager.test.ts index 9078626ba2e..8859b5fc350 100644 --- a/apps/server/src/git/GitManager.test.ts +++ b/apps/server/src/git/GitManager.test.ts @@ -765,6 +765,79 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => { }), ); + it.effect("status resolves the PR by number for an untracked prNNNN checkout", () => + Effect.gen(function* () { + // `git fetch origin :pr2182` — the review-a-PR habit: the + // PR head lands in a renamed local branch with no upstream tracking, so + // no head selector can name the PR. The number in the branch name is + // the only remaining link. + const repoDir = yield* makeTempDir("t3code-git-manager-"); + yield* initRepo(repoDir); + const remoteDir = yield* createBareRemote(); + yield* runGit(repoDir, ["remote", "add", "origin", remoteDir]); + yield* runGit(repoDir, ["checkout", "-b", "pr2182"]); + + const { manager, ghCalls } = yield* makeManager({ + ghScenario: { + // Every head-selector list comes back empty… + prListSequence: [], + // …and `gh pr view 2182` names the real head branch. + pullRequest: { + number: 2182, + title: "Dedupe order ids in Mako import batches", + url: "https://github.com/pingdotgg/codething-mvp/pull/2182", + baseRefName: "main", + headRefName: "fix/mako-import-batch-dedupe-order-ids", + state: "open", + }, + }, + }); + + const status = yield* manager.status({ cwd: repoDir }); + expect(status.pr).toEqual({ + number: 2182, + title: "Dedupe order ids in Mako import batches", + url: "https://github.com/pingdotgg/codething-mvp/pull/2182", + baseRef: "main", + headRef: "fix/mako-import-batch-dedupe-order-ids", + state: "open", + }); + expect(ghCalls.some((call) => call.startsWith("pr view 2182"))).toBe(true); + }), + ); + + it.effect("status does not resolve by number once a prNNNN branch gains tracking", () => + Effect.gen(function* () { + // With tracking, the head selector is precise; the numeric fallback must + // stay out of the way even when the selector search finds nothing (the + // tracked head may simply have no PR yet). + const repoDir = yield* makeTempDir("t3code-git-manager-"); + yield* initRepo(repoDir); + const remoteDir = yield* createBareRemote(); + yield* runGit(repoDir, ["remote", "add", "origin", remoteDir]); + yield* runGit(repoDir, ["checkout", "-b", "pr2182"]); + yield* runGit(repoDir, ["push", "-u", "origin", "pr2182:some-head-branch"]); + + const { manager, ghCalls } = yield* makeManager({ + ghScenario: { + prListSequence: [], + pullRequest: { + number: 2182, + title: "Unrelated PR that happens to share the number", + url: "https://github.com/pingdotgg/codething-mvp/pull/2182", + baseRefName: "main", + headRefName: "somebody/elses-branch", + state: "open", + }, + }, + }); + + const status = yield* manager.status({ cwd: repoDir }); + expect(status.pr).toBeNull(); + expect(ghCalls.some((call) => call.startsWith("pr view"))).toBe(false); + }), + ); + it.effect("status trims PR metadata returned by gh before publishing it", () => Effect.gen(function* () { const repoDir = yield* makeTempDir("t3code-git-manager-"); diff --git a/apps/server/src/git/GitManager.ts b/apps/server/src/git/GitManager.ts index 80e8c96ec71..b09a503b7ef 100644 --- a/apps/server/src/git/GitManager.ts +++ b/apps/server/src/git/GitManager.ts @@ -1397,6 +1397,27 @@ export const make = Effect.gen(function* () { const parsed = Arr.sort(parsedByNumber.values(), pullRequestUpdatedAtDescOrder); + // A checkout made by fetching a PR head into a locally renamed branch + // (`git fetch origin :pr2182`) has no upstream tracking, so + // no head selector above can ever name the PR and the thread stays + // badge-less forever. The branch name itself encodes the PR number, so + // resolve it directly. Gated on headBranch === localBranch: a tracked + // branch already produced the precise head selector, and this must not + // override it with a number that merely resembles the branch name. + const numericPrBranch = /^pr[-_]?(\d+)$/i.exec(headContext.localBranch); + if ( + parsed.length === 0 && + numericPrBranch?.[1] !== undefined && + headContext.headBranch === headContext.localBranch + ) { + const byNumber = yield* (yield* sourceControlProvider(cwd)) + .getChangeRequest({ cwd, reference: numericPrBranch[1] }) + .pipe(Effect.orElseSucceed(() => null)); + if (byNumber !== null) { + return toPullRequestInfo(byNumber); + } + } + const latestOpenPr = parsed.find((pr) => pr.state === "open"); if (latestOpenPr) { return latestOpenPr;