Git はなぜ空のディレクトリを無視するのか?


LAPRAS アドベントカレンダー2021 の 17 日目の記事です。

概要

Git では空のディレクトリをそのままリポジトリに追加することはできません。

この問題は.gitkeep等を作成することで解決するわけですが、そのやり方を知ったとき「なぜ普通のファイルと同じように追加できないのだろう?」と疑問に思った方も多いのではないでしょうか。

というわけで本記事では、空ディレクトリを追加できない仕様になっている理由について調査してみました。

Gitが管理するのはファイルの内容

「空ディレクトリをaddできないのは何故か?」という疑問への答えとして、
「そもそもどんなディレクトリもaddはできないから」がまず挙げられるでしょう。

もちろんgit add directory/とコマンドを打つこと自体は可能です。

しかしこのとき実際にインデックスに追加されるのは、そのdirectory自身ではなく、それに含まれるファイル(の内容に対応する blob オブジェクト)群です。

インデックスにおいては、ディレクトリはそれらファイルのパス名としてのみ登場します。

よってディレクトリ内に追跡可能なファイルが一つもなければgit addしても何も起こらないのは当然ということになります。

参考

しかし上述の「答え」は現状の仕様がそうだと言っているだけです。

実際、空ディレクトリだけでも追加できる仕様に変えれば、大量の空ディレクトリがある場面などで便利なはずです。
そのような機能が未だに実装されていないのは何故なのでしょうか?

この疑問の答えは、Git 開発者のメーリングリストにありました。

ディレクトリを管理することの難しさ

「なぜ空ディレクトリを追加できる仕様にはできないのか?」という疑問に対しては、2007年7月18日のスレッドで Linus Torvalds 自身が様々な理由を挙げてくれていました。

その中でも、最も納得感のあった説明を以下で解説します。

<空ディレクトリを管理すると起こる問題の例>

まずは空ディレクトリ内に適当なファイルを作成し、それを ignore したとします。

中のファイルを ignore しているため、扱いは空ディレクトリと依然同じになることに注意してください。

そしてこの「空ディレクトリ」が Git 管理できると仮定し、コミットしたとします。

さて、この「空ディレクトリ」がコミットされる以前のブランチへ移動するとどうなるでしょうか?

このディレクトリは Git 管理しているのだから、当然削除されることになるでしょう。

しかしその中のファイルは Git 管理から外しているのだから、Git の状態に関わらず存在すべきです!

つまり削除すべきディレクトリの中に、残しておくべきファイルがあることになり矛盾します。

問題が起こる原因

なぜこのようなことが起きてしまうのでしょうか?
Linus は以下のように説明しています。

「ディレクトリを追跡すること」は「ファイルを追跡すること」と全く異なるのだと指摘しておきたい。

セマンティクスが完全に異なっているんだ。突き詰めると、ファイルを追跡する場合は常にその全ての内容を扱うが、ディレクトリの場合はその部分集合を扱うことになるという事実に帰着する。

ディレクトリはその「内容」の一部だけが untracked になる場合があるため、ファイルでは起こらなかった問題が発生するということですね。

まとめ

上の例以外にも、様々なケースで特別な考慮が必要になり、多くの改修が必要になりうることが指摘されています。

そして Linus 自身はあまりこの問題に関心がなく、空ディレクトリの管理が必要であれば自分でパッチを書くか、.gitignoreの配置で解決せよと勧めています。

要するに空ディレクトリを無視し続けているのは、
- ファイルの内容を追跡する現行の Git のデザインと噛み合わず、様々な仕様上の問題が発生する
- .gitkeep.gitignoreで解決する問題のため、大規模な改修を行うモチベーションが低い

という理由からだと言えそうです。