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
→ 親commit
commit
→ ルートtree
- ルート
tree
→tree
blob
といった具合に、芋づる式にオブジェクトを引き出せます。
リファレンスがないと、糸口になる 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
に記録されています。
(続きます)