Linux 機で Neovim を開いて :Lazy update したら、Failed (2) で止まった。
対象は vimdoc-ja の doc/tags-ja だけではなかった。zbirenbaum/copilot.lua
の .gitattributes と .github/workflows/format.yml も「local changes」扱いになっていた。
以前、同じエラーを vimdoc-ja だけで見て、LazyUpdatePre で git 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点が重なっていた。
- 実設定は
~/.config/nvimの別コピーで、Mac の dotfiles に入れたLazyUpdatePreフックが存在しなかった - その Linux の Neovim は v0.7.2 で、前回書いた
vim.systemは 0.10 以降専用なので動かない 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 がインストールや更新のたびに走らせる :helptags が doc/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.lua の require('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 はする |
:Lazy で x → I | プラグインごと入れ直す | tags-ja はする |
| 更新前フックで reset | 更新・sync・restore・check の直前に捨てる | 更新自体は通る |
原因は lazy.nvim のバグではない。
vimdoc-ja は生成物の doc/tags-ja を git 管理しており、copilot.lua は Linux 側で追跡ファイルが汚れていた。
どちらも git ls-files -d -m に引っかかるので、更新前に HEAD へ戻すことにした。