fix(diff): refresh the Changed diff after a branch switch in the same worktree#2679
fix(diff): refresh the Changed diff after a branch switch in the same worktree#2679fiorelorenzo wants to merge 2 commits into
Conversation
…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 SummaryThis 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
Confidence Score: 5/5Safe 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.
|
| 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
%%{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
Reviews (1): Last reviewed commit: "fix(git): resolve HEAD against the calli..." | Re-trigger Greptile
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.
Renderer (first commit). A
headworktree update only invalidatedHEAD_REFgit models, not thecommitRef('HEAD')ones the diff actually registers.HEAD_REFandcommitRef('HEAD')compare equal only within their own kind, so the original model was never re-fetched after a branch switch.Main process (second commit).
GitWorktree.getFileAtRefdelegated straight torepository.readBlobAtRef, butGitRepositoryis shared per common dir and itsexecis bound to whichever worktree opened it first (usually the project's main checkout). A symbolic ref likeHEADtherefore 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
execbefore 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 existinggit showfallback still handles odd refs.Testing
git-worktree.test.tsreproduces 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.headandstatuspaths.getFileAtRef('HEAD')returns the calling worktree's HEAD content and matches the ground truthgit show HEAD:file.