diff --git a/lua/diffview/scene/file_entry.lua b/lua/diffview/scene/file_entry.lua index 88f3426b..027f5a2a 100644 --- a/lua/diffview/scene/file_entry.lua +++ b/lua/diffview/scene/file_entry.lua @@ -192,9 +192,13 @@ function FileEntry:validate_stage_buffers(stat) local is_modified = vim.bo[f.bufnr].modified if f.blob_hash then + -- `new_hash` is nil when the path no longer has a stage-0 blob at + -- all (e.g. a newly added file that was unstaged, leaving it + -- untracked). That is a content change like any other: the buffer + -- still shows the old staged blob and must be invalidated. local new_hash = self.adapter:file_blob_hash(f.path) - if new_hash and new_hash ~= f.blob_hash then + if new_hash ~= f.blob_hash then if is_modified then utils.warn( ( diff --git a/lua/diffview/scene/views/diff/diff_view.lua b/lua/diffview/scene/views/diff/diff_view.lua index 5eac5b83..2f9f65d3 100644 --- a/lua/diffview/scene/views/diff/diff_view.lua +++ b/lua/diffview/scene/views/diff/diff_view.lua @@ -828,6 +828,25 @@ local update_files_impl = debounce.debounce_trailing( ) end + -- A status change can flip a side's nulled state even when the + -- revs are identical (e.g. unstaging a newly added file: the + -- working entry goes "M" -> "?" and its a-side no longer has a + -- stage-0 blob). The NOOP path only copies `status` onto the old + -- entry; the Files' `nulled` flags are fixed at construction, so + -- the old a-side would keep showing — and try to reload — a blob + -- that no longer exists. Replace the entry instead. + if not replace_noop then + for _, sym in ipairs({ "a", "b", "c", "d" }) do + local of = old_file.layout:get_file_for(sym) + local nf = new_file.layout:get_file_for(sym) + + if (of and of.nulled or false) ~= (nf and nf.nulled or false) then + replace_noop = true + break + end + end + end + if replace_noop then if self.panel.cur_file == old_file then self.panel:set_cur_file(new_file) diff --git a/lua/diffview/vcs/adapters/git/init.lua b/lua/diffview/vcs/adapters/git/init.lua index ac410e99..24709464 100644 --- a/lua/diffview/vcs/adapters/git/init.lua +++ b/lua/diffview/vcs/adapters/git/init.lua @@ -1843,6 +1843,11 @@ end ---@param rev_arg string? ---@return string? function GitAdapter:file_blob_hash(path, rev_arg) + -- Empty output with exit code 0 is a real outcome, not a failure: the path + -- has no blob at that rev (e.g. a newly added file that was unstaged no + -- longer exists in the index at all). `fail_on_empty` would burn retries + -- and spam the log for it; instead let the empty result through and map it + -- to nil so callers can distinguish "no blob" from a blob hash. local out, code = self:exec_sync({ "rev-parse", "--revs-only", @@ -1850,10 +1855,9 @@ function GitAdapter:file_blob_hash(path, rev_arg) }, { cwd = self.ctx.toplevel, retry = 2, - fail_on_empty = true, }) - if code ~= 0 then + if code ~= 0 or not out[1] or vim.trim(out[1]) == "" then return end