Git の仕組み (2) - コミット・ブランチ・タグ

Git の仕組みシリーズの2回目です。目次がここにあります。

前回の記事では、Git オブジェクトとリファレンスが大きなツリー構造になっていることを説明しました。

f:id:koseki2:20140420183120p:plain

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

f:id:koseki2:20140420183844p:plain

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

f:id:koseki2:20140420185211p:plain

今回は commit オブジェクト、ブランチ、タグ、stash の仕組みについて説明します。

実際のデータが見たいときは、Git Object Browser にアクセスしてみてください。

f:id:koseki2:20140421034721g:plain

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 オブジェクトの ID
  • parent …… 0個以上の親 commit オブジェクトの ID
  • author, committer …… 変更・コミットしたユーザの名前、メールアドレス、変更日時
  • コミットメッセージ

などが書いてあります。

commit オブジェクトは、parent に書かれた ID でコミットツリーを形成します。

f:id:koseki2:20140611092500p:plain

tree オブジェクトとは逆に、commit オブジェクトは子が親を参照します。親は自分の子を知りません。

Git は、新しいコミットから古いコミットへとさかのぼり易く、その逆は難しい仕組みになっています。子が親を見つけるのは簡単ですが、全オブジェクトを読まない限り、親が全ての子を見つけることはできません。

Github を見ると、「前のコミット (parent)」にはリンクしていても、「次のコミット」へのリンクは見当たらないことがわかります。

f:id:koseki2:20140427181247p:plain

6. リファレンスとブランチ

Git を理解する上で、commit オブジェクトリファレンスを区別することは、とても大切です。

commit オブジェクトは、コミットツリーを形成し、一度作られたら決して変化せず、そう簡単には消えません。

一方、リファレンスは、コミットツリーの上を動き回る動的なデータで、頻繁に書き換えられ、不要になったら削除されます。

ブランチ

Git のブランチは、ブランチの先頭を指すリファレンスと、そのリファレンスから辿れる Git オブジェクトの集合で出来ています。

f:id:koseki2:20140611090743p:plain

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

f:id:koseki2:20140611090853p:plain

ブランチ先頭を指すリファレンス

ブランチの先頭を指すリファレンスは .git/refs/heads ディレクトリに保存されます。

たとえば master ブランチの先頭を指すリファレンスは、.git/refs/heads/master ファイルに保存されます。リファレンスには commit オブジェクトの ID が、テキストで書いてあります。

20baf26a647734601c3a02e20ee128bc26600c29

ブランチ先頭のリファレンスは、commitpull コマンドで書きかわります。また、reset コマンドを使って、自分で好きな位置に書き換えることができます。

f:id:koseki2:20140611101138p:plain

リファレンスからたどって、

  • リファレンス → commit
  • commit → 親 commit
  • commit → ルート tree
  • ルート treetree blob

といった具合に、芋づる式にオブジェクトを引き出せます。

f:id:koseki2:20140420172746p:plain

リファレンスがないと、糸口になる commit を見つけるのは難しくなってしまいます。Git オブジェクトは、外見からは区別が付かないからです。

リファレンスや他のオブジェクトから参照されなくなったオブジェクトは、2週間程度で削除候補になります。*1

参照されなくなったオブジェクトを削除したり、オブジェクトを `Pack` 形式にまとめ直す処理を gcガベージコレクション (ゴミ回収) と呼びます。`gc` は、リポジトリが乱雑になってくると自動で実行されます。また、$ git gc コマンドを使って、手動で実行することもできます。

HEAD リファレンス

今チェックアウトしているブランチは .git/HEAD という特別なリファレンスに記録されています。HEAD リファレンスは git checkout コマンドで書き換わります。

f:id:koseki2:20140611093204p:plain

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 を書き換えると、コミットはどこからも参照されなくなります。

f:id:koseki2:20140611093248p:plain

どこからも参照されないオブジェクトは、放っておくと、いずれガベージコレクションで消されてしまいます。

detached HEAD 中のコミットを残すには、他のブランチにマージしたり、新たにブランチを作成する必要があります。

7. 2種類のタグ

Git には2種類のタグがあります。軽量タグとアノテートタグです。

軽量タグは、コミットを直接参照するリファレンスです。ブランチと同じ、ただのリファレンスなので、名前以外の情報を持ちません。

アノテートタグは、tag オブジェクトを指すリファレンスです。tag オブジェクトは commit オブジェクトを参照します。tag オブジェクトには、tag を作った日付やユーザ名、コメントなどを含めることができます。

f:id:koseki2:20140611100731p:plain

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 に記録されています。

(続きます)

*1:記事公開当初、参照されなくなったオブジェクトは、2週間程度経つと Git が自動的に削除する、という風に説明していました。これは正しくありませんでした。一度作ったオブジェクトは、大抵はもっと長く残るようです。