fix(diff): invalidate stage buffers when a newly added file is unstaged - #298
Merged
dlyongemallo merged 1 commit intoJul 20, 2026
Merged
Conversation
Unstaging a newly added file (AM -> ??) removes its path from the index entirely, unlike unstaging a tracked file which resets the stage-0 blob back to the HEAD blob. Three stacked defects left the open diff view permanently out of sync with no recovery short of closing the tab: 1. GitAdapter:file_blob_hash() used fail_on_empty, so the legitimate "path has no blob at this rev" answer (empty output, exit 0) burned retries, spammed the log on every refresh, and returned nil. 2. FileEntry:validate_stage_buffers() gated disposal on a non-nil new hash, so the stale stage buffer survived every refresh, including explicit ones via refresh_files. 3. The NOOP branch of DiffView:update_files() copies status onto a kept entry, but each side's nulled flag is fixed at construction and derives from the status: the M -> ? transition must flip the a-side to nulled. The kept entry retained a live a-side pointing at a gone blob, producing either the stale buffer or a failed buffer reload (E5560 fast-event crash chain), depending on whether blob_hash was captured at buffer creation. Fix: treat empty rev-parse output as a real "no blob" answer, dispose stage buffers when the blob hash goes away, and force entry replacement whenever a side's nulled state would change so the a-side of a now- untracked file renders as a proper null buffer.
Owner
|
Thanks for the fix @Phaen! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Unstaging a newly added file (status
AM→??) while its diff is open leaves the view permanently out of sync. Depending on timing, one of two symptoms appears::0:(index) side keeps showing the old staged content on every refresh.R/:DiffviewRefreshdoesn't help; only closing the tab and reopening does.Failed to create diff buffer ':0:<path>'followed byE5560: nvim_buf_is_valid must not be called in a fast event context, when the stage buffer is (re)created after the blob has left the index.Which symptom you get depends on whether
blob_hashwas successfully captured when the stage buffer was created.Root cause
Three stacked defects, all downstream of the same event — the path leaving the index entirely (unlike unstaging a tracked file, which merely resets the stage-0 blob back to the HEAD blob):
GitAdapter:file_blob_hash()usedfail_on_empty = true, so the legitimate "path has no blob at this rev" answer (empty output, exit 0) was treated as a retryable job failure: 3 retries, error log spam on every refresh, and anilreturn indistinguishable from a real failure.FileEntry:validate_stage_buffers()gated disposal onif new_hash and new_hash ~= f.blob_hash, so anilhash silently skipped invalidation and the stale buffer survived every refresh.DiffView:update_files()keeps an entry when its path and revs are unchanged, copying onlystatusonto it. But each side'snulledflag is fixed at construction, and it derives from the status (Diff2.should_null): theM→?transition must flip the a-side to nulled. The kept entry instead retained a live a-side pointing at a blob that no longer exists — the source of both the stale buffer and the failed reload.Fix
file_blob_hash(): dropfail_on_empty; map empty output tonilexplicitly. "No blob at this rev" is a real answer, not a failure — this also removes the retry storm and log spam.validate_stage_buffers(): treat anilnew hash as a content change and dispose the buffer (the unmodified-buffer guard is unchanged).update_files()NOOP branch: force entry replacement when any side'snulledstate would change, so the a-side of a now-untracked file renders as a proper null buffer instead of attempting to load a gone blob.Repro
With a Diffview tab open the whole time:
:DiffviewOpen, select the file — left side is empty (null buffer), right side is the working tree file, as expected for an untracked file.git addthe file, then edit it further in the working tree.git resetthe file (it becomes untracked again).R/:DiffviewRefresh) doesn't help; only closing the tab and reopening does.** Full disclosure**: This PR was created with AI-assistance. The bug was found, reproduced and flagged by a human. I'm one of those silly web devs, don't take my lua-based PR on face value. :)