Skip to content

fix(pr): show a branch's own open PR, not a newer closed or same-named one - #29330

Closed
AmoabaKelvin wants to merge 1 commit into
stablyai:mainfrom
AmoabaKelvin:match-reviews-by-push-head
Closed

AmoabaKelvin wants to merge 1 commit into
stablyai:mainfrom
AmoabaKelvin:match-reviews-by-push-head

Conversation

@AmoabaKelvin

@AmoabaKelvin AmoabaKelvin commented Oct 16, 2026 •

Copy link
Copy Markdown
Collaborator
Files Added Deleted Net
Test 20 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​937 $\color{#cf222e}{\Huge{\mathbf{−}}}$​1910 $\color{#cf222e}{\Huge{\mathbf{−}}}$​973
Prod 25 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​767 $\color{#cf222e}{\Huge{\mathbf{−}}}$​835 $\color{#cf222e}{\Huge{\mathbf{−}}}$​68

ELI5

Orca found a branch's pull request by asking for the newest PR with the same branch name. That let a newer closed PR hide the open one, let another fork's branch with the same name lend its PR, and let a branch that tracks develop show develop's PR. Orca now asks only for PRs from the exact place the branch is pushed to, and shows the open one first.

What Changed

Before:

After:

  • The branch shows its open PR. A closed or merged PR shows only when the branch has no open one.
  • PRs from other forks never attach, and a remembered PR number whose head is somebody else's branch is dropped.
  • Create PR is blocked only by an open or draft PR.

Mechanism: one rule in src/shared/git-review-head.ts works out where a plain git push of the branch lands. The remote is branch.<b>.pushRemote, then remote.pushDefault, then the tracked remote, then origin. The remote branch name is the local name, unless push.default is upstream/tracking or the branch tracks a differently named branch on a fork remote. A branch whose upstream is refs/pull/N/head or refs/merge-requests/N/head (what gh pr checkout and glab mr checkout set up) counts as linked to that review. Orca reads all of this from one git config --list -z per repo, cached for 30 seconds and re-read as soon as the config files change.

Each provider then asks only for reviews from that repository and branch, and takes the open one first:

  • GitHub: REST pulls?head=<owner>:<branch>
  • GitLab: source_project_id and source_branch
  • Bitbucket: source.repository.full_name in the BBQL query
  • Azure DevOps: searchCriteria.sourceRepositoryId, plus each PR's fork source
  • Gitea: no server-side filter, so the PR scan it already shares is filtered on head.repo

A head rarely has more than one review. If the first page of 10 is full and none is open, Orca asks once more for open reviews only. An explicitly linked review now wins over any branch match on every provider.

The GitHub fallback that retried with the tracked upstream branch, and the default-branch policy built around it, are removed. The push-head rule covers both.

Behaviour changes to look at:

  • A branch that tracks a differently named branch on origin or upstream (say git checkout -b fix origin/develop) no longer shows that branch's PR, unless push.default is upstream.
  • Azure DevOps now shows the active PR before newer completed ones, like the other providers.
  • Bitbucket now shows a merged PR for its branch, like the other providers. It was hidden only so it wouldn't block Create PR, and the eligibility change handles that now.
  • Gitea fetches a linked PR by number instead of looking for it in the shared scan, which is one request per lookup.

Why

Asking for more pages or more states still guesses from a branch name. A long enough history or a same-named fork branch beats it, and every extra page costs API quota on every poll. Falling back to the tracked branch is what attached develop's PR in #26948. Matching by commit breaks on the first rebase. gh pr view finds a branch's PR from its push target too, and every provider can filter on it (Gitea filters the scan it already shares).

The Refresh bug behind "Pull request already exists" is a separate fix in #29231. The two PRs touch no common files.

Linked Issue

Fixes #28324
Fixes #27811

Visual Proof

Scratch repo, branch feature/stack-a, with open PR #1 and a newer closed PR #2.

Before After
open-pr-before.png open-pr-after.png

Testing

  • I manually tested these changes locally (macOS, hidden dev build against a private scratch GitHub repo: the open PR beside a newer closed one, switching the branch away and back, a new branch that only tracks another PR's branch (shows no PR), and a branch set up like gh pr checkout (shows its PR))
  • Automated tests added/updated: the push-head rule against captured git config output (own branch, tracking develop, triangular push remote, a contributor fork under another local name, refs/pull and refs/merge-requests links, URL-valued remotes, push.default, remote.pushDefault); per provider, open over newer closed, another fork's branch ignored, a full page of closed reviews, and link precedence; Create PR eligibility for open, draft, merged and closed reviews; the config cache.

Review

  • SSH: this process already returns before any of this for an SSH repo, so the new config read only runs where the existing remote lookup runs (local and WSL, inside the selected distro). If it is ever asked for a host whose git it can't run, the lookup reports "unavailable" rather than guessing a head that could attach the wrong PR.
  • Git versions: git config --list -z is far older than the 2.25 baseline.
  • No wire, saved-state or renderer changes. Mobile and the web client get the corrected result from the host.
  • Folder workspaces have no branch, so they never reach these lookups.

Agent skill upstream boundary

  • Not applicable, or this change copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.

Notes

Follow-ups, not in this PR:

  • Orca's Push button sends a branch created with git checkout -b x upstream/develop to upstream:develop. It should push where git push would.
  • GitHub hides a merged PR matched only by branch name when the branch has moved on. The other providers still show it, so a reused branch name can show an old merged PR there.
  • On GitLab and Bitbucket, a fork workflow where origin is the fork still looks for reviews in the fork's project.

Checklist

  • This PR is small and focused
  • I explained what changed and why (ELI5, the user-facing before/after, the mechanism, and why over the alternatives)
  • Before/after screenshots or videos attached for UI changes, or N/A with reason
  • Self-reviewed for correctness, security, and performance
  • Cross-platform, SSH/remote, and path/shortcut impact considered (or N/A)
  • pnpm lint, pnpm typecheck, pnpm test, and pnpm build pass (or CI will cover; local preferred)

@coderabbitai

coderabbitai Bot commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

📝 Walkthrough

Branch review lookup now resolves the pushed head and repository before searching for matching reviews across hosted providers. Shared selection prefers open reviews and requests additional open results when a full page contains none. GitHub lookup now uses review-head matching instead of tracked-upstream fallback. Review creation eligibility blocks only when an existing review is open or draft.

Priority: ➖ Normal

Severity of issue fixed: Medium

Merge Risk: 🟡 Moderate · up to 21ce3

Branch PR lookup now matches the branch's push destination, which fixes wrong-PR attachment. However, on GitHub a failure querying one repository (such as an inaccessible upstream in a fork setup) can hide the branch's PR entirely, and some hosted-provider lookups can error instead of quietly showing no review when the host is unreachable. These should be addressed before merge.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 36.07% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 61 functions across 38 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Linked Issues check Passed The PR meets the coding requirements for #28324 and #27811. readReviewHead and resolveBranchReviewTarget resolve the review head from the configured push destination. Provider lookups filter by so…
Out of Scope Changes check Passed The changes stay within the linked issue scope. Git configuration resolution, provider-specific source-repository filters, review selection, cache handling, and related tests support correct branch-to…
Title check Passed The title clearly and concisely summarizes the main change: selecting the branch’s own open pull request instead of a newer closed or same-named pull request.
Description check Passed The description is detailed and mostly complete. It includes the user impact, implementation, rationale, linked issues, visual proof, testing coverage, platform considerations, follow-ups, and checkli…
Full details: Linked Issues check

Explanation

The PR meets the coding requirements for #28324 and #27811. readReviewHead and resolveBranchReviewTarget resolve the review head from the configured push destination. Provider lookups filter by source repository and branch. selectReviewForHead prefers open reviews and performs one open-state follow-up when the first page contains only closed reviews. The GitHub, GitLab, Bitbucket, Azure DevOps, and Gitea tests cover newer closed reviews, cross-fork or cross-project branches, linked-review precedence, and remembered head validation. These changes address wrong open/closed selection after branch changes and cross-fork attachment.

Full details: Out of Scope Changes check

Explanation

The changes stay within the linked issue scope. Git configuration resolution, provider-specific source-repository filters, review selection, cache handling, and related tests support correct branch-to-review association for #28324 and #27811. Changes to Create PR eligibility are part of the same review lifecycle: closed and merged reviews no longer block creation, while open and draft reviews still block it. No unrelated product area or unrelated behavior is demonstrated.

Full details: Description check

Explanation

The description is detailed and mostly complete. It includes the user impact, implementation, rationale, linked issues, visual proof, testing coverage, platform considerations, follow-ups, and checklist status. Some optional checklist items remain unchecked, but the required information is present.

  • Fix all pre-merge checks with AI
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2


ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 56256e08-dfee-460a-a2fd-22daf6fc6e99
📥 Commits

Reviewing files that changed from the base of the PR and between 66631d0 and 21ce3ec5f64644d8a767d34aa8a919a757560905.

📒 Files selected for processing: 45
  • src/main/azure-devops/client.test.ts
  • src/main/azure-devops/client.ts
  • src/main/azure-devops/pull-request-mappers.ts
  • src/main/bitbucket/client.test.ts
  • src/main/bitbucket/client.ts
  • src/main/bitbucket/pull-request-creation.test.ts
  • src/main/bitbucket/pull-request-mappers.ts
  • src/main/gitea/client-scan-invalidation.test.ts
  • src/main/gitea/client.test.ts
  • src/main/gitea/client.ts
  • src/main/gitea/pull-request-mappers.ts
  • src/main/github/client-pr-branch-discovery.test.ts
  • src/main/github/client-pr-conflict-summary.test.ts
  • src/main/github/client-pr-fallback-number.test.ts
  • src/main/github/client-pr-review-head.test.ts
  • src/main/github/client-ssh-provider-execution-boundary.test.ts
  • src/main/github/client-test-harness.ts
  • src/main/github/client-test-mocks.ts
  • src/main/github/client-tracked-upstream-default-branch.test.ts
  • src/main/github/client-tracked-upstream-fork-owner.test.ts
  • src/main/github/client-tracked-upstream-snapshot.test.ts
  • src/main/github/client.ts
  • src/main/github/client/lookup/branch-lookup-resolution.ts
  • src/main/github/client/lookup/implicit-default-branch-pr.ts
  • src/main/github/client/lookup/pr-branch-lookup.ts
  • src/main/github/client/lookup/review-head-pr-policy.ts
  • src/main/github/client/lookup/tracked-upstream-branch.ts
  • src/main/github/client/lookup/tracked-upstream-cache.ts
  • src/main/github/default-branch-stale-pr.test.ts
  • src/main/gitlab/client-mr-branch-lookup.test.ts
  • src/main/gitlab/client-mr-test-harness.ts
  • src/main/gitlab/gitlab-project-id.ts
  • src/main/gitlab/mappers.ts
  • src/main/gitlab/merge-request-lookup.ts
  • src/main/source-control/branch-review-selection.ts
  • src/main/source-control/hosted-review-bitbucket.integration.test.ts
  • src/main/source-control/hosted-review-creation-eligibility.test.ts
  • src/main/source-control/hosted-review-creation.ts
  • src/main/source-control/hosted-review-gitea.integration.test.ts
  • src/main/source-control/repo-git-config-snapshot.test.ts
  • src/main/source-control/repo-git-config-snapshot.ts
  • src/main/source-control/review-head.ts
  • src/shared/git-config-snapshot-runner.ts
  • src/shared/git-review-head.test.ts
  • src/shared/git-review-head.ts
💤 Files with no reviewable changes: 7
  • src/main/github/client-tracked-upstream-default-branch.test.ts
  • src/main/github/client-tracked-upstream-snapshot.test.ts
  • src/main/github/client-tracked-upstream-fork-owner.test.ts
  • src/main/github/client.ts
  • src/main/github/client/lookup/implicit-default-branch-pr.ts
  • src/main/github/client/lookup/tracked-upstream-branch.ts
  • src/main/github/client/lookup/tracked-upstream-cache.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 9 remain after this review.

Comment on lines +81 to 101
for (const candidate of args.candidates) {
if (githubRepositoryWebHost(candidate).trim().toLowerCase() !== headHost) {
continue
}
const branchData = await getRestPRForHead(
candidate,
args.headRepo.owner,
args.branchName,
args.ghOptions
)
// Why: REST/list branch lookup identifies the PR cheaply; exact `gh pr view` carries review, merge-queue, and auto-merge state.
const data = await hydrateBranchLookupWithExactPR(
candidate,
branchData,
args.ghOptions,
args.executionScope
)
if (data && pullRequestMatchesHeadRepository(data, candidate, args.headRepo)) {
return { data, dataRepo: candidate }
}
// Why: branch-list failures are ambiguous for fork discovery; give exact fallback-number recovery a chance before surfacing the error.
return hasPendingError
? { data: null, dataRepo: null, pendingError }
: { data: null, dataRepo: null }
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Do not let one failing candidate abort the whole head lookup.

lookupPRByHead loops over candidates and awaits getRestPRForHead without a try block. In a fork clone the candidates are [upstream, origin]. If the upstream query throws (for example a 404 or 403 because the upstream repository is private or no longer accessible), the error propagates. getPRForBranchOutcome then reports upstream-error, and the origin candidate, which holds the PR, is never queried.

The removed lookupPRByBranchName path caught per-candidate errors and kept the first error pending. The tests for that behavior were deleted from client-pr-fallback-number.test.ts, and nothing replaces it. A persistent error on one candidate now hides the branch's PR on every poll.

Fix: catch errors per candidate and continue to the next one. Throw the first captured error only when no candidate returns a PR. Rethrow rate-limit errors immediately, so the loop does not keep spending quota.

Proposed fix
   const headHost = githubRepositoryWebHost(args.headRepo).trim().toLowerCase()
+  let pendingError: unknown = null
   for (const candidate of args.candidates) {
     if (githubRepositoryWebHost(candidate).trim().toLowerCase() !== headHost) {
       continue
     }
-    const branchData = await getRestPRForHead(
-      candidate,
-      args.headRepo.owner,
-      args.branchName,
-      args.ghOptions
-    )
+    let branchData: PullRequestLookupData | null
+    try {
+      branchData = await getRestPRForHead(
+        candidate,
+        args.headRepo.owner,
+        args.branchName,
+        args.ghOptions
+      )
+    } catch (err) {
+      pendingError ??= err
+      continue
+    }
@@
   }
+  if (pendingError) {
+    throw pendingError
+  }
   return { data: null, dataRepo: null }

Comment on lines +22 to +24
const config = await getRepoGitConfigSnapshot(repoPath, connectionId, options)
// Why: an unreadable config defaults to `origin/<branch>` rather than hiding the review.
const head = readReviewHead(config ?? new Map(), branchName)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🔎 Supported by static analysis

🏁 Script executed:

#!/bin/bash
rg -nP -C10 'export function (resolveGitRouteForHost|requireLocalRoute)\b' src/main/providers

Repository: stablyai/orca

Length of output: 2260


🏁 Script executed:

#!/bin/bash
set -o pipefail
printf '%s\n' '--- review-head and config snapshot references ---'
rg -n -F --glob '*.ts' -e 'resolveBranchReviewTarget' -e 'getRepoGitConfigSnapshot' -e 'requireReachableGitRoute' -e 'readReviewHead' src/main
printf '%s\n' '--- review-head.ts ---'
nl -ba src/main/source-control/review-head.ts
printf '%s\n' '--- config snapshot declarations ---'
rg -n -P -C18 'function (getRepoGitConfigSnapshot|requireReachableGitRoute)|export (async )?function (getRepoGitConfigSnapshot|requireReachableGitRoute)' src/main
printf '%s\n' '--- provider lookup blocks ---'
for f in src/main/azure-devops/client.ts src/main/bitbucket/client.ts src/main/gitea/client.ts src/main/gitlab/merge-request-lookup.ts; do
  echo "--- $f"
  nl -ba "$f" | sed -n '185,275p'
done

Repository: stablyai/orca

Length of output: 31361


🏁 Script executed:

#!/bin/bash
set -o pipefail
rg -n -F --glob '*.ts' -e 'resolveBranchReviewTarget' -e 'getRepoGitConfigSnapshot' -e 'requireReachableGitRoute' -e 'readReviewHead' src/main
nl -ba src/main/source-control/review-head.ts
rg -n -P -C18 'function (getRepoGitConfigSnapshot|requireReachableGitRoute)|export (async )?function (getRepoGitConfigSnapshot|requireReachableGitRoute)' src/main
for f in src/main/azure-devops/client.ts src/main/bitbucket/client.ts src/main/gitea/client.ts src/main/gitlab/merge-request-lookup.ts; do
  echo "--- $f"
  nl -ba "$f" | sed -n '185,275p'
done

Repository: stablyai/orca

Length of output: 31219


🏁 Script executed:

#!/bin/bash
printf '%s\n' '--- config snapshot implementation ---'
nl -ba src/main/source-control/repo-git-config-snapshot.ts | sed -n '50,120p'
printf '%s\n' '--- GitLab lookup implementation ---'
nl -ba src/main/gitlab/merge-request-lookup.ts | sed -n '80,215p'
printf '%s\n' '--- GitHub branch lookup resolution ---'
nl -ba src/main/github/client/lookup/branch-lookup-resolution.ts | sed -n '1,180p'
printf '%s\n' '--- provider catch/throwOnFailure context ---'
rg -n -P -C6 'throwOnFailure|try \{|catch \(' src/main/azure-devops/client.ts src/main/bitbucket/client.ts src/main/gitea/client.ts

Repository: stablyai/orca

Length of output: 31497


Preserve null-on-failure for branch lookups.

resolveBranchReviewTarget can reject with SshHostNotServedError before the provider's request error handling runs. The non-throwing Azure DevOps, Bitbucket, Gitea, and GitLab branch lookups can therefore reject instead of returning their documented null result.

Catch this resolver failure at each provider boundary. Rethrow when throwOnFailure is true; otherwise return null. Keep the existing unreachable-host behavior in getRepoGitConfigSnapshot.

Branches matched reviews by name only, so a newer closed PR, another fork's
same-named branch, or the base branch's PR could stand in for the branch's own.

Fixes stablyai#28324
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant