「GitHub トレーニングチームから学ぶ Git の内部構造」に行ってきました!Gitの中・上級者向けの素晴らしい勉強会でした。おもしろかった!
今回の勉強会で一番面白かったのは、「とりあえずコミットをしろ。そうすりゃあとでなんとでもなる」です。git reset --hard
によって消えたはずのコミットが git reflog
から復元できるなんて目から鱗でした。現在の変更を破棄したい場合でもとりあえずコミットしておけ、という教訓の意味がやっと分かりました。
末尾に勉強会のノートを添えておきます。 このイベントは、その場で図を書くような説明などアドリブが多く、とてもわかりやすかったのですが、まとまった資料を貼るのが難しそうな発表でした。したがって、資料は公開されないかもしれません。とすると、このノートはいまのところ唯一の資料です!
ちなみに、会場の様子はこんな感じでした。勉強会の後の DrinkUp も最高でした!ベルギービールのHoegaardenや、アイルランドビールのKilkennyうまー!!
勉強会のノート
(11/19: gistでの指摘を受けて「ストレージについて」の説明を修正しました)
(11/24: 誤字を修正しました)
(注:このノートの内容はgistに貼ったものと同一の内容です)
Graphs, Hashes, and Compression, Oh My!
- メインスピーカー:マシューさん(@matthewmccull)
- サブスピーカー:ジョンさん(@johndbritton)
Hash について
従来の CVCS (集中バージョン管理システム)のリビジョン番号は連番。 SVN はサーバーにデプロイした時点でリビジョン番号1と設定される。
Git は SHA1 をつかっている。40桁の16進数のフィンガープリントがついてる。これは理論上は重複しない大きさ。こうすることで単純で強固な DVCS (分散バージョン管理システム)がつくれる。
新しいファイルを追加すると、
.git/objects/55/7db03de...(SHA1 finger print)
が作成されている。$ printf "blob 12\000Hello World\n" | shasum 557db03...(SHA1 finger print)
これはファイルに対してではなく、文字列に対して SHA1 を走らせているだけ。コンテンツに対してユニークなので、同じコンテンツであれば同じ finger print になるということが重要。
最終的な finger print はファイルの内容・フォルダの内容・コミットした人から作成される。
$ echo "Hello World" | git hash-object -w --stdin 557db03...(SHA1 finger print)
ファイルの中に何が入ってるかはこれで簡単に分かる。ただし、結果は圧縮されている。
$ alias deflate="perl -MCompress::Zlib -e 'undef $/; print uncompress(<>)'" $ deflate .git/objects/55/7db03...(SHA1 finger print) blob 12Hello World
最初の word は git がサポートしている4つのうちのひとつ。
blob
はファイルtree
はフォルダcommit
はアクションtag
は重要なときに使用するラベル
12
はファイルのバイト数のキャッシュ。$ git update-index --add --cacheinfo 100644 557db03...(SHA1 finger print)
これで
.git/objects
以下にひとつのオブジェクトが保存される。$ git commit -m"First commit"
これで
.git/objects
以下に2つのオブジェクトが保存される。ひとつはcommit
で、もうひとつはtree
。これで終わり。ね、簡単でしょ?
コミット番号のおはなし
finger print は長すぎて人間には辛いので、最初の6桁や4桁を使うことが多い。短縮した finger print を復元することも可能。
$ git rev-parse f6b4 f6b410a3...(SHA1 finger print)
ただし、これは重複する finger print がなければの話。重複する finger print があるなら、もっと桁数を入力しろと言われる。
ストレージについて
ディスクに保存される方法をもっと詳しく見ていく。 従来の SCM は普通、差分保管を利用している。これは賢いアプローチに見える。しかし、差分保管はファイル履歴が長いとパフォーマンスが悪くなる。
git はいくつかの点で違う方法を利用している。git は差分を保存しない。Directed(時間について有方向) Acyclic(無閉路) Graph を使っている。ファイルに変更があった場合のみ、そのファイルを保存する。 それ以外のときは、ツリーの全体をチェックインしたときにコピーする。この方法は効率的。ディスク容量については効率的じゃない(しかし、近年それは問題にならないよね)。
blob
をハッシュするときには、ファイルの内容のみがハッシュ化される。ファイル名の方はtree
にblob
の SHA1 と組で保存されている。これによって同じ内容のファイルが複数存在したとしても、ひとつのblob
オブジェクトで済むようになっている(blob
の SHA1 をファイルポインタ代わりに使っているイメージ)。
tree
の圧縮は次のコマンドで実行できる(自動で面倒見てくれるので人間がタイプする必要はほとんどない)。$ git gc
こうすることで、Groovy の 2.1GB がたった 205MB になった。
それぞれの hash の関連性
commit
はひとつ前のcommit
と繋がっている。時系列とは逆に過去方向にデータは伸びていく。次のコマンドで親の hash が見られる。$ git log --pretty=raw tree 69834...(SHA1 finger print) parent f031b...(SHA1 finger print) ...
commit
は1~2つの親を持つことができる。いっきに複数のブランチをmergeすればさらに多くの親を持てる。前に説明したとおり、内部的には共通の祖先のファイル単位での差分を利用している。
- Q: merge の CONFLICT の解決はどこのリビジョンになるのか?
A:
merge
オブジェクトはコードの変更ももてる。そこで解決される。Q: 複数 merge する利点は何か?
A: 一気に merge されてるのでアトミックな操作になる。つまりロールバックするときに楽。
Q: pack するとき、複数の
blob
が同じ内容になって圧縮されているはず。これってパフォーマンス的に問題あるんじゃ?(~♥~)<質問内容聞き取れなかったA: ある程度の期間をおいてからパックされるので問題にはならない。
Q: Github にはすごい数のコミットがあると思うが SHA1 が衝突したことはないのか?
A: ない(ドヤァ
Q: (ネットワークパフォーマンスの話)
A:
Q: hard link 使えないと git 使えないの?
- A: 使える。hard link がない場合は hash 使ってうまくやっている。あまり見ないと思うが、ローカルからの clone は hard link になっている。
commitish & treeish
"-ish" は git で使われている DSL(ドメイン特化言語)のこと。 "commitish" は commit 用の DSL。 "treeish" は tree 用の DSL。 この命名についてはみんなで憤慨しようね!(゜♥゜)<ggiiiiitttttt
ある commit のひとつ前のやつは
9AB22F^
と指定できる(DSLみたいだね!)。キャレットを複数書くと、ひとつずつ戻っていく。9AB22F~5
で5つ前の commit になる。範囲指定も..
で指定できる。HEAD
は最新の commit を示す。これは、実際にはグラフの一部分を指しているだけ。 つまり、こんなふうにもできる:
master^^
.git/objects/
以下のどこがHEAD
なのかというデータは.git/HEAD
に保存されている。これは実際には.git/refs/heads/
以下のブランチを参照している。次のコマンドでも同じように見られる。$ git rev-parse HEAD 8db8f...(SHA1 finger print)
こういう便利な使い方もできる。これは、
git clone
し直すよりも楽だよね。$ git reset --hard origin/master
しかし、この操作によって
origin/master
以降の commit が宙に浮いてしまう。この commit は90日過ぎるとゴミ箱行きになる。それでもゴミ箱は次のコマンドで閲覧でき、git reset
を使えば戻すことが出来る。
$ git reflog
つまり、commit さえしていればなんとかなる。
また、リポジトリに問題があるかどうかは次のコマンドでチェックできる。
$ git fsck
- Q: branch を戻すと 0 byte になっちゃうときがある。どうすればいいのか。(~♥~)<うまく聞き取れなかった
A:
git fsck
は問題を確認するだけで、修復は出来ない。この場合はバックアップから戻してくるのが git のやり方。Q:
git reflog
のデータはどこに保存されているのか?- A:
.git/logs
の中。
Graph について
git の commit にはもうひとつ hash がある。次の結果の2行目の
tree
だ。$ git log --pretty=raw tree 69834...(SHA1 finger print) parent f031b...(SHA1 finger print) ...
次のコマンドでメッセージと変更が確認できる。
$ git show 3cef...(SHA1 finger print)
commit の tree に含まれるファイル一覧は次のコマンドで確認できる。
$ git show master^{tree}
一番近いタグを探すにはこうする。
$ git describe master
git show ???
の???部分の DSL について
branch:/search word
でマッチするコミットを検索できる(うまくいかなかった場合は、shell が/
をファイルパスだと勘違いしていることが原因。クォートで囲めばおk):$ git show 'HEAD:/Tokyo'
あるコミット時点でのファイルを実際のファイルツリーを変更せずに確認することもできる。その場合は
branch:FILE
と指定する:$ git show HEAD~2:file_name
他に3つパターンがある。
:0:FILE
:Stuging エリアのファイルを表示:1:FILE
:merge の途中の場合で、共通の先祖(分岐する直前の commit)のファイルを表示
:2:FILE
:merge 元のファイルが一番最近に変更された場所を表示$ git show branch:0:FILE
休憩後の質問タイム
hash の種類は次のコマンドで閲覧できる:
$ git cat-file -t 0dcd6277...(SHA1 finger print) commit
- Q:
git log
はHEAD
からツリーをたどるということであってる?A:
master
ブランチの場合はあってる。git log HEAD~2
だと、HEAD
ツリーを前に2つ辿ってね、という意味になる。Q: なぜ SHA1 を選んだのか?
- A: SHA1 を選ぶ過程で多くの議論があった(この議論では開発者の Linus は放送禁止用語を連発しつつ「変えないんだからね!」と言ったらしい)。SHA1 は今から10、20年後でも衝突せず、計算が軽いというメリットがある。SHA1 はセキュリティ専用だと思われがちだが、そもそもの目的はユニークな値を作成するための方法なので、git みたいな利用方法でも適切。
(ここで Linus の Nvidia, Fuck You! が上映される)
- Q: ローカルのリポジトリとリモートのリポジトリは何が違うのか?
A: 両者は同じ構造。
Q: ファイルシステムの変更はアトミックじゃない状況が発生しうると思うが、この状況では git はどのような振る舞いをするか?
A: git は実際には、
git add
があってから書き込まれる。また、ツリーはリファレンスでしかない。つまり、git commit
のなかで有効なオブジェクトが書き込まれたかどうかを確認できる。また、ブランチのポインタの更新は一番最後なので事実上アトミックである。これで壊れることは宝くじであたるようなもの。(~♥~)<聞き取れなかったです。助けて!Q: git clone することなくリポジトリを参照することができるか?
A: githubで見られます(ドドヤァ あまり知られていないが次のコマンドでもできる:
$ git ls-remote https://github.com/...
A: https はシンプルでネットワークエラーが起こりにくい。なのでデフォルトで使っている。SSH はフィルタリングされてたりするしね。(補足:ssh > git > https の順で速い。これは github が原因でなく、git に原因がある。github チームは改善のため努力しているところ)
Q:
git log
の順番ってどうなってるの?A:
git log --help
の Order節を参照せよ。Commit Ordering By default, the commits are shown in reverse chronological order. --date-order Show no parents before all of its children are shown, but otherwise show commits in the commit timestamp order. --author-date-order Show no parents before all of its children are shown, but otherwise show commits in the author timestamp order. --topo-order Show no parents before all of its children are shown, and avoid showing commits on multiple lines of history intermixed. For example, in a commit history like this: ---1----2----4----7 \ \ 3----5----6----8--- where the numbers denote the order of commit timestamps, git rev-list and friends with --date-order show the commits in the timestamp order: 8 7 6 5 4 3 2 1. With --topo-order, they would show 8 6 5 3 7 4 2 1 (or 8 7 4 2 6 5 3 1); some older commits are shown before newer ones in order to avoid showing the commits from two parallel development track mixed together. --reverse Output the commits in reverse order. Cannot be combined with --walk-reflogs.
Githubについての質問
- Q: pull request の commit の部分にコメントを書いても discussion のところに表示されないのはなんで?
A: コメントする場所によって扱いを変えている。discussion にコメントすると全ての pull request にコメントされる。pull request についてコメントするときは、特定の pull request についてコメントされる。(~♥~)< 聞き取れませんでした。fixme!
Q: Github のユーザーのアイコンは自動生成なのはなんで?
- A: ユーザーが区別できるようにそうしている。それと、アイコンからユーザーを連想するのって楽しいでしょう?表示されたページ