.git配下のディレクトリ構成と挙動を理解する
はじめに
こんにちは!noruです。
普段何気なく利用しているGitですが、その内部の仕組みについて疑問を抱いたことはありませんか?以前、コミットハッシュについて調べたのですが、時間が経ち忘れてしまいました。そこで、本記事ではGitの内部の仕組みを再確認し、アウトプットしたいと思います。
小話
「エンジニアたるもの、まずは一次情報を確認しないと!」と言うことで、公式ドキュメントとリポジトリを確認しました。すると、READMEの冒頭に面白いことが書いてあったので紹介します。
「GIT - the stupid content tracker」
翻訳すると「GIT - 愚かなコンテンツトラッカー」みたいですね。スラングの意味は、是非READMEを覗いて確認してみてください。続くGitの説明も結構パンチが効いていて好きですし、開発者Linus Torvalds氏のユーモアセンスが爆発しています。また、Gitは2週間で開発・リリースされたみたいです。Linuxも開発されてますし凄いですね。
事前準備
まずは全体像を把握したいので、ディレクトリ構成から見ていきましょう。
.git
配下にGitの管理情報が格納されています。事前準備として、sample-git
というディレクトリを作成&移動し、git init
を実行してリポジトリを作成します。
$ mkdir sample-git && cd sample-git
$ git init
Initialized empty Git repository in /Users/noru/project/sample-git/.git/
$ ls -a
. .. .git
.git
が生成されました。ここにGit情報が格納されています。
git init
実行時に、デフォルトだとリポジトリのGit情報は.git
に格納されますが、--separate-git-dir
オプションを使うことで格納パスを変更することもできます。
下記は.git
ではなく、.git-1
を格納パスに設定しています。
$ git init --separate-git-dir .git-1
Initialized empty Git repository in /Users/noru/project/sample-git/.git-1/
$ ls -a
. .. .git .git-1
また、Git情報が格納されるディレクトリパスはgit rev-parse --git-dir
コマンドで確認できます。
$ git rev-parse --git-dir
/Users/noru/project/sample-git/.git
ディレクトリ構成
準備が整いました。
それでは、tree .git
コマンドでディレクトリ構成を見ていきましょう。
$ tree .git
.git
├── HEAD
├── config
├── hooks
│ ├── commit-msg
│ ├── pre-commit
│ └── prepare-commit-msg
├── objects
│ ├── info
│ └── pack
└── refs
├── heads
└── tags
8 directories, 5 files
.git
配下にはHEAD, config, hooks, objects, refsの5つがありますね。
1つずつ確認していきたいと思います。
.git配下のディレクトリ・ファイル
HEAD
.git/HEAD
ファイルにはカレントブランチを指すシンボリック参照を格納します。
$ cat .git/HEAD
ref: refs/heads/main
ブランチを切り替えると、格納されているシンボリック参照も変更されます。
$ git switch -c branch-1
Switched to a new branch 'branch-1'
$ cat .git/HEAD
ref: refs/heads/branch-1
config
リポジトリ固有の設定ファイルです。
$ cat .git/config
[core]
repositoryformatversion = 0
filemode = true
bare = false
logallrefupdates = true
ignorecase = true
precomposeunicode = true
core
の各設定項目は下記のようになっています。
また、core以外にも、remoteやbranch、pull等の設定もできます。
設定名 | 概要 |
---|---|
repositoryformatversion | Gitリポジトリのフォーマットバージョンを表します。通常は0で設定されます。 |
filemode | ファイルのパーミッション管理を表します。実行ビットの変更を追跡 or 無視するか設定します。 |
bare | ベアリポジトリかどうかを表します。ベアリポジトリはワーキングディレクトリを持たず、ノンベアリポジトリはワーキングディレクトリを持っています。 |
logallrefupdates | .git/logs配下に全ての参照の更新を記録するかどうかを表します。 |
ignorecase | ファイルシステム上の大文字と小文字を区別するかどうかを表します。 |
precomposeunicode | MacOSでのファイル名のUnicode正規化をどのように扱うかを表します。 |
hooks
特定のイベントに基づいて自動でスクリプトを実行できます。
Gitには複数のフックがあり、それぞれがGitワークフローの異なるステージでトリガーされる仕様です。いくつかのフックを紹介します。
フック名 | 概要 |
---|---|
pre-commit | コミットメッセージが入力される前に実行されます。 コミットされるスナップショットを検査したり、テストが実行できるか確認したり、何かしらコードを検査する目的で使用されます。 |
commit-msg | コミットを確定する前に実行されます。コミットメッセージがチームの標準に準拠しているかチェックするのに適しています。 |
prepare-commit-msg | コミットメッセージエディターが起動する直前、デフォルトメッセージが生成された直後に実行されます。デフォルトメッセージを、コミットの作者の目に触れる前に編集できます。 |
objects
リポジトリに関連付けられたオブジェクトを格納する格納領域 (オブジェクトストア) です。
Gitのコア部分がオブジェクトで、シンプルなキー・バリュー型データストアで構成されています。
合計4種類 (blob, tree, commit, tag) 存在し、キーはSHA-1ハッシュで採番されます。.git/objects
配下にblob, tree, commitオブジェクト、.git/refs/tags
配下にtagオブジェクトが格納されます。まずは、.git/objects
配下の3オブジェクトを紹介します。
オブジェクト名 | 概要 |
---|---|
blob | ファイルを表すオブジェクトです。ファイル内容のみ格納し、ファイル名は格納されません。ファイル内容が異なる場合は、新規のblobオブジェクトが生成されます。対象のファイル名が異なっていてもファイル内容が同じであれば、同じハッシュ値が採番され、効率が良いファイル管理を実現しています。 |
tree | ディレクトリを表すオブジェクトです。blobオブジェクトや他のtreeオブジェクトのポインタ・ファイル名・ディレクトリ名を格納します。これらを使用することでディレクトリツリーを実現します。 |
commit | コミットを表すオブジェクトです。変更時のメタデータを格納します。コミットの日付、ログメッセージ、コミッター等のメタデータとディレクトリ・ファイル情報を紐づけるため、treeオブジェクトへの参照を格納します。 |
refs
特定のcommitオブジェクトを指すポインタを管理しています。
.git/refs
配下のディレクトリ構成は下記になっていて、tagオブジェクトと各branch (ローカルブランチ、リモートブランチ等) が格納されています。順に見ていきましょう。
$ tree .git/refs/
.git/refs/
├── heads
│ └── main
├── remotes
│ └── origin
│ ├── HEAD
│ └── main
└── tags
5 directories, 3 files
tag
まずは、4つ目のオブジェクトであるtagを紹介します。
オブジェクト名 | 概要 |
---|---|
tag | コメント付きタグを表すオブジェクトです。タグ名やコメント等の情報とオブジェクトへのポインタを格納します。 |
branch
次にbranchを見ていきます。
branchはローカルとリモートで別々に管理されています。
ディレクトリ名 | 概要 |
---|---|
heads | ローカルブランチを管理します。 |
remotes | リモートブランチを管理します。 |
中身を確認するとcommitハッシュ値が格納されています。
heads, remotesどちらも各ブランチ名のファイルが存在し、その中に最新のcommitハッシュ値が格納されます。
$ cat .git/refs/heads/main
8ab0b23f892bf0d9582f6d434acb1de9e2fc2f8d # ローカルブランチの最新commitハッシュ値
$ cat .git/refs/remotes/origin/main
8ab0b23f892bf0d9582f6d434acb1de9e2fc2f8d # リモートブランチの最新commitハッシュ値
$ git log -n 1
commit 8ab0b23f892bf0d9582f6d434acb1de9e2fc2f8d (HEAD -> main, origin/main, origin/HEAD) # 同様のcommitハッシュ値を差している
Author: noru <sample@exsample.com>
Date: Wed Oct 9 20:00:00 2024 +0900
README追加
また、.git/refs/remotes/origin/HEAD
にはリモートブランチのシンボリック参照が格納されています。
$ cat .git/refs/remotes/origin/HEAD
ref: refs/remotes/origin/main
Gitコマンド実行時の挙動確認
.git
配下の構造がある程度把握できたので、実際にgitコマンドを実行して挙動を確認していきましょう。git init
実行後のまっさらな状態から差分を見ていきます。
$ tree .git/
.git/
├── FETCH_HEAD
├── HEAD
├── config
├── hooks
│ ├── commit-msg
│ ├── pre-commit
│ └── prepare-commit-msg
├── objects
│ ├── info
│ └── pack
└── refs
├── heads
└── tags
8 directories, 6 files
git add
実行すると下記が行われます。
- blobオブジェクトの生成
- indexファイルの生成・更新
README.md
を作成&ステージングに上げて確認してみましょう。
$ git add .
$ git status
On branch main
No commits yet
Changes to be committed:
(use "git rm --cached <file>..." to unstage)
new file: README.md
$ tree .git/
.git/
├── FETCH_HEAD
├── HEAD
├── config
├── hooks
│ ├── commit-msg
│ ├── pre-commit
│ └── prepare-commit-msg
├── index # git addでinxexが生成された
├── objects
│ ├── 1a
│ │ └── ca66460224ce78bc701c2cdaebdf2fb6941c73 # git addでblobオブジェクトが生成された
│ ├── info
│ └── pack
└── refs
├── heads
└── tags
9 directories, 8 files
blobオブジェクトの生成
blobオブジェクトを確認してみます。
-t
オプションでオブジェクトの種類、-p
オプションで中身を出力できます。
$ git cat-file -t 1aca66460224ce78bc701c2cdaebdf2fb6941c73
blob
$ git cat-file -p 1aca66460224ce78bc701c2cdaebdf2fb6941c73
# READMEの中身です%
indexファイルの生成・更新
ステージングされたファイルは、.git/index
に置かれます。中身を確認してみましょう。
$ git ls-files --stage
100644 1aca66460224ce78bc701c2cdaebdf2fb6941c73 0 README.md
1aca66460224ce78bc701c2cdaebdf2fb6941c73
の部分が生成されたblobオブジェクトのハッシュ値です。.git/objects
配下を確認すると生成されたディレクトリとファイル名が1aca66460224ce78bc701c2cdaebdf2fb6941c73
と一致します。
検索効率向上のために、先頭2文字の1a
がディレクトリとして生成され、その中に残りのca66460224ce78bc701c2cdaebdf2fb6941c73
がファイルとして置かれます。
git commit
実行すると下記が行われます。
- COMMIT_EDITMSGにコミットメッセージを格納
- オブジェクト生成
- tree
- commit
- refs配下のコミットハッシュを更新
- logs生成・変更履歴を更新
実際にcommitして差分を確認してみましょう。
$ git commit -m "first commit"
[main (root-commit) b30cc67] first commit
1 file changed, 1 insertion(+)
create mode 100644 README.md
$ tree .git/
.git/
├── COMMIT_EDITMSG # git commitで生成された
├── FETCH_HEAD
├── HEAD
├── config
├── hooks
│ ├── commit-msg
│ ├── pre-commit
│ └── prepare-commit-msg
├── index
├── logs # git commitで生成された
│ ├── HEAD
│ └── refs
│ └── heads
│ └── main
├── objects
│ ├── 03
│ │ └── e7ce4d1727e3355c89e7407f301606d499e726
│ ├── 91
│ │ └── 9442a4e2b40579c4fecf5b2680f59de861579e # git commitで生成された
│ ├── b3
│ │ └── 0cc67360f2f1854fa973d71c2b633929e1da92 # git commitで生成された
│ ├── info
│ └── pack
└── refs
├── heads
│ └── main
└── tags
14 directories, 14 files
COMMIT_EDITMSGにコミットメッセージを格納
最新のコミットメッセージが格納されます。
コミットメッセージを書き換えると、.git/COMMIT_EDITMSG
の中身も更新されます。
$ cat .git/COMMIT_EDITMSG
first commit
オブジェクト生成
生成された.git/objects
のオブジェクトタイプを確認してみましょう。
コミットハッシュ値は、919442a4e2b40579c4fecf5b2680f59de861579e
とb30cc67360f2f1854fa973d71c2b633929e1da92
です。
$ git cat-file -t 919442a4e2b40579c4fecf5b2680f59de861579e
tree
$ git cat-file -t b30cc67360f2f1854fa973d71c2b633929e1da92
commit
treeオブジェクトとcommitオブジェクトが生成されていますね。
今度は各オブジェクトの中身を確認します。まずはtreeオブジェクトから見てみましょう。
$ git cat-file -p 919442a4e2b40579c4fecf5b2680f59de861579e
100644 blob 03e7ce4d1727e3355c89e7407f301606d499e726 README.md
blobオブジェクトへの参照を持っていることが分かります。
treeオブジェクトは、blobオブジェクトとtreeオブジェクトの参照を持っており、ツリー構造を表現します。また、100644
の部分はパーミッションモードを意味しています。
モード名 | 概要 |
---|---|
100644 | 実行できないファイル |
100755 | 実行可能なファイル |
040000 | ディレクトリ |
120000 | シンボリックリンク |
160000 | Gitlink |
次はcommitオブジェクトを見ていきましょう。
$ git cat-file -p b30cc67360f2f1854fa973d71c2b633929e1da92
tree 919442a4e2b40579c4fecf5b2680f59de861579e
author noru <sample@exsample.com> 1728623994 +0900
committer noru <sample@exsample.com> 1728623994 +0900
first commit
treeオブジェクト (919442a4e2b40579c4fecf5b2680f59de861579e
) と紐づいていることが分かります。commitするとリポジトリ内の全てのtreeオブジェクトとblobオブジェクトを自動で生成してスナップショットを撮っています。そのため、git checkout
を実行したときにコード状態を迅速に戻すことができます。他にもコミットメッセージやユーザー情報等が格納されています。
refs配下のコミットハッシュを更新
.git/refs
配下のheads
はローカルブランチ、remotos
にはリモートブランチが入り、それぞれ最新のコミットハッシュが格納されていると解説しました。commitされたので、.git/refs/heads
配下のコミットハッシュも更新されます。
$ cat .git/refs/heads/main
b30cc67360f2f1854fa973d71c2b633929e1da92
logs配下の変更履歴を更新
commitしたことで.git/logs
が生成されました。
Gitリポジトリの参照履歴が格納されます。.git/logs
配下のパスは下記となっています。
.git/logs配下のパス | 概要 |
---|---|
HEAD | 現在のHEADの変更履歴を格納します。 |
refs/heads/ | 各ローカルブランチの変更履歴を格納します。 |
refs/remotes/ | 各リモートブランチの変更履歴を格納します。 |
stash | スタッシュの変更履歴を格納します。 |
生成された.git/logs/refs/heads/main
の中身を覗いてみましょう。
$ cat .git/logs/refs/heads/main
0000000000000000000000000000000000000000 b30cc67360f2f1854fa973d71c2b633929e1da92 noru <sample@exsample.com> 1728623994 +0900 commit (initial): first commit
初回コミットなので、直前のコミット履歴は存在しません。
0000000000000000000000000000000000000000
は、直前のコミットが存在しないことを表しています。また、b30cc67360f2f1854fa973d71c2b633929e1da92
の部分はcommitハッシュ値を表しています。
git remote add
add, commitとローカルの挙動を確認してきました。
次はリモートにpushしてみたいと思います。ですが、git init
でリポジトリを作成したので、リモート設定をしていません。git remote add
を実行して.git/config
にリモート設定を追加します。
$ git remote add origin git@github.com:noru/sample-git.git
$ cat .git/config
[core]
repositoryformatversion = 0
filemode = true
bare = false
logallrefupdates = true
ignorecase = true
precomposeunicode = true
[remote "origin"] # git remote addで追加された
url = git@github.com:noru/sample-git.git
fetch = +refs/heads/*:refs/remotes/origin/*
git push
実行すると下記が行われます。
- config更新
- refs/remotes生成・更新
- logs/refs/remotes生成
実際にpushして差分を確認してみましょう。
$ git push origin main
Enumerating objects: 3, done.
Counting objects: 100% (3/3), done.
Writing objects: 100% (3/3), 238 bytes | 238.00 KiB/s, done.
Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
To github.com:noru/sample-git.git
* [new branch] main -> main
$ tree .git/
.git/
├── COMMIT_EDITMSG
├── FETCH_HEAD
├── HEAD
├── config # git pushで更新された
├── hooks
│ ├── commit-msg
│ ├── pre-commit
│ └── prepare-commit-msg
├── index
├── logs
│ ├── HEAD
│ └── refs
│ ├── heads
│ │ └── main
│ └── remotes # git pushで生成された
│ └── origin
│ └── main
├── objects
│ ├── 03
│ │ └── e7ce4d1727e3355c89e7407f301606d499e726
│ ├── 91
│ │ └── 9442a4e2b40579c4fecf5b2680f59de861579e
│ ├── b3
│ │ └── 0cc67360f2f1854fa973d71c2b633929e1da92
│ ├── info
│ └── pack
└── refs
├── heads
│ └── main
├── remotes # git pushで生成された
│ └── origin
│ └── main
└── tags
18 directories, 16 files
config更新
.git/config
にリモートブランチの情報が追加されていることが確認できます。
$ cat .git/config
[core]
repositoryformatversion = 0
filemode = true
bare = false
logallrefupdates = true
ignorecase = true
precomposeunicode = true
[remote "origin"]
url = git@github.com:noru/sample-git.git
fetch = +refs/heads/*:refs/remotes/origin/*
[branch "main"] # git pushで追加された
remote = origin
merge = refs/heads/main
refs/remotes生成・更新
リモートにmainブランチをpushしたので、.git/refs/remotes/origin/main
が生成されました。中身を確認してみましょう。
$ cat .git/refs/remotes/origin/main
b30cc67360f2f1854fa973d71c2b633929e1da92
commitしたハッシュ値と同じであることが分かります。
$ git cat-file -p b30cc67360f2f1854fa973d71c2b633929e1da92
tree 919442a4e2b40579c4fecf5b2680f59de861579e
author noru <sample@exsample.com> 1728623994 +0900
committer noru <sample@exsample.com> 1728623994 +0900
first commit
logs/refs/remotes生成・更新
.git/logs
配下にリモートブランチのディレクトリとファイルが生成されました。
中身はコミットハッシュ値が格納されています。
$ cat .git/logs/refs/remotes/origin/main
0000000000000000000000000000000000000000 b30cc67360f2f1854fa973d71c2b633929e1da92 noru <sample@exsample.com> 1728804161 +0900 update by push
おわりに
Gitのディレクトリ構成を把握し、実際にコマンドを実行して差分を確認することで、内部の仕組みや挙動の理解度が深まりました。皆さまのGitのトラブルシューティングに役立つと嬉しいです。最後まで読んでいただきありがとうございました!
Discussion