Skip to content

fix(desktop): Git Tree follows ref moves that keep the top commit (#266) - #267

Open
devlint wants to merge 4 commits into
mainfrom
fix/git-tree-stale-after-ref-moves
Open

devlint wants to merge 4 commits into
mainfrom
fix/git-tree-stale-after-ref-moves

Conversation

@devlint

@devlint devlint commented Oct 14, 2026

Copy link
Copy Markdown
Owner

Fixes #266.

Why

The Git Tree kept stale branch labels, lanes and colors (or hid a branch) after refs moved while the newest commit stayed the same: main reset onto origin/main, a branch created on an existing commit, checkout of a previous branch, main reset onto another branch's commit. Only a restart fixed it. Two caches keyed on the top commit alone were the cause:

  • Log cache (useGitRepo.ts loadLog): when the first page's top hash matched, the fresh page — with its fresh %D decorations — was thrown away, and LOG_CACHE was restored on the same test. The earlier fixes forced a reload after in-app actions, but the watcher (terminal/IDE git), the integrated terminal, undo/Time Machine, PR checkout and the rebase banner still refreshed without force.
  • Graph layout cache (CommitGraph.vue): the fingerprint was count + first + last hash + branch + WIP, and the main/master lane-0 pin was re-detected only when the top commit changed.

What

  • loadLog compares the fresh first page against the held log and the cache by hash and refs (samePageDecorations). On any difference it takes the fresh rows, drops that repo's LOG_CACHE and re-prefetches; an idle poll keeps the same array and spawns nothing extra.
  • A head/refs watcher event now does one forced reload (repoRefresh(true)), so ref moves deeper than the first page are seen too.
  • Ordering: each loadLog gets a request number and only the latest applies; a superseded forced reload hands its force to the next call. A fetch landing after a repo switch is dropped.
  • Pagination: replacing the log invalidates an in-flight loadMoreLog (_logGen, _prefetchToken), which could previously append a page at a stale --skip offset and leave a gap that was then cached.
  • The branch list (used by the graph's labels) is reloaded when loadLog sees rows change, if it was already loaded — this also covers the poller path when the live watcher is off. A list landing after a repo switch is ignored.
  • New utils/graphLayoutPin.ts: refs signature (FNV-1a hash, one pass), trunk detection, and the rule "keep the pin only on a pure append". CommitGraph uses it, so lanes and the trunk follow ref moves.

Tests

  • useGitRepo-log-ref-moves.test.ts (9, real git repos behind a mocked IPC): reset of main under an unchanged top, new branch on an existing commit, idle poll keeps the array, no stale cache restore after switching repos, in-flight page doesn't leave a gap, out-of-order results, superseded forced reload still sees a deep branch, branch list reloaded on change but not on an idle poll / not loaded when lazy. Most fail against main.
  • graphLayoutPin.test.ts: the issue's cases 1 and 3, main moved onto another branch's commit, new branch, no-change cache hit, append keeps the pin, origin/main as trunk, signature size with 5000 tags.
  • Full desktop vitest green; vue-tsc clean.

Before merging

  • Not clicked through in the app. Please replay @t1gu1's scenarios from the issue.
  • Performance trade-off: every refs event is now a forced reload, which drops the log cache and restarts the background prefetch (up to 5000 commits in 500-row pages). After an in-app action that already forced its own reload, the watcher event does it a second time. Worth profiling on a repo with more than 5000 commits.
  • Known limit: a non-forced poll still only compares the first page; deep ref moves are caught through the forced watcher reload.

Resetting main onto origin/main, creating a branch on an existing commit,
checking out a previous branch or resetting main onto another branch's commit
left the Git Tree with stale labels, lanes and colors until a restart.

- loadLog: the canonical fast paths (keep the held log, restore LOG_CACHE)
  compared only the top commit hash. They now compare the fresh first page by
  hash and decoration; on any difference the fresh entries win, the repo's
  cache is dropped and re-prefetched. A user scrolled deeper gets a refetch
  down to the viewed depth, as a forced reload does. Idle polls keep the held
  array and spawn nothing extra.
- Pagination race: replacing the log now invalidates in-flight loadMoreLog
  pages (fetched at the old --skip offset) so no gap is appended and cached.
- CommitGraph: the layout fingerprint includes a one-pass decoration
  signature, and the trunk pin is re-detected when a ref moves among the
  pinned rows, not only when the top commit changes. Infinite-scroll appends
  still keep the pin. Logic extracted to utils/graphLayoutPin.ts.
- App.vue: the head/refs watcher and the reset-to-origin path reload the
  branch list the graph labels from.
- loadLog: only the latest request applies its result, so an older poll
  result landing after a newer one can't reinstate stale decorations. A
  superseded forced reload hands its force to the request that applies; the
  deep refetch respects both.
- When loadLog takes changed rows, it reloads the branch list itself if it was
  already loaded (lazy list untouched). The poller path now refreshes the
  labels CommitGraph resolves against props.branches; App.vue's explicit
  calls are gone.
- head/refs watcher: forced reload via repoRefresh(true), since decorations
  may have moved below the first page; the redundant second loadLog is gone.
- refsSignature: 32-bit FNV-1a plus decorated-row count instead of
  concatenating every decorated row (prefix signature hashed too).
- Dead TrunkPin.secondaryHashes and redundant length checks removed.

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

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

It still miss some Git Tree renrender

1 participant