Gitの内部構造について

# はじめに

よく開発をするときにGitを使うけど、内部で何をしているのか知らなかったので、調べてみました。

# Gitの概要

Gitとは、Linus Torvaldsらが作ったバージョン管理システムである。
`git init`直後のファイル構成は以下のようになっている。

```bash
.git
├── HEAD
├── config
├── description
├── hooks
│   ├── applypatch-msg.sample
│   ├── commit-msg.sample
│   ├── fsmonitor-watchman.sample
│   ├── post-update.sample
│   ├── pre-applypatch.sample
│   ├── pre-commit.sample
│   ├── pre-push.sample
│   ├── pre-rebase.sample
│   ├── pre-receive.sample
│   ├── prepare-commit-msg.sample
│   └── update.sample
├── info
│   └── exclude
├── objects
│   ├── info
│   └── pack
└── refs
    ├── heads
    └── tags
```

- HEAD：現在のブランチの参照先
- objects：オブジェクトが保存されている
    - pack：packファイルが保存されている
- refs
    - heads：各ブランチの参照先が記述されたファイル
    - tags：各タグの参照先が記述されたファイル

# オブジェクトについて

オブジェクトとは、Gitにおける、バージョン管理の実態である。
Gitのコアの部分は、これらのオブジェクトをキーバリューで管理することによって実装されている。
オブジェクトは以下の4つ。

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

## blobオブジェクト

blobオブジェクトは、`git add`した際に生成されるオブジェクトであり、1つのblobオブジェクトにつき1つのファイルの情報を保持している。
例えば、以下のようなテキストファイルを作成した時

```hoge.txt
Hello World
```
blobオブジェクトの中身は、上のテキストファイルにヘッダーを付加した形となる。
```
blob 11\0Hello World
```
ここで、先頭の`blob`はこのオブジェクトがblobオブジェクトであることを示しており、`11`はテキストファイルの長さ、`\0`はヘッダーの終わりを表すnull文字である。

このコンテンツからblobオブジェクトを生成する。
まず `blob 11\0Hello World`に対するSHA-1チェックサムを行い、対応するSHA-１ハッシュを生成する。これがblobオブジェクトのパスとなる。
ただし、先頭2文字がサブフォルダ名、残りの38文字がファイル名となる。
この例の場合のパスは、`.git/objects/5e/1c309dae7f45e0f39b1bf3ac3cd9db12e7d689`である。

オブジェクトファイルの中身は、コンテンツをzlibで圧縮したものとなる。
ただし、blobオブジェクトにはファイルの内容だけ保存されており、ファイル名やファイルのメタデータなどは保存されていない。

## treeオブジェクト

treeオブジェクトは、`git commit`した際に生成されるオブジェクトである。
treeオブジェクトは、複数のファイルをまとめて保存するオブジェクトであり、ファイルシステムのような構造をしている。

treeオブジェクトに格納されるコンテンツはblobオブジェクトまたは他のtreeオブジェクトである。
例えばファイル構成が以下のとき、
```
.
├── hoge.txt
│
└─ foo
    └─ bar.txt
```
トップレベルに対応する、treeオブジェクトのコンテンツは以下の通りになる。
```
040000 tree dc452338139a83cf4372c4c988c6b0a1de0f9066    foo
100644 blob 5e1c309dae7f45e0f39b1bf3ac3cd9db12e7d689    hoge.txt
```

このtreeオブジェクトが生成される際に、fooに対応するtreeオブジェクト(dc452338139a83cf4372c4c988c6b0a1de0f9066)も生成されており、bar.txtのblobオブジェクトはそっちのtreeオブジェクトで参照されている。

blobオブジェクトと同様に、このコンテンツにヘッダーである`tree`とコンテンツの長さを付与し、SHA-1チェックサムと圧縮が行われ、treeオブジェクトファイルが生成される。

オブジェクトファイルの置かれる場所は`objects/`配下でblobオブジェクトと同じ場所である。

## commitオブジェクト

コミットを行うと、treeオブジェクトが生成されたのちに、commitオブジェクトが生成される。
commitオブジェクトはひとつのcommitを表すオブジェクトである。
commitオブジェクトのコンテンツは以下の通り

```
tree b59c2e7d63b3d44fac2fbf1864362a54dd87f3a6
parent 7c087a97087525b208e7d81e7fafdd950835e99b
author username <email@example.com> 1707822072 +0900
committer username <email@example.com> 1707822072 +0900

first commit
```

`tree`は先ほど生成したトップレベルのtreeオブジェクトを参照している。`parent`は親commitのcommitオブジェクトを表している。その後に`author`と`commiter`、それとコミットメッセージが格納されている。

これまでのオブジェクトと同様にcommitオブジェクトもヘッダーを付与、SHA-1チェックサム、圧縮してオブジェクトファイルとして生成される。

commitオブジェクトには親commitハッシュが含まれていることで改ざんされていないことがわかる。途中のcommitオブジェクトが改ざんされてしまうと、それ以下のcommitオブジェクトをすべて変える必要があるためである。
そのため途中のcommitオブジェクトを改ざんすると、最新のcommitが別物となってしまう。

これは実質ブロックチェーンのような仕組みである。

## tagオブジェクト

tagオブジェクトとはタグを管理するオブジェクトである。
タグには注釈付きのタグと軽量版のタグがある。

注釈付きのタグのtagオブジェクトのコンテンツは以下の通り。

```
object 7c087a97087525b208e7d81e7fafdd950835e99b
type commit
tag v1.0
tagger username <email@example.com> 1707898087 +0900

my version 1.0
```

`object`はタグをつけたcommitオブジェクトを示しており、`type`でcommitオブジェクトであることを表している。その後ろに、タグ名とユーザー情報、注釈が記述されている。

他のオブジェクトと同様にヘッダーを付与して、オブジェクトファイルとして生成される。

また、この時にtagファイルが`refs/tags`以下にタグ名をファイル名として生成される。
ファイルの中身はtagオブジェクトのSHA-1ハッシュである。（生成したtagオブジェクトのディレクトリパスを表している。）

軽量版のタグの場合はオブジェクトが生成されず、tagファイルのみが生成される。
この時のファイルの中身は注釈付きのタグとは異なり、参照するcommitオブジェクトのSHA-1ハッシュが記述されている。

# HEADとブランチ

Gitでは開発の本流から分岐し、本流の邪魔をしないようにする機能としてブランチを実装している。
これにより、並行に作業を進めることが可能となっている。
ブランチから他のブランチへ移動することも可能であり、様々な場所で作業をすることが可能である。

HEADとは今自分がどこにいるのかを表しているものである。

HEADの実態は`.git/HEAD`ファイルである。
ファイルの中身はdetached HEADの状態であるかどうかによって異なる。

detached HEADの状態の場合。つまり、HEADが既存のブランチに紐づけられていない場合。
HEADファイルの中身は、現在作業しているcommitオブジェクトのSHA-1ハッシュが記述されている。

一方、ブランチに紐づけられている場合は、以下のようにブランチファイルのパスが記述されている。

```
refs: refs/heads/master
```

ブランチを作ると、`refs/heads`以下に指定したブランチ名をファイル名として持つファイルが作成される。
`feature`ブランチを作った場合は`refs/heads/feature`のようにファイルが作られる。
この時、ブランチ名にバックスラッシュを含めると、`refs/heads/hoge/foo`のようにファイルが生成されるため、この場合は`hoge`ブランチを追加で作成することができなくなる。

ブランチファイルの中身は参照しているcommitオブジェクトのSHA-1ハッシュが記述されている。

なお、HEADファイルとブランチファイルの中身はどちらも、圧縮などはされておらず、ファイルパスまたはSHA-1ハッシュがそのままの形で記述されている。

チェックアウトをする際は、HEADファイルの中身を変更している。
他のブランチにチェックアウトするときは、そのブランチファイルのパス。
直接commitオブジェクトにチェックアウトするときは、そのSHA-1ハッシュに変更する。

なお、Gitでは通常、コミットした時点でのファイルの情報をすべてスナップショットのように保存しているため、チェックアウトする時に該当するcommitオブジェクト以下のオブジェクトのみを参照するだけで、その時の状態を再現できる。
そのため、差分などを保持している場合と比較して、高速に切り替えることが可能となっている。

# Gitの内部構造

## ステージングの仕組み

`git add`を実行すると、対象のファイルがステージングされる。

この時、内部では以下のことが行われている。

1. 対象ファイルのblobオブジェクトを生成
2. 生成したblobオブジェクトの情報をインデックスに登録

インデックスの実態は`.git/index`であり、以下のような情報を持っている。

- ファイルの種類+パーミッション
- blobハッシュ
- コンフリクトフラグ（コンフリクトが起こっていない場合は0）
- ファイル名

インデックスにはこれに加えて様々な情報が格納されている。詳しくは下のリンク先で。

https://github.com/git/git/blob/v2.12.0/Documentation/technical/index-format.txt

ここまでが`git add`を実行した時に内部で起こっていることである。

## コミットの仕組み

`git commit`を実行すると以下のことが内部で行われる。

1. indexからtreeオブジェクトを生成
2. commitオブジェクトを生成
3. HEADを新しいcommitハッシュに書き換え

コミットすると、インデックスの情報からリポジトリのルートディレクトリを含む全ディレクトリ分のtreeオブジェクトを自動で作る。

treeオブジェクトの生成が終了すると、トップレベルのtreeオブジェクトに紐づけられたcommitオブジェクトが生成される。

その後、HEADおよび現在のブランチの参照先が新しいcommitオブジェクトに更新される。

# おわりに

ここまで読んでいただきありがとうございます。
説明しやすいように、自分で勝手に作った単語などがあり、実際に使われている単語とは異なる場合があるのでご注意ください。

なにか間違いや気になる点がありましたら、ぜひコメントおねがいします。


# 参考文献
https://www.youtube.com/watch?v=-4rcs6SgT0o&t=2515s&ab_channel=CAMPHOR-

https://github.com/git/git/blob/v2.12.0/Documentation/technical/index-format.txt

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://qiita.com/tatane616/items/dbad66179754be57d2e2