Git --distributed-is-the-new-centralized
Chapters ▾

9.6 Gitの内側 - トランスファープロトコル

トランスファープロトコル

Git は2つのレポジトリ間を二つの主要な方法によってデータを移行することができます。ひとつは HTTPによって、もうひとつは、file://ssh://、また、git:// によるトランスポートに使用される、いわゆるスマートプロトコルによって。このセクションでは、これらの主要なプロトコルがどのように機能するのかを駆け足で見ていきます。

無口なプロトコル

Git の over HTTPによる移行は、しばしば無口なプロトコル(dumb protocol)と言われます。なぜなら、トランスポートプロセスの最中に、サーバ側に関する Git 固有のコードは何も必要としないからです。フェッチプロセスは、一連の GET リクエストであり、クライアントはサーバ上の Gitレポジトリのレイアウトを推測することができます。simplegit ライブラリに対する http-fetch のプロセスを追ってみましょう。

$ git clone http://github.com/schacon/simplegit-progit.git

最初にこのコマンドが行うことは info/refs ファイルを引き出す(pull down)ことです。このファイルは update-server-info コマンドによって書き込まれます。そのために、HTTPトランスポートが適切に動作するための post-receive フックとして、そのコマンドを有効にする必要があります。

=> GET info/refs
ca82a6dff817ec66f44342007202690a93763949     refs/heads/master

いまあなたはリモート参照と SHAのハッシュのリストを持っています。 次に、終了時に何をチェックアウトするのかを知るために、HEAD参照が何かを探します。

=> GET HEAD
ref: refs/heads/master

プロセスの完了時に、master ブランチをチェックアウトする必要があります。この時点で、あなたは参照を辿るプロセス(the walking process)を開始する準備ができています。開始時点はあなたが info/refs ファイルの中に見た ca82a6 のコミットオブジェクトなので、それをフェッチすることによって開始します。

=> GET objects/ca/82a6dff817ec66f44342007202690a93763949
(179 bytes of binary data)

オブジェクトバック(object back)を取得します。それは、サーバ上の緩い形式のオブジェクトで、静的な HTTP GETリクエストを超えてそれをフェッチします。zlib-uncompress を使ってそれを解凍することができます。ヘッダを剥ぎ取り(strip off)それからコミットコンテンツを見てみます。

$ git cat-file -p ca82a6dff817ec66f44342007202690a93763949
tree cfda3bf379e4f8dba8717dee55aab78aef7f4daf
parent 085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7
author Scott Chacon <schacon@gmail.com> 1205815931 -0700
committer Scott Chacon <schacon@gmail.com> 1240030591 -0700

changed the version number

次に、取り戻すためのオブジェクトがもう二つあります。それは、たった今取り戻したコミットがポイントするコンテンツのツリーである cfda3b と、親のコミットである 085bb3 です。

=> GET objects/08/5bb3bcb608e1e8451d4b2432f8ecbe6306e7e7
(179 bytes of data)

それは次のコミットオブジェクトを与えます。ツリーオブジェクトをつかみます。

=> GET objects/cf/da3bf379e4f8dba8717dee55aab78aef7f4daf
(404 - Not Found)

おっと、どうやらそのツリーオブジェクトはサーバ上の緩い形式には存在しないようです。そのため404のレスポンスを受け取っています。これには二つの理由があります。ひとつは、オブジェクトは代替のレポジトリ内に存在し得るため、もうひとつは、このレポジトリ内のパックファイルの中に存在し得るため。Git はまずリストにあるあらゆる代替の URLをチェックします。

=> GET objects/info/http-alternates
(empty file)

代替の URLのリストと一緒にこれが戻ってくるなら、Git はそこにある緩いファイルとパックファイルをチェックします。これは、ディスク上のオブジェクトを共有するために互いにフォークし合っているプロジェクトにとって素晴らしい機構(mechanism)です。しかし、このケースではリスト化された代替は存在しないため、オブジェクトはパックファイルの中にあるに違いありません。サーバー上の何のパックファイルが利用可能かを知るには、objects/info/packs のファイルを取得することが必要です。そのファイルには(さらに update-server-info によって生成された)それらの一覧が含まれています。

=> GET objects/info/packs
P pack-816a9b2334da9953e530f27bcac22082a9f5b835.pack

サーバー上にはパックファイルがひとつしかないので、あなたのオブジェクトは明らかにそこにあります。しかし念の為にインデックスファイルをチェックしてみましょう。これが便利でもあるのは、もしサーバー上にパックファイルを複数持つ場合に、どのパックファイルにあなたが必要とするオブジェクトが含まれているのかを知ることができるからです。

=> GET objects/pack/pack-816a9b2334da9953e530f27bcac22082a9f5b835.idx
(4k of binary data)

パックファイルのインデックスを持っているので、あなたのオブジェクトがその中にあるのかどうかを知ることができます。なぜならインデックスにはパックファイルの中にあるオブジェクトの SHAハッシュとそれらのオブジェクトに対するオフセットがリストされているからです。あなたのオブジェクトはそこにあります。さあ、すべてのパックファイルを取得してみましょう。

=> GET objects/pack/pack-816a9b2334da9953e530f27bcac22082a9f5b835.pack
(13k of binary data)

あなたはツリーオブジェクトを持っているのでコミットを辿ってみましょう。それらすべてはまた、あなたが丁度ダウンロードしたパックファイルの中にあります。そのため、もはやサーバーに対していかなるリクエストも不要です。Git は master ブランチの作業用コピーをチェックアウトします。そのブランチは最初にダウンロードした HEAD への参照によってポイントされています。

このプロセスのすべての出力はこのように見えます。

$ git clone http://github.com/schacon/simplegit-progit.git
Initialized empty Git repository in /private/tmp/simplegit-progit/.git/
got ca82a6dff817ec66f44342007202690a93763949
walk ca82a6dff817ec66f44342007202690a93763949
got 085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7
Getting alternates list for http://github.com/schacon/simplegit-progit.git
Getting pack list for http://github.com/schacon/simplegit-progit.git
Getting index for pack 816a9b2334da9953e530f27bcac22082a9f5b835
Getting pack 816a9b2334da9953e530f27bcac22082a9f5b835
 which contains cfda3bf379e4f8dba8717dee55aab78aef7f4daf
walk 085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7
walk a11bef06a3f659402fe7563abf99ad00de2209e6

スマートプロトコル

HTTPメソッドはシンプルですが少し非効率です。スマートプロトコルを使用することはデータ移行のより一般的な手段です。これらのプロトコルは Git をよく知っているリモートエンド上にプロセスを持っています。そのリモートエンドは、ローカルのデータを読んで、クライアントが何を持っているか、または、必要としているか、そして、それに対するカスタムデータを生成するのか知ることができます。データを転送するためのプロセスが2セットあります。データをアップロードするペア、それと、ダウンロードするペアです。

データのアップロード

リモートプロセスにデータをアップロードするため、Git は send-packreceive-pack のプロセスを使用します。send-pack プロセスはクライアント上で実行されリモートサイド上の receive-pack プロセスに接続します。

例えば、あなたのプロジェクトで git push origin master を実行したとしましょう。そして origin は SSHプロトコルを使用する URLとして定義されているとします。Git はあなたのサーバーへの SSHによる接続を開始する send-pack プロセスを実行します。リモートサーバ上で以下のようなSSHの呼び出しを介してコマンドを実行しようとします。

$ ssh -x git@github.com "git-receive-pack 'schacon/simplegit-progit.git'"
005bca82a6dff817ec66f4437202690a93763949 refs/heads/master report-status delete-refs
003e085bb3bcb608e1e84b2432f8ecbe6306e7e7 refs/heads/topic
0000

git-receive-pack コマンドは現在持っている各々の参照に対してひとつの行をすぐに返します。このケースでは、master ブランチとその SHAハッシュのみです。最初の行はサーバーの可能性(ここでは、report-statusdelete-refs)のリストも持っています。

各行は 4バイトの 16進数で始まっており、その残りの行がどれくらいの長さなのかを示しています。最初の行は 005b で始まっていますが、これは16進数では 91 であり、その行には 91バイトが残っていることを意味します。次の行は 003e で始まっていて、これは 62 です。そのため残りの 62バイトを読みます。次の行は 0000 であり、サーバーはその参照のリスト表示を終えたことを意味します。

サーバーの状態がわかったので、あなたの send-pack プロセスはサーバーが持っていないのは何のコミットかを決定します。このプッシュが更新する予定の各参照に対して、send-pack プロセスは receive-pack プロセスにその情報を伝えます。例えば、もしもあなたが master ブランチを更新していて、さらに、experiment ブランチを追加しているとき、send-pack のレスポンスは次のように見えるかもしれません。

0085ca82a6dff817ec66f44342007202690a93763949  15027957951b64cf874c3557a0f3547bd83b3ff6 refs/heads/master report-status
00670000000000000000000000000000000000000000 cdfdb42577e2506715f8cfeacdbabc092bf63e8d refs/heads/experiment
0000

すべてが '0' の SHA-1ハッシュ値は以前そこには何もなかったことを意味します。それはあなたが experiment の参照を追加しているためです。もしもあなたが参照を削除していたとすると、あなたは逆にすべての '0' が右側にあるのを見るでしょう。

Git はあなたが古い SHA1ハッシュで更新している各々の古い参照、新しい参照、そして更新されている参照に対して行を送信します。最初の行はまたクライアントの性能(capabilities)を持っています。次に、クライアントはサーバーが未だ持ったことのないすべてのオブジェクトのパックファイルをアップロードします。最後に、サーバーは成功(あるいは失敗)の表示を返します。

000Aunpack ok

データのダウンロード

データをダウンロードするときには、fetch-packupload-pack プロセスが伴います。クライアントは fetch-pack プロセスを開始します。何のデータが移送されてくるのかを取り決める(negotiate)ため、それはリモートサイド上の upload-pack プロセスに接続します。

リモートリポジトリ上の upload-pack プロセスを開始する異なった方法があります。あなたは receive-pack プロセスと同様に SSH経由で実行することができます。さらに、Git デーモンを介してプロセスを開始することもできます。そのデーモンは、デフォルトではサーバ上の 9418ポートを使用します。fetch-pack プロセスはデータを送信します。そのデータは接続後のデーモンに対して、以下のように見えます。

003fgit-upload-pack schacon/simplegit-progit.git\0host=myserver.com\0

どれくらい多くのデータが続いているのかを示す 4バイトから始まります。それから、ヌルバイトに続いて実行コマンド、そして最後のヌルバイトに続いてサーバーのホスト名が来ます。Git デーモンはコマンドが実行でき、レポジトリが存在して、それがパブリックのパーミッションを持っていることをチェックします。もしすべてが素晴らしいなら、upload-pack プロセスを発行して、それに対するリクエストを渡します。

もし SSHを介してフェッチを行っているとき、fetch-pack は代わりにこのように実行します。

$ ssh -x git@github.com "git-upload-pack 'schacon/simplegit-progit.git'"

いずれケースでも、fetch-pack の接続のあと、upload-pack はこのように送り返します。

0088ca82a6dff817ec66f44342007202690a93763949 HEAD\0multi_ack thin-pack \
  side-band side-band-64k ofs-delta shallow no-progress include-tag
003fca82a6dff817ec66f44342007202690a93763949 refs/heads/master
003e085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7 refs/heads/topic
0000

これは receive-pack が返答する内容にとても似ていますが、性能は異なります。加えて、これがクローンの場合はクライアントが何をチェックアウトするのかを知るために HEAD への参照を送り返します。

この時点で、fetch-pack プロセスは何のオブジェクトがそれを持っているかを見ます。そして "want" とそれが求める SHA1ハッシュを送ることによって、それが必要なオブジェクトを返答します。"have" とその SHA1ハッシュで既に持っているオブジェクトすべてを送ります。このリストの最後で、それが必要とするデータのパックファイルを送信する upload-pack プロセスを開始するために "done" を書き込みます。

0054want ca82a6dff817ec66f44342007202690a93763949 ofs-delta
0032have 085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7
0000
0009done

これはトランスファープロトコルのとても基本的なケースです。より複雑なケースでは、クライアントは multi_ack または side-band の性能をサポートします。しかしこの例ではスマートプロトコルのプロセスによって使用される基本の部分を示します。