Gitコミットを正しく理解していますか?
誤解1:コミットはレポジトリのあるひとつの変更のみを表している
コミットが「点」として認識されているケースです。図で解説するときに「点」で表すことが多いので、そのまま「コミット=点」と考えがちですがそれは不十分な認識です。
コミットは常に親コミットの情報も併せ持っています。ですから、その情報を使って辿ることが出来る全ての先祖コミットを含むコミットの集合を代表しています。それらのコミット群はグラフもしくはツリーの特徴を持っていて、コミットグラフやコミットツリーとも呼ばれます。(ここではコミットツリーという呼び方に統一します)
下図で、3つのコミットX,Y,Zと、それぞれのコミットツリーの例を示します。例えばコミットXの話をしている時は、実はそこまでに至るすべてのコミット(黄色部分)も含めての話をしているのです。この場合、4つのコミットを含みます。
次に同じ3つのコミットをGitkで見てみましょう。
コミットの位置が分かりやすいようにブランチX,Y,Zを作っておきました。
この記事では便宜上、コミットのIDであるハッシュ値を使わずにブランチ名を使用しますが、ブランチ名の代わりにそのブランチが指すコミットのハッシュ値を使っても同じ結果になります
ところで、Gitkのグラフをパッとみて理解する必要はありません。大切なのは、ある特定のコミットからコミットツリーをイメージできるかどうかです。
この状態のレポジトリをGitHubに上げておきます https://github.com/yoshiwatanabe/learngit
練習として、コミットXの位置から、順次親コミットを辿って「先祖コミットのコミットツリー全体」をイメージしてみましょう。下図左側のようになります。ところでコミットXの場合、ツリー構造ではなくリスト構造になっていますが、それはたまたま途中にマージコミットが無かっただけです(マージコミットに関しては別記事で解説する予定です)。コミットYとコミットZについても同様に、それぞれのコミットツリーをイメージしてみます。
このように「コミット」を考える時は「コミットの点」ではなく「コミットツリー」として考えるようにしましょう。そうすることで何となくわかったようで実はよく分からなかったGit操作が明瞭に理解できるようになるでしょう。例えばgit diff
コマンドは2つのコミットの点ではなく2つのコミットツリー間での差分を調べていると理解できます。またgit checkout
コマンドは指定されたコミットのコミットツリーに含まれるすべてのコミットを反映した状態を作業ディレクトリに展開している、と正しくより直感的に理解できるようになるでしょう。
この「コミットツリーに含まれるすべてのコミットを反映した状態」は次章で解説する2つ目の重要なイメージに関係があります。
コミットはコミットツリーに含まれるすべてのコミットを反映したレポジトリの状態を表す
よくある誤解が「コミットは差分の記録」という認識です。これは2つの意味で誤解を含んでいて、混乱の原因のひとつになっています。ひとつひとつ見ていきましょう。
コミットはあくまでもレポジトリの状態を表す
まずレポジトリが含む「どのコミット」を選んだとしても、そのコミットツリーの変更が反映されたレポジトリの状態を示します。繰り返しますがレポジトリの状態です。「差分」というイメージではなく「全体」というイメージ、「個別のコミット」に着目するのではなく、「全体としてのレポジトリ」に着目するのが大切です。
コミットはレポジトリの、とあるバージョン(リビジョン)を表しているに過ぎません。バージョンと言うと、つい直線的な成長をイメージしてしまいますが、Gitレポジトリではバージョンはタコの足のようにいろんな方向に延びていきます。それぞれの成長は直線的にコミットを積んで行き、なおかつ別の成長に合流したりもします(その際にはマージコミットという特別なコミットを、合流した両方の成長の上に積みます)。
レポジトリの状態とはすなわちファイルとディレクトリとファイルの内容の話になるので、実際にファイルを使って見ていきましょう(ただし、話を単純にするために、ディレクトリは作りませんし、ファイルの内容も加えません)。下図は、それぞれのコミットで新規にレポジトリに加えたテキストファイルを示しています。一番最初のコミットではa.txt
が加えられ、その次のコミットでb.txt
が加えられた、といった具合で開発を進めてきたと想定しましょう。
ところで、黒点の付いたコミットは、複数の親コミットを持つ特別なコミットでマージコミットと呼ばれるものです。マージコミットは、場合によってはマージコミットそのものがレポジトリに変更を加えることがありますが、今回は出来るだけシンプルにするためそういう状態が起こらないようにしています。
さて、先に上げたコミットX,Y,Zを例に使って「コミットツリーに含まれるすべてのコミットを反映したレポジトリの状態」をイメージ出来るように練習してみます。
コミットXは3つのコミット(Initial commitを除いて)を含んでいて、全部合わせてa.txt
b.txt
c.txt
の3つのファイルをレポジトリに加えました。ですから、コミットXをcheckout
した作業ディレクトリは、その3つのファイルがある状態になるはずです。
コミットYはほとんど全部のファイルを含みますが、m.txt
k.txt
l.txt
n.txt
を含みません。
最後にコミットZは、コミットYには含まれていなかったk.txt
l.txt
n.txt
を含みます。
これらの例が示す通り、コミットはレポジトリのあるバージョンの状態と同義です。ですから、あるコミットをチェックアウトすることはつまり、ある状態のレポジトリを作業ディレクトリに反映せよ、と言っているわけです。
Gitは「差分」を記録しない
差分というと、定義としては「変化した部分」だけですが、Gitコミットは決して「変化した部分」だけを記録するものではありません。正しくは「変化した部分を含んだ新たな状態」を記録する、です。別の言い方で「スナップショットを記録する」と言い、そちらの方がGitのイメージとしては正しいです。
・・・と言っておきながら、実は最適化の一部として圧縮の際に差分を考慮するそうです。ですが、話がややこしくなるので、概念上は「差分」を記録しない、あくまでも「スナップショット(状態)」を記録するとイメージします。
例えば下のビフォーアフターで考えてみましょう
ビフォー
abc
123
xyz
アフター
abc
123
xyz
456
何が変わったかというとアフターで、1行あらたに加えられて、その行の内容が456
であったということです。
「差分」ではなく「ファイル全体のスナップショット」がコミットの一部として記録されたという状況を実際に検証してみます。
まず、コミットされた時点での a.txt
ファイルの「内容」のSHA-1ハッシュ値を見てみます。ファイルをgit hash-object
コマンドに渡すとファイルの内容のハッシュ値を戻します。このハッシュ値が記録されていれば、ファイルの内容全体が記録されたことになります。
コミットが実際にどのような構造になっているかはここでは省略します(commit, tree, blobの3つのオブジェクトで構成される木構造になっています。Qiitaに良記事があります。)
あと「Gitの中身」というタイトルで、3つに分けで4年前になりますが、動画をアップしてます。(スピード感が無くて結構ダルいです、すみません)
https://youtu.be/KknQgXfH_uM
https://youtu.be/mpqblZVNof0
https://youtu.be/1buKbzimhcI
Gitオブジェクトデータベースの理解は大切ですが、どうしても理解しておかなくてはならない、という性質のものではありません。
下はa.txt
を含むディレクトリ(treeオブジェクト)の内容です。2e5e94a8b9e0feaf546dcff39348089054499b81
というハッシュ値が記録されているのが分かります。この値は、先にa.txt
の内容のハッシュ値を求めた時と同じ値です。ということで、コミットに記録されるのは「差分」ではなく「変更を含んだスナップショット」だと確認できました。
この例ではファイルひとつだけを見ていきましたが、複数のファイルに対する操作ともまったく同じように「状態のスナップショット」が最終的にひとつのコミットの元に集約されて、そのコミットで表されます。
例えば、ファイル名を変えると、treeオブジェクトが変わりますから、treeオブジェクトのハッシュ値が更新されます。ファイルを削除すると、やはりtreeオブジェクトの内容が変わるのでそのハッシュ値も変わります。同じファイルを別のディレクトリに移動した場合も同様です(ですが、ファイルの内容は変わらないので、同じblobが使われます)。
Gitレポジトリはブロックチェーンの原理とよく似ています。どちらも親を含んだハッシュ値でそのノードを表すグラフになってます。なので、ブロックチェーンでも、あるブロックチェーンはそれまでの全て先祖ブロックチェーンが改竄されていないことを保証します(その検証を可能にします)。またブロックそのものを元に集約されるトランザクションのどれもが改竄されていないことを保証します(検証可能にします)。
レポジトリの全体像のイメージ
ここまでで2つのデータ構造がイメージできるようになったでしょうか?
- 「レポジトリの状態を表すコミット(コミットツリー)構造」のイメージ
- 「ひとつのコミットの元に集約されているツリー構造(構成要素はtreeオブジェクトやblobオブジェクト)」のイメージ
この2つのデータ構造を頭の中にイメージ出来れば成功です。まず最初に、コミットツリーという2次元構造をイメージして、そのそれぞれのコミットの点について、さらにGitオブジェクト(treeやblob)からなるツリー構造がぶら下がっている3次元イメージです。
このメンタルイメージを基礎にして、他の概念、例えばブランチ、リモート、fetch
push
pull
などのコマンド、ワーキングディレクトリやステージングの理解を深めて行くと、混乱が少なくて済むと思います。
Comments