gitはどう動くのか: コミットオブジェクト周辺の話
私がgitを使いだしたのはgit入門(濱野2009)を読んでからなんですが、これが非常によかった。何のために用意された機能なのか/どのような仕組みで動いているのか、その根っこのところがきちんと解説されているので各種コマンドがどのような意味を持つのかすんなり理解できた。分散VCSは複雑そうで敬遠していたのだが、gitは構造がシンプルで直感的なので原理さえ理解すればsvnより容易に使いこなせる(というかsvnのアーキテクチャについてはいまだに理解できてない……。どこかにいい入門書ないだろうか)。
gitのアーキテクチャについて、自分なりの理解をまとめてみようと思う。
gitはどのようにリポジトリを管理しているか
- オブジェクトとは、gitがデータを扱う単位。コミット、ファイルの内容などはオブジェクトとして表される
- オブジェクトは内容に応じた一意のIDを持つ(内容のハッシュ値がIDになる)
- 一回のコミットはひとつのオブジェクトとして管理される(コミットオブジェクト)
- コミットオブジェクトは次の内容を含む
- コミットログ
- プロジェクトの内容(ディレクトリとファイルの内容)を表すオブジェクトへのポインタ
- 親となるコミットへのポインタ
- コミットオブジェクトは通常ひとつの親を持つ。マージの際は複数の親を持つこともある
- ブランチ、タグとは特定のコミットオブジェクトを指し示すポインタである
コミットオブジェクトはネットワーク状(有向非循環グラフ)につながっている。
それぞれのコミットは親のリストを保持している。親は通常ひとつだが、マージのときは複数になることも(コミットfはeとgをマージしたもので、両方を親にもつ)
親 <----------------------> 子 a ---- b ---- c ---- e ---- f | | | `----- d -----' | | | `----- g -----'
ブランチはコミットオブジェクトを指すポインタである
a ---- b ---- c <-master | `---- d <-experimental
単なるポインタなので、ブランチを削除しても即データ(=コミットオブジェクト)が失われることはない。
コミットオブジェクト操作の例
プロジェクトを作った
$ git init $ edit; edit; edit $ git commit -m 'initial commit'
masterブランチ(デフォルトのブランチ。svnで言うtrunk相当)は最初のコミット(i)を指している
i <-master
なんかコードを書いてコミット(a)
$ edit src.c $ git add src.c $ git commit -m 'commit a'
コミットiを親とするコミットオブジェクトaが作成される。masterブランチはコミットaを指す。
i ---- a <-master
新機能を追加してコミット(b)。aを親とするコミットオブジェクトbが作成され、masterはbを指す。
i ---- a ---- b <-master
実験的な機能のためにmasterブランチからexperimentalブランチを作る。
$ git branch * master $ git checkout -b experimental master $ git branch master * experimental
ブランチを作った直後は両方ともコミットbを指している。
i ---- a ---- b <-master ^-experimental
experimentalブランチ上で作業、experimentalな機能を追加してコミットした(c)。
$ git branch master * experimental $ edit; edit; edit $ git commit -m 'commit c'
experimentalブランチはコミットcを指す。masterは変化なし。
i ---- a ---- b <-master | `---- c ^-experimental
実験ブランチからmasterブランチに移動して機能を追加した(d)
$ git checkout master $ edit; edit; edit $ git commit -m 'commmit d'
masterはdを指す。
i ---- a ---- b ---- d <-master | `----- c ^-experimental
実験ブランチで作っていた機能が完成(e)。便利なので採用することにした。
experimentalブランチをmasterブランチにマージ(f)。
# experimentalでコミットeを作成済みとする $ git branch * master experimental $ git merge experimental
git mergeコマンドによりmasterとexperimentalをマージ。自動的にコミットfが作成される。
i ---- a ---- b ---- d ----------- f <-master | | `----- c ---- e -----' ^-experimental
この機能はまだ完成度が低いのでマージを取り消したい=コミットfをなかったことにしたい。
git resetでmasterブランチを一個前の位置に戻す。
$ git reset --hard HEAD^ # HEAD^: HEADが指すコミットの親を表す # --hard オプションをつけると現在のファイルの内容も置き換わる(つけないとブランチの指す先が変わるだけ)
masterはfの親であるdを指すようになった
i ---- a ---- b ---- d ----------- f | ^-master | `----- c ---- e -----' ^-experimental
gitにはオブジェクトのGC機能があって、どこからも参照されなくなったコミットfはそのうち消える。
i ---- a ---- b ---- d <- master | `----- c ---- e ^-experimental
ところでコミットdのコミットログにtypoを見つけた。恥ずかしいので直したい……
まずはmasterを一個前に戻して、
$ git reset HEAD^
<-master i ---- a ---- b ---- d | `----- c ---- e ^-experimental
dのコメントを修正したd'をコミット。
$ git commit -m 'commit d'
,----- d' <- master i ---- a ---- b ---- d | `----- c ---- e ^-experimental
(git commit --amendで一気に行うこともできる)
リモートとの連携については書かなかったが、git fetchはコミットオブジェクトの取得+リモートトラッキングブランチの更新、git pullはfetch+ローカルブランチとマージ、git pushはコミットオブジェクトの転送+リモートブランチの更新なので特にむずかしくないですね。
参考文献
美しいワークフローのための入門書
内部に詳しいが故に…