Gitの内部データ構造をGraphvizで描画してみた 第4回 ワークツリーとインデックスとblob


解決すべき問題

git addコマンドやgit commitコマンドを実行したときGitレポジトリのなかで何がおきているのだろう?Gitの内部のデータ構造(ワークツリーとインデックスとcommit/tree/blobオブジェクト)が変化するらしいが、モノが動く様子を見たことはない。図で説明してほしい。

解決方法

Pythonでツール kazurayam/visualize_git_repository.py を開発した。これを使えばいま自分の手元にあるプロジェクトの .git ディレクトリのなかにあるオブジェクト群の実物を読み出し、Graphvizでグラフを生成してPNG画像ファイルを出力することができる。

説明

デモ用にディレクトリを作りファイルを3つ作ろう。git initコマンドでGitレポジトリを作ろう。git addしたらインデクスが更新されblobオブジェクトが作られる。そしてgit commitしたらblobがレポジトリに登録される。このときGitレポジトリの中で何が起きているのだろうか?Graphvizで図示してみよう。

おまけに応用問題をひとつ。ファイルを修正してgit addしたあと、git commitせずに続けてもう一度ファイルを修正してgit addしたとしよう。Gitレポジトリの中で何が起きるのだろうか? Graphvizで図示してみよう。

ステップ1 git initした後でgit addする前

デモ用のディレクトリを作ろう。このディレクトリのことを以下で $project という記号で表すことにします。

% mkdir $project
% cd $project

このディレクトリのなかにファイルを3つ作ろう。

% echo '*~' > .gitignore
% echo '#Read me plase' > README.md
% mkdir src
% echo 'print("How do you do?");' > src/greeting.pl

このディレクトリにGitレポジトリを作ろう。

% git init

シェルコマンド ls$projectディレクトリの内容を確認しよう。

% ls -la .
total 16
drwxr-xr-x  6 kazuakiurayama  staff  192  6 12 12:58 .
drwxr-xr-x  4 kazuakiurayama  staff  128  6 12 12:58 ..
drwxr-xr-x  7 kazuakiurayama  staff  224  6 12 12:58 .git
-rw-r--r--  1 kazuakiurayama  staff    3  6 12 12:58 .gitignore
-rw-r--r--  1 kazuakiurayama  staff   17  6 12 12:58 README.md
drwxr-xr-x  3 kazuakiurayama  staff   96  6 12 12:58 src

$project/.gitというディレクトリができている。.gitディレクトリがいわゆる「Gitレポジトリ」の実体だ。

.gitディレクトリの中に何があるのだろうか?lsコマンドでみてみよう。

% ls -la ./.git
total 16
drwxr-xr-x  7 kazuakiurayama  staff  224  6 12 12:58 .
drwxr-xr-x  6 kazuakiurayama  staff  192  6 12 12:58 ..
-rw-r--r--  1 kazuakiurayama  staff   23  6 12 12:58 HEAD
-rw-r--r--  1 kazuakiurayama  staff  137  6 12 12:58 config
drwxr-xr-x  5 kazuakiurayama  staff  160  6 12 12:58 hooks
drwxr-xr-x  4 kazuakiurayama  staff  128  6 12 12:58 objects
drwxr-xr-x  4 kazuakiurayama  staff  128  6 12 12:58 refs

$project/.git/objectsというディレクトリができている。このなかにcommitオブジェクト、treeオブジェクト、blobオブジェクトのファイルが作られて保存される。

この時点でvisualize_git_repositoryツールを実行したら次のグラフが生成された。

このグラフから次のことが読みとれる。

  1. Git用語「インデックス」とは具体的には $project/.git/index という名前のバイナリファイル1個である。ところがこの段階ではgit initした直後なのでindexファイルはできていない。indexファイルはgit addコマンドで作られるのだ。

  2. $project/.git/objectsディレクトリはできているがまだ中身が空っぽだ。git initした直後なので無理もない。

ステップ2 git addしたらインデックスとblobが更新された

git addコマンドを実行しよう。何が起こるだろうか?

% git add

この時点でvisualize_git_repositoryツールを実行したら次のグラフが生成された。

このグラフから次のことが読みとれる。

  1. 3つのファイルに対応するblobオブジェクトが.git/objectsディレクトリの中にひとつづつ計3個できた。

  2. インデックスが作られた。インデックスの中には3行あって、ファイルのパスとそれに対応するblobオブジェクトのhash値が記録されている。

  3. ワークツリーのなかにsrcというディレクトリがある。ところがインデックスのなかにはディレクトリに対応する行が無い。ワークツリーのなかのファイルに相当する行だけがインデックスのなかにある。サブディレクトリの下にあるファイルのパス文字列としてサブディレクトリがあることが示唆されている。たとえばsrc/greeting.plのように。

  4. ワークツリーのなかにsrcというディレクトリがある。ところが.git/objectsのなかにはそれに対応するモノがない。

git addコマンドは2つの仕事をするのだ。第一にワークツリーにあるファイルを変換してblobオブジェクトを作りだすこと。第二にインデックスを更新すること。

git addコマンドはtreeオブジェクトを作らない。treeオブジェクトを作りだすのはgit commitコマンドの役割なのだ。

ステップ3 git commitしたらblobがツリーにつながった

さあ、コミットしよう。

% git commit -m "initial commit"

この時点でvisualize_git_repositoryツールを実行したら次のグラフが生成された。

このグラフから次のことが読みとれる。

  1. .git/objectsディレクトリのなかにcommitオブジェクトとtreeオブジェクトができた。commitからtreeへ線がつながり、treeからblobへ線がつながった。追加した3つのファイルのblobオブジェクトはひとつ残らずcommitから参照可能な形になった。

  2. ワークツリーのsrc ディレクトリに相当する行がインデックスのなかには無かった。ところが.git/objectsディレクトリのなかにはsrcに相当するtreeオブジェクトができている。git commitコマンドが実行されたとき、インデックスに記録されていたsrc/greeting.plというファイルパス文字列に基づいてtreeオブジェクトが生成されたのだ。

  3. インデックスの内容に変化は無い。git commitコマンドがインデックスを変更しないことがわかる

ステップ4 TODO.txtファイルを追加してgit addする前

ワークツリーにsrc/TODO.txtファイルを追加しよう。

% mkdir doc
% echo 'Sleep well tonight.' > doc/TODO.txt

まだ git add しない。この時点でvisualize_git_repositoryツールを実行したら次のグラフが生成された。

このグラフから次のことが読みとれる。

  1. ワークツリーでファイルをどんなにいじってもgit addしないうちはGitレポジトリになんら影響を及ぼさない。

ステップ5 git addしたらインデックスとblobが更新された

ではgit addしよう。

% git add .

この時点でvisualize_git_repositoryツールを実行したら次のグラフが生成された。

このグラフから次のことが読みとれる。

  1. ワークツリーに追加されたsrc/TODO.txtファイルに対応するblobオブジェクトが作られた。

  2. そのblobがインデックスに含まれるようになった。

  3. addされたblobオブジェクトはtreeオブジェクトと線でつながっていない。未完だ。

上記の図を見てあなたは「commitとtreeとblobがたくさんあって線で繋がっているがごちゃごちゃして見づらい」と思ったかもしれません。別の記事ではもっときれいないツリー形の図でcommitとtreeとblobからなるグラフを紹介しています。しかし本稿ではcommitとtreeとblobをツリーの形状で示すのをあえて避けました。本稿ではaddされたblobオブジェクトがtreeと線で繋がっていない状態になるということを示すことが重要で、commitとtreeとblobから成るツリーの詳細を示すことは重要でなかったからです。

ステップ6 git commitしたらblobがツリーにつながった

ではgit commitしよう。

% git commit -m "add src/TODO.txt"

この時点でvisualize_git_repositoryツールを実行したら次のグラフが生成された。

このグラフから次のことが読みとれる。

  1. doc/TODO.txtファイルに対応するblobオブジェクトがtreeオブジェクトと線でつながって、commitオブジェクトからたどれるようになった。「ファイルをコミットする」というのはこういうグラフの形状の変化を意味するのだ。

これでひと仕事済んだ。

ステップ7 READMEファイルを修正してgit addした

ワークツリーにすでにあるREADME.mdファイルを修正しgit addしよう。Gitレポジトリのなかがどう変化するだろうか?

まずREDME.mdファイルを修正しよう。

% echo 'Read me more carefully' > README.md

git addしよう。

% git add .

この時点でvisualize_git_repositoryツールを実行したら次のグラフが生成された。

このグラフから次のことが読みとれる。

  1. 修正した後のREADME.mdファイルに対応するblobオブジェクト(hash=5a79541)が追加された。

  2. インデックスをみるとREADME.mdファイルのhashは新しいblobオブジェクトのhashに交換されている。

  3. 修正される前のREADME.mdファイルに対応するblobオブジェクト(hash=aadb69a)は.git/objectsディレクトリの中に残っている。いったんcommitされたblobは原則的に削除されないのだ。このblobは過去のcommitオブジェクトからリンクされていて履歴として参照可能だ。

ステップ8 READMEファイルをもう一度修正してgit addした

READMEファイルを修正してgit addしたあと、git commitせずに、もう一度READMEファイルを修正してgit addしよう。インデックスやblobオブジェクトがどういう状態になるのだろうか?

READMEファイルを修正しよう。

% echo 'I know you didnt read me.' > README.md

addしよう。

% git add .

この時点でvisualize_git_repositoryツールを実行したら次のグラフが生成された。

このグラフから次のことが読みとれる。

  1. 修正されたREADME.mdファイルに対応する新しいblobオブジェクト(hash=9230643)ができた。インデックスが新しいblobを参照するように更新された。
  2. 前回git addしたとき作られたblobオブジェクト(hash=5a79541)は.git/objectsディレクトリに残っている。ただしこのblobは完全に孤立している。すなわちtreeオブジェクトと線でつながっていないし、インデックスとのつながりも無くなってしまった。

ステップ9 READMEファイルをgit commitした

コミットして締めくくろう。

% git commit -m "modified README.md"`

この時点でvisualize_git_repositoryツールを実行したら次のグラフが生成された。

このグラフから次のことが読みとれる。

  1. commitオブジェクトがひとつ増えた。最新のcommitオブジェクトから線が伸びて最新状態のREADME.mdファイルのblobが参照される形になった。

  2. addされたがcommitされなかった孤立したblobオブジェクト(hash=5a79541)は放置されている。

三度目のコミットが完了した。めでたしめでたし。

ゴミ掃除

孤立したblobオブジェクトはこの先どうなる運命なのだろう? じつはGitはGarbage Collectionを備えていて、適切なタイミングで自動的にゴミを掃除してくれる。

ここでは深堀りしません。

ツールについて

本稿で示したPNG画像は自作のツール visualize_git_repository で描画した。このツールはPython言語で開発した。ソースコードは下記のGitHubレポジトリにある。

このツールは下記2つのライブラリを利用している。

PNG画像を生成するにはコマンドラインで下記の操作をする。

$ cd $visualize_git_repository
$ pytest -s kazurayam/visualize_git_repository.py::test_4_index

上記の例を作るのにどういうgitコマンドを実行したのかを知りたいならプログラムのソースコードを読み解いてください。下記を入り口として解読してください。

まとめ

git addとやったとき、git commitとやったときGitレポジトリの中でデータがどのように変化していくのかをクリアに図示することができたとおもいます。

具体的で細かい情報満載のグラフを9個作った。作画ツールで矩形や曲線を手書きするやりかたでは9個も作れなかった。gitコマンドでGitレポジトリの状態を読み出してDOT言語に変換しGraphvizで図を描くというやり方だからこそできた。

  • author: kazurayam
  • date: June, 2021

連作の目次