Skip to content

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

Open
kozabaa wants to merge 1 commit into
mainfrom
fix/minidiff-git-sentinel
Open

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

Conversation

@kozabaa

@kozabaa kozabaa commented Apr 29, 2026

Copy link
Copy Markdown
Owner

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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant