2019-06-29
gitの仕組み
.git/
内の仕組みを知ったら突然 git コマンドがめちゃくちゃ明瞭に理解できるようになった。概要をざっくりメモする。
( ↓ 最高にわかりやすかった記事のみなさん)
[やさしい Git の内部構造 - yapcasia2013](https://www.slideshare.net/DQNEO/git- yapcasia2013)
https://yakst.com/ja/posts/3811
http://koseki.hatenablog.com/entry/2014/04/22/inside-git-1
intro
まず道標となる事柄を 2 つ述べる。
git commit
したとき何をしているかというと、「commit 時点のファイルツリー全体のスナップショットを丸ごと記録」している。(「直前のコミットとの差分のみを記録」しているのではない。僕は勘違いしていた。)git は「オブジェクト」というものによって色んな情報を表現している。オブジェクトには以下の 4 つの種類がある。
- blog object: ファイルの情報をもつ(ex.
app/foo/bar.txt
) - tree object: ディレクトリの情報をもつ(ex.
app/foo
) - commit object: コミットの情報をもつ
- tag object: タグの情報をもつ
- blog object: ファイルの情報をもつ(ex.
オブジェクトは ID をもつ。いわゆるコミット ID とは、commit object の ID である。
4 つの object
具体的に以下のようなコミットログがある場合を考える。
% git log --oneline --graph
* 8bab622 (HEAD -> dev, master) add devdev
* 75decf1 test
* f8f5ce7 first commmit
commit object
id: 8bab622
のコミットの中身を、以下のように見られる。
% git cat-file -p 8bab622
tree f23ca64f4ace0ab87e9b90e5a02c7d32aa197ca0
parent 75decf137844a74cac913fd0432e20a60c98dd2f
author takashi suzuki <takashi@takashinoMacBook-puro.local> 1561743875 +0900
committer takashi suzuki <takashi@takashinoMacBook-puro.local> 1561743875 +0900
add devdev
これはコミットの内容を表現している。
author とか commiter、コミットメッセージはわかるけど parent とか tree とは何か?
parent: そのコミットの 1 つ前のコミットを表現する commit object の ID
みてみる。
% git cat-file -p 75decf137844a74cac913fd0432e20a60c98dd2f
tree 14c6e43a72c3db0994438c6fd085fff416d118d9
parent f8f5ce73034a3b4de5b3e8f45f70eea4ce3f5d0c
author takashi suzuki <takashi@takashinoMacBook-puro.local> 1561730846 +0900
committer takashi suzuki <takashi@takashinoMacBook-puro.local> 1561730846 +0900
test
確かに 1 つ前のコミットの中身である。
tree object
さて、tree とはなにか?
tree: そのコミット時点でのファイルツリー全体を表現する tree object の ID
みてみる。
% git cat-file -p 14c6e43a72c3db0994438c6fd085fff416d118d9
040000 tree 29607877f62818bfb0bb871326851453cdb1e38b lib
040000 tree d4d3444596c8dc1bd3c02e8d25556c9316ce7395 bin
100644 blob e85f913914bd9d1342eae4cdd97b5520733a592a Rakefile
100644 blob ba03955e5bfac64888520b66ea96cdc5351fc4bc package.json
(以下略)
確かに、git 管理対象となるファイルツリーの、一番上のディレクトリが表示される。
さらに lib ディレクトリを表現する tree object の中身をみてみる。
% git cat-file -p 29607877f62818bfb0bb871326851453cdb1e38b
040000 tree 29a422c19251aeaeb907175e9b3219a9bed6c616 assets
040000 tree 29a422c19251aeaeb907175e9b3219a9bed6c616 tasks
100644 blob cf5106d72affa73ca65e0f046ee86a98e01f3a83 sample.rb
確かに、lib/
にあるファイルやディレクトリが表示されている。
blob object
続いて、sample.rb というファイルを表現する blob オブジェクトの中身をみてみる。
% git cat-file -p cf5106d72affa73ca65e0f046ee86a98e01f3a83
sample script
確かに、sample.rb の内容である”sample script”という文字列が表示される。
tag object
commit, tree, blob に続いて、最後 4 つめの tag object もみてみる。
準備としてタグをつける。
% git tag -a v0.1 -m "this is version 0.1"
% git log --oneline --graph
* 8bab622 (HEAD -> dev, tag: v0.1, master) add devdev
* 75decf1 test
* f8f5ce7 first commmit
tag object は commit object とは異なり、git log
とかgit show
で object
id を表示してくれないようだ。なので.git/
内に直接見に行く。
% cat .git/refs/tags/v0.1
01c56840fad536dd15b36b65827a68b2231adfb5
% git cat-file -p 01c56840fad536dd15b36b65827a68b2231adfb5
object 8bab622b7cc2b3a840ed96b191b57b43f6e05e31
type commit
tag v0.1
tagger takashi suzuki <takashi@takashinoMacBook-puro.local> 1561778282 +0900
this is version 0.1
確かに、tag object はタグ情報をもっていた。
object の正体
ここまでが「どうやら 4 種類のオブジェクトというものがあって、それぞれがファイル or ディレクトリ or コミット or タグの情報をもっているらしい」という話。
ではオブジェクトとは実際のところ何なのか。
結論からいうと、オブジェクトの正体は.git/objects
にある 1 つ 1 つのファイルである。
「author taro yamada...
という情報をもつ、id が004dbb5...
のオブジェクト」とはすなわち、「author taro yamada...
を zlib で圧縮した値をもつ、.git/objects/00/4dbb5..
というファイル」である。
さらにいうと、004dbb5...
というのは、author taro yamada...
という文字列から生成されたハッシュ値である。
実際にみてみる。
% cat .git/objects/00/4dbb5d10a9c8cd7808870bc2437378fe26528d
x��ˊ�@E]�W�~P�I�ADf�G��1��;���W�O.��p������~�U*�
����:������[=A~k���u��kd������'f#��M��3ș���f���W�%
確かに.git/objects/00/4dbb5...
というファイルが存在しており、その内容はauthor taro yamada...
という文字列を zlib で圧縮したものである。(上記はそれをターミナルで無理やり表示しようとした結果。)
ポインタ
ここまでのオブジェクトの話で commit とか tag とかはでてきたけど、git の世界にでてくるブランチとか HEAD とは何なのか。
ブランチ
ブランチの結論からいうと、「commit object に対するポインタ」である。
内部的には、.git/refs/heads/ブランチ名
というファイルがあり、その中身がそのブランチの最新コミットの id になっている。
実際にみてみよう。
% git log --oneline --graph
* 8bab622 (HEAD -> dev, tag: v0.1, master) add devdev
* 75decf1 test
* f8f5ce7 first commmit
% cat .git/refs/heads/dev
8bab622b7cc2b3a840ed96b191b57b43f6e05e31
# コミットを1つ重ねる
% git commit -am "add ddeevv"
% git log --oneline --graph
* 4dfe659 (HEAD -> dev) add ddeevv
* 8bab622 (tag: v0.1, master) add devdev
* 75decf1 test
* f8f5ce7 first commmit
% cat .git/refs/heads/dev
4dfe6598f762695be4129aba0baaff5e3947cbf2
確かに、.git/refs/heads/master
の中身が、master ブランチの最新コミットの id になっていることが分かる。
HEAD
ブランチは「commit object に対するポインタ」だったが、HEAD は「ブランチに対するポインタ」である。
(言い換えれば、HEAD は「commit object に対するポインタに対するポインタ」である。)
内部的には、.git/head
というファイルがあり、その中身が現在 HEAD があるブランチ名となっている。
実際にみてみよう。
% git log --oneline --graph
* 4dfe659 (HEAD -> dev) add ddeevv
* 8bab622 (tag: v0.1, master) add devdev
* 75decf1 test
* f8f5ce7 first commmit
% cat .git/head
ref: refs/heads/dev
# HEADを移動する
% git checkout master
Switched to branch 'master'
% cat .git/head
ref: refs/heads/master
確かに、.git/head
というファイルがあり、その中身が現在 HEAD のあるブランチ名となっている。
おわりに
最後にまとめとしてgit add
, git commit
した場合の処理を追おうかと思ったが、以下のサイトが感動的にわかりやすいので、リンクだけ貼って終わりにする。
http://koseki.github.io/git-object-browser/ja/#/step1/.git/
この記事を書いていて、じぶんの中で理解しても言語化するのはまた別の難しさがあるな…と改めて思った。アウトプットしている時間はインプットできない訳で、どれくらいこまめに/詳細にアウトプットするかは悩ましいところだ。