Skip to content

Fixes graph scope mode showing no commits for old branches#5266

Draft
ianhattendorf wants to merge 1 commit into
mainfrom
scope-mode-old-branches-not-showing-commits
Draft

Fixes graph scope mode showing no commits for old branches#5266
ianhattendorf wants to merge 1 commit into
mainfrom
scope-mode-old-branches-not-showing-commits

Conversation

@ianhattendorf

Copy link
Copy Markdown
Contributor

Summary

Scoping to an old local branch in the Commit Graph (via the focus picker, sidebar select, or overview-card click) sometimes rendered an empty filtered view: no commits visible despite the scope being active. Three issues at the same fault line, fixed together:

  • computeScopeAnchor collapses to a focal-only anchor when target === focal. The reflog-derived "base branch" for branches made via git branch X origin/Y resolves to the branch's own upstream, which equals the focal tip. The original guard returned {focalBranchTipSha} only, so the bare scope never got a real merge base. Fix: iterate the candidate chain (mergeTargetBranch → baseBranch → defaultBranch) and skip candidates that collapse to the focal tip, so defaultBranch (typically main) becomes the recovered merge target.
  • applyAnchorToPendingScope dropped focalBranchTipSha on the initial-resolve path. Only patchScopeAnchor ever set it on the live scope, so _pendingFocalTipBranchRef in graph-app — which drains on this field to load the focal tip — never fired for newly-scoped branches. Fix: apply focalBranchTipSha whenever the anchor carries one, even if the merge base is unusable.
  • onEnsureRowRequest only paged once. For shas deep in history (an old branch's tip or its merge base), a single git log --skip=N --max-count=500 call hit the getCommitsForGraphCore 10× defensive cap before reaching the target. Fix: pass limit=0 to engage getCommitsForGraphCore's existing "no count cap, break on found" mode — one streaming git log --all invocation, O(N) walk, single process spawn instead of iteration.

Plus the stash + retry machinery (_pendingAnchorForMergeBaseLoad, tryUpgradeScopeFromPendingAnchor, requestEnsureRowForMergeBase) to handle graph-app's parallel EnsureRowRequest for the focal tip racing/cancelling our merge-base request — re-issues once when rows arrive without the merge base, gives up cleanly when rows haven't grown across attempts.

Verification

Live repro in ~/dev/repos/vscode-gitlens against three branches:

Branch Before After
old-feature-combined-diff (local, 2020-04 tip) 0 visible rows 24 visible rows
old-feature-timeline (local, 2020-12 tip) 0 visible rows 19 visible rows
feature/combined-diff (remote-only) 0 visible rows n/a — popover only surfaces local branches in real UI

Cold scope-to-deep-branch: ~3.9s to render (single git log walk to the 2020-era merge base, ~9,800 rows fetched). Subsequent scopes that reuse loaded rows: ~250ms.

Known follow-ups (not in this PR)

  • The deep walk fetches every commit between HEAD and the merge base. For a 6-year-old branch that can be 16,000+ commits + a 25s --shortstat stats follow-up. A targeted slab fetch (git log mergeBase..branchRef) would load ~50–100 commits instead — see review thread for a sketch.
  • getBaseBranchFromReflog in packages/git-cli/src/providers/branches.ts still returns the branch's own upstream as "base" for git branch X origin/Y-style branches. This PR neutralizes the bad value for graph scope, but it still flows to branch-tree comparison defaults, the merge-target picker, and overview enrichment. Worth a separate ticket.

Test plan

  • Scope to a freshly-created local branch tracking a recent remote — commits render
  • Scope to a several-year-old local branch — commits render (within ~5s on a large repo)
  • Scope to the current/default branch — same behavior as before (no merge-target overlay; all ancestors visible)
  • Scope, then clear scope, then re-scope to a different branch — no stale anchors / no retry storms
  • Refs invalidation (rebase, branch delete) mid-scope — stash cleared correctly

- Iterates the merge-target candidate chain (stored merge target → base branch → default branch) in `computeScopeAnchor` instead of picking one and bailing when `target === focal`; a branch made via `git branch X origin/Y` has a reflog-derived "base" that's the branch's own upstream, which collapsed to the focal tip and produced a focal-only anchor that left the bare scope rendering 0 rows
- Switches `onEnsureRowRequest` to a single streaming `git log --all` call with `limit=0`, engaging `getCommitsForGraphCore`'s "no count cap, break on found" mode; the prior single skip-based page hit the 10× defensive cap (~5000 commits) for shas deep in history, and a naive iterative skip loop would be O(N²) total work for an N-deep target (each `--skip=N` walks N+page commits). One streaming call is O(N) and one git process spawn instead of dozens.
- Applies `focalBranchTipSha` onto the scope on the initial-resolve path (not just `patchScopeAnchor`), and applies a partial anchor when only some fields are usable; `_pendingFocalTipBranchRef` in graph-app drains on this field and would otherwise never select an old branch's tip
- Stashes the resolved anchor when its merge-base row isn't in the loaded window and re-applies it via the `DidChangeRowsNotification` handler once paging brings the row in; gracefully handles graph-app's parallel `EnsureRowRequest` for the focal tip cancelling ours mid-flight, with `_inFlightEnsureMergeBaseSha` deduping concurrent issues and `_lastEnsureMergeBaseAttempt` preventing retry-storms when the host gives up
@ianhattendorf ianhattendorf mentioned this pull request May 22, 2026
8 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant