はじめに
現在参画しているプロジェクトではソースコードをGitで管理していて、日々修正した結果をコミットしています。
ですが、Gitがどういう仕組みで履歴を管理しているか知りませんでした。
コマンドの使い方だけでなく内部の仕組みも理解しようと思い、調べたことをまとめました。
Gitオブジェクト
Gitでは以下4種類のオブジェクトでファイルの変更履歴を管理しています。
これらをまとめてGitオブジェクトと呼びます。
- blobオブジェクト
- commitオブジェクト
- treeオブジェクト
- tagオブジェクト
Gitオブジェクトについて順に解説していきます。
リポジトリを作成
commit_test
リポジトリを作成します。
$ git init commit_test
$ cd commit_test
commit_test
フォルダ内に.git
フォルダがあります。
.git
フォルダ内は以下の様な構成になっています。
$ 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つのテキストファイルを追加してステージングします。
$ echo 'Hello World!!' > test1.txt
$ mkdir sub
$ echo 'Hello World!!' > sub/test2.txt
$ git add test1.txt sub/test2.txt
すると、objects
ディレクトリ内にディレクトリとファイルが追加されます。
この新たに追加されたファイルはblobオブジェクトと言い
先ほどステージングしたファイルを特殊な形式に変換したものです。
$ ls -R .git/objects
...
.git/objects/93:
6977184a9fa89d82f86957a90b92d4924b6573
...
長い英数字はSHA1のハッシュ値であり、ステージングしたファイルにヘッダーを付与したデータから計算します。
.git/objects
直下にハッシュ値の先頭2桁を名前としたディレクトリを作成します。
Gitオブジェクトはそのディレクトリ内に保存されています。
ハッシュ値の残り38桁がGitオブジェクトのファイル名になります。
同階層に大量のファイルが保存されるのを避けるため、Gitオブジェクトは上記の方法で保存されています。
Gitオブジェクトのハッシュ値は中身のデータに以下の様なヘッダーを付けて計算します。
ヘッダーの先頭の文字はオブジェクトの種類によって異なります。
blob {対象ファイルのサイズ}\0
ヘッダー付きのtext1.txt
のハッシュ値を実際に計算して確認します。
$ 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
で中身を確認できます。
$ 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オブジェクトが追加されています。
$ ls -R .git/objects
...
.git/objects/46:
725691241905fc43ef670324d4a64b85e98758
...
.git/objects/d1:
f3392222847bfa3a0ee464a96b12a777a941b2
.git/objects/e4:
bb33236b376911ae5f075343dc094211f73d69
...
e4bb33236b376911ae5f075343dc094211f73d69
のGitオブジェクトの中身を確認します。
$ 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オブジェクトです。
中身を見ると以下のようになっています。
$ 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つ追加されます。
$ 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の作業で困った時に学んだ知識が役に立つのではと考えています。
参考文献