fix(diff): remove stale git sentinel after failed attach#2395
Conversation
# fix(diff): remove stale git sentinel after failed attach
## Problem
Opening a buffer in a directory that is **not yet a git repository** permanently
blocks `mini.diff` from ever attaching to that buffer, even after `git init` is
run later in the same session.
Symptoms:
- Gutter signs never appear after `git init` + commit
- `MiniDiff.get_buf_data()` always returns `nil`
- `MiniDiff.enable()` called manually has no effect
- No errors are logged anywhere
## Root cause
In `H.git_start_watching_index`, when the initial `git rev-parse --git-dir`
returns a non-zero exit code, `on_not_in_git` fires:
```lua
-- lua/mini/diff.lua ~line 1696
local on_not_in_git = vim.schedule_wrap(function()
if not vim.api.nvim_buf_is_valid(buf_id) then
H.cache[buf_id] = nil
return
end
MiniDiff.fail_attach(buf_id)
H.git_cache[buf_id] = {} -- ← stale sentinel
end)
```
The call chain is:
```
fail_attach(buf_id)
→ disable(buf_id)
→ source.detach(buf_id)
→ H.git_cache[buf_id] = nil ✓ invariant restored
→ H.cache[buf_id] = nil ✓ buffer marked as not attached
← returns
H.git_cache[buf_id] = {} ✗ invariant violated
```
After `detach` correctly clears the entry to `nil`, `on_not_in_git` immediately
overwrites it with an empty table `{}`. The `attach` guard then blocks every
future re-attachment attempt for that buffer ID:
```lua
local attach = function(buf_id)
if H.git_cache[buf_id] ~= nil then return false end -- {} ~= nil → returns false
...
end
```
Because `attach` returns `false`, `enable` calls `fail_attach` again, which
calls `disable`, which clears `H.cache[buf_id]`. As a result,
`get_buf_data()` always returns `nil` and the buffer is permanently wedged
for the rest of the Neovim session under that buffer ID.
## Fix
Remove the one line that re-poisons the cache after `detach` has already
cleaned it up:
```diff
local on_not_in_git = vim.schedule_wrap(function()
if not vim.api.nvim_buf_is_valid(buf_id) then
H.cache[buf_id] = nil
return
end
MiniDiff.fail_attach(buf_id)
- H.git_cache[buf_id] = {}
end)
```
## Why this is safe
The sentinel's apparent purpose is to prevent re-attach storms after a failed
attempt. That concern is already handled elsewhere:
1. **`detach` already cleaned up.** `fail_attach → disable → source.detach`
sets `H.git_cache[buf_id] = nil` before the removed line ever ran. The
extra assignment was redundant on top of being harmful.
2. **No re-attach loop is possible.** `auto_enable` is gated on
`H.is_buf_enabled(buf_id)` and fires only on `BufEnter`. After
`fail_attach`, `H.cache[buf_id]` is `nil` (buffer disabled); nothing
schedules another enable until the user switches to the buffer again —
which is exactly when a retry is wanted (the user may have just run
`git init`).
3. **The in-flight guard is unaffected.** The `attach` function sets
`H.git_cache[buf_id] = {}` *synchronously* before launching the async
`git rev-parse` job (line 663). That assignment is what prevents a second
concurrent attach during a live job. Removing the post-failure assignment
does not touch this path.
## Reproduction
```
# 1. Open Neovim in a non-git directory
cd /tmp/testdir && nvim hello.md
# 2. In another pane, while Neovim stays open:
git init /tmp/testdir
git -C /tmp/testdir add .
git -C /tmp/testdir commit -m "init"
# 3. Edit hello.md, then in Neovim:
:lua print(vim.inspect(MiniDiff.get_buf_data()))
-- Before fix: nil
-- After fix: table with config/hunks/summary
```
## Tested on
- Neovim 0.12.1
- Debian trixie-slim (Docker container, `HOME=/tmp`)
- mini.nvim `main` (confirmed by reading the source)
|
It’s intentional, when it’s nil it means the buffer is empty or the file isn’t being used, and also if the file wasn’t under git when it was opened in Neovim it will stay nil even if you add a |
Retry on user switching to the buffer again is exactly when trying to re-attach is not wanted. Because it will lead to unlimited re-attach tries for files that are not in the Git repository.
Although this would work, the intended solution is to explicitly But this PR is definitely not the wanted approach, sorry. |
Details: - Default `git` source intentionally tries to compute Git data (i.e. at least whether the file is in the Git repo) only once for each path on disk. It is to avoid CLI calls on every `BufEnter` for files that are not in the Git repo. - However, it is useful to have a way to force recomputing the Git state of the buffer. Here `:edit` is the way, but it doesn't work (although it works with 'mini.git'). This was because the internal source cache was not cleared for the buffer in that case. The assumed place would be through `on_detach` that calls `MiniDiff.disable`, but it does not reach `source.detach` since there was no source attached to the buffer in the first place. Forcing `git` source to clean its own cache on `BufUnload` seems to fix the issue. Related to #2395
Details: - The `git` source intentionally tries to compute Git data (i.e. at least whether the file is in the Git repo) only once for each path on disk. It is to avoid CLI calls on every `BufEnter` for files that are not in the Git repo. - However, it is useful to have a way to force recomputing the Git state of the buffer. The common way is `:edit`, but it doesn't work (although it works with 'mini.git'). This was because the internal source cache was not cleared for the buffer in that case. The assumed place would be through `on_detach` of `nvim_buf_attach()`, but its `MiniDiff.disable()` call does not reach `source.detach` since there was no source attached to the buffer in the first place. Forcing `git` source to clean its own cache on `BufUnload` seems to fix the issue. Related to #2395
fix(diff): remove stale git sentinel after failed attach
Problem
Opening a buffer in a directory that is not yet a git repository permanently blocks
mini.difffrom ever attaching to that buffer, even aftergit initis run later in the same session.Symptoms:
git init+ commitMiniDiff.get_buf_data()always returnsnilMiniDiff.enable()called manually has no effectRoot cause
In
H.git_start_watching_index, when the initialgit rev-parse --git-dirreturns a non-zero exit code,on_not_in_gitfires:The call chain is:
After
detachcorrectly clears the entry tonil,on_not_in_gitimmediately overwrites it with an empty table{}. Theattachguard then blocks every future re-attachment attempt for that buffer ID:Because
attachreturnsfalse,enablecallsfail_attachagain, which callsdisable, which clearsH.cache[buf_id]. As a result,get_buf_data()always returnsniland the buffer is permanently wedged for the rest of the Neovim session under that buffer ID.Fix
Remove the one line that re-poisons the cache after
detachhas already cleaned it up:local on_not_in_git = vim.schedule_wrap(function() if not vim.api.nvim_buf_is_valid(buf_id) then H.cache[buf_id] = nil return end MiniDiff.fail_attach(buf_id) - H.git_cache[buf_id] = {} end)Why this is safe
The sentinel's apparent purpose is to prevent re-attach storms after a failed attempt. That concern is already handled elsewhere:
detachalready cleaned up.fail_attach → disable → source.detachsetsH.git_cache[buf_id] = nilbefore the removed line ever ran. The extra assignment was redundant on top of being harmful.No re-attach loop is possible.
auto_enableis gated onH.is_buf_enabled(buf_id)and fires only onBufEnter. Afterfail_attach,H.cache[buf_id]isnil(buffer disabled); nothing schedules another enable until the user switches to the buffer again — which is exactly when a retry is wanted (the user may have just rungit init).The in-flight guard is unaffected. The
attachfunction setsH.git_cache[buf_id] = {}synchronously before launching the asyncgit rev-parsejob (line 663). That assignment is what prevents a second concurrent attach during a live job. Removing the post-failure assignment does not touch this path.Reproduction
Tested on
Neovim 0.12.1
Debian trixie-slim (Docker container,
HOME=/tmp)mini.nvim
main(confirmed by reading the source)I have read CONTRIBUTING.md
I have read CODE_OF_CONDUCT.md