CH 17 / 75第3部 Gitの内部構造 ── なぜそう動くのか

4つのオブジェクト(blob / tree / commit / tag)

このページで分かること

  • Git が保存しているのは4種類のオブジェクトだけだと分かる
  • blob・tree・commit・tag がそれぞれ何を持っているか説明できる
  • git cat-file でオブジェクトの中身を開けて確かめられる
  • ID が中身から計算されることと、その意味が分かる
このページの目次12 items

第3部の16で、コミットは「その時点の全体の写真」だと見ました。このページでは、その写真を分解します。.git の中に保存されているものは、突き詰めると4種類の部品しかありません。Git ではこの部品をオブジェクト(object)と呼びます。

4つのオブジェクトの関係#

まず、フォルダの中にフォルダがある状態を作って記録します。

text5 lines
cd ~/git-practice
mkdir recipes
echo "卵 2個" > recipes/cake.txt
git add recipes
git commit -m "ケーキのレシピを追加"

このコミットを分解すると、次のような入れ子になっています。

tag が commit を、commit が tree を、tree が blob と recipes の tree を包む入れ子の図。ファイル名は tree が持ち、blob は中身だけを持つ

外側から内側へ、「何を記録したか」から「ファイルの中身」へと細かくなります。

オブジェクト 持っているもの 対応するもの
blob ファイルの中身だけ ファイル
tree ファイル名・フォルダ名と、それぞれの中身のID フォルダ
commit いちばん上の tree のID、1つ前の commit のID、記録した人・日時・メッセージ 1回の記録
tag 指す commit のID、名前、作った人・メッセージ 記録に付けた目印

図は入れ子で描いていますが、実際には中に入っているのではなく、相手のIDを書いて指しているだけです。以下で1つずつ開けて確かめます。

オブジェクトを開ける:git cat-file#

git cat-file は、オブジェクトの中身を表示するコマンドです。-t で種類を、-p で中身を表示します。

commit#

text14 lines
git cat-file -t HEAD

# 出力:
# commit

git cat-file -p HEAD

# 出力:
# tree e61b7af4857312e4b55eaa25dd4c4e0cd7a03f2b
# parent 29c43994091deca933abfcfca9795596789675ee
# author Taro Yamada <taro@example.com> 1792630800 +0900
# committer Taro Yamada <taro@example.com> 1792630800 +0900
#
# ケーキのレシピを追加

commit が持っているのはこれだけです。ファイルの中身は1つも入っていません。 tree の行が「このときの全体の写真」を、parent の行が「1つ前の commit」を指しています。1792630800 は日時を秒数で表したものです。

tree#

text7 lines
git cat-file -p 'HEAD^{tree}'

# 出力:
# 100644 blob 64442fd642ae90b9c4be6583fa758d8b2e884f66  .gitignore
# 040000 tree 9687daeb9b93472bfcd466d61e6d8c3acef4e967  recipes
# 100644 blob a2a4dd6683d9012c00d693af6c5b1f02f225a091  shopping.txt
# 100644 blob d3038db570e4c542737f1ab3a559741e5af3628f  todo.txt

HEAD^{tree} は「HEAD のコミットが指す tree」という指定です(シェルが記号を誤解しないよう、引用符で囲んでいます)。第3部の16の git ls-tree と同じ一覧で、recipes フォルダが tree として入っていることが分かります。先頭の 100644 は普通のファイル、040000 はフォルダを表す印です。

text4 lines
git cat-file -p HEAD:recipes

# 出力:
# 100644 blob c0fa007ec11499026d24c4028de6083e274444fc  cake.txt

HEAD:recipes は「HEAD のコミットの recipes」という指定です。フォルダの中のフォルダは、tree の中の tree になっています。

blob#

text6 lines
git cat-file -p HEAD:shopping.txt

# 出力:
# 豆乳を買う
# 卵を買う
# パンを買う

blob はファイルの中身そのものです。ファイル名はどこにも書かれていません。 名前は tree が持っています。だから第2部の13で名前を変えたとき、中身の blob はそのままで、tree の中の名前だけが変わりました。

ID は中身から計算される#

ここまでに出てきた長い英数字は、すべてオブジェクトのハッシュ(ID)です。Git は、オブジェクトの中身から SHA-1 という計算方法で40桁の値を求め、それを ID にしています。

中身が同じなら、誰がいつ計算しても同じ値になります。試しに、第2部の08で最初に作った memo.txt と同じ中身から ID を計算してみます。

text9 lines
echo "牛乳を買う" | git hash-object --stdin

# 出力:
# 4d4d7608c63e573f12c7f6bb19bf03345608a004

git cat-file -p 4d4d760

# 出力:
# 牛乳を買う

git hash-object は「この中身なら、ID はいくつになるか」を計算するだけのコマンドです。出てきた ID を git cat-file に渡すと、第2部の08で記録した中身が出てきます。手元でも同じ 4d4d760… になるはずです。

ここから、Git の性質がいくつも説明できます。

  • 同じ中身は1つしか保存されない(第3部の16):ID が同じなら同じオブジェクトだから
  • 記録が壊れると気づける:中身が1文字でも変われば ID が合わなくなる
  • commit の ID は手元ごとに違う:commit の中身には記録した人と日時が入っているから

保存場所#

オブジェクトは、.git/objects の中に、ID の先頭2文字のフォルダと残り38文字のファイル名で保存されます。

text4 lines
ls .git/objects/a2/

# 出力:
# a4dd6683d9012c00d693af6c5b1f02f225a091

shopping.txt の blob a2a4dd6… です。中身は圧縮されているので、cat で開いても読めません。読むときは git cat-file を使います。

tag:記録に付ける目印#

4つ目の tag は、特定のコミットに名前を付けて目印にするオブジェクトです。

text10 lines
git tag -a v1.0 -m "最初の区切り"
git cat-file -p v1.0

# 出力:
# object b505e8e8f92abb8142a6ca486600de9100f98e42
# type commit
# tag v1.0
# tagger Taro Yamada <taro@example.com> 1792630800 +0900
#
# 最初の区切り

object の行が、目印を付けた commit を指しています。タグの使い方は第5部の30で扱います。

矢印の向きについて#

commit は parent で1つ前の commit を指しています。つまり、記録の上では新しいものが古いものを指す、時間と逆向きのつながりです。新しい commit を作るとき、Git が知っているのは「今の commit」だけなので、新しいほうに「親はこれ」と書くしかないためです。

このガイドの図では、コミットを左が古く右が新しい順に並べ、親を指す矢印はあえて描いていません。矢印を描くと、時間の流れと逆向きに読んでしまいやすいためです。図の線は「このコミットの次がこれ」という並びだけを表している、と読んでください。

よくある疑問#

Q. 空のフォルダが記録できないのはなぜですか?
A. tree は「中にあるファイルとフォルダの一覧」なので、中身が空だと tree として書く内容がないからです。第2部の12で触れた .gitkeep は、この仕組みに合わせた工夫です。

まとめ#

  • Git が保存しているのは blob・tree・commit・tag の4種類のオブジェクトだけ
  • blob はファイルの中身だけ、tree はファイル名と中身のIDの一覧を持つ
  • commit はいちばん上の tree と1つ前の commit を指し、記録した人・日時・メッセージを持つ
  • ID は中身から計算されるので、同じ中身は同じ ID になる
  • commit は1つ前の commit を指している。このガイドの図では、時間の流れに合わせて左から右に並べる

次のページ#

18. HEAD とは何か・detached HEAD ── 第2部で何度も使った HEAD の正体と、「今どこにいるか」が分からなくなる状態を見ます。