gitの仕組み

.git/内の仕組みを知ったら突然gitコマンドがめちゃくちゃ明瞭に理解できるようになった。概要をざっくりメモする。

( ↓ 最高にわかりやすかった記事のみなさん)

やさしいGitの内部構造 - yapcasia2013

https://yakst.com/ja/posts/3811

http://koseki.hatenablog.com/entry/2014/04/22/inside-git-1

intro

まず道標となる事柄を2つ述べる。

  1. git commitしたとき何をしているかというと、「commit時点のファイルツリー全体のスナップショットを丸ごと記録」している。(「直前のコミットとの差分のみを記録」しているのではない。僕は勘違いしていた。)

  2. gitは「オブジェクト」というものによって色んな情報を表現している。オブジェクトには以下の4つの種類がある。

    • blog object: ファイルの情報をもつ(ex. app/foo/bar.txt)
    • tree object: ディレクトリの情報をもつ(ex. app/foo)
    • commit object: コミットの情報をもつ
    • tag object: タグの情報をもつ

オブジェクトは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/

この記事を書いていて、じぶんの中で理解しても言語化するのはまた別の難しさがあるな…と改めて思った。アウトプットしている時間はインプットできない訳で、どれくらいこまめに/詳細にアウトプットするかは悩ましいところだ。