fix(git): drop a terminal PR once its branch moves past it - #9443
Conversation
A long-lived branch keeps matching a merged pull request by name forever. Release `develop` into `main` and every later thread on `develop` is stamped with that same historical number, and settles against its merged state. `lookupStatusPr` already suppresses terminal matches, but only on the default branch, so an integration branch never qualifies. Compare the branch against the commit the change request was opened from instead: a branch still sitting on that commit is described by it, and a branch that has moved on was reused for later work. Commits rather than dates, so squash and rebase merges keep their badge — the recorded head commit is the branch's own, not the base's. `headRefOid` is optional, and the check is skipped whenever the answer is not knowable: a forge that does not report the head commit, a deleted branch with no ref left to compare, or a failed git call. Every one of those keeps today's behaviour rather than dropping a badge. Only GitHub populates it here; the other forges keep their current behaviour until they carry the field too. Refs pingdotgg#4970 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Want higher recall? High effort reviews run extra passes and find more bugs. A team admin can switch effort levels in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit f764477. Configure here.
ApprovabilityVerdict: Not approved Macroscope's review found this PR not approvable — This targeted fix changes production status and thread-settlement behavior by comparing terminal PR head OIDs with local or remote branch tips, alongside additive GitHub metadata plumbing. It also adds line-level static-analysis suppressions in the test file, so human review is warranted. You can add or adjust custom eligibility rules. Learn more. |
Two problems with the first pass, both from review. `for-each-ref` matches a pattern literally *or* up to a slash, so `refs/heads/feature/foo` also reports `refs/heads/feature/foo/child`. Git forbids holding both at once, so the child only surfaces once the branch itself is gone — exactly when the merged badge is meant to be kept — and standing in for the deleted branch dropped it instead. Every row is now matched back against the ref it has to be, the way `branchPullRequest` already reads its own branch ref. Collecting every ref also meant a branch committed to but not yet pushed kept its badge, because the remote-tracking ref still sat on the old head and any match was enough. Read one tip instead, preferring the local ref: that is where a thread's work lands, so a branch with unpushed commits has still moved on from a merged change request. Head commits are compared case-insensitively. Refs pingdotgg#4970 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Both review findings were real. Fixed in 2afe53b. Cursor Bugbot — tip check matches sibling branch refs. Correct, with one refinement. So a sibling can never pin a badge on a live branch. The other half is the real one: the child only surfaces once the branch itself is gone — exactly when the merged badge is meant to be kept — and standing in for the deleted branch dropped it. Every row is now matched back against the ref it has to be, following the exact- Macroscope — stale badge when the local branch advances without pushing. The underlying observation is right, but dropping the Head commits are also compared case-insensitively now. Two tests added, both verified to fail against the previous commit:
248 tests pass across On the flagged suppressions — the only ones added are |
|
Both blocking findings are clear on 2afe53b — Cursor Bugbot pass (was 1 issue), Macroscope Correctness pass, and the approvability verdict no longer lists a blocking correctness issue. On the remaining note about line-level static-analysis suppressions in the test file: I checked whether they can be dropped, and they can't. Removing the four this PR adds fails The fake That leaves the verdict resting on "human review is warranted" for a change to status and settlement behavior, which seems right — flagging it for a maintainer rather than something further I can address in code. |

Refs #4970.
What changed
lookupStatusPrsuppresses merged/closed matches only when the branch is the repository's default branch:A long-lived integration branch never qualifies, so it keeps matching its merged release PR by name forever. Release
developintomainand every thread created ondevelopafterwards is stamped with that same historical number — and, throughbranchPullRequest, settles against its merged state.This compares the branch against the commit the change request was opened from instead. A branch still sitting on that commit is described by it; a branch that has moved on was reused for later work, and the change request is history.
ChangeRequestgains an optionalheadRefOid, populated fromgh's existingpr list --json(one extra field, no extra call).GitManagerdrops a terminal change request when every ref the branch is known by points somewhere other than that commit.Why commits and not dates
Comparing the branch tip's date against the PR's
updatedAtlooked simpler, but it breaks squash and rebase merges — and it would have brokenstatus finds a merged PR after its remote branch was deleted, which backdates the PR while committing "now". The recorded head commit is the head branch's own commit, so it survives any merge strategy.Not knowable means "keep it"
The check returns
false, preserving today's behaviour, when:Losing the comparison can never drop a badge that is otherwise correct.
Cost
One
git for-each-refper lookup, insideprLookupCache(2 min TTL) rather than on the 1 s status path, and only for terminal candidates.Tests
Two added to
GitManager.test.ts, both against real git repositories with a fakegh:status drops a merged PR once its long-lived branch moves past it—developmerged intomain, committed to again, PR gone.status keeps a merged PR while its branch still sits on the merged commit— the squash-merge case, PR kept.apps/serversrc/git/,src/sourceControl/andThreadSettlementReactorpass (246 tests). Full-repotypecheckandlintclean; four exact--jsonassertions updated for the new field.No UI change — the badge simply stops appearing where it was wrong.
🤖 Generated with Claude Code
Note
Medium Risk
Changes PR association logic used by status and branch settlement; conservative fallbacks limit regressions, but wrong OID comparison could hide valid merged badges or leave stale ones.
Overview
Fixes long-lived branches (e.g.
developafter a release merge) incorrectly keeping a merged PR badge and thread settlement context forever, because lookup only suppressed terminal PRs on the default branch.ChangeRequestand GitHubgh pr list/view --jsonnow include optionalheadRefOid.GitManagercompares the branch tip (local ref first, then remote-tracking viagit for-each-ref, with guards against nested ref name false positives) to that OID; for merged/closed PRs only, a mismatch clears the match so later work on the same branch name is not tied to the old release PR. Open PRs are unchanged. If OID or tip is unknown or git fails, behavior stays as before (badge kept).Tests cover moved-past (pushed and unpushed), squash-merge in-place, deleted branch with nested ref pattern, and updated JSON field expectations across GitHub CLI/source control layers.
Reviewed by Cursor Bugbot for commit 2afe53b. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Drop merged/closed PR from branch status once branch tip moves past recorded head commit
headCommitIdfield through the full stack:ChangeRequestcontract,GitHubPullRequestSchemadecoder,GitHubCli.listOpenPullRequestsandGitHubCli.getPullRequestJSON field selections,GitHubSourceControlProvider.toChangeRequest, andPullRequestInforeadBranchTipOidin GitManager.ts to resolve a branch's current commit from the exact local ref or matching remote-tracking ref, and tightensisRemoteTrackingRefForso nested child refs (e.g.featurevsfeature/sub) are not mistaken for the branchbranchMovedPastChangeRequestignores missing/blank head IDs and converts git lookup failures tofalse, preserving the existing PR association when comparison data is unavailableGitManagerPR lookup now fetches branch tips viareadBranchTipOidinstead of relying solely on cached data; branches whose tips no longer match the recorded PR head commit will lose that PR from status outputMacroscope summarized 2afe53b.