.git/
内の仕組みを知ったら突然gitコマンドがめちゃくちゃ明瞭に理解できるようになった。概要をざっくりメモする。
( ↓ 最高にわかりやすかった記事のみなさん)
[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��k\d������'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/]
この記事を書いていて、じぶんの中で理解しても言語化するのはまた別の難しさがあるな…と改めて思った。アウトプットしている時間はインプットできない訳で、どれくらいこまめに/詳細にアウトプットするかは悩ましいところだ。