Git
Chapters ▾ 2nd Edition

10.4 Gitの内側 - Packfile

Packfile

Gitリポジトリtestのオブジェクトデータベースに戻りましょう。 この時点で、オブジェクトは11個あります。内訳はブロブが4つ、ツリーが3つ、コミットが3つ、そしてタグが1つです。

$ find .git/objects -type f
.git/objects/01/55eb4229851634a0f03eb265b69f5a2d56f341 # tree 2
.git/objects/1a/410efbd13591db07496601ebc7a059dd55cfe9 # commit 3
.git/objects/1f/7a7a472abf3dd9643fd615f6da379c4acb3e3a # test.txt v2
.git/objects/3c/4e9cd789d88d8d89c1073707c3585e41b0e614 # tree 3
.git/objects/83/baae61804e65cc73a7201a7252750c76066a30 # test.txt v1
.git/objects/95/85191f37f7b0fb9444f35a9bf50de191beadc2 # tag
.git/objects/ca/c0cab538b970a37ea1e769cbbde608743bc96d # commit 2
.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4 # 'test content'
.git/objects/d8/329fc1cc938780ffdd9f94e0d364e0ea74f579 # tree 1
.git/objects/fa/49b077972391ad58037050f2a75f74e3671e92 # new.txt
.git/objects/fd/f4fc3344e67ab068f836878b6c4951e3b15f3d # commit 1

Gitはzlibを使用してこれらのファイルの内容を圧縮します。また、格納しているものもそれほど多くないため、これらすべてのファイルを集めても925バイトにしかなりません。 Gitの興味深い機能を実際に見てみるために、幾つか大きなコンテンツをリポジトリに追加してみましょう。 実例を示すために、Gritライブラリから repo.rb ファイルを追加します。これは約22Kバイトのソースコードファイルです。

$ curl https://raw.githubusercontent.com/mojombo/grit/master/lib/grit/repo.rb > repo.rb
$ git add repo.rb
$ git commit -m 'added repo.rb'
[master 484a592] added repo.rb
 3 files changed, 709 insertions(+), 2 deletions(-)
 delete mode 100644 bak/test.txt
 create mode 100644 repo.rb
 rewrite test.txt (100%)

結果のツリーを見ると、repo.rb ファイルに対応するブロブオブジェクトのSHA-1ハッシュ値が分かります。

$ git cat-file -p master^{tree}
100644 blob fa49b077972391ad58037050f2a75f74e3671e92      new.txt
100644 blob 033b4468fa6b2a9547a70d88d1bbe8bf3f9ed0d5      repo.rb
100644 blob e3f094f522629ae358806b17daf78246c27c007b      test.txt

それから、 git cat-file を使用すれば、そのオブジェクトの大きさも分かります。

$ git cat-file -s 033b4468fa6b2a9547a70d88d1bbe8bf3f9ed0d5
22044

ここで、このファイルに少し変更を加えて、何が起こるか見てみましょう。

$ echo '# testing' >> repo.rb
$ git commit -am 'modified repo a bit'
[master 2431da6] modified repo.rb a bit
 1 file changed, 1 insertion(+)

このコミットによって作られたツリーを見てみると、興味深いことがわかります。

$ git cat-file -p master^{tree}
100644 blob fa49b077972391ad58037050f2a75f74e3671e92      new.txt
100644 blob b042a60ef7dff760008df33cee372b945b6e884e      repo.rb
100644 blob e3f094f522629ae358806b17daf78246c27c007b      test.txt

repo.rb に対応するブロブが、別のブロブになっています。つまり、400行あるファイルの最後に1行だけ追加しただけなのに、Git はその新しいコンテンツを完全に新しいオブジェクトとして格納するのです。

$ git cat-file -s b042a60ef7dff760008df33cee372b945b6e884e
22054

これだと、ディスク上にほとんど同じ内容の22Kバイトのオブジェクトが2つあることになります。 もし、Gitが2つのうち1つは完全に格納し、2つめのオブジェクトは1つめとの差分のみを格納できたら、素晴らしいと思いませんか?

実は、それができるのです。 Gitが最初にディスク上にオブジェクトを格納する際のフォーマットは、 “緩い” (“loose”)オブジェクトフォーマットと呼ばれます。 一方、容量の節約と効率化のため、Gitはときどき、緩いフォーマットのオブジェクトの中の幾つかを1つのバイナリファイルにパックします。このバイナリファイルを “packfile” と呼びます。 あまりにたくさんの緩いオブジェクトがそこら中にあるときや、git gc コマンドを手動で実行したとき、または、リモートサーバーにプッシュしたときに、Gitはパック処理を行います。 何が起こるのかを知りたいなら、 git gc コマンドを呼べば、オブジェクトをパックするよう手動でGitに指示できます。

$ git gc
Counting objects: 18, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (14/14), done.
Writing objects: 100% (18/18), done.
Total 18 (delta 3), reused 0 (delta 0)

オブジェクトディレクトリの中を見ると、大半のオブジェクトは削除され、新しいファイルが2つ作られたことがわかります。

$ find .git/objects -type f
.git/objects/bd/9dbf5aae1a3862dd1526723246b20206e5fc37
.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4
.git/objects/info/packs
.git/objects/pack/pack-978e03944f5c581011e6998cd0e9e30000905586.idx
.git/objects/pack/pack-978e03944f5c581011e6998cd0e9e30000905586.pack

削除されずに残ったオブジェクトは、どのコミットからも指されていないブロブです。このケースでは、以前に作成した “what is up, doc?” の例のブロブと “test content” の例のブロブが該当します。 これらのブロブはどのコミットにも加えられなかったため、宙ぶらりんになっていると見なされ、新しいpackfileにはパックされません。

残りの2つのファイルが、新しく作られたpackfileとインデックスです。 packfileには、ファイルシステムから削除されたすべてのオブジェクトの内容が1つのファイルに含まれています。 インデックスには、特定のオブジェクトを速くシークできるように、packfile中でのオフセットが記録されています。 素晴らしいことに、gc を実行する前のディスク上のオブジェクトは合計で約22Kバイトあったのに対して、新しいパックファイルはたった7Kバイトになっています。 オブジェクトをパックすることで、ディスク使用量の⅔を削減できたのです。

Gitはどうやってこれを行うのでしょうか? Git はオブジェクトをパックするとき、似たような名前とサイズのファイルを探し出し、ファイルのあるバージョンから次のバージョンまでの差分のみを格納します。 packfileの中を見ることで、容量を節約するためにGitが何を行ったのかを知ることができます。 git verify-pack という配管コマンドを使用して、何がパックされているのか見ることができます。

$ git verify-pack -v .git/objects/pack/pack-978e03944f5c581011e6998cd0e9e30000905586.idx
2431da676938450a4d72e260db3bf7b0f587bbc1 commit 223 155 12
69bcdaff5328278ab1c0812ce0e07fa7d26a96d7 commit 214 152 167
80d02664cb23ed55b226516648c7ad5d0a3deb90 commit 214 145 319
43168a18b7613d1281e5560855a83eb8fde3d687 commit 213 146 464
092917823486a802e94d727c820a9024e14a1fc2 commit 214 146 610
702470739ce72005e2edff522fde85d52a65df9b commit 165 118 756
d368d0ac0678cbe6cce505be58126d3526706e54 tag    130 122 874
fe879577cb8cffcdf25441725141e310dd7d239b tree   136 136 996
d8329fc1cc938780ffdd9f94e0d364e0ea74f579 tree   36 46 1132
deef2e1b793907545e50a2ea2ddb5ba6c58c4506 tree   136 136 1178
d982c7cb2c2a972ee391a85da481fc1f9127a01d tree   6 17 1314 1 \
  deef2e1b793907545e50a2ea2ddb5ba6c58c4506
3c4e9cd789d88d8d89c1073707c3585e41b0e614 tree   8 19 1331 1 \
  deef2e1b793907545e50a2ea2ddb5ba6c58c4506
0155eb4229851634a0f03eb265b69f5a2d56f341 tree   71 76 1350
83baae61804e65cc73a7201a7252750c76066a30 blob   10 19 1426
fa49b077972391ad58037050f2a75f74e3671e92 blob   9 18 1445
b042a60ef7dff760008df33cee372b945b6e884e blob   22054 5799 1463
033b4468fa6b2a9547a70d88d1bbe8bf3f9ed0d5 blob   9 20 7262 1 \
  b042a60ef7dff760008df33cee372b945b6e884e
1f7a7a472abf3dd9643fd615f6da379c4acb3e3a blob   10 19 7282
non delta: 15 objects
chain length = 1: 3 objects
.git/objects/pack/pack-978e03944f5c581011e6998cd0e9e30000905586.pack: ok

ここで 033b4 というブロブを覚えているでしょうか。これは repo.rb ファイルの最初のバージョンですが、このブロブは2つ目のバージョンである b042a というブロブを参照しています。 出力の3つ目のカラムはpackfile中のオブジェクトのサイズを示しています。ここで、b042a はファイルのうち22Kバイトを占めていますが、033b4 はたったの9バイトしか占めていないことがわかります。 さらに興味深いのは、2つめのバージョンのファイルは元のままの状態で格納されているのに対して、最初のバージョンは増分として格納されていることです。これは、直近のバージョンのファイルほど、高速にアクセスしたいだろうというのが理由です。

この機能の本当に素晴らしいのは、いつでも再パックが可能なことです。 Git は時折データベースを自動的に再パックして、常に容量をより多く節約しようと努めますが、自分で git gc を実行すれば、いつでも手動で再パックを行えます。