ツナワタリマイライフ

日常ネタから技術ネタ、音楽ネタまで何でも書きます。

「エンジニアのためのgit教科書」からgitの内部構造を学ぶ 実践編その1

はじめに

読みました。

もうgitがないと仕事にならない生活をしています。仕事のすべてはgitに預けた、ここにないものは何もない、というスタイルで仕事しているのでチームメンバーに対しての情報開示が非常に楽ですし、差分管理、つまりdiffが容易に確認できて、save / resetが簡単にできるが気持ちよすぎて、何でもgit以下においちゃいますね。でもバイナリはダメですよ。

で、今でこそ一通りのコマンドは使いこなせるようになりましたが、まだまだ周囲にはgitの使い方がわかってないひとも多く、チーム内ではわりとgitがわかるほうになってきました。

せっかくなのでもう一歩深く学んでgitマスターになって、勉強会開こう!ということで、何事も知るには内部構造。.git以下の動きを解説してくれるこの本を読みました。

この本、かなり良いです。ていねいに、各コマンドで何が生成され、中身がどうなっているのかを、実際に動かしながら示してくれます。基本オブジェクトの解説から入り、各コマンドの内部の動き、最後には低レベルのgitコマンドを使ってのgit commit / add のshell実装までします。本書の内容を実際に動かし、内容を理解できたら1段階レベルがあがるでしょう。

とはいえ、動かしながらだと内容も大きくなるので2回に分けて実践していきます。

git init

git initしたら何が起きるの?

適当なディレクトリを作って試してみましょう。

take@MacBook-Air ~/g/git_study> mkdir git_init_test
take@MacBook-Air ~/g/git_study> cd git_init_test/
take@MacBook-Air ~/g/g/git_init_test> git init
Initialized empty Git repository in /Users/take/github/git_study/git_init_test/.git/
take@MacBook-Air ~/g/g/git_init_test> tree .git/
.git/
├── HEAD
├── config
├── description
├── hooks
│   ├── applypatch-msg.sample
│   ├── commit-msg.sample
│   ├── post-update.sample
│   ├── pre-applypatch.sample
│   ├── pre-commit.sample
│   ├── pre-push.sample
│   ├── pre-rebase.sample
│   ├── pre-receive.sample
│   ├── prepare-commit-msg.sample
│   └── update.sample
├── info
│   └── exclude
├── objects
│   ├── info
│   └── pack
└── refs
    ├── heads
    └── tags

8 directories, 14 files

HEAD,config,descriptionの3ファイルに、hooks,info,objects,refsのディレクトリが作成されます。

HEAD

headファイルは現在のブランチを示すシンボリック参照情報を保存しています。

take@MacBook-Air ~/g/g/git_init_test> cat .git/HEAD 
ref: refs/heads/master

config

git configで設定できる、gitのconfigファイルです。これはリポジトリ固有設定になります。システム固有設定(–system)は/etc/gitconfig、ユーザ固有設定(–global)は~/.gitconfig になります。

take@MacBook-Air ~/g/g/git_init_test> cat .git/config 
[core]
    repositoryformatversion = 0
    filemode = true
    bare = false
    logallrefupdates = true
    ignorecase = true
    precomposeunicode = true

参考:Git - Git の設定

詳細はman git-configで見るとして、ここではconfig内容の解説は行いません。

description

take@MacBook-Air ~/g/g/git_init_test> cat .git/description 
Unnamed repository; edit this file 'description' to name the repository.

リポジトリのdescriptionです。githubではここの値は読まないようですが、gitlabはどうなんだろうか。会社行ったら確認します。

hooks

ここ以下はgitが用意しているhookのsampleが置かれています。これらの拡張子を外して編集すれば動かすことができます。デフォルトのコミットメッセージを用意したり、コミット規約を守らせるためにshellでスクリプトが書けます。

take@MacBook-Air ~/g/g/git_init_test> ls .git/hooks/
applypatch-msg.sample     post-update.sample        pre-commit.sample         pre-rebase.sample         prepare-commit-msg.sample
commit-msg.sample         pre-applypatch.sample     pre-push.sample           pre-receive.sample        update.sample

info/exclude

take@MacBook-Air ~/g/g/git_init_test> cat .git/info/exclude 
# git ls-files --others --exclude-from=.git/info/exclude
# Lines that start with '#' are comments.
# For a project mostly in C, the following would be a good set of
# exclude patterns (uncomment them if you want to use them):
# *.[oa]
# *~

除外ファイルパターンを記載することができます。なお.gitignoreは除外ディレクトリパターンと記載していますが。。。.gitignoreでも除外ファイル記載可能なので、大きな違いは除外パターンを共有したいかどうか、ではないでしょうか。自分だけ除外したい場合はここのinfo/excludeに記載するといいでしょう。(.gitはgitの管理対象ではないでしょうから)

参考:Git | excludeファイルにローカル環境だけ無視したいファイルを登録 - Tbpgr Blog

object

gitはオブジェクトという単位で情報を持ちます。objectには

  • blob … ファイルのデータを保持
  • tree … blobへの参照と、1階層分のディレクトリ情報
  • commit … 変更時のメタデータ。treeへの参照。

の3種類があります。

refs

ブランチの参照とタグオブジェクトを格納する場所です。

git addしてみる

では、git addしたときどのように変化するか見てみましょう。

take@MacBook-Air ~/g/g/git_init_test> echo "git test" > test.txt
take@MacBook-Air ~/g/g/git_init_test> git add test.txt 

実行前の.gitディレクトリをコピーしておいたので、diffをとってみます。

take@MacBook-Air ~/g/g/git_init_test> diff -r .git_before/ .git
Only in .git: index
Only in .git/objects: f6

indexファイルと、objectが増えたようですね。

index

indexファイルはindex情報、どのファイルが追跡状態にあるかを保存してます。しかし、バイナリファイルなので中身を読むことはできません。

stageしているファイルは以下で確認できます。git statusでも確認できますね。

take@MacBook-Air ~/g/g/git_init_test> git ls-files --stage
100644 f6edd6e7a290f009aa685d3acd3153b495a69ea8 0 test.txt

brob object

brobオブジェクトが作成されました。git cat-fileで見てみましょう。

take@MacBook-Air ~/g/g/git_init_test> git cat-file -t f6edd6e7a290f009aa685d3acd3153b495a69ea8 
blob
take@MacBook-Air ~/g/g/git_init_test> git cat-file -p f6edd6e7a290f009aa685d3acd3153b495a69ea8 
git test

この16進数の数字はSHA1のhashです。

take@MacBook-Air ~/g/g/git_init_test> openssl sha1 test.txt
SHA1(test.txt)= 06aaaf302f1370f12298c6cbf7e436c8a6bdef05

確認できましたね。brobの内容のハッシュがobject名になっています。かつ、先頭2文字でディレクトリを分けていますね。名前空間をわけて処理をいい感じにしてるんでしょうね。

take@MacBook-Air ~/g/g/git_init_test> tree .git/objects/
.git/objects/
├── f6
│   └── edd6e7a290f009aa685d3acd3153b495a69ea8
├── info
└── pack

3 directories, 1 file

git commitしてみる

さて、次はcommitをしてみましょう。同じように現在の.gitをコピーしておいて、diffをとってみようと思います。

take@MacBook-Air ~/g/g/git_init_test> git commit -m "test commit"
[master (root-commit) 379e87a] test commit
 1 file changed, 1 insertion(+)
 create mode 100644 test.txt
take@MacBook-Air ~/g/g/git_init_test> diff -r .git_before/ .git
Only in .git: COMMIT_EDITMSG
Binary files .git_before/index and .git/index differ
Only in .git: logs
Only in .git/objects: 37
Only in .git/objects: 5e
Only in .git/refs/heads: master

indexファイルが変わり、COMMIT_EDITMSG、logs、そしてobjectが2つ増えました。refs以下にheadsが増えました。

COMMIT_EDITMSG

直前のcommitメッセージが保存されています。–amendで直前のみ変更されるのはこのためなんでしょうね。

take@MacBook-Air ~/g/g/git_init_test> cat .git/COMMIT_EDITMSG 
test commit

logs

HEADの参照情報です。直前のコミットがないので0000… になっていますね。

take@MacBook-Air ~/g/g/git_init_test> cat .git/logs/HEAD 
0000000000000000000000000000000000000000 379e87a44b39250d5717de286499cd1c52f9d623 kondo takeshi <take.she12@gmail.com> 1502256942 +0900 commit (initial): test commit

reflogで同等の情報を得られます。

take@MacBook-Air ~/g/g/git_init_test> git reflog
379e87a HEAD@{0}: commit (initial): test commit

commit object

ではHEADが参照している379e87aのオブジェクトを見てみましょう。

take@MacBook-Air ~/g/g/git_init_test> git cat-file -t 379e87
commit
take@MacBook-Air ~/g/g/git_init_test> git cat-file -p 379e87
tree 5ed0c0c091b633152a962cfd92f538204ae2847c
author kondo takeshi <take.she12@gmail.com> 1502256942 +0900
committer kondo takeshi <take.she12@gmail.com> 1502256942 +0900

test commit

これがメタデータを保存しているcommitオブジェクトですね。そしてcommitオブジェクトはtreeオブジェクトを参照しています。

なお、これもコミット内容のsha1ハッシュなはずだが、単純に内容をhashかけただけではだめだった。

これはcommitという文字と、commitオブジェクトの中身のバイト数、そしてコミットオブジェクトの中身を合わせているらしいが、なぜか再現できず。。。

参考:Gitのコミットハッシュ値は何を元にどうやって生成されているのか - Mercari Engineering Blog

tree object

take@MacBook-Air ~/g/g/git_init_test> git cat-file -t 5ed0c0
tree
take@MacBook-Air ~/g/g/git_init_test> git cat-file -p 5ed0c0
100644 blob f6edd6e7a290f009aa685d3acd3153b495a69ea8   test.txt

treeオブジェクトはファイルモードと、blobオブジェクトの参照を持っています。

おわりに

エンジニアのためのgit教科書の初級編相当をなぞってみました。.git以下にあるファイルの種類、3つのオブジェクトの内容と関係、git add/commit時の動きを学びました。

次回はより多くのコマンドについて挙動を追いかけてみます。