Git の仕組み

# はじめに
Gitは、分散型バージョン管理システムの一つであり、ソフトウェア開発において広く利用されています。Gitを使用することで、複数人で同じプロジェクトを管理する際に必要なバージョン管理やコードの共有が容易になります。

この記事では、コミットやブランチ、タグ、HEAD 等を介して、Git がどのように機能するのかについて説明していきます。

# 基本的な仕組み
BLOB やツリー、コミットは、Git のデータ構造の主要な構成要素であり、これらの要素が Git の基礎を構成しています。

これらの要素を理解するために、例を挙げて説明します。

まず、空のリポジトリを作成する場合を考えてみましょう。コマンド `git init` を実行すると、Git は自動的に `.git` という名前の隠しフォルダが作成されます。このフォルダは、Git の内部で使用されるデータを保存するために使用されます。

![スクリーンショット 2023-05-08 1.24.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2659159/6a0b286d-a2ba-75fd-7a92-b7d8a2826781.png)

## BLOB
Git では、`git add` コマンドを使用して、リポジトリにファイルを追加します。
今回は、`myfile.txt` という名前のファイルを作成し、コマンド `git add myfile.txt` を使用して追加してみます。

```bash
### myfile.txt を作成
$ echo "hello" > myfile.txt

### Git リポジトリに myfile.txt を追加
$ git add myfile.txt 
```

この操作を行うと、Git は `.git/objects` サブフォルダに BLOB と呼ばれるファイルが作成されます。

![スクリーンショット 2023-05-08 1.31.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2659159/32f9fbf4-3c2f-e52e-ef5d-ca6d6fb50e6d.png)

この BLOB には `myfile.txt` のコンテンツが保存されます。これには、作成タイムスタンプや作成者などの関連するメタデータを含まれていません。

```bash
### `git catfile` コマンドで BLOB ファイルの内容を表示
### （コンテンツ種別に応じた表示の最適化を行うため、`-p`　オプションを付与）
$ git cat-file -p ce013625030ba8dba906f756967f9e9ca394464a 
hello
```

BLOB 名は、そのコンテンツのハッシュ値を基に命名されます。コンテンツがハッシュ化した後、最初の2文は `.git/objects` にサブフォルダ名として、残りの文字列は BLOB 名として使用されます。

つまり、ファイルを Git に追加する場合、以下の手順が行われます。

1. Git がファイルの内容を取得し、ハッシュ化する。
2. Git は `.git/objects` フォルダ内に BLOB を作成する。
   - ハッシュ値の最初の2文字を使用し、サブフォルダを作成する。
   - 作成したサブフォルダ配下で、ハッシュ値の残りの文字列の名前で BLOB を作成する。
3. Git は、（圧縮された）元のファイルの内容を BLOB 内に保存する。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2659159/f2a99a2c-431a-5323-abe7-161df7913fae.png" width="700px"/>

なお、`myfile.txt` と `ourfile.txt` という別のファイルがあり、2つとも同じコンテンツである際には、それらは同じハッシュ値となるため、同じ BLOB に格納されます。

```bash
### myfile.txt をコピーし、ourfile.txt を作成
$ cp -p myfile.txt ourfile.txt

### ourfile.txt を `git add` し、BLOB が新しく生成されていないことを確認
$ git add ourfile.txt 
$ find .git/objects -type f
.git/objects/ce/013625030ba8dba906f756967f9e9ca394464a
$ 
```

また、`myfile.txt` の中身を少し修正して再度リポジトリに追加すると、Git によって前述したプロセスが再度実行されます。この際、ハッシュ値が変わるため、新しい BLOB が作成されることになります。

```bash
### myfile.txt の内容を少し変更し、再度 `git add` を実施
$ cat myfile.txt 
hello!
$ git add myfile.txt 

### BLOB ファイルが新しく生成されたことを確認
$ find .git/objects -type f
.git/objects/4e/ffa19f4f75f846c3229b9dbdbad14eff362f32
.git/objects/ce/013625030ba8dba906f756967f9e9ca394464a

### それぞれの BLOB について中身を確認
$ git cat-file -p ce013625030ba8dba906f756967f9e9ca394464a
hello
$ git cat-file -p 4effa19f4f75f846c3229b9dbdbad14eff362f32
hello!
```

## ツリー
ここでは、`mysubfolder` という名前のサブフォルダを作成し、その中に `yourfile.txt` という名前のファイルを作成してリポジトリに追加していこうと思います。

```bash
### mysubfolder フォルダを作成
$ mkdir mysubfolder

### mysubfolder 配下に yourfile.txt を作成
$ echo "world" > mysubfolder/yourfile.txt

### Git リポジトリに mysubfolder/yourfile.txt を追加
$ git add mysubfolder/yourfile.txt 
```

この操作により、Git は新たに `yourfile.txt` の BLOB を作成します。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2659159/726feb6a-e8df-f8b5-619a-c5bcf2dbc9de.png" width="600px" />

続けて、コマンド `git commit` を使って `myfile.txt` と `yourfile.txt`の両方をコミットします。

```bash
### myfile.txt と mysubfolder/yourfile.txt　をコミット
$ git commit -m"first commit"
[master (root-commit) c262f57] first commit
 2 files changed, 2 insertions(+)
 create mode 100644 myfile.txt
 create mode 100644 mysubfolder/yourfile.txt
```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2659159/36311b42-51c9-6651-211f-d47c0b211c64.png" width="300px" />


この操作では、Git は次の2つの手順を実行します。

1. リポジトリのルートツリーを作成する。
2. コミットを作成する。

**ルートツリー**とは、リポジトリ全体のファイルやフォルダの構造が記録される場所です。このツリーには、リポジトリに含まれるすべてのファイルやサブフォルダーへの参照が含まれ、再帰的な構造になっています。
ルートツリーの各行は、ファイルや他のサブツリーを参照しています。同様に、そのファイルやサブツリーも他のファイルやサブツリーを参照しています。これによって、ツリーはディレクトリと同じような構造を持ちます。つまりは、ディレクトリからファイルやサブフォルダにアクセスできるのと同じように、ツリーから BLOB やサブツリーにアクセスすることができます。

```bash
$ git cat-file -p 0fa5b90dcd339c2f52b492a5126ccb760478e
100644 blob cc628ccd10742baea8241c5924df992b5c019f71	yourfile.txt

$ git cat-file -p 9b35f55d56eb0c921c533ae31a6a0ed9c7859
100644 blob ce013625030ba8dba906f756967f9e9ca394464a	myfile.txt
040000 tree 0fa5b90dcd339c2f52b492a5126ccb760478ecbd	mysubfolder
```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2659159/28d6fcb4-b4ed-710f-8e4a-658be05920a8.png" width="550px" />

Git がルートツリーやそれに関連する全てのサブツリーを作成したら、先程説明したように、それらをハッシュ化して保存します。
具体的には、各ツリーをハッシュ化し、最初の2文字を使用して `.git/objects` にサブフォルダーを作成し、残りのハッシュ文字列を保存されたファイル名として使用します。これにより、ツリー内のデータ構造の数と同じ数だけの新しいファイルが作成されます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2659159/31b42100-2e4e-2d4f-adf3-b1d58d37af82.png" width="600px" />

## コミット
コミットを実行すると、以下のメタデータ情報を含んだ形でファイルとして保存されます。

- ルートツリー (tree)
- 親コミット (parent commit) &nbsp;&nbsp;&nbsp;&nbsp;
- …存在する場合
- 作成者の名前および電子メール (author)
- コミッターの名前および電子メール (committer)
- コミットメッセージ (feat)

```bash
$ git cat-file -p c262f5725e96f52c6bf1dbebcaf2680570fba
tree 9b35f55d56eb0c921c533ae31a6a0ed9c7859057
author John Doe <johndoe@example.com> 1683579147 +0900
committer John Doe <johndoe@example.com> 1683579147 +0900

first commit
```
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2659159/67b18741-9240-38a2-bf46-f34318a49fb1.png" width="550px" />

コミットファイルが作成されると、Git はその内容をハッシュ化し、そのハッシュを使用して、新しいファイルにコンテンツを保存します。最初の2文字は `.git/objects` のサブフォルダー名、残りのハッシュ文字列は BLOB 名として構成されます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2659159/94c95281-c510-9206-1ea6-9d095c41fe54.png" width="600px" />

# 処理実行時の挙動
## ブランチ
ブランチは、コードの変更履歴を分岐させることができる Git の機能です。ブランチを使用することで、複数の人が同じプロジェクトで作業をする場合に、各自が独立して作業を進めることができます。

`git branch mybranch` コマンドを実行し、`mybranch` という名前の新しいブランチを作成すると、Git は `.git/refs/heads` に `mybranch` という名前の新しいファイルを生成します。このファイルには、ブランチの作成元のコミットのハッシュが含まれます。

```bash
### mybranch というブランチを作成
$ git branch mybranch

### ブランチの状態を確認
$ git branch
* master
  mybranch
```
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2659159/f764c071-1372-4231-269a-f3fcafc03246.png" width="200px" />

```bash
$ cat  .git/refs/heads/mybranch
c262f5725e96f52c6bf1dbebcaf2680570fba434

$ cat .git/logs/refs/heads/mybranch
0000000000000000000000000000000000000000 c262f5725e96f52c6bf1dbebcaf2680570fba434 John Doe <johndoe@example.com> 1683662253 +0900	branch: Created from master
```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2659159/c3d0dace-1c5e-1cc2-9625-08214f3c1361.png" width="450px" />

次に、`mybranch` で新たにコミットを行うと、前述した通り、Git は、ルートツリーとコミットファイルを作成し、ブランチファイルを新しいコミットのハッシュに更新します。
したがって、ブランチはコミットを追跡するファイルであり、これらのファイルの内容はコミットを実行するたびに更新されます。

```bash
### ブランチを mybranch に切り替え
$ git checkout mybranch

### myfile2.txt ファイルを作成し、Git リポジトリに追加
$ echo "branch test" > myfile2.txt
$ git add myfile2.txt 

### myfile2.txt をコミット
$ git commit -m "second commit"
[mybranch 92efc63] second commit
 1 file changed, 1 insertion(+)
 create mode 100644 myfile2.txt
```

```bash
$ cat  .git/refs/heads/mybranch
92efc63e7150cc2924db956c2f540facfb46dfa2

$ cat .git/logs/refs/heads/mybranch
0000000000000000000000000000000000000000 c262f5725e96f52c6bf1dbebcaf2680570fba434 John Doe <johndoe@example.com> 1683662253 +0900	branch: Created from master
c262f5725e96f52c6bf1dbebcaf2680570fba434 92efc63e7150cc2924db956c2f540facfb46dfa2 John Doe <johndoe@example.com> 1683663583 +0900	commit: second commit
```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2659159/29acd24f-922c-8fc2-7191-0264e9df6133.png" width="450px" />

## タグ
タグは、特定のコミットへの永続的な参照です。

`git tag mytag` コマンドを実行し、`mytag` という名前の新しいタグを作成した場合、Git はパス `.git/refs/tags` に `mytag` という名前の新しいファイルを生成します。

```bash
$ git tag mytag
```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2659159/18dca73d-1d01-ff7a-ab57-82c1ea5a1350.png" width="200px" />

ブランチと同様、このファイルにはタグの作成元となったコミットのハッシュが含まれています。しかし、ブランチファイルとは異なり、タグファイルは作業を進めても更新されず、作成元となった特定のコミットを指し続けます。

```bash
$ cat .git/refs/tags/mytag
92efc63e7150cc2924db956c2f540facfb46dfa2
```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2659159/55991549-b6ab-fcd4-e368-26a8a3560cdc.png" width="450px" />

## HEAD
HEAD とは、現在の作業ブランチの最新のコミットを指すポインタです。つまり、作業しているブランチの一番最新のコミットを示します。

Git は、現在作業しているブランチを認識するために HEAD を使用します。例えば、`git branch` を実行すると、Git は HEAD を見て、現在どのブランチにいるかを判断します。
また、次に行われるコミットの親コミットを参照する際にも HEAD が使用されます。コミットを作成すると、親コミットが新しいコミットに登録されます。

通常、master ブランチを使用している場合、HEAD は master ブランチを指しています。このとき、HEAD ファイルには `ref: refs/heads/master` と記載されています。
仮に `mybranch` というブランチに切り替えて `.git` フォルダー内の HEAD ファイルを開くと、`ref: refs/heads/mybranch` と記載されていることが確認できます。
したがって、HEAD はコミットを直接指すのではなく、ブランチの最新のコミットを指すポインターとして機能します。これにより、Git は現在チェックアウトされているコミットを追跡することができます。

```bash
### master ブランチに切り替え、HEAD ファイルを確認
$ git checkout master
Switched to branch 'master'
$ cat .git/HEAD
ref: refs/heads/master

### mybranch ブランチに切り替え、HEAD ファイルを確認
$ git checkout mybranch
Switched to branch 'mybranch'
$ cat .git/HEAD
ref: refs/heads/mybranch
```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2659159/95c32d64-cf31-7c8e-d3e9-b9809a6490cb.png" width="600px" />

ブランチでコミットを実行すると、Git は HEAD ファイルの内容を読み取り、親コミットとして参照するコミットを書き込むため、HEAD は間接的に次のコミットの親を提供していると言えます。

```bash
$ cat .git/refs/heads/mybranch
92efc63e7150cc2924db956c2f540facfb46dfa2

$ git cat-file -p 92efc63e7150cc2924db956c2f540facfb46dfa2
tree 02f94dfeb44bdbc4e5253e100b701b388a1a59fe
parent c262f5725e96f52c6bf1dbebcaf2680570fba434
author John Doe <johndoe@example.com> 1683663583 +0900
committer John Doe <johndoe@example.com> 1683663583 +0900

second commit
```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2659159/e363b9bf-725c-4964-37be-b7ef7c72b44d.png" width="650px" />

これにより、Git で以前のコミットにチェックアウトし、そこから変更を開始することができます。このモードは「分離モード」と呼ばれます。HEAD は、ブランチを参照するのではなく、直接特定のコミットを指します。

分離モードは、主にコミットのチェックアウトや修正、評価、比較を行う場合に使用されます。通常、HEAD がブランチを指している場合は、新しいコミットを作成すると、ブランチが自動的に移動し、新しいコミットがブランチに関連付けられます。しかし、分離モードでは、新しいコミットが作成されても、HEAD が指すコミットが変更されないため、新しいコミットはブランチに関連付けられない点に留意する必要があります。

## マージ
Git のマージは、異なるブランチで変更を加えたコミットを結合するプロセスです。これにより、コードの変更を統合し、ブランチの履歴を保持することができます。

Git には、主に　No-ff マージとFast-forward マージという2つのタイプがあります。

### No-ff マージ
No-ff マージは、2つのブランチが分岐している場合に発生します。この場合、Git は、2つの親を持つ新しいコミットを作成します。1つ目の親は現在のブランチの最新のコミットであり、2つ目の親はマージするブランチの最新のコミットです。この新しいコミットにより、両方のブランチの変更が反映されます。
No-ff マージでは、競合が発生する可能性があります。競合は、同じファイルまたは行を変更しようとする2つの変更がある場合に発生します。Git は、競合がある場合には手動で解決する必要があります。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2659159/a8bb5ab4-2e5b-4652-d871-500e6d990f1b.png" width="400px" />

### Fast-forward マージ
Fast-forward マージは、2つのブランチが分岐しており、ブランチ A がブランチ B を追い越していない場合に発生します。つまり、ブランチ A がブランチ B を継続している場合です。この場合、Git は、現在のブランチの最新のコミットを、マージするブランチの最新のコミットに単純に移動するだけです。これにより、2つのブランチの履歴が結合され、変更が反映されます。

Fast-forward マージでは、競合が発生することはありません。これは、マージするブランチに、現在のブランチにない新しい変更がないためです。このため、Fast-forward は、通常のマージよりも簡単であり、競合の解決が不要です。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2659159/d6bfe0bd-5cd7-3e7c-06b3-f757939d8820.png" width="400px" />

# 参考文献
- Webサイト, "How Git truly works", Alberto Prospero, https://towardsdatascience.com/how-git-truly-works-cd9c375966f6