Gitオブジェクト
Gitはバージョン管理に必要なデータを主に「オブジェクト」と呼ばれる概念で表現し、.git/objects
ディレクトリで管理しています。
オブジェクトには以下の4種類があります。
-
blobオブジェクト
- ファイルの情報が入っているオブジェクト(バックアップ)
-
treeオブジェクト
- ディレクトリ情報が入っているオブジェクト
-
commitオブジェクト
- コミットの情報が入っているオブジェクト
-
tagオブジェクト
- annotated tagの情報が入っているオブジェクト
そして、Gitは一種のKey-Value Storeとしてこれらのオブジェクトを管理しています。
Gitでは、Keyはオブジェクトの中身に対するSHA-1ハッシュ値から作られ、Valueはオブジェクトの中身です。一連の流れは以下の通りです。
- それぞれのオブジェクト(Value)を作成
- オブジェクトの中身をSHA-1ハッシュ化した値をKeyとする
-
オブジェクトをzlib圧縮した上で、
.git/objects/
以下にKey名でファイルとして保存-
この時、検索効率性のために、Keyの先頭2文字でサブディレクトリを切っています
-
この時、検索効率性のために、Keyの先頭2文字でサブディレクトリを切っています
以下は.git/objects/
の一例です。Keyを使えばいつでもオブジェクトの中身(Value)にアクセスできるようになっています。
.git/ └── objects ├── 2c │ ├── 14dff3277ae4751173f3c26802620de960aaa1 │ └── 4e28fab2bb84c8af445b0c81f7e7fd27817faf ├── bc │ └── 96df368963ca37edb955c1c0403471b7c90720 ├── f9 │ ├── 1c1a8a1e11f3147eee31b2bd9f9fb52936f87b │ ├── 595a6fb7692405a5c4a10e1caf93d7a5bd9c37 │ └── ccb461cbfb1837f611b5e31a070945b8cdb867 ├── info └── pack
それでは次に、それぞれのオブジェクトについて説明していきます。こちらのリポジトリの.git
ディレクトリの内容に沿って解説していくので、必要に応じて手元にcloneしてください。ここで、Gitのオブジェクトファイルは圧縮された状態で転送されるようになっています(packfile)。そのため、cloneだけでなく以下の手順で解凍する必要があるので注意してください。
# git cloneする $ git clone git@github.com:tonouchi510/git-chapter-3.git # 空のリポジトリを用意する $ mkdir temp && cd temp $ git init # 先ほどcloneしたリポジトリのpackfileをunpackし、objectファイルを展開する $ git unpack-objects < ../git-chapter-3/.git/objects/pack/pack-3a78e8c8bdf27096f21963d7891a4e5236841060.pack # 元のリポジトリにオブジェクトファイルを転送 $ find .git/objects -not -name 'pack' -a -not -name 'info' -type d -mindepth 1 | xargs -I {} mv {} ../git-chapter-3/.git/objects/ $ cd ../git-chapter-3
これで今回参照したいobjectsはすべて取り出せた状態になります。
基本構成
オブジェクトは、最初にオブジェクトの種類やサイズなどのメタデータが記録され、そのあとにコンテンツが続きます。
-
オブジェクトの種類:
blob/tree/commit/tag
- オブジェクトのサイズ:数値(単位はbyte)
- オブジェクトのコンテンツ:オブジェクトの種類によってさまざま
blob オブジェクト
blobオブジェクトは、ファイルの実際のバックアップに当たるオブジェクトです。実際に、
Key=f733735eb5e737a9c15756e0556f2155660fa4b0
のオブジェクトについて見ていきます(README.mdに対応するblobオブジェクト)。
このままだとzlib圧縮されていて分からないため、デコードして表示します(※環境によってはzlibは使えない可能性があるため、その場合は後述するcat-fileで代替してください)。
$ cat .git/objects/f7/33735eb5e737a9c15756e0556f2155660fa4b0 | zlib d blob 17# git-tutorial-3
基本構成で説明したように、ファイルの先頭にオブジェクトの種類とファイルサイズをつけたものが記録されていることがわかります。次に、Key=c6c63202bf8abde33a80dfc0a230ad1e9791a33b
のオブジェクトについても見てみます。
$ cat .git/objects/c6/c63202bf8abde33a80dfc0a230ad1e9791a33b | zlib d blob 16# git-chapter-3
この2つはどちらもREADME.md
についてのblobオブジェクトになります。ここで、blobオブジェクトはファイルの差分ではなく、ある時点でのファイルの中身そのものを記録していることが分かるかと思います。ここはGitの機能を語る上で重要な要素なので、よく覚えておいてください。
ちなみに、オブジェクトの内容を見たい場合は、cat-file
という便利なサブコマンドもあるので、そちらも使ってみてください。
# tオブションはオブジェクトの種類 $ git cat-file -t c6c63202bf8abde33a80dfc0a230ad1e9791a33b blob # sオブションはオブジェクトのサイズ $ git cat-file -s c6c63202bf8abde33a80dfc0a230ad1e9791a33b 16 # pオブションはオブジェクトの中身 $ git cat-file -p c6c63202bf8abde33a80dfc0a230ad1e9791a33b # git-chapter-3
tree オブジェクト
次に、treeオブジェクト(Key=3f1c1a8a1e11f3147eee31b2bd9f9fb52936f87b
)を見てみます。
$ git cat-file -t 3f1c1a8a1e11f3147eee31b2bd9f9fb52936f87b tree $ git cat-file -s 3f1c1a8a1e11f3147eee31b2bd9f9fb52936f87b 82 $ git cat-file -p 3f1c1a8a1e11f3147eee31b2bd9f9fb52936f87b 100644 blob bc96df368963ca37edb955c1c0403471b7c90720 chapter_1.txt 100644 blob aaa8bd84e6e4837be33eee9d1ddb9d06d8d2e46c chapter_2.txt
treeオブジェクトはディレクトリとしての情報を保持しています。こちらはreports
ディレクトリのある時点のtreeオブジェクトです。このtreeオブジェクトが作られた時点で、そのディレクトリに存在したファイルと、そのバージョンのblobハッシュ値(Key)が記録されています。このKeyから該当のblobオブジェクトにアクセスできます。
ちなみにリポジトリのルートディレクトリに対応するtreeオブジェクトはこのようになってます。
$ git cat-file -p f9ccb461cbfb1837f611b5e31a070945b8cdb867 100644 blob c6c63202bf8abde33a80dfc0a230ad1e9791a33b README.md 040000 tree 3f1c1a8a1e11f3147eee31b2bd9f9fb52936f87b reports 100644 blob 9daeafb9864cf43055ae93beb0afd6c7d144bfa4 test.txt
これを見ると、先ほどのreports
ディレクトリに関するtreeオブジェクトへの参照(Key=3f1c1a8a1e11f3147eee31b2bd9f9fb52936f87b
)も保持していることが分かると思います。ルートディレクトリのtreeオブジェクトがあれば、そこから辿ることでリポジトリの全てのファイルにアクセスすることができます。つまり、Gitはファイルシステムになっているのです。
commit オブジェクト
次に、commitオブジェクト(Key=bdcc6040408bacfb69727c0f2637bbcec1dbc487
)を見ていきます。
$ git cat-file -p bdcc6040408bacfb69727c0f2637bbcec1dbc487 tree f9ccb461cbfb1837f611b5e31a070945b8cdb867 parent 63c833da2ed9fd43d3f000f35d2ee141ba42e411 author tonouchi510 <xxxxx@gmail.com> 1670215632 +0900 committer tonouchi510 <xxxxx@gmail.com> 1670215632 +0900 Add chapter_2.txt
記載されている情報を整理すると、commitオブジェクトに含まれている情報は以下の通りです。
- リポジトリのルートディレクトリのtreeオブジェクトのハッシュ値(Key)
- 親commitのハッシュ値
- committerとauthorのタイムスタンプ・名前・メールアドレス
- コミットメッセージ
これらのいずれか1つでも変わると、(SHA-1が衝突しない限り)別のcommitハッシュになります。そのためコミットは一意に定まると言えます。また、親コミットのハッシュも含んでいるため、改ざんに強いという特徴もあります。
コミットの仕組み
ここまで説明した3つのオブジェクトを使えば、コミットの仕組みを実現できます。コミットには大きく3つの段階があります。
- コードを編集する
- 編集したコードをaddする
- commitする
ステップ2で編集したコードをadd
しています。この時Git内部では何が行われているのでしょうか。教科書的な回答として、「コミットに含めたいファイルをindexに登録している」という説明を見たことがあるかもしれません。
indexとは
具体的にどういうことか見ていくために、もう一つだけ新しい概念としてindexについて説明します。indexの情報は.git/index
に記録されているので、オブジェクトと同じように見ていきます。
$ cat .git/index DIRCc�[��c�c�[�d:�KCH��S_X�/�S��2����:����0����; README.mdc�b�WF;c�b�Te�KK���S_X�/�S����6�c�7�U��@4q�� reports/chapter_1.txtc�b��uc�b��9KK���S_X�/�S0������{�>����lreports/chapter_2.txtc�w�2�c�w�2�Ka��S_X�/�S�����L�0U��������D�test.txtTREE94 1 �̴a��7��� E�greports2 0 ?��~�1�����)6�{�\��(l ����y}ܗ8.cA
バイナリファイルなので、そのままcatするだけだと文字化けしてしまいます。indexの中身を見るためのサブコマンドも用意されているので、そちらで確認してみます。
$ git ls-files --stage 100644 c6c63202bf8abde33a80dfc0a230ad1e9791a33b 0 README.md 100644 bc96df368963ca37edb955c1c0403471b7c90720 0 reports/chapter_1.txt 100644 aaa8bd84e6e4837be33eee9d1ddb9d06d8d2e46c 0 reports/chapter_2.txt 100644 9daeafb9864cf43055ae93beb0afd6c7d144bfa4 0 test.txt
左から、ファイルの種類+パーミッション、blobハッシュ、コンフリクトフラグ、ファイル名が並んでいます。indexには、現在参照しているバージョンのすべてのファイルへの参照をもっています。
addの内部動作
addした時に起こる内部挙動は、基本的に以下の2点です。
- indexの更新
- blobオブジェクトの生成
例えば、適当にファイルを作成してaddすると、indexは以下のように更新されています。
$ echo hoge > temp/hoge.txt $ git add temp/hoge.txt $ git ls-files --stage 100644 c6c63202bf8abde33a80dfc0a230ad1e9791a33b 0 README.md 100644 bc96df368963ca37edb955c1c0403471b7c90720 0 reports/chapter_1.txt 100644 aaa8bd84e6e4837be33eee9d1ddb9d06d8d2e46c 0 reports/chapter_2.txt 100644 9c595a6fb7692405a5c4a10e1caf93d7a5bd9c37 0 temp/hoge.txt
temp/hoge.txt
がindexに追加されていることがわかります。また、blobハッシュも記録されており、該当Keyを見にいくとblobオブジェクトも生成されていることが分かると思います。すでにあるファイルを編集した場合も、新しいblobオブジェクトが生成され、そのハッシュ値に書き変わります。
ここで注意点として、新たにディレクトリが追加されてはいますが、add
の段階ではまだtreeオブジェクトの生成は行いません。
commit
次に、commit
した時に起こる内部挙動は、以下の通りです。
- indexからtreeオブジェクトを生成
- commitオブジェクトを生成
- HEADを新しいcommitハッシュに書き換え
それぞれ詳しく解説していきます。まず、treeオブジェクトの生成です。commitすると差分だけでなく、リポジトリのルートディレクトリを含むすべてのディレクトリのtreeオブジェクトを構築します。
ただしこの時、indexに変更があった部分だけが新しいblobおよびtreeオブジェクトに書き換えられ、それ以外の参照が変わらない部分はそのまま流用します。
新しいルートディレクトリまでのtreeオブジェクトが生成できたら、次はcommitオブジェクトを作成します。
先ほども述べたように、commitオブジェクトはリポジトリのルートにあたるtreeオブジェクトを参照しているため、そこからリポジトリ全体をたどることができ、commit時点のリポジトリの状態を再現できるようになっています。
また、参照しているblobオブジェクト(ファイル)も差分バックアップではなく、変更があったファイルのフルバックアップでした。つまり、コミットとは**ある時点のスナップショット**になっていると言えます。これがコミットの全貌になります。
差分を記録しているわけではないため、あるコミット(バージョン)に遷移する際に、逐一コミットをたどって差分を適用しているわけではありません。Gitがcheckoutなどで別のコミットに移った時に瞬時にその状態を復元できるのはこれが理由です。Gitではバージョン間を遷移する際にかかる時間は、履歴の遠さには依存せず、変更されたファイル数に依存するのです。
最後に、現在参照しているコミットを指す.git/HEAD
を必要に応じて書き換えます。
$ cat .git/HEAD ref: refs/heads/main
HEADが特定のコミットハッシュでなく、このようにブランチを参照しているので、.git/refs/heads/main
の参照しているコミットハッシュを書き換えます。以上がcommitまでの一連の処理になります。
tag オブジェクト
最後に、他の3つのオブジェクトに比べたらあまり重要ではありませんが、tagオブジェクトについても解説しておきます。それに関連して、まずGitのrefsについて説明しておきます(こちらは重要です)。
refsについて
refsは特定のコミットを指すポインタのようなものです。コミットハッシュのエイリアスとも言えます。具体的には以下のものがrefsに該当します。
-
tag
- light-weight tag
- annotated tag
-
branch
- HEAD(すでにcommitの仕組みで登場)
それぞれ順に見ていきます。light-weight tag
を作成し、確認してみます。タグを作ると、.git/refs/tags/
以下に保存されます。
$ git tag test-1 $ git log -n 1 --oneline bdcc604 (HEAD -> main, tag: test-1) Add chapter_2.txt $ cat .git/refs/tags/test-1 bdcc6040408bacfb69727c0f2637bbcec1dbc487
次にbranch
です。こちらは基本的にはlight-weight tag
と同様です。違いとしては、保存場所が.git/refs/heads
以下になっている点と、branchの場合はそのbranchでコミットを実行すると、指しているコミットハッシュが自動で書き変わっていくことです。
$ cat .git/refs/heads/main bdcc6040408bacfb69727c0f2637bbcec1dbc487
次にHEAD
です。HEAD
は先ほども言った通り、現在のcommitを指すrefsです。
.git/HEAD
に保存されており、commit
やcheckout
などで書き変わります。また、branch
名でcheckout
した時はコミットハッシュではなくbranch
の場所が書き込まれるようになっています。
最後に、annotated tag
について説明します。他のrefsと違い、こちらはtagオブジェクトとして保存されます。といっても、実態としては単にコメントが付けられるタグというだけです。一応中身を見ていきます。
$ git tag -a test-annotated -m "message" $ git log -n 1 --oneline bdcc604 (HEAD -> main, tag: test-annotated, tag: test-1) Add chapter_2.txt $ cat .git/refs/tags/test-annotated 50b0321cc154b323e6df2eaf31a8559f3a1e6dc8
他のrefsと違い、コミットハッシュではなくtag
オブジェクトのハッシュが記録されています。オブジェクトなので、cat-file
サブコマンドを使って中身を見てみます。
$ git cat-file -t 50b0321cc154b323e6df2eaf31a8559f3a1e6dc8 tag $ git cat-file -p 50b0321cc154b323e6df2eaf31a8559f3a1e6dc8 object bdcc6040408bacfb69727c0f2637bbcec1dbc487 type commit tag test-annotated tagger tonouchi510 <xxxxx@gmail.com> 1670236054 +0900 message
commitオブジェクトに似た構造になっています。あまり必要性は高くないかもしれませんが、メッセージなどの情報を記録したい場合にはannotated tag
の使用も検討してみてください。
commitの仕組みまとめ
ここまでで、全てのオブジェクトと、commitの仕組みを解説できました。commitの流れを再度まとめておきます。
- コードを編集する
-
編集したコードをaddする
- addしたファイルのblobオブジェクトの生成
- indexの更新
-
commitする
- リポジトリ全体のtreeオブジェクトの生成
- commitオブジェクトを生成
-
HEADの書き換え
ここまでは、commitまでの観点のみで、オブジェクトやrefsの使われ方を紹介しましたが、他のサブコマンドも基本的にはこれらの参照や書き換えで実現されています。
まとめ
Gitの内部構造について、大枠の部分を紹介しました。Gitへの抵抗感を払拭できたでしょうか。
内部構造を知った今では、(あまりないとは思いますが)Gitが壊れた時に、.git/
ディレクトリ以下からオブジェクトの参照をたどったり、書き換えたりなど、自力での復旧もやろうと思えばできてしまうはずです(※実際MIXI社内ではそういった事例も過去に発生したようです)。
Git入門シリーズとして3回に渡りGitの解説をしてきましたが、本記事で最後となります。Git理解への一助となれたら幸いです。