gitを仕組みで理解しよう ~ addやcommitは何してるのか ~

## はじめに
gitって良く使うと思うんですけど、内部の仕組みって少し見えにくいですよね。
普段使っているものも仕組みを理解すると、より使いやすくなると思います！
そこで今回はaddやcommitしたときに何が起きるのかを解説していきます。

**目次**

- [1. まずは図で理解](#1まずは図で理解)
- [2. gitファイルを見て理解](#2gitファイルを見ながら理解)

## 1.まずは図で理解
まずは図を見ながら理解していきましょう。
このパートを見るだけでも概念は理解できると思います。
以下のような順番で説明をしていきます。

- 1-1. 初期状態
- 1-2. git add
- 1-3. git commit
- 1-4. git add 2回目
- 1-5. git commit 2回目

#### 1-1. 初期状態
以下の図のようにワークツリーにはfile1, file2があるとします。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/799854/c08f48b4-83c7-3e70-3e67-1d5e56f4b6e6.jpeg" width="80%">

#### 1-2. git add
次にこの状態からfile1のみ変更して`git add file1`をします。
そうすると以下の図のように2つのことが起きます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/799854/ff65a532-906e-7150-53f2-710ccfd5581c.jpeg" alt="add 1回目.jpg" width="80%">

**step1. リポジトリに圧縮ファイルが生成される。**
このとき生成される圧縮ファイルはblobオブジェクトと呼ばれます。
blobオブジェクトの名前には40桁の英数字の名前が割り振られます（ファイルの内容を元にSHA1でハッシュ化した値です）。
図には始めの5桁のみを記載しています。

**step2. ステージングエリアのindexに、file1と圧縮ファイルの紐付けが記録される。**

blobオブジェクトはファイル名だけでは、それがfile1の圧縮ファイルなのかは分かりません。
そのためblobオブジェクトとfile1の内容を紐付けてあげる必要があります。
この紐付けを一時的に行うのがindexの役割になります。


#### 1-3. git commit
次に`commit`をしていきます。
こちらも以下の図の通り、2つのことがおきます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/799854/914cb1d9-ce13-4d12-cf4c-a79766d8cebe.jpeg" alt="commit 1回目.jpg" width="80%">

**step1. リポジトリにtreeオブジェクトが生成される**
treeオブジェクトはindexの内容がそのまま書き込まれます。要はblobオブジェクトと元となるファイルの紐付けの情報です。
indexとの違いはファイルが上書きされるかどうかです。
indexはgit addするたびに書き換えられて上書きされてしまいますが、treeオブジェクトはcommitするたびに新しいファイルが生成されます。

**step2. リポジトリにcommitオブジェクトが生成される**
commitオブジェクトは以下のような情報が記載されています。よく`git log`で目にする情報ですね。

```plaintext:commitオブジェクトの内容
・ treeオブジェクトのファイル名
・ 親commitの情報（1回目はなし）
・ commitした人の情報
・　　commitした日時
・ commitメッセージ
```

ここまでで、addとcommitの動きを一通り見ることができました。
今はfile1のみを変更してaddとcommitをしたので、次はこの状態からさらにfile1, file2の両方を変更してadd, commitしていきます。

#### 1-4. git add 2回目
file1, file2に変更を加えて、`git add file1 file2`をしたとします。
1回目の時と同様に、2つのことが起きます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/799854/a78f300e-4ad5-e952-3e2a-629808c1e6ed.jpeg" alt="add 2回目.jpg" width="80%">



**step1. リポジトリにblobオブジェクトが生成される。**
今回はfile1とfile2のblobオブジェクトが生成されます。
1回目のaddと合わせて、現在は3つのblobオブジェクトがリポジトリにあります。

**step2. ステージングエリアの`index`にfile1, file2とblobオブジェクトの紐付けが記録される。**
1回目のaddで記録された内容は上書きされて、新しい紐付け情報がindexに記録されます。


#### 1-5. git commit 2回目
次に`commit`をしていきます。
こちらも1回目の時と同様に2つのことがおきます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/799854/5aa9810d-ac0e-2da1-23e2-aee7ab5b17d3.jpeg" alt="commit 2回目.jpg" width="80%">

**step1 リポジトリにtreeオブジェクトが生成される**
今回もindexの内容がそのまま記載されたtreeオブジェクトが生成されます。

**step2 リポジトリにcommitオブジェクトが生成される**
今回のcommitオブジェクトの内容は以下のとおりです。
1回目のcommitオブジェクトのファイル名も記載されます。

```plaintext:commitオブジェクトの内容
・ treeオブジェクトのファイル名
・ 親commitの情報（1回目のcommitのファイル名）
・ commitした人の情報
・　　commitした日時
・ commitメッセージ
```

以上がaddからcommitの流れでした。
これで大まかな流れは把握できたのではないでしょうか？
次からは実際にgitのファイルの中身を見ていきながら、より理解を深めていこうと思います。

## 2.gitファイルを見ながら理解
[まずは図で理解](#まずは図で理解)の項でやってきたことを実際に手を動かしながら、ファイルの中身を見ていきます。先ほどと同様に以下の順番で進めていきます。


- 2-1. 初期状態
- 2-2. git add
- 2-3. git commit
- 2-4. git add 2回目
- 2-5. git commit 2回目

#### 2-1. 初期状態
- まずは以下のコマンドで初期状態を作っていきます。

```
$ mkdir git_study
$ cd git_study
$ git init
$ touch file1 file2
```

- 存在するディレクトリやファイルを見てみます

```
$ ls -a
.     ..    .git  file1 file2
```

この`.git`ディレクトリの中に各オブジェクトやindexが生成・保存されてgit管理がなされます。
現在の`.git`ディレクトリの中を見てみましょう。
詳細な説明は割愛しますが、`objects`というディレクトリに各種オブジェクトが格納されていきます。

```bash
$ tree -L 1
.
├── HEAD
├── config
├── description
├── hooks
├── info
├── objects
└── refs
```

#### 2-2. git add
- では図解の時と同様に、file1に変更を加えてgit addしてみます。

```
$ echo 'file1に変更を加える' >> file1
$ git add file1
```
図ではこのような状態です。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/799854/b128721c-f77c-3a39-85ac-06c1672a1f23.jpeg" alt="add1回目" width="80%">

- まずはblobオブジェクトが生成されているかを確認してみます。

```
$ find .git/objects -type f
.git/objects/5e/c12a6e49bd9d4d647ac2a6859664d4e5635e2c
```
図解の方で40桁の英数字をファイル名に割り当てると記載しました。実際には40桁の内はじめの2桁をサブディレクトリとして、後半の38桁をファイル名として割り当てていきます。
ややこしいので40桁の英数字の方は以降オブジェクトのIDと呼ぶこととします。

- 次にオブジェクトの中身をみてみます。

すると、先程file1に入力した内容が出てきました。

```
$ git cat-file -p 5ec12a6e49bd9d4d647ac2a6859664d4e5635e2c
file1に変更を加える
```

**【補足】** cat-fileコマンドについて
> cat-fileコマンドでオブジェクトのIDを指定すると、オブジェクトの情報を取得することができます。
`-p`オプションの場合はファイルの中身を確認することができます。

- それではindexの中身を見てみましょう

```
$ git ls-files --stage
100644 5ec12a6e49bd9d4d647ac2a6859664d4e5635e2c 0	file1
```
先程のblobオブジェクトのIDと生成元のファイル名が記載していることがわかります。

**【補足】** indexの中身について
>blobオブジェクトやファイル名の情報以外にも`100644`や`0`という情報があります。
`100644`の方はモードとよばれる数値で、ファイルの種類とパーミッションを表しています。前半の100という数字がファイルの種類（シンボリックリンクだと120になったりします）、後半の644はパーミッションです。（詳細は[こちらのstack overflow](https://stackoverflow.com/questions/737673/how-to-read-the-mode-field-of-git-ls-trees-output)を参照）
blobオブジェクトのIDの次に書いてある0という数字については、競合の状態を表しています。競合がない場合は 0、競合がある場合は 1、2、3 のいずれかになります。

#### 2-3. git commit
- それではcommitしていきます。

```
$ git commit -m "file1を編集"
```
図ではこのような状態です。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/799854/67f2efd7-c32e-f45f-3338-e9f167444e54.jpeg" alt="commit1回目" width="80%">


- 生成されているオブジェクトを確認していきましょう

```
$ find .git/objects -type f
.git/objects/5e/c12a6e49bd9d4d647ac2a6859664d4e5635e2c
.git/objects/e9/4966e6df9cd077175d3cbcb06af43c94b236e8
.git/objects/22/792282d627343ef6e6194b74afa67be86decae
```

addの際に生成したblobオブジェクトのIDが`5ec12a6e49bd9d4d647ac2a6859664d4e5635e2c`だったので、他の2つがtreeオブジェクトとcommitオブジェクトですね。
試しに`e94966e6df9cd077175d3cbcb06af43c94b236e8`の方のオブジェクトの型を見てみようと思います。

```
$ git cat-file -t e94966e6df9cd077175d3cbcb06af43c94b236e8
commit
```
こちらはcommitオブジェクトだったようです。

- せっかくなので、このcommitオブジェクトのファイルの中身を見てみます。

```
$ git cat-file -p e94966e6df9cd077175d3cbcb06af43c94b236e8
tree 22792282d627343ef6e6194b74afa67be86decae
author hiroaki-u <メールアドレス> 1639398186 +0900
committer hiroaki-u <メールアドレス> 1639398186 +0900

file1を編集
```
treeオブジェクトや編集者、コミット日時、コミットメッセージの情報があることがわかりました（日時はunixtimeで書いてあります）。
ちなみに、これらの情報を見やすくしてlog化したのがgit logです。

```
$ git log
commit e94966e6df9cd077175d3cbcb06af43c94b236e8
Author: hiroaki-u <メールアドレス>
Date:   Mon Dec 13 21:23:06 2021 +0900

    file1を編集
```

commitオブジェクトに書いてある内容とほぼ一緒ですね。
committerとtreeオブジェクトのIDが抜けているくらいです。

**【補足】** committerとAuthorの違いについて
>
committer：コミットした人
Author：ファイルを編集した人

>gitはrebase等でファイルの内容を編集せずに新たなcommitを生成することが可能です。
そのため、ファイルの編集者とcommitした人を分けて管理をしています。


- treeオブジェクトの中身も見てみましょう。

indexの内容とほぼ同じであることがわかりました。

```
$ git cat-file -p 22792282d627343ef6e6194b74afa67be86decae
100644 blob 5ec12a6e49bd9d4d647ac2a6859664d4e5635e2c	file1

// indexの中身（再掲）
$ git ls-files --stage
100644 5ec12a6e49bd9d4d647ac2a6859664d4e5635e2c 0   file1
```

それでは図解の項目同様、今度はfile1とfile2に変更を加えてaddとcommitをしていこうと思います。

#### 2-4. git add 2回目
- まずはfile1とfile2に変更を加えてaddしていきます。

```
echo 'file1に追加で変更を加える' >> file1
echo 'file2に変更を加える' >> file2
git add file1
```
図ではこのような状態です。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/799854/7f1fa71c-0ca8-bd88-a64d-ae7a94bbe690.jpeg" width="80%">


- それではindexの中身を見ていきます。


```
$ git ls-files --stage
100644 8a641c80c839c937316c6a0e9cede8f37bfd0fdc 0	file1
100644 b315cc0613ccba1d1fcc93332406070386af7bd2 0	file2
```

今度はfile1とfile2それぞれについて、blobオブジェクトとの紐付けが記録されていることがわかります。
また、1回目に記載されていた内容が残っていないことから、内容が上書きされていることもわかります。`

- これら2つのblobオブジェクトの中身も確認しておきます。


```
// file1のblobオブジェクトの中身
$ git cat-file -p 8a641c80c839c937316c6a0e9cede8f37bfd0fdc
file1に変更を加える
file1に追加で変更を加える

// file1の中身
$ cat file1
file1に変更を加える
file1に追加で変更を加える

// file2のblobオブジェクトの中身
git cat-file -p b315cc0613ccba1d1fcc93332406070386af7bd2
file2に変更を加える

// file2の中身
$ cat file2
file2に変更を加える
```

file1とfile2のファイルの内容が記録されていることがわかりました。
gitは差分を記録していると勘違いしそうになりますが、これを見るとファイルのスナップショットを記録しているということがわかりますね。

#### 2-5. git commit 2回目
- それではcommitしていきます。

```
git commit -m "file1とfile2を更新"
```

図ではこのような状態です。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/799854/c8b5f8d3-baa9-9d75-adb9-920de87bc212.jpeg" width="80%">


- git logにてcommitオブジェクトを確認していきます。

さきほどのcommitと今回のcommitの情報を見ることができます。

```
$ git log
commit ef056f9bc48970d7ee65e3cc69204e5ea9ea7ee7 (HEAD -> master)
Author: hiroaki-u <メールアドレス>
Date:   Mon Dec 13 21:54:58 2021 +0900

    file1とfile2を更新

commit e94966e6df9cd077175d3cbcb06af43c94b236e8
Author: hiroaki-u <メールアドレス>
Date:   Mon Dec 13 21:23:06 2021 +0900

    file1を編集
```

- 今回のコミットオブジェクトのIDを元にファイルの中身を再度見てみます。

```
$ git cat-file -p ef056f9bc48970d7ee65e3cc69204e5ea9ea7ee7
tree 9483b5d7584b17a3f3f2863e75fb1cb98b2d75de
parent e94966e6df9cd077175d3cbcb06af43c94b236e8
author hiroaki-u <メールアドレス> 1639400098 +0900
committer hiroaki-u <メールアドレス> 1639400098 +0900

file1とfile2を更新
```

1回目と基本的には同じですが、2回目以降にはparentと書いてあるIDがあります。
これは1回目のコミットオブジェクトのIDと同じです。
gitはcommitオブジェクトに親commitのIDの値をもつことで、記録を辿れるようにしています。

説明はこれで以上になります。
この記事を見て、少しでもgitの理解が深まったら嬉しいです！

## 参考文献
- [Gitのコミットの裏側で起こっていること](https://made.livesense.co.jp/entry/2017/08/22/080000)
- [公式ドキュメント](https://git-scm.com/book/ja/v2
)
- [ステージファイル](https://git-scm.com/docs/git-ls-files)
- [git/index-format.txt](https://github.com/git/git/blob/master/Documentation/technical/index-format.txt)