Git
章 ▾ 第2版

10.6 Gitの内部構造 - 転送プロトコル

転送プロトコル

Gitは、2つのリポジトリ間でデータを転送する際に、主に「ダム」プロトコルと「スマート」プロトコルの2つの方法を使用できます。このセクションでは、これら2つの主要なプロトコルがどのように動作するかを簡単に説明します。

ダムプロトコル

HTTP経由で読み取り専用で提供されるリポジトリを設定する場合、ダムプロトコルが使用される可能性が高くなります。このプロトコルは、転送プロセス中にサーバー側でGit固有のコードを必要としないため、「ダム」と呼ばれます。フェッチプロセスは一連のHTTP GETリクエストであり、クライアントはサーバー上のGitリポジトリのレイアウトを想定できます。

注記

ダムプロトコルは、最近では比較的まれにしか使用されません。セキュリティ保護やプライベート化が難しいため、ほとんどのGitホスト(クラウドベースとオンプレミスの両方)では、ダムプロトコルの使用を拒否します。通常は、少し後で説明するスマートプロトコルを使用することをお勧めします。

simplegitライブラリのhttp-fetchプロセスを追ってみましょう。

$ git clone http://server/simplegit-progit.git

このコマンドで最初に行うことは、info/refsファイルをプルダウンすることです。このファイルはupdate-server-infoコマンドによって書き込まれます。そのため、HTTP転送を適切に機能させるには、post-receiveフックとして有効にする必要があります。

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

これで、リモート参照とSHA-1のリストができました。次に、HEAD参照がどれであるかを確認して、完了時に何をチェックアウトするかを把握します。

=> GET HEAD
ref: refs/heads/master

プロセスが完了したら、masterブランチをチェックアウトする必要があります。この時点で、ウォーキングプロセスを開始する準備が整いました。開始点は、info/refsファイルで確認したca82a6コミットオブジェクトであるため、まずそれをフェッチすることから始めます。

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

オブジェクトが返されます。そのオブジェクトはサーバー上でloose形式であり、静的なHTTP GETリクエストでフェッチしました。zlib解凍してヘッダーを取り除き、コミットコンテンツを確認できます。

$ 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

Change version number

次に、さらに2つのオブジェクトを取得する必要があります。cfda3bは、先ほど取得したコミットが指すコンテンツのツリー、085bb3は親コミットです。

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

これで、次のコミットオブジェクトが得られます。ツリーオブジェクトを取得します。

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

おっと、どうやらそのツリーオブジェクトはサーバー上でルース形式ではないようです。そのため、404レスポンスが返ってきました。これにはいくつかの理由が考えられます。オブジェクトが代替リポジトリにあるか、このリポジトリのパックファイルにあるかのどちらかです。Gitは最初にリストされている代替リポジトリを確認します。

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

もしこれで代替URLのリストが返ってきた場合、Gitはそこにあるルースファイルとパックファイルを確認します。これは、互いにフォークしたプロジェクトがディスク上でオブジェクトを共有するための優れたメカニズムです。しかし、このケースでは代替リポジトリがリストされていないため、オブジェクトはパックファイルにあるはずです。このサーバーで利用可能なパックファイルを確認するには、objects/info/packsファイルを取得する必要があります。このファイルには、パックファイルの一覧が含まれています(これもupdate-server-infoによって生成されます)。

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

サーバーには1つのパックファイルしかないので、明らかにオブジェクトはその中にあるはずですが、念のためインデックスファイルを確認します。これは、サーバーに複数のパックファイルがある場合に、どのパックファイルに目的のオブジェクトが含まれているかを確認するのにも役立ちます。

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

パックファイルのインデックスを手に入れたので、オブジェクトがそこにあるかどうかを確認できます。インデックスには、パックファイルに含まれるオブジェクトのSHA-1と、それらのオブジェクトへのオフセットがリストされているからです。オブジェクトはそこにあるので、パックファイル全体を取得します。

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

ツリーオブジェクトを手に入れたので、コミットの走査を続けます。それらもすべて、ダウンロードしたばかりのパックファイル内にあるため、サーバーに追加のリクエストを行う必要はありません。Gitは、最初にダウンロードしたHEAD参照が指していたmasterブランチの作業コピーをチェックアウトします。

スマートプロトコル

ダムプロトコルはシンプルですが、少し非効率的であり、クライアントからサーバーへのデータの書き込みを処理できません。スマートプロトコルは、より一般的なデータ転送方法ですが、リモートエンドでGitについてインテリジェントなプロセスが必要です。これは、ローカルデータを読み取り、クライアントが持っているものと必要なものを把握し、それに対してカスタムパックファイルを生成できます。データ転送には、データをアップロードするためのペアと、データをダウンロードするためのペアの2種類のプロセスがあります。

データのアップロード

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

SSH

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

$ ssh -x git@server "git-receive-pack 'simplegit-progit.git'"
00a5ca82a6dff817ec66f4437202690a93763949 refs/heads/master□report-status \
	delete-refs side-band-64k quiet ofs-delta \
	agent=git/2:2.1.1+github-607-gfba4028 delete-refs
0000

git-receive-packコマンドは、現在持っている各参照に対して1行を即座に返します。この場合、masterブランチとそのSHA-1のみです。最初の行には、サーバーの機能リスト(ここでは、report-statusdelete-refs、およびクライアント識別子を含む他のいくつかの機能)もあります。

データはチャンクで送信されます。各チャンクは、チャンクの長さ(長さ自体を示す4バイトを含む)を指定する4文字の16進数値で始まります。チャンクには通常、単一行のデータと後続の改行が含まれています。最初のチャンクは00a5で始まり、これは16進数で165であり、チャンクの長さが165バイトであることを意味します。次のチャンクは0000であり、サーバーが参照リストを終えたことを意味します。

これでサーバーの状態がわかったため、send-packプロセスはサーバーが持っていないコミットを判別します。このプッシュで更新される各参照について、send-packプロセスはreceive-packプロセスにその情報を伝えます。たとえば、masterブランチを更新してexperimentブランチを追加する場合、send-packの応答は次のようになります。

0076ca82a6dff817ec66f44342007202690a93763949 15027957951b64cf874c3557a0f3547bd83b3ff6 \
	refs/heads/master report-status
006c0000000000000000000000000000000000000000 cdfdb42577e2506715f8cfeacdbabc092bf63e8d \
	refs/heads/experiment
0000

Gitは、更新する各参照に対して、行の長さ、古いSHA-1、新しいSHA-1、および更新される参照を含む行を送信します。最初の行にはクライアントの機能も含まれています。すべて「0」のSHA-1の値は、以前には何もなかったことを意味します。これは、experiment参照を追加しているためです。参照を削除する場合は、反対の結果が表示されます。右側にすべて「0」が表示されます。

次に、クライアントはサーバーがまだ持っていないすべてのオブジェクトのパックファイルを送信します。最後に、サーバーは成功(または失敗)を示す応答を返します。

000eunpack ok
HTTP(S)

このプロセスは、HTTP上ではほとんど同じですが、ハンドシェイクが少し異なります。接続は、次のリクエストで開始されます。

=> GET http://server/simplegit-progit.git/info/refs?service=git-receive-pack
001f# service=git-receive-pack
00ab6c5f0e45abd7832bf23074a333f739977c9e8188 refs/heads/master□report-status \
	delete-refs side-band-64k quiet ofs-delta \
	agent=git/2:2.1.1~vmg-bitmaps-bugaloo-608-g116744e
0000

これが最初のクライアント-サーバー交換の終わりです。次に、クライアントは別のリクエスト(今回はPOST)を、send-packが提供するデータとともに実行します。

=> POST http://server/simplegit-progit.git/git-receive-pack

POSTリクエストには、send-packの出力とパックファイルがペイロードとして含まれます。次に、サーバーはHTTPレスポンスで成功または失敗を示します。

HTTPプロトコルは、このデータをチャンク転送エンコーディングでさらにラップする場合があることに注意してください。

データのダウンロード

データをダウンロードする場合、fetch-packプロセスとupload-packプロセスが関与します。クライアントは、リモート側のupload-packプロセスに接続して、どのデータをダウンロードするかをネゴシエートするfetch-packプロセスを開始します。

SSH

SSH経由でフェッチを実行している場合、fetch-packは次のようなものを実行します。

$ ssh -x git@server "git-upload-pack 'simplegit-progit.git'"

fetch-packが接続すると、upload-packは次のようなものを返します。

00dfca82a6dff817ec66f44342007202690a93763949 HEAD□multi_ack thin-pack \
	side-band side-band-64k ofs-delta shallow no-progress include-tag \
	multi_ack_detailed symref=HEAD:refs/heads/master \
	agent=git/2:2.1.1+github-607-gfba4028
003fe2409a098dc3e53539a9028a94b6224db9d6a6b6 refs/heads/master
0000

これはreceive-packが応答するものと非常によく似ていますが、機能が異なります。さらに、HEADが指しているもの(symref=HEAD:refs/heads/master)を返して、クライアントがこれがクローンである場合に何をチェックアウトするかを認識できるようにします。

この時点で、fetch-packプロセスは、自分が持っているオブジェクトを確認し、必要なオブジェクトを「want」と、次に必要なSHA-1を送信することで応答します。「have」と、次にすでに持っているSHA-1を送信することで、すでに持っているすべてのオブジェクトを送信します。このリストの最後に、「done」を書き込んで、upload-packプロセスが、必要なデータのパックファイルを送信を開始するようにします。

003cwant ca82a6dff817ec66f44342007202690a93763949 ofs-delta
0032have 085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7
0009done
0000
HTTP(S)

フェッチ操作のハンドシェイクには、2つのHTTPリクエストが必要です。最初は、ダムプロトコルで使用したのと同じエンドポイントへのGETです。

=> GET $GIT_URL/info/refs?service=git-upload-pack
001e# service=git-upload-pack
00e7ca82a6dff817ec66f44342007202690a93763949 HEAD□multi_ack thin-pack \
	side-band side-band-64k ofs-delta shallow no-progress include-tag \
	multi_ack_detailed no-done symref=HEAD:refs/heads/master \
	agent=git/2:2.1.1+github-607-gfba4028
003fca82a6dff817ec66f44342007202690a93763949 refs/heads/master
0000

これは、SSH接続でgit-upload-packを呼び出すのと非常によく似ていますが、2番目の交換は別のリクエストとして実行されます。

=> POST $GIT_URL/git-upload-pack HTTP/1.0
0032want 0a53e9ddeaddad63ad106860237bbf53411d11a7
0032have 441b40d833fdfa93eb2908e52742248faf0ee993
0000

繰り返しますが、これは上記と同じ形式です。このリクエストへの応答は、成功または失敗を示し、パックファイルを含みます。

プロトコルの概要

このセクションでは、転送プロトコルの非常に基本的な概要について説明しました。プロトコルには、multi_ackside-band機能など、他にも多くの機能が含まれていますが、それらについて詳しく説明することは本書の範囲外です。クライアントとサーバー間の一般的なやり取りについて、大まかな感じをつかんでいただけたかと思います。これ以上の知識が必要な場合は、おそらくGitのソースコードを調べる必要があるでしょう。

scroll-to-top