Git の仕組み (2) - コミット・ブランチ・タグ
Git の仕組みシリーズの2回目です。目次がここにあります。
前回の記事では、Git オブジェクトとリファレンスが大きなツリー構造になっていることを説明しました。

また、Git オブジェクトがどのように記録されているか、

ファイルツリーの変更がルート tree オブジェクトの ID に反映される仕組みなどを見てきました。

今回は commit オブジェクト、ブランチ、タグ、stash の仕組みについて説明します。
実際のデータが見たいときは、Git Object Browser にアクセスしてみてください。

5. commit オブジェクト
先に説明した通り、Git オブジェクトデータベースには、複数のファイルツリーを保存できます。
個々のファイルツリーは、最上位 (ルート) にある tree オブジェクトの ID で区別することができます。ファイルツリーは、大抵の場合、過去のファイルツリーと tree blob オブジェクトを共有しています。
けれども、それらのファイルツリーがどんな順番で作られたかは、ファイルツリーを見てもわかりません。
例えば A と B の2つのファイルツリーがあったとして、A にファイルを追加して B になったのか、B からファイルを削除して A になったのかは、ファイルツリーからでは判別が付きません。
「いつ、だれが、なぜファイルツリーを変えたか」を管理するのは、 commit オブジェクトの役割です。commit オブジェクトは、次のようにテキストで書かれています。
tree 3db13f863b338b2dbaa12f564a2a63841396e59c parent 44c68b1afc3ae211b248a9385f89b5061cbf2f94 author KOSEKI Kengo <koseki@gmail.com> 1370766628 +0900 committer KOSEKI Kengo <koseki@gmail.com> 1370766628 +0900 ビスケットが増える重大なバグを修正した
commit オブジェクトには、
tree…… 1つのtreeオブジェクトの IDparent…… 0個以上の親commitオブジェクトの IDauthor, committer…… 変更・コミットしたユーザの名前、メールアドレス、変更日時- コミットメッセージ
などが書いてあります。
commit オブジェクトは、parent に書かれた ID でコミットツリーを形成します。

tree オブジェクトとは逆に、commit オブジェクトは子が親を参照します。親は自分の子を知りません。
Git は、新しいコミットから古いコミットへとさかのぼり易く、その逆は難しい仕組みになっています。子が親を見つけるのは簡単ですが、全オブジェクトを読まない限り、親が全ての子を見つけることはできません。
Github を見ると、「前のコミット (parent)」にはリンクしていても、「次のコミット」へのリンクは見当たらないことがわかります。

6. リファレンスとブランチ
Git を理解する上で、commit オブジェクトとリファレンスを区別することは、とても大切です。
commit オブジェクトは、コミットツリーを形成し、一度作られたら決して変化せず、そう簡単には消えません。
一方、リファレンスは、コミットツリーの上を動き回る動的なデータで、頻繁に書き換えられ、不要になったら削除されます。
ブランチ
Git のブランチは、ブランチの先頭を指すリファレンスと、そのリファレンスから辿れる Git オブジェクトの集合で出来ています。

リファレンスが指すのは、必ずしもコミットツリーの枝の先端とは限りません。リファレンスは、コミットツリーのどこか特定の位置を指します。ツリーの途中や根元が枝分かれしていることもあります。

ブランチ先頭を指すリファレンス
ブランチの先頭を指すリファレンスは .git/refs/heads ディレクトリに保存されます。
たとえば master ブランチの先頭を指すリファレンスは、.git/refs/heads/master ファイルに保存されます。リファレンスには commit オブジェクトの ID が、テキストで書いてあります。
20baf26a647734601c3a02e20ee128bc26600c29
ブランチ先頭のリファレンスは、commit や pull コマンドで書きかわります。また、reset コマンドを使って、自分で好きな位置に書き換えることができます。

リファレンスからたどって、
- リファレンス →
commit commit→ 親commitcommit→ ルートtree- ルート
tree→treeblob
といった具合に、芋づる式にオブジェクトを引き出せます。

リファレンスがないと、糸口になる commit を見つけるのは難しくなってしまいます。Git オブジェクトは、外見からは区別が付かないからです。
リファレンスや他のオブジェクトから参照されなくなったオブジェクトは、2週間程度で削除候補になります。*1
参照されなくなったオブジェクトを削除したり、オブジェクトを `Pack` 形式にまとめ直す処理を gc、ガベージコレクション (ゴミ回収) と呼びます。`gc` は、リポジトリが乱雑になってくると自動で実行されます。また、$ git gc コマンドを使って、手動で実行することもできます。
HEAD リファレンス
今チェックアウトしているブランチは .git/HEAD という特別なリファレンスに記録されています。HEAD リファレンスは git checkout コマンドで書き換わります。

HEAD は、通常は、リファレンスを参照するリファレンスです。
ref: refs/heads/master
ただし例外もあります。HEAD が、他のリファレンスではなく、直接 commit オブジェクトを参照している状態です。これを、detached HEAD (分離ヘッド) と呼びます。
detached HEAD
detached HEAD は、git checkout 時に commit オブジェクトの ID を指定したりすると発生します。例えば、
$ git checkout 20baf26
とすると、.git/HEAD ファイルには
20baf26a647734601c3a02e20ee128bc26600c29
と ID が書かれた状態になります。これが detached HEAD です。detached HEAD に入ると、
Note: checking out '20baf26'. You are in 'detached HEAD' state. You can look around, make experimental changes and commit them, and you can discard any commits you make in this state without impacting any branches by performing another checkout. If you want to create a new branch to retain commits you create, you may do so (now or later) by using -b with the checkout command again. Example: git checkout -b new_branch_name
といった警告がでます。
detached HEAD 状態でもコミットはできます。ただし、HEAD 以外にそのコミットを参照するリファレンスが無いので、別のブランチをチェックアウトして HEAD を書き換えると、コミットはどこからも参照されなくなります。

どこからも参照されないオブジェクトは、放っておくと、いずれガベージコレクションで消されてしまいます。
detached HEAD 中のコミットを残すには、他のブランチにマージしたり、新たにブランチを作成する必要があります。
7. 2種類のタグ
Git には2種類のタグがあります。軽量タグとアノテートタグです。
軽量タグは、コミットを直接参照するリファレンスです。ブランチと同じ、ただのリファレンスなので、名前以外の情報を持ちません。
アノテートタグは、tag オブジェクトを指すリファレンスです。tag オブジェクトは commit オブジェクトを参照します。tag オブジェクトには、tag を作った日付やユーザ名、コメントなどを含めることができます。

8. 一時待避 (stash)
Git には、まだコミットしていないファイルを一時的に退避させる git stash というコマンドがあります。
git stash は毎回 2つの commit オブジェクトを作成します。
- 現在の
indexを保存するcommitオブジェクト (親は HEAD) - 作業ディレクトリを保存する
commitオブジェクト (親は上のコミット)
の2つです。
stash を index に反映するには git stash pop --index のように --index オプションを指定します。
2つ作られた commit オブジェクトの内、後者を指すリファレンスが .git/refs/stash ファイルに保存されます。
.git/refs/stash に記録されるのは、一番最近の stash だけです。昔の stash は、ログ .git/logs/refs/stash に記録されています。
(続きます)