Gitを深く理解するためのメンタルイメージその１：コミット

# Gitコミットを正しく理解していますか？

## 誤解１：コミットはレポジトリのあるひとつの変更のみを表している
コミットが「点」として認識されているケースです。図で解説するときに「点」で表すことが多いので、そのまま「コミット＝点」と考えがちですがそれは不十分な認識です。

コミットは常に親コミットの情報も併せ持っています。ですから、その情報を使って辿ることが出来る全ての先祖コミットを含むコミットの集合を代表しています。それらのコミット群はグラフもしくはツリーの特徴を持っていて、コミットグラフやコミットツリーとも呼ばれます。（ここではコミットツリーという呼び方に統一します）

下図で、3つのコミットX,Y,Zと、それぞれのコミットツリーの例を示します。例えばコミットXの話をしている時は、実はそこまでに至るすべてのコミット（黄色部分）も含めての話をしているのです。この場合、4つのコミットを含みます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103139/f4ad2ca3-ba66-4cfd-7437-3ca3eaa56e05.png)

次に同じ３つのコミットをGitkで見てみましょう。
コミットの位置が分かりやすいようにブランチX,Y,Zを作っておきました。

> この記事では便宜上、コミットのIDであるハッシュ値を使わずにブランチ名を使用しますが、ブランチ名の代わりにそのブランチが指すコミットのハッシュ値を使っても同じ結果になります

ところで、Gitkのグラフをパッとみて理解する必要はありません。大切なのは、ある特定のコミットからコミットツリーをイメージできるかどうかです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103139/1e3279ab-6a2e-7d85-52fa-6029cf563abd.png)

> この状態のレポジトリをGitHubに上げておきます https://github.com/yoshiwatanabe/learngit

練習として、コミットXの位置から、順次親コミットを辿って「先祖コミットのコミットツリー全体」をイメージしてみましょう。下図左側のようになります。ところでコミットXの場合、ツリー構造ではなくリスト構造になっていますが、それはたまたま途中にマージコミットが無かっただけです（マージコミットに関しては別記事で解説する予定です）。コミットYとコミットZについても同様に、それぞれのコミットツリーをイメージしてみます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103139/10059c65-b002-e569-5964-d0ca3d7c1b07.png)

このように「コミット」を考える時は「コミットの点」ではなく「コミットツリー」として考えるようにしましょう。そうすることで何となくわかったようで実はよく分からなかったGit操作が明瞭に理解できるようになるでしょう。例えば```git diff ```コマンドは２つのコミットの点ではなく**２つのコミットツリー間での差分**を調べていると理解できます。また```git checkout```コマンドは指定されたコミットの**コミットツリーに含まれるすべてのコミットを反映した状態**を作業ディレクトリに展開している、と正しくより直感的に理解できるようになるでしょう。

この「コミットツリーに含まれるすべてのコミットを反映した状態」は次章で解説する2つ目の重要なイメージに関係があります。

# コミットはコミットツリーに含まれるすべてのコミットを反映したレポジトリの状態を表す

よくある誤解が「コミットは差分の記録」という認識です。これは２つの意味で誤解を含んでいて、混乱の原因のひとつになっています。ひとつひとつ見ていきましょう。

## コミットはあくまでもレポジトリの状態を表す

まずレポジトリが含む「どのコミット」を選んだとしても、そのコミットツリーの変更が反映された**レポジトリの状態**を示します。繰り返しますが**レポジトリの状態**です。「差分」というイメージではなく「全体」というイメージ、「個別のコミット」に着目するのではなく、「全体としてのレポジトリ」に着目するのが大切です。

コミットはレポジトリの、とあるバージョン（リビジョン）を表しているに過ぎません。バージョンと言うと、つい直線的な成長をイメージしてしまいますが、Gitレポジトリではバージョンはタコの足のようにいろんな方向に延びていきます。それぞれの成長は直線的にコミットを積んで行き、なおかつ別の成長に合流したりもします（その際にはマージコミットという特別なコミットを、合流した両方の成長の上に積みます）。

レポジトリの状態とはすなわちファイルとディレクトリとファイルの内容の話になるので、実際にファイルを使って見ていきましょう（ただし、話を単純にするために、ディレクトリは作りませんし、ファイルの内容も加えません）。下図は、それぞれのコミットで新規にレポジトリに加えたテキストファイルを示しています。一番最初のコミットでは```a.txt```が加えられ、その次のコミットで```b.txt```が加えられた、といった具合で開発を進めてきたと想定しましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103139/9ab870b4-68c5-b4ff-699e-1c4ea34b660d.png)


> ところで、黒点の付いたコミットは、複数の親コミットを持つ特別なコミットでマージコミットと呼ばれるものです。マージコミットは、場合によってはマージコミットそのものがレポジトリに変更を加えることがありますが、今回は出来るだけシンプルにするためそういう状態が起こらないようにしています。

さて、先に上げたコミットX,Y,Zを例に使って「コミットツリーに含まれるすべてのコミットを反映したレポジトリの状態」をイメージ出来るように練習してみます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103139/10059c65-b002-e569-5964-d0ca3d7c1b07.png)

コミットXは３つのコミット（Initial commitを除いて）を含んでいて、全部合わせて```a.txt``` ```b.txt``` ```c.txt``` の3つのファイルをレポジトリに加えました。ですから、コミットXを```checkout```した作業ディレクトリは、その３つのファイルがある状態になるはずです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103139/698ff14e-c9c4-0953-9275-c1dfab8e3556.png)

コミットYはほとんど全部のファイルを含みますが、```m.txt``` ```k.txt``` ```l.txt``` ```n.txt```を含みません。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103139/dc12c3da-6235-b713-ff33-c96d5dbd5012.png)

最後にコミットZは、コミットYには含まれていなかった```k.txt``` ```l.txt``` ```n.txt```を含みます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103139/88b42daa-4306-9148-fd53-f9b4f6211d86.png)

これらの例が示す通り、コミットはレポジトリのあるバージョンの状態と同義です。ですから、あるコミットをチェックアウトすることはつまり、ある状態のレポジトリを作業ディレクトリに反映せよ、と言っているわけです。

# Gitは「差分」を記録しない

差分というと、定義としては「変化した部分」だけですが、Gitコミットは決して「変化した部分」だけを記録するものではありません。正しくは「変化した部分を含んだ新たな状態」を記録する、です。別の言い方で「スナップショットを記録する」と言い、そちらの方がGitのイメージとしては正しいです。

> ・・・と言っておきながら、実は[最適化の一部として圧縮の際に差分を考慮する](https://gist.github.com/matthewmccullough/2695758)そうです。ですが、話がややこしくなるので、概念上は「差分」を記録しない、あくまでも「スナップショット（状態）」を記録するとイメージします。

例えば下のビフォーアフターで考えてみましょう

ビフォー

``` a.txt
abc
123
xyz
```

アフター

``` a.txt
abc
123
xyz
456
```

何が変わったかというとアフターで、1行あらたに加えられて、その行の内容が```456```であったということです。

「差分」ではなく「ファイル全体のスナップショット」がコミットの一部として記録されたという状況を実際に検証してみます。

まず、コミットされた時点での ```a.txt``` ファイルの「内容」のSHA-1ハッシュ値を見てみます。ファイルを```git hash-object``` コマンドに渡すとファイルの内容のハッシュ値を戻します。このハッシュ値が記録されていれば、ファイルの内容全体が記録されたことになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103139/6b284789-05a3-fe49-fdea-cce4826f5a35.png)

コミットが実際にどのような構造になっているかはここでは省略します（commit, tree, blobの3つのオブジェクトで構成される木構造になっています。Qiitaに良記事があります。）

あと「Gitの中身」というタイトルで、３つに分けで4年前になりますが、動画をアップしてます。（スピード感が無くて結構ダルいです、すみません）

https://youtu.be/KknQgXfH_uM
https://youtu.be/mpqblZVNof0
https://youtu.be/1buKbzimhcI

> Gitオブジェクトデータベースの理解は大切ですが、どうしても理解しておかなくてはならない、という性質のものではありません。

下は```a.txt```を含むディレクトリ（treeオブジェクト）の内容です。```2e5e94a8b9e0feaf546dcff39348089054499b81```というハッシュ値が記録されているのが分かります。この値は、先に```a.txt```の内容のハッシュ値を求めた時と同じ値です。ということで、コミットに記録されるのは「差分」ではなく「変更を含んだスナップショット」だと確認できました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103139/324977ac-0344-6db6-993a-d34fe71a5b93.png)

この例ではファイルひとつだけを見ていきましたが、複数のファイルに対する操作ともまったく同じように「状態のスナップショット」が最終的にひとつのコミットの元に集約されて、そのコミットで表されます。

例えば、ファイル名を変えると、treeオブジェクトが変わりますから、treeオブジェクトのハッシュ値が更新されます。ファイルを削除すると、やはりtreeオブジェクトの内容が変わるのでそのハッシュ値も変わります。同じファイルを別のディレクトリに移動した場合も同様です（ですが、ファイルの内容は変わらないので、同じblobが使われます）。

> Gitレポジトリはブロックチェーンの原理とよく似ています。どちらも親を含んだハッシュ値でそのノードを表すグラフになってます。なので、ブロックチェーンでも、あるブロックチェーンはそれまでの全て先祖ブロックチェーンが改竄されていないことを保証します（その検証を可能にします）。またブロックそのものを元に集約されるトランザクションのどれもが改竄されていないことを保証します（検証可能にします）。

# レポジトリの全体像のイメージ

ここまでで２つのデータ構造がイメージできるようになったでしょうか？

* 「レポジトリの状態を表すコミット（コミットツリー）構造」のイメージ
* 「ひとつのコミットの元に集約されているツリー構造（構成要素はtreeオブジェクトやblobオブジェクト）」のイメージ

この2つのデータ構造を頭の中にイメージ出来れば成功です。まず最初に、コミットツリーという2次元構造をイメージして、そのそれぞれのコミットの点について、さらにGitオブジェクト（treeやblob）からなるツリー構造がぶら下がっている3次元イメージです。

このメンタルイメージを基礎にして、他の概念、例えばブランチ、リモート、```fetch``` ```push``` ```pull``` などのコマンド、ワーキングディレクトリやステージングの理解を深めて行くと、混乱が少なくて済むと思います。