Fix lazy.nvim Local Changes That Block vimdoc-ja and copilot.lua Updates

Reset dirty vimdoc-ja and copilot.lua trees before lazy.nvim updates

* This page contains promotional content

On a Linux machine I opened Neovim, ran :Lazy update, and it stopped with Failed (2).
The problem was not only doc/tags-ja in vimdoc-ja. .gitattributes and .github/workflows/format.yml in zbirenbaum/copilot.lua were also treated as “local changes”.

I had seen the same error on vimdoc-ja alone before, and I had put in a workaround that ran git checkout -- doc/tags-ja on LazyUpdatePre.
That was not enough this time.


The symptom

The message that showed up at startup was the following.

Failed (2)
  ● copilot.lua  7.63ms  CopilotChat.nvim
      You have local changes in `~/.local/share/nvim/lazy/copilot.lua`:
        * .gitattributes
        * .github/workflows/format.yml
      Please remove them to update.
      You can also press `x` to remove the plugin and then `I` to install it again.
  ● vimdoc-ja  0.05ms  start
      You have local changes in `~/.local/share/nvim/lazy/vimdoc-ja`:
        * doc/tags-ja
      Please remove them to update.
      You can also press `x` to remove the plugin and then `I` to install it again.

I had no memory of editing the plugin sources themselves.
lazy.nvim refuses the update once it decides the git working tree is dirty.


Why the earlier checkout was not enough

Before an update, lazy.nvim runs git ls-files -d -m and will not pull if any tracked file has a diff.
It discards doc/tags automatically, but doc/tags-ja and the copilot.lua files are outside that exception.

Looking at the Linux machine, three things were stacked on top of each other.

  1. The live config was a separate copy under ~/.config/nvim, and the LazyUpdatePre hook I had put in the Mac dotfiles was not there
  2. Neovim on that Linux box was v0.7.2, so the vim.system I had written last time never ran; it is 0.10 and later only
  3. Even if I checked out doc/tags-ja, startup :helptags dirtied it again right away

Number 3 was the messiest.
Even after a manual git checkout -- doc/tags-ja, opening Neovim again made it dirty.
The checkout was not failing. The file was being regenerated immediately after I restored it.


The cause is different for each plugin

vimdoc-ja tracks tags-ja in git

vimdoc-ja commits the generated doc/tags-ja (the helptags file) into the repository.
Because helplang = 'ja', the :helptags that lazy.nvim runs on every install or update regenerates doc/tags-ja locally.

If that regenerated file differs from the committed content by even one byte, lazy.nvim concludes there are local changes in the git repository and stops the update.
The diff I saw this time was that the leading !_TAG_FILE_ENCODING line was gone.

The maintainer (folke) has stated clearly that there are no plans to add this kind of special-case handling on the lazy.nvim side.
The root issue is the structure of vimdoc-ja tracking generated files in git.

In the past, version differences in the Vim used to generate helptags were also a factor.
Vim 9.0.0110 changed how pseudo-tags in code examples inside the help were handled, which made the generated result more likely to drift.
After vimdoc-ja-working#1434 (in Japanese) (merged 2024-02-07) updated the generating Vim to 9.1.0065, it happens less often.
Even so, as long as :helptags runs every time, the drift itself can still come back.

The history is collected in vim-jp/vimdoc-ja#279 (in Japanese).

copilot.lua is not a generated file

.gitattributes and .github/workflows/format.yml are not files that Neovim or lazy.nvim writes.
The same plugin directory was clean on the Mac. Something on the Linux side had rewritten them.

Line-ending conversion, a YAML formatter, or an editor auto-format are candidates, but I have not pinned down which one touched them.
It was not an intentional patch, so I decided to discard all local changes on tracked files.


Workaround: clear the error that is showing now

If you just want the update to go through, reset the working tree to HEAD.

# reset the target plugins to HEAD
git -C ~/.local/share/nvim/lazy/vimdoc-ja reset --hard HEAD
git -C ~/.local/share/nvim/lazy/copilot.lua reset --hard HEAD

Pressing x (remove) then I (install again) on the :Lazy screen gets you back to the same state.
That said, tags-ja will drift again the next time :helptags runs.


Permanent fix: reset the working tree to HEAD before the update

Doing reset by hand every time is a pain, so I decided to discard the diffs right before lazy.nvim starts the update.
It sits just before the require('lazy').setup(...) call in ~/dotfiles/nvim/lua/plugins.lua.

The changes from last time are the following.

  • git reset --hard HEAD instead of git checkout -- doc/tags-ja
  • Cover both vimdoc-ja and copilot.lua
  • Widen the events from LazyUpdatePre only to LazySyncPre / LazyRestorePre / LazyCheckPre as well
  • Use vim.fn.system, which still works on Neovim 0.7, instead of vim.system
-- Reset the given plugin's working tree to HEAD.
---@param pluginName string plugin directory name (under stdpath('data')/lazy)
---@return nil
local function discardPluginLocalChanges(pluginName)
  local pluginDir = vim.fn.stdpath('data') .. '/lazy/' .. pluginName
  if vim.fn.isdirectory(pluginDir) ~= 1 then
    return
  end

  -- vim.system is Neovim 0.10+, so 0.7 environments use vim.fn.system
  local output = vim.fn.system({ 'git', '-C', pluginDir, 'reset', '--hard', 'HEAD' })
  if vim.v.shell_error ~= 0 then
    vim.notify(
      string.format(
        'discardPluginLocalChanges: git reset --hard に失敗しました。pluginName=%s cwd=%s code=%s output=%s',
        pluginName,
        pluginDir,
        tostring(vim.v.shell_error),
        output
      ),
      vim.log.levels.WARN
    )
    return
  end

  -- If it is still dirty after reset, lazy's git ls-files -d -m will stop the update
  local dirtyFiles = vim.fn.system({ 'git', '-C', pluginDir, 'ls-files', '-d', '-m' })
  if vim.v.shell_error == 0 and dirtyFiles ~= '' then
    vim.notify(
      string.format(
        'discardPluginLocalChanges: reset 後も差分が残っています。pluginName=%s cwd=%s dirtyFiles=%s',
        pluginName,
        pluginDir,
        dirtyFiles
      ),
      vim.log.levels.WARN
    )
  end
end

-- Discard known local diffs before Lazy update-related operations.
---@return nil
local function discardLazyUpdateBlockers()
  -- vimdoc-ja tracks generated doc/tags-ja in git, so a local regenerate via :helptags
  -- that differs makes lazy.nvim refuse the update with "local changes"
  -- (https://github.com/vim-jp/vimdoc-ja/issues/279)
  discardPluginLocalChanges('vimdoc-ja')
  -- also discard diffs such as .gitattributes / workflow in copilot.lua
  discardPluginLocalChanges('copilot.lua')
end

vim.api.nvim_create_autocmd('User', {
  pattern = { 'LazyUpdatePre', 'LazySyncPre', 'LazyRestorePre', 'LazyCheckPre' },
  group = vim.api.nvim_create_augroup('lazy_discard_local_changes', { clear = true }),
  callback = discardLazyUpdateBlockers,
})

Because it goes through stdpath('data'), I do not have to hard-code the plugin’s real path.
On failure it sends the function name, target plugin, path, exit code, and output to vim.notify.

The limitation is clear.
Intentional local patches on the target plugins disappear on every update.
tags-ja becoming dirty again after an update is expected; the hook discards it before the next one.

I took a workaround that is actually used in the community (mimikun/dotfiles ), and widened it for my own config.


Summary

ApproachWhat it doesRecurs
git reset --hard HEADDiscard the diff that is showing nowtags-ja does
x then I in :LazyReinstall the plugin from scratchtags-ja does
reset in the pre-update hookDiscard right before update / sync / restore / checkThe update itself goes through

The cause was not a bug in lazy.nvim.
vimdoc-ja tracks generated doc/tags-ja in git, and on the Linux side the tracked files in copilot.lua were dirty.
Both get caught by git ls-files -d -m, so I decided to reset them to HEAD before the update.
I hope this is useful for anyone whose updates have stopped on the same error.

Categories: editor 

See also