ソフトウェアエンジニアの @DQNEO です。こんにちは。
Gitの内部構造を深掘りするシリーズ3回目です。
前回までのお話はこちら
今日はみんなだいすき「ステージング領域」の中身について解説してみます。
ステージング領域とは何か?
簡単に説明すると「次にコミットしたときにコンテンツとして登録されるもの」リストです。(別名「インデックス」ともいいます。) このリストは、 git add
やgit rm
したときに書き換わります。
(古くはcacheと呼ばれていました。内部実装やgit diff --cached
に今もその名残があります。)
git addのマニュアルに説明があります。
The “index” holds a snapshot of the content of the working tree, and it is this snapshot that is taken as the contents of the next commit.
(訳) 「インデックス」は、ワーキングツリーの内容のスナップショットを保持する。次回コミットしたときにコンテンツとして保存されるのがこのスナップショットである。
スナップショットとはもともと写真用語で「ある瞬間を切り取ったもの」という意味です。
コミット行為を「写真を撮る」行為にたとえると、ステージングするとは「物を撮影台(stage)の上に載せる」ようなイメージです。
差分ではなくスナップショット
「スナップショット」という言葉に違和感を覚えた人がいるかもしれません。
git diff --cached
やgit add -p
などステージングを扱うコマンドは差分を見せるものが多いので、「ステージング=差分」という感覚を持ってしまうのはある意味当然ではあります。
ですがステージング領域に登録されているのは実は差分ではなく、(差分適用後の)コンテンツそのものです。
ステージング領域をのぞいてみよう
では実際に、git add
するとどこにどういう形で保存されるのかを見てみましょう。
例として弊社のOSSである Dietcube というレポジトリをとりあげます。(レポジトリは何でもよいです)
$ git clone https://github.com/mercari/dietcube /tmp/dietcube $ cd /tmp/dietcube $ git tag 1.0.0 1.0.1 1.0.2 1.0.3 $ git checkout 1.0.0
ここで、README.md
ファイルに蛇足のようなHello Worldをつけたして、git add
してみましょう。
$ echo '蛇足のようなHello World' >> README.md $ git add README.md
差分を確認します。
$ git diff --cached diff --git a/README.md b/README.md index c5162f0..99040e7 100644 --- a/README.md +++ b/README.md @@ -42,3 +42,4 @@ Authors * @YuiSakamoto * @kajiken * @DQNEO +蛇足のようなHello World
さて、この1行追記された README.md
はどこにどのように格納されているでしょうか?
実は
.git/index
(インデックス).git/objects/
(レポジトリのデータベース)
という2種類の場所に書き込みが行われます。
.git/index
を見てみましょう。
$ cat .git/index DIRC/Xې:Xې:�����y[�����(��Y��M�+!a
おっとバイナリファイルでした。hexdumpしてみます。
$ hexdump -C .git/index | head -n 25 00000000 44 49 52 43 00 00 00 02 00 00 00 2f 58 db b5 09 |DIRC......./X...| 00000010 00 00 00 00 58 db b5 09 00 00 00 00 01 00 00 04 |....X...........| 00000020 01 fc b1 14 00 00 81 a4 00 00 01 f5 00 00 00 00 |................| 00000030 00 00 00 79 5b aa 86 d3 f2 b4 e6 28 b9 19 8f 0c |...y[......(....| 00000040 59 b0 a2 4d a5 2b 21 61 00 0a 2e 67 69 74 69 67 |Y..M.+!a...gitig| 00000050 6e 6f 72 65 00 00 00 00 00 00 00 00 58 db b5 09 |nore........X...| 00000060 00 00 00 00 58 db b5 09 00 00 00 00 01 00 00 04 |....X...........| 00000070 01 fc b1 15 00 00 81 a4 00 00 01 f5 00 00 00 00 |................| 00000080 00 00 00 de 4e b8 68 35 fe 5c 93 24 77 6d 26 ca |....N.h5.\.$wm&.| 00000090 e1 08 4c 0c aa b9 fe e7 00 07 2e 70 68 70 5f 63 |..L........php_c| 000000a0 73 00 00 00 58 db b5 15 00 00 00 00 58 db b5 15 |s...X.......X...| 000000b0 00 00 00 00 01 00 00 04 01 fc b1 5b 00 00 81 a4 |...........[....| 000000c0 00 00 01 f5 00 00 00 00 00 00 00 e2 33 c0 f6 1b |............3...| 000000d0 5a 46 b1 8e 37 c2 b5 24 22 93 bb 2a dc d1 d1 47 |ZF..7..$"..*...G| 000000e0 00 0b 2e 74 72 61 76 69 73 2e 79 6d 6c 00 00 00 |...travis.yml...| 000000f0 00 00 00 00 58 db b5 09 00 00 00 00 58 db b5 09 |....X.......X...| 00000100 00 00 00 00 01 00 00 04 01 fc b1 18 00 00 81 a4 |................| 00000110 00 00 01 f5 00 00 00 00 00 00 04 32 d0 bf fb f4 |...........2....| 00000120 b4 a4 d1 f0 23 5b 58 d3 6b 1a 38 07 c9 c0 9d 7f |....#[X.k.8.....| 00000130 00 07 4c 49 43 45 4e 53 45 00 00 00 58 db b5 1c |..LICENSE...X...| 00000140 00 00 00 00 58 db b5 1c 00 00 00 00 01 00 00 04 |....X...........| 00000150 01 fc b1 5c 00 00 81 a4 00 00 01 f5 00 00 00 00 |...\............| 00000160 00 00 02 62 99 04 0e 76 09 95 c5 4a 51 87 f4 13 |...b...v...JQ...| 00000170 8d d1 46 78 81 bf 87 32 00 09 52 45 41 44 4d 45 |..Fx...2..README| 00000180 2e 6d 64 00 58 db b5 15 00 00 00 00 58 db b5 15 |.md.X.......X...|
人間に読めないバイナリ列の中にところどころファイル名のようなものが見えます。
24行目あたりに README.md
というのが見えるでしょうか。これはバイナリでいうと 52 45 41 44 4d 45 2e 6d 64
で、左のバイナリ表示部に確かにそれがあります。
読めない部分にはいったい何の情報が書かれているのでしょうか?
ここは考えてもわからないので公式ドキュメントを漁ってみましょう。
git/index-format.txt at v2.12.0 · git/git · GitHub
ここにindexファイルのバイナリ仕様が書かれています。
簡単に説明すると、まず大きく「ヘッダ部」と「ボディ部」に別れます。
「ヘッダ部」は固定文字列”DIRC”、インデックスのバージョン番号、インデックス内のエントリの数が格納されています。
$ hexdump -C .git/index | head -n 1 00000000 44 49 52 43 00 00 00 02 00 00 00 2f 58 db b5 09 |DIRC......./X...|
hexdumpの結果の1行目を見ると、たしかに DIRC
(44 49 52 43
)で始まっています。
次の00 00 00 02
はこのindexファイルのフォーマットがバージョン2であることを示しています。(バージョンには2,3,4の3種類あります。今回は2に限定して解説します。)
「ボディ部」はエントリのリストで成り立っています。エントリというのはインデックスにあるファイルの情報のことで、具体的にはstat(2)の結果、SHA-1(後述)、パス名などを格納しています。
先程のhexdumpの出力の20-25行目あたりをよく見てみましょう。
00000130 00 07 4c 49 43 45 4e 53 45 00 00 00 58 db b5 1c |..LICENSE...X...| 00000140 00 00 00 00 58 db b5 1c 00 00 00 00 01 00 00 04 |....X...........| 00000150 01 fc b1 5c 00 00 81 a4 00 00 01 f5 00 00 00 00 |...\............| 00000160 00 00 02 62 99 04 0e 76 09 95 c5 4a 51 87 f4 13 |...b...v...JQ...| 00000170 8d d1 46 78 81 bf 87 32 00 09 52 45 41 44 4d 45 |..Fx...2..README| 00000180 2e 6d 64 00 58 db b5 15 00 00 00 00 58 db b5 15 |.md.X.......X...|
LICENSE
とREADME.md
が、エントリの名前(パス名)です。 LICENSE
(4c 49 43 53 45 4e 53 45
)の直後にゼロパディング(00 00 00
)があります。その後ろからREAMDE.md
までが 「README.md
のエントリ」になります。
エントリ部をとりだして見やすく改行してみます。
58 db b5 1c - ctime sec (2017/03/29 22:22:36) 00 00 00 00 - ctime nano sec 58 db b5 1c - mtime sec (2017/03/29 22:22:36) 00 00 00 00 - mtime nano sec 01 00 00 04 - dev 01 fc b1 5c - inode 00 00 81 a4 - mode 00 00 01 f5 - uid 00 00 00 00 - gid 00 00 02 62 - size (=610 bytes) 99 04 0e 76 09 95 c5 4a 51 87 f4 13 8d d1 46 78 81 bf 87 32 - SHA-1 00 09 - flag 52 45 41 44 4d 45 2e 6d 64 - path name (README.md)
ここに出てくるSHA-1とは何でしょうか?
実はこれは、エントリに割り振られた固有のIDです。ここではエントリの実体はファイルなので、これはBlob ObjectのIDということになります。(Blob Objectについては以前書いた下記記事をご参照ください。)
レポジトリデータベースである.git/object
の中をのぞくと、このSHA-1と同じ名前のファイルが実在することを確認できます。
これが、先ほどの git add
で保存されたコンテンツの中身です。
$ ls .git/objects/99/ -l total 4 -r--r--r-- 1 DQNEO wheel 393 3 29 22:22 040e760995c54a5187f4138dd1467881bf8732
ディレクトリ名 99
と ファイル名040e760....
をくっつけると 99040e760995c54a5187f4138dd1467881bf8732
という40文字の文字列になります。
先程のステージング情報(.git/index
)に書かれていたSHA-1と同じであることがわかります。
このBlob Objectの中身を表示してみましょう。git cat-file -p
を使います。
$ git cat-file -p 99040e760995c54a5187f4138dd1467881bf8732 Dietcube ========= Dietcube is the world super fly weight & flexible PHP framework. [中略] * @sotarok * @YuiSakamoto * @kajiken * @DQNEO 蛇足のようなHello World
README.mdファイルの全体が表示されました。
とまあこんな感じで、git add
すると差分ではなくてファイルが丸ごとレポジトリに格納されていることがわかりました。
.git/indexのパーサを書いてみよう
先程は .git/index
バイナリをhexdumpと気合で解読しましたが、もうちょっと賢い方法はないものでしょうか。
もちろんあります。git ls-files --stage
というコマンドを使うと .git/index
を解析して人の目に優しい形で表示してくれます。
が、それではつまらないのでプログラマたるもの自力でパーサを書いてみましょう。
Gitの実装を読んでみると、 indexファイルの解析処理はread-cache.c
というファイルのdo_read_index()
関数で行われています。
https://github.com/git/git/blob/v2.12.0/read-cache.c#L1568-L1629
一見複雑ですが、やってることの本質はシンプルで、
- ファイルをopen(2)
- mmap(2)でメモリ上に読み込み
- バイナリデータのヘッダー部分を解析
- ボディ部分(格納されているファイルエントリのリスト)をエントリ単位でループしつつ解析
という感じです。
C言語で130行ほど書けば自作パーサが作れてしまいます。
ソースコードをgistに置いておきました。
git index parser by C · GitHub
(低レイヤのビット演算処理はGit実装からコピーしてきたので全部自作ではないですが…汗)
これを使ってパースしてみましょう。
$ gcc -g -Wall -O0 -std=c99 -lz -o parse_git_index parse_git_index.c $ ./parse_git_index .git/index 100644 5baa86d3f2b4e628b9198f0c59b0a24da52b2161 0 .gitignore 100644 4eb86835fe5c9324776d26cae1084c0caab9fee7 0 .php_cs 100644 33c0f61b5a46b18e37c2b5242293bb2adcd1d147 0 .travis.yml 100644 d0bffbf4b4a4d1f0235b58d36b1a3807c9c09d7f 0 LICENSE 100644 99040e760995c54a5187f4138dd1467881bf8732 0 README.md 100644 f552a39a97905cf34932bcf9462b57b4451a161f 0 composer.json [中略] 100644 8d3ea9040dccf7bedd7e4a722e22465a0968df00 0 tests/RouterTest.php 100644 ebf2d4f1b31939a2a0142a9a7df40430e13b8a6e 0 tests/bootstrap.php
と、自作パーサでインデックスを解析してきれいに表示することができました。
いちおう自作パーサとgit ls-files --stage
の出力が同じであることを確認しておきます。
$ diff <(./parse_git_index .git/index) <(git ls-files --stage) $ echo $? 0
同じでした!
まとめ
git add
した時点でコンテンツがレポジトリに格納されることを確認しました。- ステージング領域を格納している
.git/index
ファイルのバイナリの読み方を解説しました。 - C言語でパーサを書いてみました。
Gitの中身を深掘りすると、C言語やバイナリ解析のよい練習になると思います。
みなさんも興味があったらぜひやってみてください!