ton-tech-ton

参加レポート【GitHub トレーニングチームから学ぶ Git の内部構造@名古屋】

git, github, study

11月17日に行われたGitHub トレーニングチームから学ぶ Git の内部構造@名古屋 のレポートです。

メモが追いつかなかったり、聞き取れなかったりしたとこは抜けてます。すんません。 ちょっと長いですがどうぞ。

最初に

ハッシュタグは@githubjp 今日の講師はマシュー(@matthewmccull)とジョン(@johndbritton))

マシュー日本語勉強中だって:)

今回の内容はGitの内部構造について。Gitの紹介とは違う。

Gitの定義

  • the stupid content tracker (man gitしたときに最初に出てくる説明)

2005年 git誕生

githubは自社の製品を自分たちの作業にも使っている。

ここからGitの内部構造について

Airchitecture

Hashes ハッシュについて

集中バージョン管理システムのリビジョン番号は連番になっている(svnとか)。 連番になっているのは親しみやすい。

一方GitではSHA-1ハッシュを使っている。 複雑だが強力。一意になる。 本当に全体で一意な識別番号になる。 40桁の16進数(20バイト)

なんでこんなわかりづらいのつかってるの? ハッシュはコンテンツそのものを表す。 これは新しくgitを使い始めた人は発狂するかも。 (FFFFUUUUUUがgiiiiiiiittになったやつが表示されてたw)

しかしハッシュを示すことで、全く同じ内容を持っているか聞くことができる。 連番ではそれはできない

Q&A

Q1.ハッシュは本当にぶつからないの?

A1. githubは世界で一番gitリポジトリを持っていると言えるが、ハッシュはぶつかったことがない。

Q2. もし万が一ハッシュがぶつかった場合は?

A2. ハッシュオブジェクトが上書きされることはない。

補足 (SHA-1 に関するちょっとしたメモ)

(エラーになるとも言っていたような気もするのですが、聞き逃しました><)

Hashed Content ハッシュ化されたコンテンツについて

Git commits whitout Git (Gitを使わずにコミットしてみよう)

1
2
3
mkdir project1
cd project1
git init project1

これでgitのデータベースが作成できる。(広い意味でのデータベース)

.gitのリポジトリの中身は全てテキストか圧縮されたテキストになっている。

.gitリポジトリの中身をざっと紹介

  • COMMIT_EDITMSG : 最新のコミットメッセージ
  • HEAD : 現在のコミットポイント
  • MERGE_* (MERGE_HEAD, MERGE_MSG, MERGE_MODEなど。マージ中に出てくる) : マージ情報へのポインタ
  • config : 設定ファイル
  • description : web公開したときの情報
  • hooks : コマンドフックスクリプト
  • index : ショッピングカート (addした時のステージングエリアの情報を保持するファイル。コミットするための情報をステージングエリアというショッピングカードに突っ込んでいくようなイメージで言った、のだと思う)
  • logs : 履歴
  • objects : コードの中身
  • refs : ブランチやタグへのポインタ

GitはLinuxの文化から始まったツール。 でもLinuxツールなのにconfigはWindowsのini形式を使ってる!(笑)

git addしたときにコンテンツはどこに保存される? → objectsの中に保存される

コミットもしてないのになぜ保存されるの?

Gitはコミットしてもされなくても差別はしない

同じテキストを書くと常に同じハッシュオブジェクトが生成される。(確かめてみよう)

1
2
echo "Hello World" > hello.txt
git add hello.txt

(ここで.git/objectsの中を見てみよう)

1
2
3
4
tree .git/objects
.git/objects
├── 55
│   └── 7db03de997c86a4a028e1ebd3a1ceb225be238
1
printf "blob 12\000Hello World\n" | shasum

と同じハッシュになるはずだ。ごめんgitの機能使った(笑)

Git特有のハッシュではなく標準的なSHA-1アルゴリズムを使っているので、どの言語でもGitのライブラリを作成できる。

Gitの全てはテキストになっている。 configファイルもオブジェクトもテキスト。 実はタグもただのテキスト、ブランチもテキスト:)

さっき追加されたオブジェクトをcommitすると複数のオブジェクトが生成される。 treeのために一つ。commitのためにもひとつ。

ディレクトリとファイルの構造について

Gitは一つのディレクトリの中に多くのオブジェクトファイルが入らないようにしている。 ハッシュの最初の2桁でディレクトリを作り、残りの38桁がファイル名になる。

1
git hash-object hello.txt

でファイルのハッシュを標準出力できる。

Gitはこのような高レベルな機能と低レベルな機能を提供している。(レイヤー的な意味で)

ファイルに書き込まなくてもコンテンツのハッシュを表示できる

1
echo 'Hello World' | git hash-object -w --stdin

Gitはユーザーのために高レベルな機能を提供しつつ、開発者向けに低レベルな機能を提供している。

(オブジェクトの中身を見てみよう)

1
cat .git/objects/55/7db03de997c86a4a028e1ebd3a1ceb225be238

さっきテキストって言ったのにバイナリじゃないか

zlibで解凍すればおk

Git特有の圧縮アルゴリズムじゃなくて、スタンダードなzlibだから大丈夫

(perlを使った解凍エイリアスを紹介)

1
alias deflate="perl -MCompress::Zlib -e 'undef $/; print uncompress(<>)'"

これを使って

1
deflate .git/objects/55/7db03de997c86a4a028e1ebd3a1ceb225be238

とすると、

1
blob 12Hello World

と表示される。

  • 最初のblobはァイルの種類
  • 12はコンテンツのサイズ
  • 次にコンテンツ Hello World

全てこのフォーマットになっている。

Q&A

Q1. エクセルみたいなバイナリはどうやって扱ってるの?

A1. テキストの場合と同じ

blobとコンテンツサイズの後にバイナリをそのまま突っ込んでる。

Q2. ファイルシステムが十分速いことを前提としてるのか?(メモれなかった。違ったら教えて下さい)

A2. リポジトリの大きさが大きくなっていくにつれて同じペースで処理の時間も遅くなっていく

SSDの方が当然速いよね。 GitHubのデータセンターは全部SSD!!容量はペタバイト!!(すげえ)

Hash Shortcut 短縮ハッシュについて

ハッシュは全部打つ必要はない。ユニークである最小の長さを利用できる。

最小4桁でいける。一意に決定できなかったときは、もっと長い桁を入力しろと言われる。

オブジェクトが増えてくると4桁じゃ推測できなくなる。 マシューが関わってるリポジトリだと大体6桁書けばおk

1
git rev-parse 9ab22f

rev-parseを使うことで短い桁数から全桁のハッシュを表示できる。

このコマンドはほとんどの高レベルコマンドが最初に走らせてるコマンド。 短縮ハッシュはgit内部では使われない。40桁にしてから使う。

1
git rev-parse HEAD

HEADがどのハッシュを指しているか見れる。

1
git rev-parse master

masterにいる場合は上2つのコマンドは同じ結果を返す。

Storage 保管の仕方について

バージョン管理システム(SCM)は普通差分を保管する。 (CVS/Subversion/darcs/Mercurial など)

Gitは差分を使ってない。

差分保管は、変更した部分だけを保存するということ。 リポジトリの履歴が長いと、各差分を計算してたどっていく必要があるから遅くなる。

Gitは無閉路有向グラフ(DAG) Gitリポジトリは片方にしか進めない。 戻れない(Acycle)無閉路、非循環。 (ここらへんどういう関係で出てきたのかあいまい)

GItはチェックインするときにツリーのすべてをコピーする。 フォルダ全部をコピーするようなもので、あまり効率的には思えない。なぜこのような方法をとるのか?

全てのバージョン(コミット)ごとに全て内容を保存するということは、すごく単純!

バージョン3の内容が欲しいとき、バージョン3の内容を取り出すために計算することはなにもない!

その代わりに、ディスク容量が犠牲になる。だけど最近はディスク容量はあまり問題にならないだろう。

Gitではディスク容量節約のために2つの方法をとっている。

  1. ファイル内容が同一だった場合、同一blobへハードリンクする

    このときディスク容量としては差分保管の場合と同じ。 実際ディスク容量が新たにとられるときは、変更があったときだけ。

    マシューが昔働いてた会社ではコードがコピペされることが多かったので効果的だ(笑)

  2. 圧縮する

    Gitはほぼプレーンテキストを管理している。 zlibで各blobをcommitの際に圧縮する。

    git gc

    でリポジトリの全てを圧縮する?(あってる?) 同じような内容がいっぱいあるのでリポジトリ全てを圧縮するとかなり効果ある。

    Groovyリポジトリは2100MBが205MBになった。

    トリックがあるわけじゃなくて、コードの内容に重複が多かったから。 ブランチとかほとんど同じ内容だよね

Hash relationships ハッシュの関係性

他のコミットとの関係

種類は4つしかない

  • Blob
  • Tree
  • Commit
  • Tag

blobはファイルの内容をそのまま(raw)保存する。バイナリでも。

treeはblobオブジェクトのファイル名を持つ。blob自体はファイル名を持ってない。

これまでだったらそれが一般的だったか、Gitは違う。 このことによって、違うファイル名で同じ内容だった場合でも同一とみなすことができ、ディスク容量の節約になる。

treeの中はblob hash filenameとなっている。

複数ディレクトリの場合はtreeの中にtreeがある

commitオブジェクトはコミットした人の情報も持っている。 一つ前のコミットオブジェクトの情報も持っている。 構造としては連結リストに似ているよね。

HEADのポインタとは? ただのハッシュだよ:)

(中身を見てみよう。ハッシュが書いてあるだけ)

1
cat .git/HEAD
1
cat .git/refs/heads/master

みんなbranchコマンドは使ったことある?

1
git branch newbranch

じゃあ、vimでブランチを作ったことがある人ー? (実は前にやったことがあるなんて言えなかった)

vimで.git/refs/heads/の中にhandcraftedという名前でファイルを作って、ハッシュを書き込むと、handcraftedというブランチが作れる。

あまりオススメはしないけどね;P

1
git checkout handcrafted

(できた)

タグはどうやって生成されるでしょうか? これもただのテキストだよ:)

同じように.git/refs/tagsの中にタグ名のファイル作ってハッシュ書けばおk

最後にもう一つ。コミットをアンドゥしたい場合は?

ブランチ名を変数、ハッシュをアドレス値と考えると、 違うアドレス値の値をブランチ変数に入れてるだけ。

ただ、ゴミが残る。gcしないと。

gitでは90日間ゴミが残る。間違えても90日は大丈夫。 なのでgitを知ってる人はあまり心配せずにリポジトリを変更できる。 間違えてもすぐ戻せるから。タグでもコミットでも削除しても大丈夫。

Q&A

(これは自分が質問した内容。他の質問は頭に入ってませんでしたw)

Q1. Gitはなぜ空のディレクトリを追加できないの?

Treeオブジェクトの構造を見ていると中身が空のTreeオブジェクトも作れそうな気がするけど

A1. Gitはファイルへの変更を追うことしかしない

(必要ないと思っている)

これは1年に1回くらい議論にあがってる。

ディレクトリを追跡するにはなにかファイルを追加しないといけない。 よくやるのはディレクトリの中に.gitignoreファイルを置くとか。

だれかパッチくれたら直すけどそこまで誰もこだわってない(笑) パッチ送ったらいいんじゃないかな:)

?? (内容よく覚えてないがメモってた)

1
git cat-file -t d91c

-tオプションでハッシュオブジェクトのファイルタイプが返ってくる(blobとかtreeとか)

commitish & treeish

gitの2つの言語

みんなカタカナとひらがなと漢字使ってるから2つくらい大丈夫だよね(笑)

  • commitish : comit用の言語
  • treeish : tree用の言語

いいづらい

commitish

  • 9ab22f 短いハッシュはcommitish
  • 9ab22f^ キャレット (あるコミットの1つ前のコミット)
  • 9ab22f^^ キャレット複数 (あるコミットの2つ前のコミット)
  • 9ab22f~5 チルダと番号 (あるコミットの5つ前のコミット)
  • 9ab22f..56cd77 .. 範囲指定 2つのコミット間の全てのコミット
  • HEAD このブランチの最新のコミット
  • HEAD^ 最新のコミットの一つ前
  • HEAD~5 最新のコミットの5つ前
  • HEAD…9ab22f^ 最新のコミットから9ab22fの一つ前のコミットまで
  • master
  • master~5

(コミットした時間の話) 各国の時間どうすんの?プログラマは時間とか日付がきらい><

そんな厳密に考えてない(? あいまい)

  • remotes/origin/master このリモートブランチの最新のコミット origin/master と略せる

(ここらへんの話メモってない)

Q&A

Q1. キャレットとチルダは同じ機能なの?

A1. キャレットは1つにつき1つコミットを戻る。多く戻るときはチルダ使ったほうが効率的

Q2. マージブランチからキャレットとかでたどるときはどう判別するの?

A2. デフォルトではプライマリーブランチをたどる。(マージで取り込んだ側)

  • ^1のように1をつけるとプリマリーブランチをたどる。(master)
  • ^2のように2をつけるとmergeされた側のブランチをたどる。(トピックブランチ)

キャレットの後の番号はブランチの線の番号。 番号を入れなかった場合はmaster側のブランチをたどる。 プライマリーブランチ以外のブランチを辿りたいときだけ番号を追加する。普段あまり使わないよね。

The Graph グラフについて

verification

ちょっと破壊活動してみよう:P

コミットオブジェクトに不正な文字を入れてみる。

ちゃんとerror: sha1 mismatch と言われる。 gitは修復はしない。修復はできないが問題があるか確認できるのは良いこと

1
git fsck

fsckはファイルシステムチェックのこと。 Linuxでやるファイルシステムチェックと同じコンセプト。

もう一つ。

1
git verify-pack -v .git/objects/pack/FILENAME

ファイルシステムを確認するためのコマンドはこの2つだけ。

最後のトピック

1
master^{tree}

これはこのコミット内のtreeを検索する。 この^はキャレットではない。マーカー。 masterコミットの中に入り込めって意味(^)

masterの中に入ってtreeを取ってこいというコマンド

コミットは以下のような情報を持つ。

  • author name
  • commiter name
  • timestamp
  • commit message
  • content
1
git cat-file -p master^{tree}

でmasterの中のtreeオブジェクトを表示できる

describe

git describe HASH で一番近いタグを表示する。

masterから一番近いタグを表示

1
2
git describe master
v1.8.4-474-g128a96

(補足

表示形式は

1
{最も最近のタグ名}-{タグからの連番}-g{hash}

となっているっぽい

補足終わり)

コミットメッセージ検索

1
git cat-file -p :/somewords

somewordsで始まるコミットメッセージを検索 見つかるまで過去をたどる

blob

1
REF:FILE

で特定のコミットの中のファイルの中身を表示できる

1
git show master:zlib.c

このコミットの中のzlib.cの中身を表示する

過去のどのバージョンもこうやって表示することができるので、最新の内容と比較できる。

ファイルが存在するか確認することにも使える

1
git show master~50:z

zshとかはここまで打つとファイルを補完できる(!)

1
:0:FILE

0番でステージングエリアのファイルを表示

1
:1:FILE

1でmergeしたファイルの共通の先祖を表示

1
:2:FILE

2でmerge先のファイルが一番最近変更された場所を表示

1
:3:FILE

3でmerge元のファイル

マージ途中だった場合

1
git checkout :3:FILE

でマージしたい特定のファイルの変更だけ取り込む事ができる

Q&A

Q1. ハッシュの衝突検査はいつ行うの?

A1. pushするとき、ハッシュの中身がpush先にあった場合、pushを中止する。

(githubチームもこの質問には自信なさげだった?)

ただハッシュは絶対に上書きされない。 実際に衝突が起こる可能性は非常に低い。

Q2. 同じ内容、同じauthor、同じ時間にコミットすれば違うPCでも同じオブジェクトが作られるってことでおk?

A2. その通り

ただそんなことをやっても、それはそのコミットの内容をお互いに送信する必要がなくなるだけ:)

Q3. ブロブハッシュの名前空間はリポジトリ単位でチェックしてるのかgithub全体でチェックしてるのか?

A3. github全体でチェックしてる。

(あんまり聞き取れなかった)

オリジナルのリポジトリにアクセスできる人はフォークされた全てのリポジトリにアクセスできる

このファイル共有の方法はDropboxやSkydriveなどで問題になった。

アップロード時間によって既に存在してるかどうか分かる。既にあればリンクするだけ。

githubでは、プライベートリポジトリは別々に分けられているから大丈夫。

Q4. 差分管理との性能の違いは?なんか測ったの? (あいまい)

A4. 明確に差は言えないが、差分管理だと確実にコミットする度に遅くなっていく。

差分管理だとcloneした段階で全ての差分を計算している。 gitはコミットに差はでない

gitプロジェクトは9年前につくられ、34000以上のコミットがあるが、まだ速い。

Q5. なんでzlibなのか?

A5. スタンダードだし。GPLだし。gitは標準的なアルゴリズムを採用する。

(GPLと言ったような気がするんですが、zlibってzlib licenseですよね?)

Q6. gitを教えるとき、どのくらいから内部構造について教えたほうがいいのか

A6. 色々意見あるけど、ジョンは内部について最初から教える。

gitの履歴はグラフだということを教える。

gitのコマンドの全てはそのグラフを編集するためにあるということを教える。 (checkoutやrebase)

明日の仕事にどう使えるかに重点を置く。 svnなどからgitに移すトレーニングとか。

後々に内部を見たいと思わせるように少しだけ内部構造について紹介する。

終わり

レポートは以上です。

内容はとてもわかりやすくて面白かったです。非常に濃密な時間でした。その分疲れましたw

自分が知らなかった領域についても知ることができたのでよかったです。

GitHubチームのお二人、ありがとうございました:)

Comments