概要
以下の用語について、どのようなものなのかを理解するためのハンズオンです。
- Gitオブジェクト
- blobオブジェクト
- treeオブジェクト
- commitオブジェクト
Gitオブジェクト
git add や git commit した時、「blobオブジェクト」「treeオブジェクト」「commitオブジェクト」が作成されます。
Gitではこれらのファイルを「Gitオブジェクト」と呼んでいます。Gitオブジェクトは「.git/objects」ディレクトリの下に保存されます。
blobオブジェクト
blobオブジェクトのファイル名はハッシュIDになります。
このハッシュIDは、ヘッダー(ファイル内容の文字数など、ファイルのメタ情報)とファイル内容を、SHA-1というハッシュ関数で40文字の英数字に変換したものです。ハッシュIDのうち、先頭2文字をディレクトリ名に、残り38文字をファイル名にして保存します。
それでは実際にどのようなファイル名になるのか、確認してみましょう。
$ mkdir sample
$ cd sample
$ git init
$ echo 'Hello, world!' > hello.txt
# greetingのハッシュIDを表示します
$ git hash-object hello.txt
af5626b4a114abcb82d63db7c8082c3c4756e51b
このようにハッシュIDは、「af5626b4a114abcb82d63db7c8082c3c4756e51b」という40文字の英数字になります。
次に git add してblobオブジェクトを作成してみましょう。
# git add することでblobオブジェクトを作成します
$ git add hello.txt
# .git以下のファイル構造を表示します。以下は今回関係している部分だけを抜粋
$ tree .git
.git
|-- objects
|-- af
`-- 5626b4a114abcb82d63db7c8082c3c4756e51b
blobオブジェクトは「.git/objects/af/5626b4a114abcb82d63db7c8082c3c4756e51b」として保存されています。
ここで重要なことは、ハッシュIDというのは、ファイルの中身に対して一意になるということです。中身が同じファイルであれば必ず同じハッシュIDになります。そのため、ファイルの中身が同じであれば git add しても追加で圧縮ファイルが作られることはありませんし、ファイルの中身に変更があれば git add すると別の圧縮ファイルが作成されます。
treeオブジェクト
blobオブジェクトは、新規作成や変更したファイルの中身を圧縮したものを保存していて、ファイル名もファイルの中身をベースにハッシュ関数で作成されたものでした。つまり、blobオブジェクトにはもともとのファイル名の情報がどこにも残っていないことになります。
そこで、ファイル名とファイルの中身の組み合わせ(ファイル構造)を保存するためにあるのがtreeオブジェクトです。コミットをするとtreeオブジェクトが作成されます。
それでは実際に確認してみましょう。
なお、Gitオブジェクトの中身を確認するにはgit cat-file -p <オブジェクト名> (オブジェクト名はGitオブジェクトのハッシュIDやブランチ名など。詳細は公式ドキュメントのSPECIFYING REVISIONSを参照)コマンドを使用します。
# コミットしてtreeオブジェクトを作成します
# -m オプションを付けることでエディタを立ち上げずにコミットできます
$ git commit -m 'add hello.txt'
[master (root-commit) 8b1643e] add hello.txt
1 file changed, 1 insertion(+)
create mode 100644 hello.txt
# master ブランチ上での最後のコミットが指しているtreeオブジェクトの中身を表示します
$ git cat-file -p master^{tree}
100644 blob af5626b4a114abcb82d63db7c8082c3c4756e51b hello.txt
最後のコミットが指しているtreeには、blobオブジェクト「af5626b4a114abcb82d63db7c8082c3c4756e51b」が hello.txt というファイル名だ、ということが保存されています。
ではここで、ディレクトリを追加してコミットすると何が起こるでしょうか。
$ mkdir subdir
# subdir ディレクトリの下に goodmorning というファイルを作成します
$ echo 'Goodmorning!' > subdir/goodmorning.txt
$ git add subdir
$ git commit -m 'add subdir'
[master a86afd5] add subdir
1 file changed, 1 insertion(+)
create mode 100644 subdir/goodmorning.txt
# ツリーファイルのIDを取得するために、最後のコミットの中身を表示します
# git cat-file -p master^{tree} コマンドでも大丈夫です
$ git cat-file -p HEAD
tree 81e240d8d3c9082b4cd9254344b29561fb7b0b51
parent 8b1643ee865a1ea2a3d62809337cde926cc709c2
author knwldg <knwldg@gmail.com> 1555420948 +0900
committer knwldg <knwldg@gmail.com> 1555420948 +0900
add subdir
# ツリーファイルの先頭の文字を指定して、ツリーファイルの中身を表示します
$ git cat-file -p 81e240
100644 blob af5626b4a114abcb82d63db7c8082c3c4756e51b hello.txt
040000 tree 46394df7f2e6c48aacdafbbb887540dbcf936c9b subdir
blogオブジェクトに関してはさっきと同じです。そこに、treeオブジェクト「46394df7f2e6c48aacdafbbb887540dbcf936c9b」のツリー名は subdir だよ、というのが追加されています。
ここが注目ポイントで、ツリーファイルの中にツリーファイルが含まれているんですね。このように、ツリーファイルは一つのディレクトリに対応していて、ツリーファイルの中にツリーファイルと圧縮ファイルが含まれるようになっています。
一応 subdir のツリーファイルの中身も確認しておきましょう。
# ツリーファイルの先頭の文字を指定して、ツリーファイルの中身を表示します
$ git cat-file -p 46394d
100644 blob fa476f276a6fa984a789416f63f925e999834081 goodmorning.txt
subdir ディレクトリには blobオブジェクト「fa476f276a6fa984a789416f63f925e999834081」がgoodmorning.txt というファイル名で保存されています。
ここまでを振り替えると、一つのファイルにblobオブジェクトが対応していて(※)、一つのディレクトリに一つのtreeオブジェクトが対応していることがわかります。treeオブジェクトは構造や名前を持たないblobオブジェクトに構造を与えるためのもので、blobオブジェクトやtreeオブジェクトを保存しているのです。
※ ファイルの中身が同じでファイル名が違う場合、圧縮ファイルはファイルの中身をベースに作成されるため、圧縮ファイルは同じものになります。
commitオブジェクト
treeオブジェクトが作成されたことで、ファイルの構造がわかるようになりました。しかしまだ、いつ、誰が、何を、何のために変更したのかということがわかりません。
そこで、その情報を保存するためにあるのがcommitオブジェクトです。
早速コミットファイルの中身を確認してみましょう。
# 最新のコミットファイルの中身を表示します
$ git cat-file -p HEAD
tree 81e240d8d3c9082b4cd9254344b29561fb7b0b51
parent 8b1643ee865a1ea2a3d62809337cde926cc709c2
author knwldg <knwldg@gmail.com> 1555420948 +0900
committer knwldg <knwldg@gmail.com> 1555420948 +0900
add subdir
まず、コミットした時点のtree「81e240d8d3c9082b4cd9254344b29561fb7b0b51」が保存されています。これはこのプロジェクトの一番上のディレクトリのtreeオブジェクトになります。一番上の階層のツリーをcommitオブジェクトに保存することで、コミットした時点でのスナップショットを記録しています。
次がparent、親コミットを保存しています。親コミットは「8b1643ee865a1ea2a3d62809337cde926cc709c2」です。Gitはこのように親コミットを保存することでコミットの履歴を辿れるようにしているんでしたね。
あとは作成者の名前とメールアドレス、改行、コミットメッセージと続きます。これで、変更者と変更理由がわかります。
まとめ
Gitは変更履歴を保存する時、blobオブジェクト、treeオブジェクト、commitオブジェクトという形でスナップショットを記録しています。
Gitの実体は基本的にはこれだけです。とてもシンプルですね。
Gitのコマンドは、この3つのGitオブジェクトに対して何らかの操作をしているだけです。
これから色々なコマンドを学んでいく際は、コマンドを闇雲に覚えるのではなく、このデータ構造に対してどういう操作をしているコマンドなのかということをイメージすれば、Gitが実際どのようなことをしているかがわかると思います。