Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                

Git の仕組み (1)

目次

  1. はじめに
  2. Git の全体像
  3. Git オブジェクトの ID と 中身
  4. tree と blob オブジェクト
  5. commit オブジェクト
  6. リファレンスとブランチ
  7. 2種類のタグ
  8. 一時待避 (stash)
  9. インデックス
    • キャッシュとしての役割
  10. マージ
    • Fast-Forward マージ
    • non Fast-Forward マージ
    • rebase
    • reset
  11. 2種類のブランチ
    • リポジトリが自分のブランチを持っている
    • 「リモートブランチ」について
    • 追跡ブランチ
    • ブランチの大三角形
  12. リモートと関係するコマンド、しないコマンド
  13. リモートリポジトリと refspec
    • リモートリポジトリ指定
    • refspec
    • fetch の refspec は remotes がローカル
  14. コミットツリーの森、孤児のコミット
  15. 関係ない2つのリポジトリを混ぜる
  16. bareリポジトリ
  17. 分散リポジトリ

1. はじめに

Git の難しさは独特です。

Git は、洗練されたオブジェクトモデルを、雑多なコマンド群が包み込む構造になっています。

オブジェクトモデルというのは、「Git オブジェクト」を単位としたデータ構造のことです。どのようにデータを記録してあるかが、 Git の最も魅力的な部分です。Git の内部を見ていくと、実にシンプルな仕組みで動いていることに驚きます。

一方、Git コマンドには現実の複雑さをそのまま反映しているような部分もあって、学んでいてもあまり楽しくありません。

Git は、核となるオブジェクトモデルに、これこれの操作を加えたい、という順序で発展してきており、コマンドから学び始めるのは、そもそも方向が逆なのです。

オブジェクトモデルがスタート地点で、ゴールとして雑多なコマンドがあるのに、使い始めるときはどうしてもコマンドから入門せざるを得ない。コマンドは泥臭い部分もあって、あまり楽しくない。

そこで、基本的な使い方がある程度わかったら、内部のオブジェクトモデルから入門し直すことをおすすめします。オブジェクトモデルがわかると、自然な流れでコマンドを理解できるようになります。

内部動作と実装を学ぶことは、Git がどうしてこんなに便利で有効なのかを根本的に理解するのに重要です。しかし初心者にとっては不必要に複雑で混乱を招いてしまうという人もいました。そのため、遅かれ早かれ学習の仕方に合わせて読めるように、この話題を最後の章に配置しました。いつ読むかって? それは読者の判断にお任せします。

Git - Gitの内側

私が Git を理解しようと調査した時、高級なコマンドの視点から眺めるよりボトムアップ式に理解することが役立った。そしてボトムアップ視点で見る Git がこんなにも美しくシンプルであるなら、私が調べたことを他の人も興味を持って読んでくれるのではないか、そうして私が経験した苦労を避けられるのではと考えた。

見えないチカラ: 【翻訳】Gitをボトムアップから理解する


これらの優れた記事を頼りに、この記事もまた、ボトムアップの入り口から行けるところまで行くことを目標としています。

技術的な前提知識が必要な話はほとんどありません。エンジニアでなくても十分に理解できる内容だと思います (難しかったら自分の文章力のせいです)。

生のデータが見たい方へ

エンジニアの方の中には、日本語の説明よりデータを見た方が早い、という方もいるかもしれません。

この記事とは別に Git Object Browser という Git の内部をブラウズするアプリケーションを作成しました。さらにそのアプリから書き出したデータに注釈を加えたサイトを以下で公開しています。

Part1 は、どの Git コマンドで .git ディレクトリのどこが変わるかについて、30のステップで説明しました。Part2 は複数のリポジトリを行き来しながら、リポジトリがお互いにオブジェクトをやり取りする様子を観察できるようにしてあります。

f:id:koseki2:20140421034721g:plain

このサイトは、本記事とは独立した内容です。記事を読んでいてよくわからなくなったら、実際のデータを見るとイメージがわくかもしれません。

2. Git の全体像

Git リポジトリは、大きく分けると、

  • 作業ディレクトリ
  • .git ディレクトリ

の 2 つのディレクトリでできています。

作業ディレクトリには、Git で管理されるファイルが入っています。私たちが直接編集するのは、作業ディレクトリのファイルです。

作業ディレクトリの一番上の階層に .git ディレクトリ があります。.git ディレクトリがリポジトリの本体です。また、.git ディレクトリは、そこから下が Git で管理されていることの目印にもなっています。

リポジトリから別のリポジトリを作るとき、普通は git clone コマンドを使いますが、単に .git ディレクトリを別のディレクトリにコピーするだけでも、新しいリポジトリを作ることができます。

.git の中身

.git に含まれる要素で大切なのは、

  • Git オブジェクト
  • リファレンス
  • インデックス

の3つです。ほかに補助的な要素として、

  • ログ
  • 設定

があります。

f:id:koseki2:20140420170528p:plain

  • Gitオブジェクトは .git/objects ディレクトリに、
  • リファレンスは .git/refs ディレクトリに、
  • インデックスは .git/index ファイルに、

それぞれ格納されています。

Git オブジェクトデータベース

Git オブジェクトの集合を Git オブジェクトデータベースと呼びます。

最初のうち、1 個の Git オブジェクトは 1 個のファイルに書いてあります。たとえば、

.git/objects/30/3ff981c488b812b6215f7db7920dedb3b59d9a

というファイルに Git オブジェクトが格納されています。

オブジェクトの数が増えてくると、複数のオブジェクトが pack 形式と呼ばれるファイルにまとめられることもあります。

1 オブジェクト 1 ファイルの形式でも pack 形式でも、 Git オブジェクトの中身は一緒です。記録方式が違うだけです。

4種類のオブジェクト

Git オブジェクトは 4 種類あります。treeblobcommittag の4種類です。

f:id:koseki2:20140420170220p:plain

tree はディレクトリ、blob はファイルに相当します。commit はコミット、tag はタグです。

Git オブジェクトは、ID (オブジェクト名) で区別できます。ID は 40 文字の 16 進数 (0〜9 と a〜f) で表現されます。例えば、

0b48b1325567ae55dbfaefb250e7460e4fbb7d6d

といった具合です。

ID だけ見ても、オブジェクトの種類 (treeblobcommittag のどれか) は区別がつきません。4種類のオブジェクトがごちゃ混ぜに .git/objects ディレクトリに入っています。

リファレンス

オブジェクトの他に、リファレンスと呼ばれる重要なファイルがあります。

リファレンスは、特定の commit オブジェクト (または tag オブジェクト) を指し示すブックマークのようなものです。

f:id:koseki2:20140420180706p:plain

リファレンスが指し示す commit オブジェクトからたどって、芋づる式に他のオブジェクトを引き出すことができます。

f:id:koseki2:20140420172746p:plain

リファレンスは .git/refs ディレクトリ以下に保存されています。

リファレンスには Git オブジェクトの ID がテキストで書いてあります。たとえば .git/refs/heads/master というリファレンスに、

0b48b1325567ae55dbfaefb250e7460e4fbb7d6d

と書いてあったとします。この 40 文字が commit オブジェクトの ID です。この ID を持つオブジェクトは、

.git/objects/0b/48b1325567ae55dbfaefb250e7460e4fbb7d6d

というファイルに保存されています。ID の先頭 2 文字分のディレクトリがあって、のこり 38 文字がファイル名です。

リファレンスのリファレンス

また、リファレンスには、もう一つ別の形式があります。他のリファレンスを指し示すリファレンスです。

たとえば、.git/HEAD リファレンスには、

ref: refs/heads/master

のように書いてあります。この形式のリファレンスは、 commit オブジェクトを指し示すのではなく、別のリファレンス .git/refs/heads/master を指し示しています。

f:id:koseki2:20140420180623p:plain

HEAD は特別なリファレンスファイルで、いまチェックアウトしているブランチを示します。HEAD に ref: refs/heads/master と書いてある場合、 master ブランチがチェックアウトされています。

別の名前のブランチ (例えば example) をチェックアウトすると、HEAD は ref: refs/heads/example と書き換わります。

大きなツリー

リファレンスと Git オブジェクトの参照関係は、全体で大きなツリー構造になっています。

この大きなツリー構造は、4層に整理することができます (各層の名前は勝手につけたものです。公式な呼び方ではありません)。

f:id:koseki2:20140420183120p:plain

Git にコミットしたファイルやディレクトリは、① ファイルツリーに保存されます。ファイルツリーには tree オブジェクトと blob オブジェクトでできています。

コミットのたび、ファイルツリーを1つぶら下げた commit オブジェクトが ② コミットツリー に追加されます。commit オブジェクトも、ファイルツリーと同様にツリー構造を形成します。

コミットツリーの特定の位置を ③ リファレンスが指し示します。また、④ リファレンスのリファレンス が特定のリファレンスを指し示します。

MacOSX のタイムマシンの UI がイメージしやすいかもしれません。


Mac OS X 10.5 Leopard feature video - Time ...

ファイルツリーが垂直方向に成長するとしたら、コミットツリーは奥から手前に成長します。コミットツリーのある地点を指すのがリファレンスです。

f:id:koseki2:20140420183642p:plain

歴史が一直線にならぶ Mac のタイムマシンとは異なり、Git のコミットツリーは枝分かれしたり合流しながら成長していきます。

3. Git オブジェクトの ID と 中身

Git オブジェクトの ID は、SHA1 というハッシュ関数で生成されます。

Git オブジェクトの中身は、 Zlib という圧縮ライブラリで生成されます。

下図のように、オブジェクトの種類 + (スペース) + データのサイズ + (\0) + データSHA1 関数に入力した結果が ID になり、Zlib に入力した結果が中身になります。

f:id:koseki2:20140420183844p:plain

ハッシュ関数は、同じ入力には同じハッシュ値、違う入力には違うハッシュ値を返します。Git オブジェクトの中身が同じなら ID も同じ、中身が違えば ID も異なります。

世界中、誰が作ったどのリポジトリでも、同じオブジェクトには同じ ID が付いています。

たとえば、空の blob の ID を Google で検索すると、こんな風に たくさんみつかります。

空の .gitignore ファイルが入った tree オブジェクトの ID を検索すると、こんな感じです

Git オブジェクトの ID と中身は不可分です。ある ID が付いたオブジェクトの中身だけ書き換える、というような操作はあり得ません。中身を変えると、別の ID が付いた別のオブジェクトになります。Git オブジェクトには「変更」に当たる操作は無く、「新規作成」と「削除」だけが可能です。

ハッシュ関数 SHA1 の簡単な説明

Git オブジェクトの ID を作るハッシュ関数について、ごく簡単に見ておきましょう。

Git で使われるハッシュ関数 SHA1 は、入力されたデータを 40 文字にして返します。 *1

この 40 文字をハッシュ値と呼びます。

ハッシュ関数は、同じ入力に対して常に同じハッシュ値を返します。例えば、

入力 SHA1 ハッシュ値
(空文字列) da39a3ee5e6b4b0d3255bfef95601890afd80709
a 86f7e437faa5a7fce15d1ddcb9eaeaea377667b8
b e9d71f5ee7c92d6dc9e92ffdad17b8bd49418f98
c 84a516841ba77a5b4648de2cd0dfcb30ea46dbb4
git 46f1a0bd5592a2f9244ca321b129902a06b53e03
夏目漱石 坊ちゃん (青空文庫 XHTML版) adf63acaf3cff383174e95477b9a37b7296afccb

といった具合です。何百テラもあるような巨大なデータでも、SHA1ハッシュ値は 40 文字になります。 *2

そして、このハッシュ値は、非常に重複しにくくなっています。あなたと私が同姓同名でなければ、あなたの氏名の SHA1 と、私の氏名の SHA1 は確実に異なります。

仮に、 2 つの異なる blob オブジェクトに同じ SHA1 ハッシュ値が割り当てられた場合、2つのファイルの内 1 つが消失してしまいます。けれども、そのようなことを心配する必要はありません。

SHA1 ハッシュの重複 (衝突) しにくさについて、『Pro Git』は次のように説明しています。

たった一つの衝突が 50% の確率で発生するために必要なオブジェクトの数は約 2^80 となります (中略)。つまり一兆二千億のそのまた一兆倍です。これは、地球上にあるすべての砂粒の数の千二百倍にあたります。

地球上の人類 65 億人が全員プログラムを書いていたとします。そしてその全員が、Linux カーネルのこれまでの開発履歴 (100 万の Git オブジェクト) と同等のコードを一秒で書き上げ、馬鹿でかい単一の Git リポジトリにプッシュしていくとします。これを五年間続けたとして、SHA-1 オブジェクトの衝突がひとつでも発生する可能性がやっと 50% になります。それよりも「あなたの所属する開発チームの全メンバーが、同じ夜にそれぞれまったく無関係の事件で全員オオカミに殺されてしまう」可能性のほうがよっぽど高いことでしょう。

Git - リビジョンの選択

SHA1 ハッシュ関数については、

という 3 点だけ覚えておいてください。

4. tree と blob オブジェクト

Git に追加したディレクトリやファイルは、tree オブジェクトや blob オブジェクトとして、オブジェクトデータベースに保存されます。

tree はディレクトリ、blob はファイルに相当します。

ただし、tree は自分のディレクトリ名を知りません。blob は自分のファイル名を知りません。

f:id:koseki2:20140420184051p:plain

blob オブジェクトには、ファイルの中身がそのまま書いてあります。

tree オブジェクトには、次のように、あるディレクトリに含まれるファイル名・サブディレクトリ名と、そのオブジェクト ID が一覧で書かれています。

100644 aaa.txt \0 72943a16fb2c8f38f9dde202b7a70ccc19c52f34
100644 bbb.txt \0 f761ec192d9f0dca3329044b96ebdb12839dbff6
100644 ccc.txt \0 b2a7546679fdf79ca0eb7bfbee1e1bb342487380
40000 ddd \0 0d546c66a969f7b205bf2fd450ef5e4b9efb70bf
100755 eee.sh \0 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
120000 fff-symlink \0 35a6b12323dd0e2f9a2a3e5abe291920151ede4a

先頭の数字は、

  • ファイルの種類
  • 実行可能ファイルかどうか
    • 644 実行不可
    • 755 実行可

を示しています。

空白の後にファイル名があり、ヌルバイト(\0)が挟まった後、最後に blob または tree オブジェクトの ID が (本当はバイナリで) 書いてあります。

tree に記録されている情報はこれだけです。ファイルのタイムスタンプや所有者のような情報は、blob にも tree にも書かれていません。あるファイルを誰がいつ作ったかは、treeblob を見てもわかりません。

blob はファイルを git add したときに作成されます。一方、treegit commit した時に作成されます。このタイミングの違いについては、後でインデックスについて説明する時に詳しく見ます。

tree と blob の参照関係

あるディレクトリに同じ中身のファイル A.txt と B.txt があった場合、blob オブジェクトは1個だけ作成されます。

f:id:koseki2:20140420184333p:plain

tree オブジェクトは以下のようなデータになります。ファイルの中身が同じなので blob は同じ 72943a1... を参照しています。

100644 A.txt \0 72943a16fb2c8f38f9dde202b7a70ccc19c52f34
100644 B.txt \0 72943a16fb2c8f38f9dde202b7a70ccc19c52f34

ファイル B.txt を別のディレクトリに移動すると、移動元と移動先の tree が更新されます。blob は元のままです。

f:id:koseki2:20140420184346p:plain

tree 1

100644 A.txt \0 72943a16fb2c8f38f9dde202b7a70ccc19c52f34

tree 2

100644 B.txt \0 72943a16fb2c8f38f9dde202b7a70ccc19c52f34

ルートツリーの ID でツリー全体を識別する

treeblob を変更すると、新しいオブジェクトが作られます。

例えば、あるファイルを書き換えて、git add すると、新しい blob オブジェクトが、Git オブジェクトデータベースに追加されます。

さらに、blob が更新されると、そのオブジェクトを含む親 tree オブジェクトも更新されます。tree オブジェクトには、子の ID が書き込まれているからです。

例えば、次のような tree オブジェクトがあったとして、

100644 A.txt \0 72943a16fb2c8f38f9dde202b7a70ccc19c52f34
100644 B.txt \0 72943a16fb2c8f38f9dde202b7a70ccc19c52f34

A.txt の内容が書かれた blob が入れ替わると、

100644 A.txt \0 f761ec192d9f0dca3329044b96ebdb12839dbff6
100644 B.txt \0 72943a16fb2c8f38f9dde202b7a70ccc19c52f34

tree が変化します。同様に、子の tree が変わると、親 tree も変わります。

f:id:koseki2:20140420185211p:plain

変化は親へ親へと伝播して、最終的に、一番上のルート tree オブジェクトが入れ替わります。

f:id:koseki2:20140420185225p:plain

どれほど巨大なファイルツリーであっても、末端のどこか一箇所ファイルを書き換えると、変化は親をさかのぼってルートの tree ID に反映されます。つまり、ルート tree ID は、ファイルツリー全体を一意に識別する ID になっています。

ルート tree オブジェクトの ID さえあれば、他のファイルツリーと混同することなくファイルツリー全体を取り出すことができます。

新しいファイルツリーによって、古いファイルツリーが影響を受けることはありません。新しいファイルツリーが追加された後でも、古いルート tree オブジェクトからたどって、古いファイルツリーを取り出すことができます。

ファイルツリーは、過去のファイルツリーと tree blob オブジェクトの大半を共有しつつ、差分だけを新しいオブジェクトで置き換えて、増えていく仕組みになっています。

(続きます)

*1:正確には 16 進数文字列で40文字、160ビットの値です。

*2: SHA1 は最大で 2 ^ 64 - 1 = 約 16 エクサバイトまでの入力を計算できるそうです。 http://ja.wikipedia.org/wiki/Secure_Hash_Algorithm