Skip to content

fix(diff): remove stale git sentinel after failed attach#2395

Closed
kozabaa wants to merge 1 commit into
nvim-mini:mainfrom
kozabaa:fix/minidiff-git-sentinel
Closed

fix(diff): remove stale git sentinel after failed attach#2395
kozabaa wants to merge 1 commit into
nvim-mini:mainfrom
kozabaa:fix/minidiff-git-sentinel

Conversation

@kozabaa

@kozabaa kozabaa commented Apr 29, 2026

Copy link
Copy Markdown

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/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:

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:

   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)

  • I have read CONTRIBUTING.md

  • I have read CODE_OF_CONDUCT.md

# 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)
@krovuxdev

Copy link
Copy Markdown

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 .git later while it’s still open.
The solution is to restart Neovim, for example using :restart, or just reopen it.
Please don’t use AI :/

@echasnovski

Copy link
Copy Markdown
Member

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).

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.

The solution is to restart Neovim, for example using :restart, or just reopen it.

Although this would work, the intended solution is to explicitly :edit in the file. This causes detach and the re-attach. However, this works in 'mini.git' but not in 'mini.diff'. I'll take a look, this should be more or less straightforward.

But this PR is definitely not the wanted approach, sorry.

echasnovski added a commit that referenced this pull request May 1, 2026
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
echasnovski added a commit that referenced this pull request May 1, 2026
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants