他人のファイルを消すフォーク — NeovimとVimのundoファイルが残した問い
他人のファイルを消すフォーク — NeovimとVimのundoファイルが残した問い
フォークが親のファイル形式を変えるとき、ディスク上にすでに置かれた親のファイルは誰のものか — 開発者の内部状態か、ユーザーのデータか?
導入
2026年9月27日、Hacker Newsに「On caring for user data: NeoVim caused Vim undo files to be deleted」というタイトルの投稿が上がり、384ポイントを集めた。リンク先はMarcin Wicharyのブログ Unsung の8月28日付の記事「They had no concept of a duty of care to their users.」である。WicharyはコンピュータサイエンティストDavid ChisnallのMastodon投稿をほぼ丸ごと引用し、短い論評を添えている。
Chisnallの話はこうだ。彼は2000年頃からVimを使い続け、5冊の本、博士論文、数十本の論文、150本を超える記事をVimで書いてきた。最も気に入っている機能は永続undo(persistent undo)である。先週、再起動を一回挟む前に消した何かを、undoでさかのぼって取り戻せる。ところが初期のNeovimを試すとundoが効かず、Vimで開き直しても効かなかった。彼の記述によれば、Neovimはundoファイルの形式を変え、名前は変えず、Vimのundoファイルを見つけると削除し、Vimが読めないファイルに置き換えた。issueを立てると「永続undoの形式は不安定で、今後も変わるだろう」という返答だったという。
この記事は「オープンソース開発者に義務はあるのか」というおなじみの論争に広がった。しかし、この件でより長く残る教訓は別のところにあると考える。フォークは親のコードだけを受け継ぐのではない。親がユーザーのディスク上に残したファイル、そしてそのファイルを扱うルールまで受け継ぐ。本稿では2021年の実際のGitHubスレッドと、現在の両者のソースコードを突き合わせ、この件が「態度の問題」である前にどのような構造の問題だったのかを見ていく。
2021年のPRと2026年の回想 — 記録が語ること
まず事実関係を整理する。回想と記録は一部食い違っているため、両者を区別して見る必要がある。
Hacker Newsのコメントでjeremyjhがリンクした元の変更は、NeovimのPR #13973「Save extmark undo information when writing to undofile」である。chentoastが2021年2月20日に作成し、3月3日にマージされた。目的はissue #13048の修正、つまりtree-sitterのようにバイト単位のバッファ変更イベントに依存する機能が、undoファイルから復元された状態とも正しく噛み合うようにすることだった。
スレッドでメンテナのbfredlは、最初から形式のバージョン(UF_VERSION)を上げるべきだと述べた。続けて、undoファイルは「長期保存ではなくランタイム状態のシリアライズ(クラッシュ復旧など)」とみなしており、「反乱(mutiny)が起きれば」旧形式の互換性を追加できるが、使う人がいないならその価値はない、と書いている。マージ時の告知は明確だった。新しいバージョンはマージ以前のundoファイルを読まず、非互換バージョンのエラーが出る。古いundoファイルを復旧したいならNvim 0.4を別に保管しておけ、と。
反論はスレッド内ですぐに出た。jamessanは「バージョン付きのファイル形式なのに、なぜ旧バージョンを読む能力を残さないのか」と問うた。bfredlの答えは技術的なものだった。Vimのundoヘッダは変更された行全体の単位で動くが、新形式はバイト単位の分解能を加える。行全体を置き換える偽のバイト編集を作れば変換はできるが、tree-sitterの性能が落ち、バイト単位の更新を使うプラグインが想定外の動きをする。プラグイン作者のlervagは、これまでVimとNeovimの両方の互換性を保ってきたが、この変更で設定を分けなければならなくなったと残念がり、blueyedはNeovim 0.5用のundodirを別に指定する回避策を共有した。
Chisnall本人は2021年7月13日にこのPRへコメントを残している。彼はundoファイルがクラッシュ復旧用だという定義に強く反対し、まだコミットしていないソースツリーの履歴が再起動や数ヶ月の空白を越えて残ることこそVimで一番好きな機能だと書いた。そして二つの代替案を示した。ファイル名を変えていれば最小驚きの原則に反しなかったはずだし、旧形式は読んで新形式でのみ書く方式なら急激なデータ損失を避けられた、というものだ。
ここで記録と回想が分かれる。bfredlは、Neovimのデフォルトパスはすでに違う(当時は ~/.cache/nvim/undo/)と指摘し、「読めないファイルは上書きしないはずだ」と答えた。Chisnallは、自分はVIMINIT環境変数でVim用のvimrcを読ませていたため、NeovimがVimと同じundodirを使うことになったと説明した。そのうえで上書きについては「そうではない(It doesn’t)」と認めつつ、代わりにNeovimで先に編集したファイルにはVimが読めないundoファイルが、その逆も同様に残るようになったと書いた。そして最初の10分でNeovimをアンインストールし、Vimに戻った。
つまり2026年のMastodon投稿が言う「Vimのundoファイルを削除して置き換えた」という記述と、2021年のスレッドで本人が同意した「読めないファイルは上書きしない」の間には緊張がある。ただしこの二つは完全には矛盾しない。次節で見るように、コードが言う「読めない」とユーザーが感じる「読めない」は別の概念だからだ。
Hacker Newsのコメントもこの点を突いている。utopcellは、Neovimのデフォルトのundodirは XDG_STATE_HOME 配下(~/.local/state/nvim/undo/)で、Vimはデフォルトで編集ファイルの隣に保存するので、両エディタが同じ場所を使うよう設定した場合にのみ衝突が起きる、と整理した(直接の引用ではなく、コメントの要旨の要約)。jeremyjhは当初「分かっていてリリースした」と強く批判していたが、ユーザーがundodirを共有していたという細部を確認し、話はかなり変わるとコメントを修正した(直接の引用ではなく、コメントの要旨の要約)。
マジックナンバーは確かめ、バージョンは確かめない
Neovimの現在のmasterのソース src/nvim/undo.c でundoファイルを書くパスを見ると、何が起きたのかを構造的に理解できる。同じ名前のundoファイルがすでに存在する場合、次の順序で判断する。
// If the undo file already exists, verify that it actually is an undo
// file, and delete it.
if (os_path_exists(file_name)) {
if (name == NULL || !forceit) {
// Check we can read it and it's an undo file.
int fd = os_open(file_name, O_RDONLY, 0);
if (fd < 0) { /* "Will not overwrite with undo file, cannot read" */ goto theend; }
// ... read UF_START_MAGIC_LEN bytes ...
if (len < UF_START_MAGIC_LEN || memcmp(mbuf, UF_START_MAGIC, UF_START_MAGIC_LEN) != 0) {
/* "Will not overwrite, this is not an undo file" */ goto theend;
}
}
os_remove(file_name);
}
(引用は現在のmasterからの抜粋・省略であり、2021年当時のコードとは細部が異なる可能性がある。)
ここで「読めない」とは、ファイルを open() できないという意味である。権限の問題だ。そして「undoファイルではない」とは、先頭9バイトのマジックナンバーが違うという意味である。両方を通過すると、ファイルは削除されて新しく書かれる。バージョンは確認しない。
ところがマジックナンバーはVimとNeovimでまったく同じである。どちらのソースも UF_START_MAGIC を "Vim\237UnDo\345" と定義している。一方でバージョンは違う。現在のVim masterの UF_VERSION は2、Neovim masterは3だ。読み込みパスでは、Neovimはバージョンが合わなければ E824: Incompatible undo file エラーを出して読み込みを諦める。しかし書き込みパスはそのファイルを「undoファイルである」と判定し、削除する。
結果として、同じundodirを使うユーザーがNeovimでファイルを開くと、Vimのundo履歴は読まれず(エラー)、バッファを保存するとその履歴ファイルは削除されてNeovim形式に置き換わる。bfredlの言う「読めないファイルは上書きしない」はコードレベルでは事実である。そしてChisnallが感じた「自分の履歴が消えた」もユーザーレベルでは事実である。二つの文は「読む」を別々に定義しているにすぎない。
この書き込みガードはNeovimが新しく作ったものではない。Vimの src/undo.c にも同じメッセージ(「Will not overwrite with undo file, cannot read」「Will not overwrite, this is not an undo file」)と同じ構造がある。Vimのドキュメントもこれを隠していない。:help undo-persistence のE824の説明は「undoファイルのバージョンが、より新しいVimで書かれたことを示している。その情報を残したいならバッファを保存するな」と書く。:wundo の説明は、既存ファイルがundoファイルらしく見えれば上書きし、上書きは先に削除してから新規作成する方式だと明かしている。
つまりVimの中では、この設計は合理的だった。一つのプログラムが形式バージョンを単調増加させる限り、「マジックナンバーが合えば自分のファイル」という判断はほぼ外れない。より新しいバージョンのファイルに出会うのはユーザーがVimをダウングレードしたときくらいで、ドキュメントはそのケースを警告している。問題は、フォークが同じマジックナンバーを維持したまま独自にバージョンを上げたことで生じた。その瞬間、「マジックナンバーが合う」は「自分が書いたファイルである」をもはや意味しなくなった。マジックナンバーは「同じ系統」を、バージョンは「同じ方言」を表すのに、ガードは系統しか確かめていない。
Hacker Newsのコメントのうち、lokarの「別のファイル名を選びさえすればよかった」(直接の引用ではなく、コメントの要旨の要約)は、この構造を最も短く要約している。実際、Neovimはデフォルトパスを分けていた。しかしパスは設定で上書きできる値であり、NeovimはVimの設定をそのまま読み込む経路(VIMINITなど)をサポートしている。ファイル名や拡張子、あるいはマジックナンバーそのものを変えていれば、設定が重なっても衝突しなかったはずだ。
反対側の論拠も記録しておくべきだろう。justinmkは、Vim自体も外部ツール(git、nanoなど)がファイルを変更するとundoファイルをリセットすることを、再現手順付きで指摘した(直接の引用ではなく、コメントの要旨の要約)。Vimのドキュメントによれば、Vimはファイル内容のハッシュで同期状態を確認し、ずれていればundoファイルを無視する。永続undoはもともと壊れやすい機能であり、これをバックアップやバージョン管理のように使うのは期待しすぎだ、という主張である。soraminazukiは逆に、ドキュメントが明示している無効化条件は「ファイル内容が変わった場合」だけで、「別のエディタやアップグレード」はそこに含まれていないと反論した(直接の引用ではなく、コメントの要旨の要約)。
二つの主張は同じ事実を別の角度から見ている。履歴が無視されることと、履歴が削除されることは違う。Vimがハッシュ不一致でundoファイルを無視するときも、そのファイルはバッファを保存した瞬間に新しいファイルへ置き換わる。結果は似ているが、ユーザーが予測できる条件(自分が別のツールでファイルを直した)と予測できない条件(別のエディタを一度開いてみた)とでは、信頼に与える影響が違う。
フォークが受け継ぐ、ディスク上の契約
この件から一般化できることは三つある。
第一に、ファイル形式の互換性は、文書化されていなくても契約になる。NeovimはVimとの完全互換を目標に掲げたことはない。Hacker Newsでnatbennettは、初期のNeovimが「破壊的変更を厭わないVim」として受け止められていたと振り返り、kpsはPOSIX vi準拠がNeovimの非目標であることを挙げた(いずれも直接の引用ではなく、コメントの要旨の要約)。それでもNeovimはVimの設定ファイルを読み、Vimと同じオプション名 undofile と undodir を使い、同じマジックナンバーのファイルを作る。ユーザーはこの外見から互換性を推論する。throwaway899773の指摘どおり、同じ名前の機能が片方では安定し、もう片方では不安定で、その違いを事前に知っていなければならないのなら、データ損失は予測可能な結果である(直接の引用ではなく、コメントの要旨の要約)。2021年のスレッドでMRAAGHが、undoファイルの非互換をNeovimの vim-differences ドキュメントに書くべきだと提案したのも同じ文脈だ。
逆にskybrianは、約束は明示的であるべきで、ユーザーが一方的に想定した「注意義務」を相手が同じ意味で理解するとは期待できない、と述べた(直接の引用ではなく、コメントの要旨の要約)。これも正しい。しかしファイル形式は、ドキュメントが沈黙しているときにも何かを語る。同じマジックナンバーを使うということは「このファイルはその系統のファイルだ」という宣言である。その宣言を保ったまま意味を変えれば、明示的な約束がなくても暗黙の約束を破ったことになる。
第二に、「このファイルは何か」という定義が、プロジェクトの内側とユーザーの側とで異なりうる。bfredlにとってundoファイルはクラッシュ復旧用のランタイム状態だった。Chisnallにとっては数ヶ月分の作業履歴だった。Hacker Newsでdtechと RVuRnvbM2e は、undoファイルは ~/.cache にあるのだからキャッシュであり消えてもよいと主張したが、jorams は、VimもNeovimもデフォルトでは ~/.cache を使わず、Neovimのデフォルトパスはステートディレクトリ(~/.local/state)だと反論した(いずれも直接の引用ではなく、コメントの要旨の要約)。XDGの規約において、cacheとstateはまさに「消してよいか」で区別される。Neovimがデフォルトパスをstate側に置いたことは、少なくともディレクトリ配置のレベルでは、このファイルを消してよいキャッシュとは見ていなかったと読める。開発者の意図がどうであれ、ユーザーが長く蓄積したファイルはユーザーのデータとして扱うほうが安全である。
第三に、自分が作っていないファイルを扱うときのルールである。applfanboysbgonは、マイグレーションの前には必ず対象ファイルをバックアップすると書いた(直接の引用ではなく、コメントの要旨の要約)。Chisnallが2021年に提案した「旧形式は読み、新形式でのみ書く」「名前を変える」と並べると、実践可能なルールにまとめられる。
- 読めないファイルは消さない。ここでの「読めない」には、権限エラーだけでなくバージョン不一致も含める。
- 形式を変えるときは、識別子(マジックナンバー、拡張子、パス)の少なくとも一つを一緒に変える。バージョンフィールドだけを変えると、同じ系統の別実装は区別できない。
- 削除や置き換えがどうしても必要なら、原本を脇に退避する。ディスクの数キロバイトはユーザーの信頼より高くない。
これらのルールは難しくない。実際、Neovimの書き込みガードにバージョン比較を一行加えるだけで、一つ目のルールは満たされる。それが2021年に行われなかった理由は、技術的な難しさではなく「このファイルはクラッシュ復旧用」という定義にあった可能性が高い。定義が決まると、その定義に合わない使い方は「使う人がいない」として処理される。スレッドでの「反乱が起きれば」互換性を追加するという言葉は、ユーザーが声を上げるまでそのユースケースが見えないという構造をそのまま示している。
興味深い後日談もある。2022年8月、Arnie97は同じPRスレッドに、旧形式のundoファイルを新形式へ引き上げる簡易スクリプト ufbump を共有し、2023年1月にはEricDuminilが、そのスクリプトで0.4.4のundoファイルを0.8.2向けに移行できたと礼を述べている。メンテナが「価値がない」と見た変換を、ユーザーが自分で作って使ったわけだ。Hacker Newsでutopcellは、今ならLLMに尋ねれば40行以下の変換コードが数秒で得られると書き、asveikauは逆に、LLMの時代にこうした細部を気にかけない速度がむしろ上がっていると懸念した(いずれも直接の引用ではなく、コメントの要旨の要約)。変換コストがゼロに近づくほど、「変換する価値がない」という論拠は弱くなる。残るのは「変換すべきファイルだと認識していたか」という問いだけである。
結論
Hacker Newsの論争は、かなりの部分が「無料ソフトウェアの開発者に義務はあるのか」へ流れた。recursivedoubtsは当初、無料で配る贈り物に義務はないと書いたが、既存のファイルを後方互換性のない形で変更したという部分を見落としていたとしてコメントを修正した(直接の引用ではなく、コメントの要旨の要約)。この修正が論争の核心を示している。機能を作らないことと、ユーザーの既存ファイルを書き換えることは別のカテゴリーである。
記録をたどる限り、この件に悪意や無関心があったと断定するのは難しい。bfredlはマージ時に非互換を告知し、復旧方法(旧バージョンの保管)を案内し、後にはVim側と新しい共通形式を議論したいとまで書いている。Chisnall自身も2021年には上書きではないことに同意していた。2026年の回想は当時より断定的である。5年の間に記憶が鋭くなったのか、当時の表現が慎重だったのかは、記録だけでは分からない。
しかし構造ははっきりしている。同じマジックナンバーを使う二つの実装が異なるバージョンを書き、書き込みガードはマジックナンバーだけを確かめて削除する。これは誰か一人の開発者の判断ではなく、フォークという行為が生み出した条件である。親のファイル識別子を受け継いだ瞬間、親が作ったファイルも自分のコードの削除パスの上に乗ってくる。
あなたのツールは、自分が作っていないファイルに出会ったとき何を確かめているだろうか。「自分の形式か」だけを問うのか、それとも「自分がこのファイルを消してよいのか」まで問うのか。
出典:
- https://news.ycombinator.com/item?id=49867067
- https://unsung.aresluna.org/they-had-no-concept-of-a-duty-of-care-to-their-users/
- https://github.com/neovim/neovim/pull/13973
- https://github.com/neovim/neovim/pull/13973#issuecomment-879048287
- https://github.com/neovim/neovim/blob/master/src/nvim/undo.c
- https://github.com/vim/vim/blob/master/src/undo.c
- https://github.com/vim/vim/blob/master/runtime/doc/undo.txt