.gitをGit管理してディレクトリの内部を簡単に調査してみた


# はじめに

今回の記事は、`.git`ディレクトリの内部を調査してみた。という体験談系の記事になります。
`.git`ディレクトリの中身がどうなっているのか少し気になる方を対象としています。

以下のGitコマンドを実行した際にディレクトリがどのように変化するのかをGit管理して簡単に調査しました。
ここに記載のない`git merge`などのコマンドやplumbingコマンドは今回の記事では取り扱いません。

- `git init`
- `git add`
- `git commit`
- `git branch`, `git checkout`


再三になりますが、かなり簡単な調査しか行なっていません。注意してください。

## 環境

- git: git version 2.42.0

# 空のリポジトリを作成

早速、`.git`ディレクトリの調査を開始したいと思います。

そもそもディレクトリが存在しなければ調査しようがないため、まずは空のプロジェクトルートで`git init`コマンドを実行します。
コマンドを実行したら`ls`コマンドでどのようなファイルやディレクトリが生成されたのかを確認したいと思います。

```bash
$ git init
$ ls .git
HEAD  branches  config  description  hooks  info  objects  refs
```

`git init`コマンドを実行すると、`.git`ディレクトリの中に上記のようなファイルやディレクトリが生成されていました。

これらのファイルやディレクトリの説明は[10.1 Gitの内側 - 配管（Plumbing）と磁器（Porcelain）](https://git-scm.com/book/ja/v2/Git%E3%81%AE%E5%86%85%E5%81%B4-%E9%85%8D%E7%AE%A1%EF%BC%88Plumbing%EF%BC%89%E3%81%A8%E7%A3%81%E5%99%A8%EF%BC%88Porcelain%EF%BC%89)というページに掲載されています。
上記リンクに掲載されている内容を表としてまとめたものが以下になります。

| .gitの中身  | 内容                                                         | 
| ----------- | ------------------------------------------------------------ | 
| description | GitWebプログラム用                                           | 
| config      | プロジェクト固有の設定ファイル                               | 
| info/       | グローバルレベルの除外設定ファイルが格納                     | 
| hooks/      | クライアントサイド・サーバーサイドのフックスクリプト         | 
| objects/    | すべてのコンテンツ                                           | 
| refs/       | コンテンツ内のコミットオブジェクトを指すポインタ（ブランチ） | 
| HEAD        | 現在チェックアウトしているブランチを指す                     | 
| index       | ステージングエリアの情報                                     | 

`branches`ディレクトリにように、環境によってはこの表に存在しないファイルが表示されることもあります。
逆に`index`ファイルは現状では生成されていません。このファイルはステージングエリアの情報を持つファイルなので何もインデックスに追加していない現在は不要なようです。

目的のディレクトリが無事に生成され、現状の中身も確認できたため、ここからディレクトリをGitで管理していきたいと思います。
`.git`ディレクトリに移動して新規リポジトリの作成と、現状までに生成された内容のコミットを行ないます。なお今後、`.git`ディレクトリで実行しているに関しては`.git $ <コマンド>`と表します。

```bash
.git $ git init
.git $ git add .
.git $ git commit -m "空のリポジトリを作成"
```

# ファイルのステージング

続いて、`git add`コマンドを実行してファイルをステージングした際に、`.git`ディレクトリがどのように変化するのかを確認していきます。

まずは適当な新規ファイルを作成してその変化を確認してみます。

```bash
$ echo 'text 1' > text1.txt
.git $ git status
nothing to commit, working tree clean
```

`git status`コマンドを実行した結果、新規ファイルを作成しただけではディレクトリに何の変化もないことが確認できました。

では上記で作成したファイルを`git add`コマンドでインデックスに追加して、ディレクトリにどのような変化が現れるのかを確認してみます。

```bash
$ git add text1.txt

.git $ git status -uall
Untracked files:
        index
        objects/a1/18c4fd81877803e4becf8fbf49642edc9ac688
```

ステージングエリアについての情報を持つ`index`ファイルが新たに生成され、また、`objects`ディレクトリにもハッシュ化された名前のファイルが生成されています。

`objects`ディレクトリの`a1`に格納されている`18c4fd81877803e4becf8fbf49642edc9ac688`ファイル（以降、オブジェクト）についてより詳しく見ていきましょう。

オブジェクトの詳細を確認するための`git cat-file`というコマンドが用意されているので、こちらを使用します。

このコマンドを実行する際はオブジェクトの名前を指定することになります。ただし、ここで指定するオブジェクト名はファイル名そのものではなく、オブジェクトが格納されているディレクトリの値も加える必要があります。
例えば今回のケースでは、`18c4fd81877803e4becf8fbf49642edc9ac688`オブジェクトが`a1`ディレクトリに格納されているので、オブジェクト名は`a118`となります。（このオブジェクト名で悩んだことが記事にすることを決めた理由の１つでもあります）

`git cat-file`コマンドのより詳細な情報が必要な方は[こちら](https://git-scm.com/docs/git-cat-file)を確認してください。

では早速、オブジェクトの詳細を見ていきたいと思います。

```bash
$ git cat-file -p a118
text 1
$ git cat-file -t a118
blob
```

`-p`オプションを指定することで、そのオブジェクトの内容を表示できます。text1.txtファイルには「text 1」と書き込んだので、まさしくその内容が表示されています。

`-t`オプションを指定するとコンテンツの代わりにオブジェクトのタイプが表示されます。今回のようなテキストファイルはブロブオブジェクトだとわかりました。
なおブロブオブジェクトには元のファイル名の情報が格納されていないようです。ではどこにその情報があるのかと言いますと、後々登場するツリーオブジェクトに存在します。

ファイルをステージングした際の`.git`ディレクトリの変化を確認できました。それではここまでの内容をコミットし、先に進みたいと思います。

```bash
.git $ git add .
.git $ git commit -m "初回ステージング"
```

# 初回コミット

続いて、`git commit`コマンドを実行してコミットした際に、ディレクトリがどのように変化するのかを観察していきたいと思います。

前回の章ですでにtext1.txtファイルをインデックスに追加してあるため、このままコミットします。

```bash
$ git status
Changes to be committed:
        new file:   text1.txt

$ git commit -m "初回コミット"
$ git log --oneline
2bcee38 (HEAD -> main) 初回コミット
```

`.git`ディレクトリに移動して、どのように変更したのかを確認してみます。

```bash
.git $ git status
Changes not staged for commit:
        modified:   index

Untracked files:
        COMMIT_EDITMSG
        logs/
        objects/2b/
        objects/c0/
        refs/
```

`index`ファイルが更新されている他、新たに複数の変化が確認できます。例えば`objects`ディレクトリには新たに`2b`と`c0`というサブディレクトリが作成されています。

まずは`COMMIT_EDITMSG`ファイルと`refs`ディレクトリに生成されたファイルの内容を確認してみます。

```bash
.git $ cat COMMIT_EDITMSG
初回コミット
.git $ cat refs/heads/main
2bcee38b06826f23bf2c9afe65bdd2973c34a263

$ git log --oneline
2bcee38 (HEAD -> main) 初回コミット
```

`COMMIT_EDITMSG`ファイルには直前のコミットメッセージが、`refs/heads/main`ファイルには何らかのオブジェクト名が記述されていました。
なお`refs/heads/main`ファイルに記載されているオブジェクトは、上記で`git log`コマンドを実行した際に表示されたオブジェクト名と一致しています。

では`2bcee38`オブジェクトの内容を確認してみます。

```bash
$ git cat-file -t 2bce
commit
$ git cat-file -p 2bce                                         
tree c0ba53ea459ad30a13b955d1dcf1dff2e0c467e3
author user <user@example.com> 1699229366 +0900
committer user <user@example.com> 1699229366 +0900

初回コミット
```

`2bcee38`オブジェクトはコミットオブジェクトと呼ばれるオブジェクトでした。このオブジェクトタイプは、スナップショットを誰が、いつ、どのような理由で保存したのか。という情報を持っています。

`2bcee38`オブジェクトの内容に目を向けてみると、先頭にトップレベルのツリーのオブジェクト名が表示されています。こちらのオブジェクトも`git cat-file`コマンドで内容を確認してみたいと思います。

```bash
$ git cat-file -t c0ba
tree
$ git cat-file -p c0ba
100644 blob a118c4fd81877803e4becf8fbf49642edc9ac688    text1.txt
```

ツリーオブジェクトには、モード（100644など）、オブジェクトタイプ、オブジェクト名、ファイル名が含まれていることが確認できます。ブロブオブジェクトが抱えていたファイル名の問題はこのツリーオブジェクトが解決してくれています。
なお今回の例ではファイルのみですが、管理対象にディレクトリが含まれる場合は、以下のようにサブツリーも表示されるようになります。

```bash
# 別のプロジェクトでの例
$ git cat-file -t a632
tree
$ git cat-file -p a632
040000 tree 7e31d175b6c4fb6633d81c5d75e3724ed5bbecac    .github
100644 blob b512c09d476623ff4bf8d0d63c29b784925dbdf8    .gitignore
```

`git commit`コマンド実行時の様子も確認できたため、ここまでの内容をコミットします。

```bash
.git $ git add .
.git $ git commit -m "初回コミット"
```

# 2度目のコミット

既存のファイルに変更を加えずにコミットした際の様子も確認してみます。

比較対象として、また別のファイルを作成します。

```bash
$ echo 'text 2' > text2.txt
$ git add .

.git $ git status
Changes not staged for commit:
        modified:   index
Untracked files:
        objects/e2/

$ git cat-file -p e263
text 2

.git $ git add .
.git $ git commit -m "新規ファイルステージング"
```

インデックスに追加すると`objects`ディレクトリが更新されました。
ここまでで、text1.txtファイルを表す`a118`オブジェクトには何の変化もありません。

ではコミットを行ないます。

```bash
$ git commit -m "text2.txt作成"

.git $ git status
Changes not staged for commit:
        modified:   COMMIT_EDITMSG
        modified:   index
        modified:   logs/HEAD
        modified:   logs/refs/heads/main
        modified:   refs/heads/main

Untracked files:
        objects/49/
        objects/a1/b6c7cffe7915762cbc358a7c440659b7745185
```

前の章で確認したように`refs`ディレクトリや`objects`ディレクトリが更新されています。

それではコミットオブジェクトとツリーオブジェクトの内容を確認してみたいと思います。

```bash
.git $ cat refs/heads/main                                  
a1b6c7cffe7915762cbc358a7c440659b7745185

$ git cat-file -p a1b6
tree 4970f83ccf395e09eda0ec57b1f87442839e252a
parent 2bcee38b06826f23bf2c9afe65bdd2973c34a263
author user <user@example.com> 1699273802 +0900
committer user <user@example.com> 1699273802 +0900

text2.txt作成

$ git cat-file -p 4970
100644 blob a118c4fd81877803e4becf8fbf49642edc9ac688    text1.txt
100644 blob e263913fb333115e7f9e5b6b7a38434ca5391d27    text2.txt
```

コミットオブジェクトには初回コミット時のオブジェクトと比べると新たに親コミットのオブジェクト名が表示されています。
またツリーオブジェクトには今回新たに作成したtext2.txtファイルのオブジェクトが確認できます。さらに、更新していないtext1.txtファイルは今までと同じオブジェクトであることも確認できました。

このことから、２度目以降のコミット時には親コミットの情報を保持し、さらに変更がないファイルには何も手を加えないことがわかりました。

この章でも`.git`ディレクトリにさまざまな変化が起こったので、これまでの内容をコミットします。

```bash
.git $ git add .
.git $ git commit -m "2度目のコミット"
```

# ブランチ操作

続いて、ブランチの作成やブランチの変更を行なった場合の変化を確認していきます。

`git branch develop`コマンドを実行して新たにdevelopブランチを作成します。

```bash
$ git branch
* main
$ git branch develop
$ git branch        
  develop
* main
```

ブランチを新たに作成したので、`.git`ディレクトリがどのように変更されているのかを確認してみます。

```bash
.git $ git status                     
Untracked files:
        logs/refs/heads/develop
        refs/heads/develop

.git $ cat refs/heads/develop     
a1b6c7cffe7915762cbc358a7c440659b7745185
.git $ cat refs/heads/main     
a1b6c7cffe7915762cbc358a7c440659b7745185
```

ブランチを作成すると`refs`ディレクトリ（と、`logs`ディレクトリ）に変更がかかることがわかりました。
またmainブランチをもとにブランチを作成したので、どちらも同じコミットオブジェクトを指しています。

続いて、ブランチを変更した場合どのような挙動になるのかを確認します。

```bash
.git $ git add .
.git $ git commit -m "ブランチ作成"

$ git checkout develop

.git $ git status
On branch main
Changes not staged for commit:
        modified:   HEAD
        modified:   logs/HEAD

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

ブランチを切り替えると、`HEAD`ファイル（と、`logs/HEAD`ファイル）に変更がかかっています。
`HEAD`ファイルの中身を確認してみると、HEADが切り替え先のdevelopブランチを指していることがわかりました。

ブランチ操作を行なうとGitは`HEAD`ファイルや`refs`ファイルなどに変更をかけることが確認できました。

# まとめ

今回の記事では`.git`ディレクトリをGit管理することで`.git`ディレクトリの内部がどのように変化するのかを確認し、その結果をまとめました。

今回の調査では`git init`コマンドなど数個のコマンドしか使いませんでしたが、その他のコマンドも試してみることでよりGitへの理解が深まるかもしれません。さらにGitには低レベルの処理を行なうためのplumbingコマンドなども用意されているため、こちらも合わせることでより内部を知ることができると思います。

機会があればその他のコマンドやplumbingコマンドも試してみたいと思います。

# 参考

https://git-scm.com/book/ja/v2