lazy.nvimで更新できない:vimdoc-jaとcopilot.luaのlocal changes

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

* 本ページはプロモーションが含まれています

Linux 機で Neovim を開いて :Lazy update したら、Failed (2) で止まった。
対象は vimdoc-jadoc/tags-ja だけではなかった。zbirenbaum/copilot.lua.gitattributes.github/workflows/format.yml も「local changes」扱いになっていた。

以前、同じエラーを vimdoc-ja だけで見て、LazyUpdatePregit checkout -- doc/tags-ja する回避を入れていた。
今回はそれだけでは通らなかった。

症状

起動時に出てきたメッセージは次のとおり。

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.

プラグイン本体を自分でいじった覚えはない。
lazy.nvim は git の作業ツリーが汚れていると判断すると、更新を拒否する。


なぜ以前の checkout では足りなかったか

lazy.nvim は更新前に git ls-files -d -m を走らせ、追跡ファイルに差分があると pull しない。
doc/tags だけは自動で捨てるが、doc/tags-ja も copilot.lua のファイルも対象外だ。

Linux 機を見ると、次の3点が重なっていた。

  1. 実設定は ~/.config/nvim の別コピーで、Mac の dotfiles に入れた LazyUpdatePre フックが存在しなかった
  2. その Linux の Neovim は v0.7.2 で、前回書いた vim.system は 0.10 以降専用なので動かない
  3. doc/tags-ja は checkout しても、起動時の :helptags がすぐ汚し直す

3番が一番ややこしかった。
手動で git checkout -- doc/tags-ja しても、Neovim を開き直すとまた dirty になる。
「checkout が失敗している」のではなく、戻した直後に再生成されていた。


原因はプラグインごとに違う

vimdoc-ja は tags-ja を git 管理している

vimdoc-ja は、生成済みの doc/tags-ja(helptags ファイル)をリポジトリにコミットしている。
helplang = 'ja' なので、lazy.nvim がインストールや更新のたびに走らせる :helptagsdoc/tags-ja をローカルで再生成する。

この再生成結果がリポジトリのコミット内容と 1 バイトでもズレると、lazy.nvim は「git リポジトリにローカル変更がある」と判断して更新を止める。
今回見えた差分は、先頭の !_TAG_FILE_ENCODING 行が消えていることだった。

開発元(folke 氏)は、「lazy.nvim 側でこの種の例外処理を入れる予定はない」と明言している。
根本は、vimdoc-ja が生成物を git 管理下に置いている構造の問題だ。

過去には、helptags 生成に使う Vim のバージョン差も一因だった。
Vim 9.0.0110 で、ヘルプ内コード例中の疑似タグの扱いが変わったため、生成結果がズレやすかった。
vimdoc-ja-working#1434 (2024-02-07 マージ)で生成用 Vim を 9.1.0065 に更新してから、発生頻度は下がっている。
それでも :helptags を毎回走らせる限り、ズレ自体は再発しうる。

経緯は vim-jp/vimdoc-ja#279 にまとまっている。

copilot.lua は生成物ではない

.gitattributes.github/workflows/format.yml は、Neovim や lazy.nvim が書き出すファイルではない。
同じプラグインディレクトリを Mac で見るとクリーンだった。Linux 側で何かが書き換えた、と考えてよい。

改行コード変換、YAML フォーマッタ、エディタの自動整形などが候補だが、どれが触ったかは特定していない。
意図したパッチではなかったので、追跡ファイルのローカル変更はまとめて捨てることにした。


その場しのぎ:今出ているエラーを消す

更新を通したいだけなら、作業ツリーを HEAD に戻せばよい。

# 対象プラグインを HEAD に戻す
git -C ~/.local/share/nvim/lazy/vimdoc-ja reset --hard HEAD
git -C ~/.local/share/nvim/lazy/copilot.lua reset --hard HEAD

:Lazy の画面で x(削除)→ I(再インストール)でも同じ状態に戻せる。
ただし tags-ja は、次に :helptags が走ればまたズレる。


恒久対策:更新前に作業ツリーを HEAD へ戻す

毎回手で reset するのは面倒なので、lazy.nvim が更新を始める直前に捨てることにした。
~/dotfiles/nvim/lua/plugins.luarequire('lazy').setup(...) 呼び出し直前に置いている。

前回からの変更点は次のとおり。

  • git checkout -- doc/tags-ja ではなく git reset --hard HEAD
  • 対象を vimdoc-ja と copilot.lua の両方にする
  • イベントを LazyUpdatePre だけでなく LazySyncPre / LazyRestorePre / LazyCheckPre にも広げる
  • vim.system ではなく、Neovim 0.7 でも動く vim.fn.system を使う
-- 指定プラグインの作業ツリーを HEAD に戻す。
---@param pluginName string プラグインディレクトリ名(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 は Neovim 0.10+ のため、0.7 環境では 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

  -- reset 後も dirty なら、lazy の git ls-files -d -m が更新を止める
  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

-- Lazy 更新系の前に、既知のローカル差分を破棄する。
---@return nil
local function discardLazyUpdateBlockers()
  -- vimdoc-ja は doc/tags-ja を生成物ごと git 管理しており、:helptags による
  -- ローカル再生成でズレると lazy.nvim が「local changes」で更新を拒否する
  -- (https://github.com/vim-jp/vimdoc-ja/issues/279)
  discardPluginLocalChanges('vimdoc-ja')
  -- copilot.lua の .gitattributes / workflow などの差分もまとめて破棄する
  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,
})

stdpath('data') 経由なので、プラグインの実体パスをハードコードしなくて済む。
失敗時は関数名、対象プラグイン、パス、終了コード、出力を vim.notify に出す。

制限ははっきりしている。
対象プラグインへ意図的に当てたローカルパッチは、更新のたびに消える。
更新後に tags-ja がまた dirty になるのは想定どおりで、次の更新前にフックが捨てる。

コミュニティで実際に使われている回避策(mimikun/dotfiles )を参考に、自分の設定へ広げた。


まとめ

やり方内容再発
git reset --hard HEAD今出ている差分を捨てるtags-ja はする
:LazyxIプラグインごと入れ直すtags-ja はする
更新前フックで reset更新・sync・restore・check の直前に捨てる更新自体は通る

原因は lazy.nvim のバグではない。
vimdoc-ja は生成物の doc/tags-ja を git 管理しており、copilot.lua は Linux 側で追跡ファイルが汚れていた。
どちらも git ls-files -d -m に引っかかるので、更新前に HEAD へ戻すことにした。

カテゴリー: editor 

関連項目