Skip to content

fix(vcs): resolve the default branch from the remote, not a stale origin/HEAD - #7394

Open
AntoineArt wants to merge 2 commits into
pingdotgg:mainfrom
AntoineArt:fix/default-branch-from-remote
Open

AntoineArt wants to merge 2 commits into
pingdotgg:mainfrom
AntoineArt:fix/default-branch-from-remote

Conversation

@AntoineArt

@AntoineArt AntoineArt commented Aug 18, 2026 •

Copy link
Copy Markdown

What changed

resolveDefaultBranchName and the status-path defaultBranchCache now ask the remote for its default branch with git ls-remote --symref <remote> HEAD, and fall back to the local refs/remotes/<remote>/HEAD snapshot only when the remote cannot be reached.

Why

refs/remotes/<remote>/HEAD is written once, when the repository is cloned, and neither fetch nor pull ever moves it again — only an explicit git remote set-head <remote> -a does. A project that changes its default branch after you cloned leaves your clone pointing at the old one, permanently and silently. Nothing in the codebase asked the remote.

That value is load-bearing. readRemoteStatus in GitManager.ts suppresses merged/closed change requests only when isDefaultBranch is true:

// On the default branch, only surface open PRs.
// Merged/closed matches are usually reverse-merge history, not the thread's PR context.
if (details.isDefaultBranch && latest.state !== "open") {
  return { pr: null, headContext };
}

When the snapshot is stale, the real default branch is classified as an ordinary topic branch, the guard never fires, and findLatestPrForHeadContext falls through to parsed[0] — the most recently updated change request sourced from that branch, in any state. A thread on a long-lived default branch such as dev then binds to a change request that merged weeks earlier and re-settles after every completed turn.

Reproduced on a GitLab project whose default branch is dev while the clone's origin/HEAD still pointed at origin/main: a thread created on dev was bound to a merge request last updated five weeks before the thread existed, and manual un-settles did not stick. Details in #4970.

The same stale value also feeds aheadOfDefaultCount and the isDefault flag in listBranches, so both get more accurate as a side effect.

Cost

One extra git subprocess per repository per cache window, on paths that already do network work:

  • Cached 30 minutes on success — a project's default branch changes far more rarely than the local snapshot it corrects.
  • Cached 5 minutes when the remote is unreachable, so an offline window does not pin the fallback in place for half an hour.
  • Reuses STATUS_UPSTREAM_REFRESH_ENV and STATUS_UPSTREAM_REFRESH_TIMEOUT, the same non-interactive env and 5s timeout as the background status fetch, so it cannot prompt for credentials or hang.
  • ls-remote is plain git over the existing transport — no forge CLI, no API quota, and it works identically for GitHub, GitLab, Bitbucket, Azure DevOps and self-hosted remotes.

Tests

Two integration tests in GitVcsDriverCore.test.ts, both against real git repositories:

  • prefers the remote default branch over a stale local origin HEAD — remote HEAD is dev, local snapshot says main, asserts isDefaultBranch is true on dev. Fails on main before this change.
  • falls back to the local origin HEAD when the remote is unreachable — origin URL points at a nonexistent path, asserts the local snapshot still resolves.

vp fmt --check, vp lint and tsgo --noEmit are clean. The full server suite has 13 pre-existing failures on cebac35 in my environment (11 in GitManager.test.ts PR-worktree tests, 1 in ProviderRegistry.test.ts, 1 in GitVcsDriverCore.test.ts newline-path test); the set is identical with and without this change.

Notes

I read CONTRIBUTING — happy for this to sit or be closed. It is scoped to one behaviour and adds no surface area. The complementary fix discussed in #4970, making the terminal change-request fallback time-aware so a recycled feature branch cannot inherit its own historical MR, is deliberately not in here.


Note

Medium Risk
Touches core VCS status and PR-binding logic (isDefaultBranch, aheadOfDefaultCount) with extra network subprocesses, but behavior is gated by caching and existing non-interactive fetch timeouts.

Overview
Default branch detection now asks the remote for HEAD instead of trusting refs/remotes/origin/HEAD, which Git sets at clone time and does not update when the forge moves the default branch.

GitVcsDriverCore adds a remoteDefaultBranchCache that runs git ls-remote --symref <remote> HEAD (non-interactive env, 5s timeout). Successful probes cache for 30 minutes; null/unreachable results and probe failures use the shorter 5-minute TTL so offline or broken remotes are retried without hammering every status read. That cache is not cleared on local mutations like fetch, since those cannot change the remote’s default HEAD.

defaultBranchCache and resolveDefaultBranchName prefer the live remote answer and fall back to symbolic-ref refs/remotes/<remote>/HEAD only when the probe yields nothing. New parseDefaultBranchFromLsRemoteSymref parses the symref line from ls-remote output.

Integration tests cover remote-wins over a stale local origin/HEAD, unreachable-remote fallback, and failed-probe cooldown across fetch + clock advance.

Reviewed by Cursor Bugbot for commit f4ff541. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Resolve the default branch from the remote via git ls-remote instead of stale local origin/HEAD

  • Adds remoteDefaultBranchCache in GitVcsDriverCore.ts that runs git ls-remote --symref <remote> HEAD to fetch the current default branch, caching successes for 30 minutes and failures/nulls for 5 minutes.
  • Updates defaultBranchCache and resolveDefaultBranchName to prefer the live remote result and fall back to git symbolic-ref refs/remotes/origin/HEAD only when the remote is unreachable.
  • Adds tests covering the remote-wins case, the fallback-to-local case, and the cooldown behavior for failed probes across mutations.
  • Behavioral Change: default branch resolution now reflects remote HEAD changes without requiring a local fetch; stale origin/HEAD is only used as a fallback.

Macroscope summarized f4ff541.

@coderabbitai

coderabbitai Bot commented Aug 18, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: a7d0724e-d799-4c29-a1b1-696d03a79e50

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

@github-actions github-actions Bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:L 100-499 changed lines (additions + deletions). labels Aug 18, 2026

@cursor cursor 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.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Want fixes drafted automatically? Bugbot Autofix can create code changes for findings. A team admin can enable Autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 0c8e14b7426bd8fb30d23bbfd27a4e75233795f3. Configure here.

Comment thread apps/server/src/vcs/GitVcsDriverCore.ts
@macroscopeapp

macroscopeapp Bot commented Aug 18, 2026 •

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — This fix introduces network calls to probe remotes for default branch resolution (where none existed before), changing runtime behavior system-wide. Combined with the author being a first-time contributor, human review is recommended to validate the caching strategy and timeout implications.

You can add or adjust custom eligibility rules. Learn more.

@AntoineArt
AntoineArt force-pushed the fix/default-branch-from-remote branch 2 times, most recently from c9c3486 to 92834ef Compare August 19, 2026 07:26
@github-actions github-actions Bot added size:M 30-99 changed lines (additions + deletions). and removed size:L 100-499 changed lines (additions + deletions). labels Aug 19, 2026
Comment thread apps/server/src/vcs/GitVcsDriverCore.ts Outdated
…gin/HEAD

`refs/remotes/<remote>/HEAD` is written once, at clone time, and neither
fetch nor pull ever moves it again. A project that changes its default
branch afterwards leaves every existing clone pointing at the old one, so
`isDefaultBranch` reports false for the branch that actually is default.

That misclassification is load-bearing: `readRemoteStatus` only suppresses
merged/closed change requests when `isDefaultBranch` is true, so a thread on
a long-lived default branch such as `dev` binds to the last merged MR/PR
from that branch and re-settles after every completed turn.

Probe the remote with `ls-remote --symref` and fall back to the local
snapshot when it is unreachable. The probe reuses the non-interactive env
and timeout already used for background status fetches, and is cached for
30 minutes on success, 5 minutes when the remote could not be reached — the
same 5 minutes when the probe times out or cannot spawn, so a degraded
remote cannot turn every status read into a fresh probe.

Refs pingdotgg#4970

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@AntoineArt
AntoineArt force-pushed the fix/default-branch-from-remote branch from 92834ef to 1a414e4 Compare August 19, 2026 07:38
@shivamhwp

Copy link
Copy Markdown
Collaborator

Note: GPT-6 on behalf of shivam (@shivamhwp).

The remote HEAD probe now runs on the local-status path too. readStatusDetailsLocal reads defaultBranchCache, whose loader awaits remoteDefaultBranchFor. A cold statusDetailsLocal therefore launches git ls-remote; an unreachable or slow remote can hold local file-change status behind the five-second network timeout. resolveDefaultBranchName also brings this probe into branch/ref listing. These paths previously read local refs.

Please refresh the remote default branch in the remote/background path and let local status consume the cached answer or local HEAD immediately. Keep the success cache and failure cooldown without making local updates await the probe.

This handles stale default-branch detection, while #9443 addresses a reused non-default branch moving beyond a terminal PR. #4970 needs that remaining case too.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:M 30-99 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants