git fetch
の裏側でどんな通信が行われてリモートリポジトリの内容が取得できるのか調べたのでまとめる。もともとは git の HTTP や SSH といったプロトコルでどのように実現されているか、というところに興味があった。Git v2.7.1 を基にしている。
事前準備
手を動かしてプロトコルを理解できるよう、gist の小さなリポジトリ を使う。適当なディレクトリ下に bare リポジトリとして clone しておく。
% cd ~/tmp # お好きな場所で % git clone --bare https://gist.github.com/5cfc0b016df3dc4683ef.git Cloning into bare repository '5cfc0b016df3dc4683ef.git'... remote: Counting objects: 9, done. remote: Compressing objects: 100% (3/3), done. remote: Total 9 (delta 0), reused 0 (delta 0), pack-reused 0 Unpacking objects: 100% (9/9), done. Checking connectivity... done. % ls 5cfc0b016df3dc4683ef.git/ HEAD config description hooks/ info/ objects/ packed-refs refs/
また、以下で「適当な空のリポジトリ」が必要な場合は以下のようにして作ったものを利用することとする。
% cd ~/tmp % rm -rf empty-repo % git init --bare empty-repo.git
pack プロトコル
クライアントが git fetch
(とか、それを引き起こす git pull
)を実行すると、指定されたリモート(省略した場合は "origin")のサーバと以下の流れに沿ってデータのやりとりを行う。
- Reference discovery: リモートリポジトリの ref の一覧をサーバがクライアントに伝える。
- Packfile negotiation: 要求する object などの情報をクライアントがサーバに伝える。また、すでにローカルに存在する object をクライアントがサーバに伝え、サーバはそれを元にクライアントの要求に応えるのに必要十分な内容を選択する。
- Packfile の送受信: object の列を圧縮した packfile をサーバが送信する。クライアントは受信した packfile をローカルリポジトリに展開する。
これが行われる際、クライアント/サーバそれぞれで git のサブコマンドが実行される。クライアント側では git fetch
または git fetch-pack
、サーバ側は git upload-pack
である。
以下、この流れにそって詳しく見ていく。
pkt-line フォーマット
まずはこのプロトコルで使用されるデータフォーマットについて。送受信されるデータは 4 バイトの 16 進数文字列表現による長さと、それに続くペイロードで一つの塊としてエンコードされる。長さはそれ自身の 4 バイトぶんを含む。このフォーマットを pkt-line と呼ぶ。
具体例をみるのが分かりやすい(Documentation Common to Pack and Http Protocols より):
pkt-line actual value --------------------------------- "0006a\n" "a\n" "0005a" "a" "000bfoobar\n" "foobar\n" "0004" ""
長さ部分が "0000"(でペイロードが長さ 0)であるものは特に flush-pkt と呼ばれ、要求や応答の終わりを示すのに使われる特殊な pkt-line である。
Reference discovery
サーバは、クライアントにリモートリポジトリの ref(ブランチとタグ)と、それが指す object name(SHA-1)の一覧を伝える。
手元では、git upload-pack --advertise-refs --stateless-rpc <dir>
でサーバ側の応答を生成できる。
% git upload-pack --stateless-rpc --advertise-refs 5cfc0b016df3dc4683ef.git/ 00d10ab1a827b3193d55b023c1051c6d00bb45057e46 HEAD\0multi_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.7.1 # "\0" は NUL 003f0ab1a827b3193d55b023c1051c6d00bb45057e46 refs/heads/master 0000 # 終端の改行なし
"SHA1 ref" という内容で、ref とそれが現在指す object の pkt-line が並ぶ。最初の行の、NUL を挟んだ残りの部分に含まれているのは capabilities と呼ばれ、サーバおよびクライアントがサポートしている機能の一覧や、このセッションに関するメタ情報を含んでいる。例えば
- 次節の packfile negotiation の拡張プロトコルに対応しているかどうか
- packfile (後述)の拡張フォーマットに対応しているかどうか
など。いろいろあるんだけどひとつひとつ取りあげることはしない。詳しくは Git Protocol Capabilities に。
Packfile negotiation
サーバから ref の一覧を受け取って、クライアントがどの object を要求するかを決める。必要な ref の指している先がすでにローカルに存在していることがわかった場合や、git ls-remote
の中ではこれ以降のステップは実行されない。
手元では、git upload-pack --stateless-rpc <dir>
でサーバ側の応答を生成できる。クライアントからの要求は標準入力に渡す。
以下は非常に単純な例。うまくいくと標準出力にバイナリデータが出力さる(ターミナルが壊れることもある)。
{ echo '003ewant 0ab1a827b3193d55b023c1051c6d00bb45057e46 no-progress'; \ echo -n '0000'; \ echo '0032have 136802d3c5782043066e192863c45c421b88f0a8'; \ echo '0009done'; \ } | git upload-pack --stateless-rpc 5cfc0b016df3dc4683ef.git/
この例では、
want f04e005b13815c4455474c5550f069f6776eff5e
では特定の object をリモートから受け取りたいこと、no-progress
capability でリモートの進捗("Counting objects: 458, done." みたいなの)を受け取らないこと、0000
で受け取りたい object の列挙の終わり、have 054bc353ea512a5b846545981306ca1153c39b5f
ではこの object がローカルリポジトリに存在していること、done
でクライアント要求の終わり
を伝えている。want も have もサーバ側はこれを受け取って、packfile(詳細は次の項)を生成しクライアントに送信する。
0031ACK 136802d3c5782043066e192863c45c421b88f0a8 PACK[バイナリデータ…]
サーバの応答の ACK
は、クライアントが have
で伝えてきた object がリモートリポジトリにも存在しますよ、という意味。
Packfile negotiation とあるように、ここは本当はインタラクティブに行われる。このフェーズについて詳しくみると:
- クライアントは欲しい object のリストを
want
行で送信する。 - クライアントはローカルリポジトリの object を最大 256 個まで
have
行で送信する(git の実装では新しい順)。 - サーバは
have
行を受け取った際、その object がリモートリポジトリにも存在していたらACK object-id
を送信する。 - クライアントはサーバの
ACK
を受け取って、have
で送信する object を調整する。十分な情報が集まったらdone
を送信する。- 例えばあるコミットについて
ACK
を受け取った場合、その親をたどる必要はなくなる。
- 例えばあるコミットについて
このフェーズになると git-upload-pack のための入力を手で送信するのは大変なので、クライアント側も git fetch-pack
を使ってみる。GIT_TRACE_PACKET
環境変数でこのプロトコルに関するデバッグ出力を有効にできる。
適当な空のリポジトリを作って以下を実行する。
% echo refs/heads/master | GIT_TRACE_PACKET=1 git --git-dir=empty-repo.git/ fetch-pack --stdin --no-progress 5cfc0b016df3dc4683ef.git/ 13:42:52.135504 pkt-line.c:80 packet: upload-pack> 0ab1a827b3193d55b023c1051c6d00bb45057e46 HEAD\0multi_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.7.1 13:42:52.136424 pkt-line.c:80 packet: upload-pack> 0ab1a827b3193d55b023c1051c6d00bb45057e46 refs/heads/master 13:42:52.136447 pkt-line.c:80 packet: upload-pack> 0000 13:42:52.136233 pkt-line.c:80 packet: fetch-pack< 0ab1a827b3193d55b023c1051c6d00bb45057e46 HEAD\0multi_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.7.1 13:42:52.136805 pkt-line.c:80 packet: fetch-pack< 0ab1a827b3193d55b023c1051c6d00bb45057e46 refs/heads/master 13:42:52.136812 pkt-line.c:80 packet: fetch-pack< 0000 13:42:52.137172 pkt-line.c:80 packet: fetch-pack> want 0ab1a827b3193d55b023c1051c6d00bb45057e46 multi_ack_detailed side-band-64k no-progress ofs-delta agent=git/2.7.1 13:42:52.137181 pkt-line.c:80 packet: fetch-pack> 0000 13:42:52.137192 pkt-line.c:80 packet: fetch-pack> done 13:42:52.137212 pkt-line.c:80 packet: upload-pack< want 0ab1a827b3193d55b023c1051c6d00bb45057e46 multi_ack_detailed side-band-64k no-progress ofs-delta agent=git/2.7.1 13:42:52.137342 pkt-line.c:80 packet: upload-pack< 0000 13:42:52.137352 pkt-line.c:80 packet: upload-pack< done 13:42:52.137357 pkt-line.c:80 packet: upload-pack> NAK 13:42:52.137451 pkt-line.c:80 packet: fetch-pack< NAK 13:42:52.142308 pkt-line.c:80 packet: sideband< PACK ... 13:42:52.142630 pkt-line.c:80 packet: upload-pack> 0000 13:42:52.143079 pkt-line.c:80 packet: sideband< 0000 0ab1a827b3193d55b023c1051c6d00bb45057e46 refs/heads/master
refs/heads/master
なる ref に対応する object を取得するよう、git fetch-pack
に標準に入力から伝えている。--no-progress
はパケットのやりとりを簡単にするためのオプション。デバッグ出力のうち、upload-pack>
で始まる行がサーバ側の応答であり、fetch-pack>
で始まる行がクライアント側の要求である。
これを実行すると git fetch-pack
により packfile が展開され、ローカルリポジトリに object が追加される。git --git-dir=empty-repo.git/ show 0ab1a827b3193d55b023c1051c6d00bb45057e46
などとして、当該コミットがリポジトリに存在することが確認できる。
サーバとクライアントのネゴシエーションにはいくつかバージョンがあって、複数の object を ACK できる multi_ack
やそれを拡張した multi_ack_detailed
capability が存在する。最新の git であれば multi_ack_detailed
が有効なはず。ここを詳しくは見ないので Packfile transfer protocols を参照。
Packfile の送受信
サーバは PACK
のあとに packfile と呼ばれるバイナリデータを送信する。これは複数の object をひとつのファイルにまとめたもので、git gc
の際に生成されることもある。ここではこの詳細なフォーマットについては立ち入らない。詳しく知りたい場合は Pro Git: Gitの内側 - Packfile を参照。
それではこの項で何を語るのかという話になるのだけれど、ここで興味深いのが side-band
および side-band-64k
という capability。これがクライアントによって指定された場合、サーバは packfile の送信を多重化する。つまり packfile 自体のデータに他の情報も織り込んで送信する。
このモードが有効な場合、packfile の送受信は pkt-line フォーマット上で行われる。その際、ペイロード部の最初の 1 バイトの値によってデータのチャンネルが決まる:
\1
: ペイロードは packfile のデータそのもの。\2
: ペイロードは進行状況。"Compressing objects: 100% (3/3)" みたいなやつ\3
: ペイロードはエラー情報。
先の例では git fetch-pack
にあえて --no-progress
をつけていたけど、これを外せば多重化された内容が確認できる。適当な空のリポジトリに対して:
% echo refs/heads/master | GIT_TRACE_PACKET=1 git --git-dir=empty-repo.git/ fetch-pack --stdin 5cfc0b016df3dc4683ef.git/ [中略] 13:50:05.646040 pkt-line.c:80 packet: fetch-pack> want 136802d3c5782043066e192863c45c421b88f0a8 multi_ack_detailed side-band-64k ofs-delta agent=git/2.7.1 # no-progress なし [中略] 13:50:05.650561 pkt-line.c:80 packet: sideband< \2Counting objects: 3, done. remote: Counting objects: 3, done. 13:50:05.651377 pkt-line.c:80 packet: sideband< \2Total 3 (delta 0), reused 0 (delta 0) remote: Total 3 (delta 0), reused 0 (delta 0) 13:50:05.651396 pkt-line.c:80 packet: sideband< PACK ... 13:50:05.651612 pkt-line.c:80 packet: upload-pack> 0000 13:50:05.651784 pkt-line.c:80 packet: sideband< 0000 Unpacking objects: 100% (3/3), done. 136802d3c5782043066e192863c45c421b88f0a8 refs/heads/master
git clone
や git pull
時にいつも見るメッセージはここでやりとりされていたわけだ。
packfile への圧縮・packfile からの展開
複数の object をひとつのデータに効率よくまとめたものが packfile。これは git pack-objects
と git unpack-objects
を使って変換できる。前者は現在のリポジトリの複数の object を packfile フォーマットにまとめ、後者はその逆の操作を行う。
以下はあるリポジトリに含まれている2個の object を git pack-objects
と git unpack-objects
で別の適当な空のリポジトリに送る例。
% git --git-dir=5cfc0b016df3dc4683ef.git/ rev-parse master~1:README.txt master:README.txt | git --git-dir=5cfc0b016df3dc4683ef.git/ pack-objects --stdout | git --git-dir=empty-repo.git/ unpack-objects Counting objects: 2, done. Total 2 (delta 0), reused 0 (delta 0) Unpacking objects: 100% (2/2), done. % git --git-dir=empty-repo.git/ cat-file -p 18b287e01c9f4b756d96e9a62491aa9868fb679bAn example simple repository.
リモートからの転送の際、packfile にまとめられて送られてくる object の数が多い時は git unpack-objects
の代わりに git index-pack
が使用される。これは object を単純にリポジトリに展開するのではなく、packfile 内の object の索引(.idx)を作成するコマンド。以下、git unpack-objects
と書いているところは git index-pack
にもなりうる。
各種トランスポートの実装
ここまで pack protocol を見たところで、これを転送するトランスポートの仕組みを見てみる。
file トランスポート
ローカルマシンの中で完結する file トランスポートは、実はこれまで見てきた標準入出力による pack protocol とほぼ同じ。
file プロトコルのリモートリポジトリに対して git fetch
を呼び出すと、リモートリポジトリ(= ローカルのディレクトリ)で git upload-pack
をサブプロセスとして起動し、パイプを使って通信する。ここでは git-fetch-pack ではなく git-fetch 自体が negotiation を担当する。
このとき起動されるプロセスは以下のような親子関係になっている(はず)。親子プロセスはたいていの場合パイプで通信しあっている。
LOCAL REMOTE git fetch |------------------------------------ git upload-pack `- git unpack-objects `- git pack-objects
ssh トランスポート
ssh トランスポートも file トランスポートのわりと素朴な拡張で、ローカルのディレクトリで git upload-pack
を実行する代わりに ssh
した先で git-upload-pack <dir>
を実行する(git upload-pack
ではない)。パイプでやりとりするのは同じ。
ssh://git@example.com/path/repo.git
という URL であれば ssh git@exmaple.com 'git-upload-pack /path/repo.git'
が実行される。
ちなみに似たようなものとして SCP-like syntax と呼ばれる git@example.com:path/repo.git
という記法があるが、この形の場合起動されるコマンドは ssh git@example.com 'git-upload-pack path/repo.git'
と、パスの先頭に /
がないものとなり、似ているようで異なるので注意が必要である。~
も特別扱いされる。詳しくは Packfile transfer protocols を参照。
LOCAL REMOTE git fetch |- ssh <-----------[SSH]-----------> git-upload-pack `- git unpack-objects `- git pack-objects
git トランスポート
git トランスポートは TCP 9418 ポートに pkt-line でリクエストを送ることで開始される。
git://gist.github.com/5cfc0b016df3dc4683ef.git
であれば:
% echo -e -n '0043git-upload-pack /5cfc0b016df3dc4683ef.git\0host=gist.github.com\0' \ | nc gist.github.com 9418 00eb0ab1a827b3193d55b023c1051c6d00bb45057e46 HEADmulti_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.6.5~peff-relaxed-fsck-1348-ga22efe0 003f0ab1a827b3193d55b023c1051c6d00bb45057e46 refs/heads/master
リモートは git-daemon
の実装であれば git upload-pack
を呼び出してレスポンスを生成する。
LOCAL REMOTE git fetch <---------[TCP]--------> git-daemon `- git unpack-objects `- git upload-pack `- git pack-objects
http(s) トランスポート
http(s) での通信は、古くは dumb protocol と呼ばれる .git
以下のファイルを一々転送するようなプロトコルが用いられていたが、今後これが話題になることはないと思うのでこでは触れない。
このトランスポートでは 2 回の HTTP リクエストがおこなわれる。
- Reference discovery.
GET /path/repo.git/info/refs?service=git-upload-pack
- Packfile negotiation + Packfile.
POST /path/repo.git/git-upload-pack
送受信される内容はわずかに特別なヘッダが付与されることを除けばこれまで見てきたものと同じである。Reference discovery であれば手で実行できるので見てみよう。
% curl -i 'https://gist.github.com/5cfc0b016df3dc4683ef.git/info/refs?service=git-upload-pack' -H 'User-Agent: git/2.7.1' HTTP/1.1 200 OK Server: GitHub Babel 2.0 Content-Type: application/x-git-upload-pack-advertisement Transfer-Encoding: chunked Expires: Fri, 01 Jan 1980 00:00:00 GMT Pragma: no-cache Cache-Control: no-cache, max-age=0, must-revalidate Vary: Accept-Encoding X-GitHub-Request-Id: 7047A333:5489:AB8B02B:56DD99A0 X-Frame-Options: DENY 001e# service=git-upload-pack 000000f30ab1a827b3193d55b023c1051c6d00bb45057e46 HEAD\0multi_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.6.5~peff-relaxed-fsck-1348-ga22efe0 # "\0" は NUL 003f0ab1a827b3193d55b023c1051c6d00bb45057e46 refs/heads/master 0000
実装は git-fetch にビルトインではなく、git-remote-http(s)
というヘルパーコマンドを通じて行われる。
このヘルパーコマンドも標準入力から命令を受け取ることになっていて、詳細は [githelp:remote-helpers] に記載されている。以下のコマンドを空のリポジトリで実行すれば雰囲気をつかめるだろう。
% { echo list; \ echo fetch 0ab1a827b3193d55b023c1051c6d00bb45057e46 refs/heads/master; \ echo \ } | GIT_CURL_VERBOSE=1 git --git-dir=empty-repo.git/ remote-https https://gist.github.com/5cfc0b016df3dc4683ef.git [省略]
LOCAL REMOTE git fetch `- git remote-https <---[HTTP]---> (httpd) `- git fetch-pack `- git upload-pack `- git unpack-objects `- git pack-objects
まとめ
git の object の転送プロトコルについて簡単に見ていった。調べはじめた時には HTTP 通信専用のヘルパがあるとは知らず手間取ったけれど、豊富なサブコマンドをパイプでつないで転送を実現していることがわかった。予想に反して結構複雑な作りになっていた……。
何か間違ってるところがあったら教えてください。
file トランスポートで見たように転送のコア部分は git のサブコマンドで実現されているので、git のリモートサーバは意外と簡単に実装することができる: github.com/motemen/mir はそういうひとつ(だがシンプルではない)
ここで説明しきれていない話:
- Packfile のフォーマット
- Packfile negotiation の詳細な挙動
- multi_ack の挙動
- カスタムトランスポート