k-tokitoh

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 つ述べる。

  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��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 になっていることが分かる。

ブランチは「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/

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