Gitの内部はどうなっているのか

## はじめに

現在参画しているプロジェクトではソースコードをGitで管理していて、日々修正した結果をコミットしています。
ですが、Gitがどういう仕組みで履歴を管理しているか知りませんでした。

コマンドの使い方だけでなく内部の仕組みも理解しようと思い、調べたことをまとめました。

## Gitオブジェクト

Gitでは以下4種類のオブジェクトでファイルの変更履歴を管理しています。
これらをまとめて**Gitオブジェクト**と呼びます。

* blobオブジェクト
* commitオブジェクト
* treeオブジェクト
* tagオブジェクト

Gitオブジェクトについて順に解説していきます。

## リポジトリを作成

`commit_test`リポジトリを作成します。

```sh
$ git init commit_test
$ cd commit_test
```

`commit_test`フォルダ内に`.git`フォルダがあります。
`.git`フォルダ内は以下の様な構成になっています。

```sh
$ ls -1 .git
HEAD
branches
config
description
hooks
info
objects
refs
```

当記事内で主に解説するのは`objects`ディレクトリと`refs`ディレクトリです。
`refs`ディレクトリにはブランチやタグの情報が保存されます。
`objects`ディレクトリにはGitオブジェクトが保存されます。
初期状態では空のディレクトリ以外は何も保存されていません。

```
$ ls -R .git/objects
.git/objects:
info  pack

.git/objects/info:

.git/objects/pack:
```

## blobオブジェクト

2つのテキストファイルを追加してステージングします。

```sh
$ echo 'Hello World!!' > test1.txt
$ mkdir sub
$ echo 'Hello World!!' > sub/test2.txt
$ git add test1.txt sub/test2.txt
```

すると、`objects`ディレクトリ内にディレクトリとファイルが追加されます。
この新たに追加されたファイルは**blobオブジェクト**と言い
先ほどステージングしたファイルを特殊な形式に変換したものです。

```sh
$ ls -R .git/objects
...
.git/objects/93:
6977184a9fa89d82f86957a90b92d4924b6573
...
```

長い英数字はSHA1のハッシュ値であり、ステージングしたファイルにヘッダーを付与したデータから計算します。

`.git/objects`直下にハッシュ値の先頭2桁を名前としたディレクトリを作成します。
Gitオブジェクトはそのディレクトリ内に保存されています。
ハッシュ値の残り38桁がGitオブジェクトのファイル名になります。
同階層に大量のファイルが保存されるのを避けるため、Gitオブジェクトは上記の方法で保存されています。

Gitオブジェクトのハッシュ値は中身のデータに以下の様なヘッダーを付けて計算します。
ヘッダーの先頭の文字はオブジェクトの種類によって異なります。

`blob {対象ファイルのサイズ}\0`

ヘッダー付きの`text1.txt`のハッシュ値を実際に計算して確認します。

```sh
$ echo -en 'blob 14\0Hello World!!\n' | sha1sum
936977184a9fa89d82f86957a90b92d4924b6573  -
```

Gitオブジェクトはヘッダー付きのデータを`zlib`というライブラリで圧縮して保存されています。
Gitオブジェクトの中身は`git cat-file -p <ハッシュ値>`で確認することができます。

```
$ git cat-file -p 936977184a9fa89d82f86957a90b92d4924b6573
Hello World!!
```

また、先ほど2つのファイルをステージングしましたがblobオブジェクトは1つしか追加されていません。
これは2つのファイルの中身が同じであるためです。
中身が同じならばハッシュ値も同じになるため、1つのblobオブジェクトの追加で済みます。
この仕組みのおかげでストレージの使用量を抑えることができます。

## indexファイル

ファイルをステージングすると`.git`直下に`index`ファイルが作成されます。
`index`ファイルはステージングされたファイルの情報を持っています。
`git ls-files --stage`で中身を確認できます。

```sh
$ git ls-files --stage
100644 936977184a9fa89d82f86957a90b92d4924b6573 0    sub/test2.txt
100644 936977184a9fa89d82f86957a90b92d4924b6573 0    test1.txt
```

2つのファイルが同一のblobオブジェクトを指していることが確認できます。
このように特定のGitオブジェクトのハッシュ値を指すことを**ポインタ**と呼びます。

## commitオブジェクト

次にコミットをします。

```
$ git commit -m "First commit"
[main (root-commit) e4bb332] First commit
 2 files changed, 2 insertions(+)
 create mode 100644 sub/test2.txt
 create mode 100644 test1.txt
```

`.git/objects`内を見ると新たに3つのGitオブジェクトが追加されています。

```sh
$ ls -R .git/objects
...
.git/objects/46:
725691241905fc43ef670324d4a64b85e98758
...
.git/objects/d1:
f3392222847bfa3a0ee464a96b12a777a941b2

.git/objects/e4:
bb33236b376911ae5f075343dc094211f73d69
...
```

`e4bb33236b376911ae5f075343dc094211f73d69`のGitオブジェクトの中身を確認します。

```sh
$ git cat-file -p e4bb33236b376911ae5f075343dc094211f73d69
tree 46725691241905fc43ef670324d4a64b85e98758
author Test User <test_user@example.com> 1665907306 +0900
committer Test User <test_user@example.com> 1665907306 +0900

First commit
```

これは**commitオブジェクト**であり、以下の情報を持っています。

* 1つ前のcommitオブジェクト(親コミット)へのポインタ
* treeオブジェクトへのポインタ
* 作成者の情報
* コミットメッセージ

※上記は最初のコミットのため親コミットへのポインタは持っていません
コミットIDとは、このcommitオブジェクトのハッシュ値を指しています。

また、コミットすると`.git/refs/heads`にブランチの情報が保存されます。

```
$ ls .git/refs/heads/
main
```

## treeオブジェクト

**treeオブジェクト**とはファイルの階層構造を管理するためのGitオブジェクトです。
中身を見ると以下のようになっています。

```sh
$ git cat-file -p 46725691241905fc43ef670324d4a64b85e98758
040000 tree d1f3392222847bfa3a0ee464a96b12a777a941b2    sub
100644 blob 936977184a9fa89d82f86957a90b92d4924b6573    test1.txt
```

treeオブジェクトは他のGitオブジェクトへのポインタを持っています。
左から順に以下の様になっています。

* パーミッション
* オブジェクトの種類
* ハッシュ値
* 名前

blobオブジェクトは自身のファイル名を持たないため、treeオブジェクトがファイル名を管理しています。
treeオブジェクトの階層構造を表すと以下のようになっています。

```
tree 46725691241905fc43ef670324d4a64b85e98758
 ┃
 ┣━ tree d1f3392222847bfa3a0ee464a96b12a777a941b2 sub
 ┃   ┃
 ┃   ┗━ blob 936977184a9fa89d82f86957a90b92d4924b6573 test2.txt
 ┃
 ┗━ blob 936977184a9fa89d82f86957a90b92d4924b6573 test1.txt
```

## tagオブジェクト

タグには**注釈付きタグ**と、**軽量タグ**の2種類があります。
まずは注釈付きタグを追加します。

```
$ git tag -a annotated -m "Annotated tag"
```

すると`.git/object`内に新たにGitオブジェクトが1つ追加されます。

```sh
$ ls -R .git/objects
...
.git/objects/69:
2d5fdea7e0e929154459f590d5505de1cd1ea5
...
```

中身は以下の様になっています。

```
$ git cat-file -p 692d5fdea7e0e929154459f590d5505de1cd1ea5
object e4bb33236b376911ae5f075343dc094211f73d69
type commit
tag annotated
tagger Test User <test_user@example.com> 1665907510 +0900

Annotated tag
```

これは**tagオブジェクト**であり、以下の情報を持っています。

* 他のGitオブジェクトへのポインタ
* オブジェクトの種類
* タグ名
* 作成者の情報
* メッセージ

タグを作成すると`.git/refs/tags`にGitオブジェクトへのポインタが保存されています。
このファイルは圧縮されていないため、`cat`コマンドで中身を確認できます。

```
$ ls .git/refs/tags
annotated
$ cat .git/refs/tags/annotated 
692d5fdea7e0e929154459f590d5505de1cd1ea5
```

注釈タグはtagオブジェクトへのポインタです。

### 軽量タグの場合

軽量タグの場合は、tagオブジェクトは追加されません。

```
$ git tag lightweight
$ cat .git/refs/tags/lightweight 
e4bb33236b376911ae5f075343dc094211f73d69
```

軽量タグはcommitオブジェクトのポインタです。

## ブランチ

Gitオブジェクトの仕組みを理解するとブランチについても理解することができます。
`main`ブランチの中身を確認します。

```
$ cat .git/refs/heads/main
e4bb33236b376911ae5f075343dc094211f73d69
```

ブランチはcommitオブジェクトへのポインタです。
軽量タグと似ていますが、ブランチはコミットするたびに常に新しいハッシュ値に変わります。

「ブランチ」という名前から枝のイメージが先行して過去の履歴を保持していると考えてしまいますが、実体は特定の1つのコミットしか指していません。

## HEAD

`.git/HEAD`ファイルには現在作業中のブランチへのポインタが保存されています。

```
$ cat .git/HEAD 
ref: refs/heads/main
```

ブランチを切り替えるとポインタも変わります。

```
$ git checkout -b branch1
Switched to a new branch 'branch1'
$ cat .git/HEAD 
ref: refs/heads/branch1
```

## 終わりに

Gitの内部ではハッシュ値や圧縮を使ってファイルを効率良く管理していることが分かりました。
内部を理解しても普段の作業が劇的に変わるわけではないですが、Gitの作業で困った時に学んだ知識が役に立つのではと考えています。

## 参考文献

https://git-scm.com/book/ja/v2/Git%E3%81%AE%E5%86%85%E5%81%B4-Git%E3%82%AA%E3%83%96%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88

https://zenn.dev/kaityo256/articles/objects_of_git

https://ja.wikipedia.org/wiki/Zlib

https://ja.wikipedia.org/wiki/SHA-1