Skip to content

fix(diff): refresh the Changed diff after a branch switch in the same worktree#2679

Open
fiorelorenzo wants to merge 2 commits into
generalaction:mainfrom
fiorelorenzo:fix/diff-stale-head-after-branch-switch
Open

fix(diff): refresh the Changed diff after a branch switch in the same worktree#2679
fiorelorenzo wants to merge 2 commits into
generalaction:mainfrom
fiorelorenzo:fix/diff-stale-head-after-branch-switch

Conversation

@fiorelorenzo

Copy link
Copy Markdown
Contributor

What

The Changed and Staged diffs use the literal ref HEAD (commitRef('HEAD')) for their original side. After switching branches inside a task worktree, that original kept showing the previous branch's content, so the Changed diff showed stale changes. Fixes #2576.

Root cause

There were two separate problems and both had to be fixed.

  1. Renderer (first commit). A head worktree update only invalidated HEAD_REF git models, not the commitRef('HEAD') ones the diff actually registers. HEAD_REF and commitRef('HEAD') compare equal only within their own kind, so the original model was never re-fetched after a branch switch.

  2. Main process (second commit). GitWorktree.getFileAtRef delegated straight to repository.readBlobAtRef, but GitRepository is shared per common dir and its exec is bound to whichever worktree opened it first (usually the project's main checkout). A symbolic ref like HEAD therefore resolved against that first worktree, not the caller, so a task worktree on its own branch read the wrong branch's blob. Even with the renderer invalidation in place the re-fetch kept returning the wrong worktree's HEAD, which is why fixing only the renderer was not enough.

The fix

Resolve the ref to an immutable commit oid with the worktree-bound exec before reading. The oid is content addressed so the shared object store reads it correctly regardless of cwd. Already-immutable oids and resolution failures pass through untouched, and the existing git show fallback still handles odd refs.

Testing

  • New unit test in git-worktree.test.ts reproduces the exact scenario: a linked worktree on its own branch sharing a repository bound to the main checkout. It fails without the main-process fix and passes with it.
  • New unit test for the renderer invalidation helper covers both the head and status paths.
  • Verified against the real built package and a real worktree: with both fixes getFileAtRef('HEAD') returns the calling worktree's HEAD content and matches the ground truth git show HEAD:file.
  • format, lint, typecheck and tests pass.

…rktree

The "Changed" diff renders the working tree against a git model whose original
ref is `commitRef('HEAD')` (`{kind:'commit', sha:'HEAD'}`). The worktree-update
invalidation bridge only re-fetched models matching `HEAD_REF` (`{kind:'head'}`),
and `refsEqual` compares unequal across kinds, so the commit-'HEAD' originals
were never invalidated when HEAD moved. After a commit plus a branch switch in
the same worktree, the diff kept the previous branch's HEAD content and showed
changes from both branches.

On a head update, invalidate the models that follow HEAD under either
representation (the `head` DiffMode and the commit-'HEAD' ref). The logic is
extracted into a pure, unit-tested helper.

Fixes generalaction#2576
…epository

The Changed/Staged diff original is fetched with the literal ref `HEAD`
(commitRef('HEAD')) through getFileAtRef. GitWorktree.getFileAtRef delegated
straight to repository.readBlobAtRef, but GitRepository is shared by every
worktree on the same common dir and its exec is bound to whichever worktree
opened it first. A symbolic ref like HEAD therefore resolved against that
first worktree (typically the project's main checkout), not the caller — so a
task worktree on its own branch read the wrong branch's blob, and switching
branches inside the worktree never moved the diff original (generalaction#2576).

Resolve the ref to an immutable commit oid with the worktree-bound exec before
reading; the oid is content-addressed so the shared object store reads it
regardless of cwd. Already-immutable oids and resolution failures pass through
untouched, and the existing `git show` fallback still handles odd refs.

This is the main-process half of generalaction#2576; the renderer half (invalidating the
commitRef('HEAD') models on a head update) is the first commit on this branch.
Without this commit, that invalidation re-fetches but still returns the wrong
worktree's HEAD, so the Changed diff stays stale after a branch switch.
@greptile-apps

greptile-apps Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a two-part bug (#2576) where the Changed diff showed stale content after a branch switch inside a linked worktree. The renderer side now invalidates both HEAD_REF and commitRef('HEAD') models on a head update, while the main-process side adds resolveRefOid to translate symbolic refs to content-addressed OIDs before delegating to the shared GitRepository, ensuring the correct worktree's HEAD is resolved.

  • Renderer fix (git-model-invalidation.ts): Extracted the invalidation logic into invalidateWorktreeGitModels, which now queries both HEAD_REF and commitRef('HEAD') on any non-status update, with URI-level de-duplication to avoid double invalidation.
  • Main-process fix (git-worktree.ts): Added resolveRefOid that runs rev-parse --verify --quiet ${ref}^{commit} with the worktree-bound exec, so symbolic refs like HEAD are resolved against the calling worktree rather than the first-opened worktree that owns the shared GitRepository.exec.
  • Tests: New unit test in git-worktree.test.ts reproduces the exact worktree-sharing scenario; new unit tests in git-model-invalidation.test.ts cover both update kinds and the de-duplication path.

Confidence Score: 5/5

Safe to merge. Both root causes are addressed with targeted, well-scoped changes and the new tests reproduce the exact failure scenario.

The worktree ref-resolution fix is correct: the resolveRefOid short-circuit for 40/64-char OIDs avoids redundant git calls, and the ^{commit} peel plus catch-pass-through keeps all edge cases on the same path as before. The renderer invalidation change is a straightforward extraction into a pure helper with de-duplication, and both update kinds are covered by new unit tests. The integration test opens the main worktree first — the exact ordering that triggered the original bug — so it would catch a regression immediately.

No files require special attention; the integration test at packages/core/src/git/git-worktree.test.ts is the most operationally important new addition and covers the primary regression scenario end-to-end.

Important Files Changed

Filename Overview
packages/core/src/git/git-worktree.ts Adds resolveRefOid to translate symbolic refs to OIDs with the worktree-bound exec before delegating to the shared repository; already-immutable OIDs and failures pass through unchanged. Logic is correct and well-documented.
apps/emdash-desktop/src/renderer/lib/monaco/git-model-invalidation.ts New pure helper that invalidates HEAD_REF and commitRef('HEAD') models on a head update, STAGED_REF only on a status update, with Set-based de-duplication. Correctly extracted from invalidation-bridges.ts for testability.
apps/emdash-desktop/src/renderer/lib/monaco/invalidation-bridges.ts Delegates worktree invalidation to the new invalidateWorktreeGitModels helper; existing repo and FS bridges are unchanged. Simple, clean refactor.
packages/core/src/git/git-worktree.test.ts Adds an integration test that creates a linked worktree on its own branch and verifies HEAD resolves per-worktree. The test does not explicitly remove the linked worktree directory from the filesystem after the run.
apps/emdash-desktop/src/renderer/lib/monaco/git-model-invalidation.test.ts New unit tests cover both the head and status update paths and the URI de-duplication case using a minimal in-memory registry. Tests are correct and well-structured.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Renderer
    participant Bridge as invalidation-bridges.ts
    participant Helper as invalidateWorktreeGitModels
    participant Registry as MonacoModelRegistry
    participant Worktree as GitWorktree (linked)
    participant Exec as BoundExec (worktree-bound)
    participant Repo as GitRepository (shared)
    Note over Renderer,Registry: Renderer path — branch switch event
    Renderer->>Bridge: "gitWorktreeUpdateChannel {kind:'head'}"
    Bridge->>Helper: invalidateWorktreeGitModels(registry, wsId, 'head')
    Helper->>Registry: "findGitUris({ref: HEAD_REF})"
    Helper->>Registry: "findGitUris({ref: commitRef('HEAD')})"
    Note right of Helper: de-duplicate URIs in Set
    Helper->>Registry: invalidateModel(uri) x N
    Note over Worktree,Repo: Main-process path — getFileAtRef
    Worktree->>Worktree: getFileAtRef('file.ts', 'HEAD')
    Worktree->>Worktree: resolveRefOid('HEAD')
    Worktree->>Exec: "exec(['rev-parse','--verify','--quiet','HEAD^{commit}'])"
    Note right of Exec: uses worktree-bound cwd
    Exec-->>Worktree: "oid = 'abc123...'"
    Worktree->>Repo: readBlobAtRef('abc123...', 'file.ts')
    Note right of Repo: content-addressed, cwd irrelevant
    Repo-->>Worktree: file contents
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Renderer
    participant Bridge as invalidation-bridges.ts
    participant Helper as invalidateWorktreeGitModels
    participant Registry as MonacoModelRegistry
    participant Worktree as GitWorktree (linked)
    participant Exec as BoundExec (worktree-bound)
    participant Repo as GitRepository (shared)
    Note over Renderer,Registry: Renderer path — branch switch event
    Renderer->>Bridge: "gitWorktreeUpdateChannel {kind:'head'}"
    Bridge->>Helper: invalidateWorktreeGitModels(registry, wsId, 'head')
    Helper->>Registry: "findGitUris({ref: HEAD_REF})"
    Helper->>Registry: "findGitUris({ref: commitRef('HEAD')})"
    Note right of Helper: de-duplicate URIs in Set
    Helper->>Registry: invalidateModel(uri) x N
    Note over Worktree,Repo: Main-process path — getFileAtRef
    Worktree->>Worktree: getFileAtRef('file.ts', 'HEAD')
    Worktree->>Worktree: resolveRefOid('HEAD')
    Worktree->>Exec: "exec(['rev-parse','--verify','--quiet','HEAD^{commit}'])"
    Note right of Exec: uses worktree-bound cwd
    Exec-->>Worktree: "oid = 'abc123...'"
    Worktree->>Repo: readBlobAtRef('abc123...', 'file.ts')
    Note right of Repo: content-addressed, cwd irrelevant
    Repo-->>Worktree: file contents
Loading

Reviews (1): Last reviewed commit: "fix(git): resolve HEAD against the calli..." | Re-trigger Greptile

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.

[bug]: Diff panel shows changes from previous branch after switching branches

1 participant