.git の中を観察してみた

前から .git の中がどのように変わっていくかを調べてみたいと思っていたところ、同僚が .git の中に .git を作って差異を観察したら面白いのではというアイディアを提供してくださったので、それをやってみました！

なお、前提条件として、ざっくり Git がある程度どのようなデータ構造を持っているかを知識として持った上で調べています。

過去の記事: [Git のデータ構造を図で整理](https://qiita.com/Teloo/items/85f36d9dbf20f915324f)

## はじめに

以下のコマンドの実行時にどのように .git の中身が遷移していくかをみていきました。

```
$ git init
$ echo "Hello World!" > test.txt
$ git add test.txt
$ git commit -m 'First commit!'
$ git branch feature
$ git checkout feature
$ git remote add origin <リポジトリのURL>
$ git checkout master
$ git push origin feature
```

なお、使用する Git のバージョンは v2.17.1 です。

## git init

git-sample というディレクトリを作成して、`git init` して、.git の中身を確認してみます。
なお、ディレクトリとファイルの区別がつくように、tree コマンドの結果でディレクトリだったものには末尾に「/」を追記しています。

```bash
$ mkdir git-sample
$ cd git-sample
$ git init
Initialized empty Git repository in /home/user/projects/git-sample/.git/
$ cd .git
$ tree
.
├── HEAD
├── branches/
├── 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 の中身をみてみます。
まだ存在していませんが、.git/refs 以下の master ブランチを指していると思われるファイルパスが書かれています。

```bash
$ cat HEAD
ref: refs/heads/master
```

次にconfig の中身をみてみます。
このリポジトリの設定情報が書かれています。

```bash
$ cat config
[core]
        repositoryformatversion = 0
        filemode = true
        bare = false
        logallrefupdates = true
```

description の中身をみてみます。
このリポジトリの説明を書くような雰囲気を感じますが、用途は不明です。

```bash
$ cat description 
Unnamed repository; edit this file 'description' to name the repository.
```

info/exclude の中身をみてみます。
.gitignore みたいなファイルのようにみえるのですが、.gitignore とはまた別の機能なのでしょうか？

```bash
$ cat info/exclude
# git ls-files --others --exclude-from=.git/info/exclude
# Lines that start with '#' are comments.
# For a project mostly in C, the following would be a good set of
# exclude patterns (uncomment them if you want to use them):
# *.[oa]
# *~
```

branches/, objects/, refs/ は現状空ディレクトリですが、これからの操作で変わっていくものだと思います。
hooks/ は git のコマンドを実行したときのフックを設定する箇所で今現在はサンプルのみが置いてあるようですね。

## 準備

以降、.git の差異を確認できるように .git 下で git init をして初回コミットしておきましょう。

```bash
$ pwd
/home/user/projects/git-sample/.git
$ git init
$ git add -A
$ git commit -m 'git init'
$ cd ..
```

以降、細かくコマンドの実行までは記載しませんが、これを利用して .git の差異を観察しています。


## ステージングエリアへの追加

ステージングエリアにファイルを追加したときの.git の差異を観察します。

```bash
$ echo "Hello World!" > test.txt
$ git add test.txt
```

.git の変化を確認したところ、差異は以下の 2 つでした。

```
new file:   index
new file:   objects/98/0a0d5f19a64b4b30a87d4206aade58726b60e3
```

index はバイナリファイルで中身の確認は難しいのですが、git add によって何らかの形式でステージングの状態を保存しているものだと推測します。

後者の object について確認します。
test.txt の中身を指しているようなので、blob 形式のオブジェクトのようです。

```bash
$ git cat-file -p 980a0d5f19a64b4b30a87d4206aade58726b60e3
Hello World!
```

## コミット

コミット時の .git の差異を観察します。

```bash
$ git commit -m 'First commit!'
```

.git の変化を確認したところ、差異は以下の 7 つでした。

```
new file:   COMMIT_EDITMSG
modified:   index
new file:   logs/HEAD
new file:   logs/refs/heads/master
new file:   objects/2a/222bc765ac38f3a056a088a2640c0ae4a85c5e
new file:   objects/37/6357880b048faf2553da6bc58ae820cea3690a
new file:   refs/heads/master
```

COMMIT_EDITMSG について確認します。
コミットしたときのコミットメッセージの内容が記録されているようでした。

```bash
$ cat COMMIT_EDITMSG
First commit!
```

index はバイナリファイルであり中身の確認は難しいのですが、コミットしたことで HEAD の状態とワークツリーの間に差分がなくなったので、そのために更新されたのではないかと推測しています。

logs に追加されたファイルを確認します。
それぞれ HEAD やブランチで初めて設定された commit オブジェクトのキーが出力されています。

```bash
$ cat logs/HEAD
0000000000000000000000000000000000000000 2a222bc765ac38f3a056a088a2640c0ae4a85c5e ***** <*******@************> 1577066716 +0900       commit (initial): First commit!
$ cat logs/refs/heads/master 
0000000000000000000000000000000000000000 2a222bc765ac38f3a056a088a2640c0ae4a85c5e ***** <*******@************> 1577066716 +0900       commit (initial): First commit!
```

objects に追加されたファイルを確認します。
片方は今回のコミットを表す commit 形式のオブジェクト、もう一方はそのコミットのルートディレクトリを指す tree 形式のオブジェクトのようです。

```bash
$ git cat-file -p 2a222bc765ac38f3a056a088a2640c0ae4a85c5e
tree 376357880b048faf2553da6bc58ae820cea3690a
author ***** <*******@************> 1577066716 +0900
committer ***** <*******@************> 1577066716 +0900

First commit!
$ git cat-file -p 376357880b048faf2553da6bc58ae820cea3690a
100644 blob 980a0d5f19a64b4b30a87d4206aade58726b60e3    test.txt
```

## ブランチの作成

ブランチ作成時の .git の差異を観察します。

```bash
$ git branch feature
```

.git の変化を確認したところ、差異は以下の 2 つでした。

```
new file:   logs/refs/heads/feature
new file:   refs/heads/feature
```

logs に追加されたファイルを確認します。
feature ブランチが作成されたことが記録されているようです。

```bash
$ cat logs/refs/heads/feature
0000000000000000000000000000000000000000 2a222bc765ac38f3a056a088a2640c0ae4a85c5e ***** <*******@************> 1577067962 +0900       branch: Created from master
```

refs/heads/feature に追加されたファイルを確認します。
2a222bc765ac38f3a056a088a2640c0ae4a85c5e は前述のコミットしたオブジェクトを指すキーですので、おそらく feature ブランチはこのコミットを指していることを表しているようです。

```bash
$ cat refs/heads/feature 
2a222bc765ac38f3a056a088a2640c0ae4a85c5e
```

## ブランチの切り替え

ブランチ切り替え時の .git の差異を観察します。

```bash
$ git checkout feature
```

.git の変化を確認したところ、差異は以下の 2 つでした。

```
modified:   HEAD
modified:   logs/HEAD
```

HEAD の差異を確認します。
「refs/heads/master」から「ブランチの作成」の項で追加された「refs/heads/feature」ファイルへ内容が変わったようです。

```diff
@@ -1 +1 @@
-ref: refs/heads/master
+ref: refs/heads/feature
```

logs に追加されたファイルを確認します。
HEAD を master ブランチから feature ブランチに切り替えたことが記録されているようです。

```diff
--- a/logs/HEAD
+++ b/logs/HEAD
@@ -1 +1,2 @@
 0000000000000000000000000000000000000000 2a222bc765ac38f3a056a088a2640c0ae4a85c5e ***** <*******@************> 1577066716 +0900        commit (initial): First commit!
+2a222bc765ac38f3a056a088a2640c0ae4a85c5e 2a222bc765ac38f3a056a088a2640c0ae4a85c5e ***** <*******@************> 1577068724 +0900        checkout: moving from master to feature
```

## リモートリポジトリの追加

リモートリポジトリの追加時の .git の差異を観察します。

```bash
$ git remote add origin git@github.com:*****/git-sample.git
```

.git の変化を確認したところ、差異は以下の 1 つでした。

```
modified:   config
```

config の差異を確認します。
リモートリポジトリ origin の URL と fetch したときのブランチ情報を .git のどこに作成するかが追記されているようです。

```diff
@@ -3,3 +3,6 @@
        filemode = true
        bare = false
        logallrefupdates = true
+[remote "origin"]
+       url = git@github.com:*****/git-sample.git
+       fetch = +refs/heads/*:refs/remotes/origin/*
```

## プッシュ

master プッシュ時の .git の差異を観察します。

```bash
$ git checkout master
$ git push origin feature
```

.git の変化を確認したところ、差異は以下の 2 つでした。

```
new file:   logs/refs/remotes/origin/master
new file:   refs/remotes/origin/master
```

logs に追加されたファイルを確認します。
origin/master ブランチが push に伴って更新されたことが記録されているようです。

```bash
$ cat logs/refs/remotes/origin/master
0000000000000000000000000000000000000000 2a222bc765ac38f3a056a088a2640c0ae4a85c5e ***** <*******@************> 1577070100 +0900       update by push
```

refs/remotes/origin/master を確認します。
2a222bc765ac38f3a056a088a2640c0ae4a85c5e は前述の commit オブジェクトを指すキーですので、おそらく origin/master ブランチはこのコミットを指していることを定義しているのではないかと推測されます。

```bash
$ cat refs/remotes/origin/master
2a222bc765ac38f3a056a088a2640c0ae4a85c5e
```

## 最終結果

最終的には .git はこうなりました。

```bash
$ tree
.
├── COMMIT_EDITMSG
├── HEAD
├── branches/
├── 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
├── index
├── info/
│   └── exclude
├── logs/
│   ├── HEAD
│   └── refs/
│       ├── heads/
│       │   ├── feature
│       │   └── master
│       └── remotes/
│           └── origin/
│               └── master
├── objects/
│   ├── 2a/
│   │   └── 222bc765ac38f3a056a088a2640c0ae4a85c5e
│   ├── 37/
│   │   └── 6357880b048faf2553da6bc58ae820cea3690a
│   ├── 98/
│   │   └── 0a0d5f19a64b4b30a87d4206aade58726b60e3
│   ├── info/
│   └── pack/
└── refs/
    ├── heads/
    │   ├── feature
    │   └── master
    ├── remotes/
    │   └── origin/
    │       └── master
    └── tags/
```

## まとめ

実施したサンプルが少ないですが、ここまでの結果から以下のように推測しています。

* .git/HEAD
    * 今現在の使用中のブランチを表す refs/ 以下のファイルが記載されている
    * git checkout などのブランチ切替時に更新される
* .git/index
    * ステージングエリアに追加したときや、コミット時などのタイミングで更新される
* .git/logs
    * HEAD や refs/ 以下のファイルがどのように変わっていたかの履歴が記録される
* .git/object/
    * 各種オブジェクトが登録される
    * ステージングエリアに追加したときに、blob オブジェクトが追加される
    * コミットしたときに、ルートの tree オブジェクトと commit オブジェクトが追加される
* .git/refs/heads/
    * ローカルブランチの作成時に、commit オブジェクトのキーが書かれたブランチ名のファイルが作成される
    * 初回コミット時に、新しい commit オブジェクトのキーが書かれたブランチ名のファイルが作成される
* .git/refs/remotes/<リモートリポジトリ名>/
    * 初回 push 時に、新しい commit オブジェクトのキーが書かれたブランチ名のファイルが作成される

もちろん、ワークツリーやリポジトリの状態によっては、異なる動作になるかもしれませんので断定はできません。
ですが、実際の挙動を追ってみたことで、ある程度 .git の中がどのように変化していくのかの推測はできるようになったかなという感じです。