はじめに
よく開発をするときにGitを使うけど、内部で何をしているのか知らなかったので、調べてみました。
Gitの概要
Gitとは、Linus Torvaldsらが作ったバージョン管理システムである。
git init
直後のファイル構成は以下のようになっている。
.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つのファイルの情報を保持している。
例えば、以下のようなテキストファイルを作成した時
Hello World
blobオブジェクトの中身は、上のテキストファイルにヘッダーを付加した形となる。
blob 11\0Hello World
ここで、先頭のblob
はこのオブジェクトがblobオブジェクトであることを示しており、11
はテキストファイルの長さ、\0
はヘッダーの終わりを表すnull文字である。
このコンテンツからblobオブジェクトを生成する。
まず blob 11\0Hello World
に対するSHA-1チェックサムを行い、対応するSHA-1ハッシュを生成する。これが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
を実行すると、対象のファイルがステージングされる。
この時、内部では以下のことが行われている。
- 対象ファイルのblobオブジェクトを生成
- 生成したblobオブジェクトの情報をインデックスに登録
インデックスの実態は.git/index
であり、以下のような情報を持っている。
- ファイルの種類+パーミッション
- blobハッシュ
- コンフリクトフラグ(コンフリクトが起こっていない場合は0)
- ファイル名
インデックスにはこれに加えて様々な情報が格納されている。詳しくは下のリンク先で。
ここまでがgit add
を実行した時に内部で起こっていることである。
コミットの仕組み
git commit
を実行すると以下のことが内部で行われる。
- indexからtreeオブジェクトを生成
- commitオブジェクトを生成
- HEADを新しいcommitハッシュに書き換え
コミットすると、インデックスの情報からリポジトリのルートディレクトリを含む全ディレクトリ分のtreeオブジェクトを自動で作る。
treeオブジェクトの生成が終了すると、トップレベルのtreeオブジェクトに紐づけられたcommitオブジェクトが生成される。
その後、HEADおよび現在のブランチの参照先が新しいcommitオブジェクトに更新される。
おわりに
ここまで読んでいただきありがとうございます。
説明しやすいように、自分で勝手に作った単語などがあり、実際に使われている単語とは異なる場合があるのでご注意ください。
なにか間違いや気になる点がありましたら、ぜひコメントおねがいします。
参考文献