最近写 Anni
的时候遇到一个问题。Anni
的元数据仓库是以 Git
仓库的形式存在的,但对客户端而言这种形式并不方便交互。
如果客户端只是利用 GitHub
的 API
下载 HEAD
的压缩文件,那么仓库和 GitHub
就有了强关联;但如果想要 clone
的话,直接使用 git
命令显然是行不通的,需要一个合适的 Git
实现。
由于客户端需要,我们不仅需要 Anni
编写使用语言的 Rust
版本,还需要一个 Dart
的版本,甚至一个 Web
版本,而后二者目前存在的实现相比 Rust
的实现功能更少,完全不能满足使用需要。
Git
对于很多人,尤其是我,一直都是一个大黑箱。于是正好借着这个机会,我花了一天时间简单地了解了一下这个黑箱中的 fetch
部分,其结果就是这篇文章了。
环境准备
我们以 gitea
作为实验环境,通过 pacman -S gitea
直接安装即可,安装完成后通过 systemctl start gitea
启动。
默认监听的端口是 3000
,我们可用通过 localhost:3000
进行访问。
我们任意新建一个仓库。方便起见,我们直接使用网页自带自带的初始化。初始化完成后如下图所示:
基于对 Git
的简单认识,我们知道这个仓库目前存在 3
个 Object
:一个 COMMIT
、一个 TREE
和一个 BLOB
。
Wireshark 抓包
我们对仓库进行 clone
:
git clone http://localhost:3000/yesterday17/Test.git --depth=1
同时打开 wireshark
对 Loopback: lo
进行抓包:
可以看到,总共有三个 HTTP
请求,我们一个一个来看。
1
请求
我们的请求如下:
其中值得注意的是 GET
的 Path
和 Git-Protocol
头。在第一个请求中,我们请求 /info/refs?service=git-upload-pack
,并且设置 Git-Protocol
的值为 version=2
。
返回
该请求的返回如下:
返回的 body
明文如下:
001e# service=git-upload-pack 0000000eversion 2 0015agent=git/2.30.1 000cls-refs 0012fetch=shallow 0012server-option 0017object-format=sha1 0000
可以看到,返回的格式如下:
// 为方便观察,手动增加了部分空格 // 之后的 body 均如此 001e # service=git-upload-pack // 固定 0000 // 此处不存在换行,仅为表示需要 // 0000 表示结束 0015 agent=git/2.30.1 // 服务端 Git 版本 000c ls-refs // 表示服务端支持 ls-refs 0012 fetch=shallow // 表示服务端支持的操作 0012 server-option // 表示可以接收任意数量的 server option 0017 object-format=sha1 // 服务端使用的 hash 算法 0000
在每一行之前,有四个有趣的 16
进制数字,表示的是之后的字节数量。比如第一行是 001e
,表示之后有 30
个字节,而:
# service=git-upload-pack 0000
正好是 30
个字符。
2
在获取了服务端的信息之后,我们就可以开始正式请求了。
请求
这里我们发现,首先是 method
变成了 POST
;其次是请求的 path
发生了变化,变成了 /git-upload-pack
;最后是 Content-Type
,为 application/x-git-upload-pack-request
。让我们来看看 body
:
0014 command=ls-refs # 命令 0014 agent=git/2.30.1 # 本地 Git 版本 0016 object-format=sha1 # 使用的 hash 算法 0001 0009 peel 000c symrefs 0014 ref-prefix HEAD # 获取 HEAD 001b ref-prefix refs/heads/ # 获取 ref/heads 001a ref-prefix refs/tags/ # 获取 ref/tags 0000
返回
返回结果如下:
0052 869bda16bc91c702d812e18fd9a2653dd8c9f461 HEAD symref-target:refs/heads/master 003f 869bda16bc91c702d812e18fd9a2653dd8c9f461 refs/heads/master 0000
可以看到,从这次请求中,我们得到了 ref/heads/master
的 hash
数据。
3
在得到了 commit
的 hash
之后,我们就可以向服务器请求数据了。
请求
请求的 body
如下:
0016 object-format=sha1 0011 command=fetch # 命令为 fetch 0014 agent=git/2.30.1 0001 000d thin-pack 000f include-tag 000d ofs-delta # Pack 使用 OFS_DELTA(下一篇博客详述) 000c deepen 1 # 只 clone 1 层 0032 want 869bda16bc91c702d812e18fd9a2653dd8c9f461 0032 want 869bda16bc91c702d812e18fd9a2653dd8c9f461 0009 done 0000
这里的 ofs-delta
和 Git
的打包有关,下一篇博客再述。其他的部分都比较直观。
返回
返回的 body
如下:
0011 shallow-info 0034 shallow 869bda16bc91c702d812e18fd9a2653dd8c9f461 0001 000d packfile 0021.枚举对象中: 3, 完成. 007e.对象计数中: 33% (1/3) 对象计数中: 66% (2/3) 对象计数中: 100% (3/3) 对象计数中: 100% (3/3), 完成. 0043.总共 3(差异 0),复用 0(差异 0),包复用 0 00dc.PACK........Պxܕ˻ B1..A6ϼAĖcl̆..ؖﶆ<ET3Ó.2ִʥ..z.X\!ʖ奱mѻC.}̉;ߕgŝF8˴뢨ï`õ).ץ8`B4տL涪ک ߛ|.1G1Υ.xܳ40031Q.rutMa蘙.v:wc9ޱT..݁..8xܓV.I-.ᢂ. 7...%..ïۭ̊؍6諊 0006.& 0000
可以发现其中有一些乱码和点(.
),这就是实际的二进制部分了。这部分的结构也很简单,在 packfile
之后 就是实际的 Pack
部分了,分为三种类型。
第一种类型就是实际的 Pack
内容,为 0x01
。
第二种类型为进度消息,由服务端发送以告知客户端 fetch
的进度,类型为 0x02
。
最后一种是错误信息,当服务端出现致命错误时发出,类型为 0x03
。
类型信息存储在四位 16 进制数之后的一个字节中,也就是上文中诸如 0021
、007e
、00dc
后面的点(.
)。
日志输出
Git
提供了调试日志,我们可以来看一下:
❯ GIT_TRACE=1 GIT_TRACE_PACKET=1 git clone http://localhost:3000/yesterday17/Test.git --depth 1 11:45:42.031851 git.c:444 trace: built-in: git clone http://localhost:3000/yesterday17/Test.git --depth 1 正克隆到 'Test'... 11:45:42.039795 run-command.c:664 trace: run_command: git remote-http origin http://localhost:3000/yesterday17/Test.git 11:45:42.040655 git.c:730 trace: exec: git-remote-http origin http://localhost:3000/yesterday17/Test.git 11:45:42.040693 run-command.c:664 trace: run_command: git-remote-http origin http://localhost:3000/yesterday17/Test.git 11:45:42.047808 pkt-line.c:80 packet: git< # service=git-upload-pack 11:45:42.047858 pkt-line.c:80 packet: git< 0000 11:45:42.047861 pkt-line.c:80 packet: git< version 2 11:45:42.047863 pkt-line.c:80 packet: git< agent=git/2.30.1 11:45:42.047865 pkt-line.c:80 packet: git< ls-refs 11:45:42.047867 pkt-line.c:80 packet: git< fetch=shallow 11:45:42.047869 pkt-line.c:80 packet: git< server-option 11:45:42.047871 pkt-line.c:80 packet: git< object-format=sha1 11:45:42.047873 pkt-line.c:80 packet: git< 0000 11:45:42.047946 pkt-line.c:80 packet: clone< version 2 11:45:42.047957 pkt-line.c:80 packet: clone< agent=git/2.30.1 11:45:42.047964 pkt-line.c:80 packet: clone< ls-refs 11:45:42.047969 pkt-line.c:80 packet: clone< fetch=shallow 11:45:42.047974 pkt-line.c:80 packet: clone< server-option 11:45:42.047980 pkt-line.c:80 packet: clone< object-format=sha1 11:45:42.047985 pkt-line.c:80 packet: clone< 0000 11:45:42.047989 pkt-line.c:80 packet: clone> command=ls-refs 11:45:42.047997 pkt-line.c:80 packet: clone> agent=git/2.30.1 11:45:42.047998 pkt-line.c:80 packet: git< command=ls-refs 11:45:42.048001 pkt-line.c:80 packet: clone> object-format=sha1 11:45:42.048004 pkt-line.c:80 packet: git< agent=git/2.30.1 11:45:42.048006 pkt-line.c:80 packet: clone> 0001 11:45:42.048028 pkt-line.c:80 packet: git< object-format=sha1 11:45:42.048029 pkt-line.c:80 packet: clone> peel 11:45:42.048030 pkt-line.c:80 packet: git< 0001 11:45:42.048034 pkt-line.c:80 packet: clone> symrefs 11:45:42.048035 pkt-line.c:80 packet: git< peel 11:45:42.048039 pkt-line.c:80 packet: clone> ref-prefix HEAD 11:45:42.048040 pkt-line.c:80 packet: git< symrefs 11:45:42.048044 pkt-line.c:80 packet: git< ref-prefix HEAD 11:45:42.048044 pkt-line.c:80 packet: clone> ref-prefix refs/heads/ 11:45:42.048050 pkt-line.c:80 packet: clone> ref-prefix refs/tags/ 11:45:42.048053 pkt-line.c:80 packet: git< ref-prefix refs/heads/ 11:45:42.048054 pkt-line.c:80 packet: clone> 0000 11:45:42.048057 pkt-line.c:80 packet: git< ref-prefix refs/tags/ 11:45:42.048061 pkt-line.c:80 packet: git< 0000 11:45:42.050613 pkt-line.c:80 packet: clone< 869bda16bc91c702d812e18fd9a2653dd8c9f461 HEAD symref-target:refs/heads/master 11:45:42.050613 pkt-line.c:80 packet: git> 0002 11:45:42.050624 pkt-line.c:80 packet: clone< 869bda16bc91c702d812e18fd9a2653dd8c9f461 refs/heads/master 11:45:42.050628 pkt-line.c:80 packet: clone< 0000 11:45:42.050637 pkt-line.c:80 packet: clone< 0002 11:45:42.051301 pkt-line.c:80 packet: clone> command=fetch 11:45:42.051308 pkt-line.c:80 packet: clone> agent=git/2.30.1 11:45:42.051311 pkt-line.c:80 packet: clone> object-format=sha1 11:45:42.051317 pkt-line.c:80 packet: clone> 0001 11:45:42.051320 pkt-line.c:80 packet: clone> thin-pack 11:45:42.051321 pkt-line.c:80 packet: git< object-format=sha1 11:45:42.051323 pkt-line.c:80 packet: clone> include-tag 11:45:42.051327 pkt-line.c:80 packet: clone> ofs-delta 11:45:42.051335 pkt-line.c:80 packet: clone> deepen 1 11:45:42.051339 pkt-line.c:80 packet: clone> want 869bda16bc91c702d812e18fd9a2653dd8c9f461 11:45:42.051342 pkt-line.c:80 packet: clone> want 869bda16bc91c702d812e18fd9a2653dd8c9f461 11:45:42.051344 pkt-line.c:80 packet: clone> done 11:45:42.051347 pkt-line.c:80 packet: clone> 0000 11:45:42.051354 pkt-line.c:80 packet: git< command=fetch 11:45:42.051359 pkt-line.c:80 packet: git< agent=git/2.30.1 11:45:42.051362 pkt-line.c:80 packet: git< 0001 11:45:42.051365 pkt-line.c:80 packet: git< thin-pack 11:45:42.051369 pkt-line.c:80 packet: git< include-tag 11:45:42.051372 pkt-line.c:80 packet: git< ofs-delta 11:45:42.051375 pkt-line.c:80 packet: git< deepen 1 11:45:42.051379 pkt-line.c:80 packet: git< want 869bda16bc91c702d812e18fd9a2653dd8c9f461 11:45:42.051383 pkt-line.c:80 packet: git< want 869bda16bc91c702d812e18fd9a2653dd8c9f461 11:45:42.051386 pkt-line.c:80 packet: git< done 11:45:42.051389 pkt-line.c:80 packet: git< 0000 11:45:42.057628 pkt-line.c:80 packet: git> 0002 11:45:42.057664 pkt-line.c:80 packet: clone< shallow-info 11:45:42.057672 pkt-line.c:80 packet: clone< shallow 869bda16bc91c702d812e18fd9a2653dd8c9f461 11:45:42.057676 pkt-line.c:80 packet: clone< 0001 11:45:42.057754 pkt-line.c:80 packet: clone< packfile 11:45:42.057798 run-command.c:664 trace: run_command: git --shallow-file /home/yesterday17/Test/.git/shallow.lock index-pack --stdin -v --fix-thin '--keep=fetch-pack 97854 on Yesterday17-PC' 11:45:42.057820 pkt-line.c:80 packet: sideband< \2\37777777746\37777777636\37777777632\37777777744\37777777670\37777777676\37777777745\37777777657\37777777671\37777777750\37777777661\37777777641\37777777744\37777777670\37777777655: 3, \37777777745\37777777656\37777777614\37777777746\37777777610\37777777620. remote: 枚举对象中: 3, 完成. 11:45:42.057976 pkt-line.c:80 packet: sideband< \2\37777777745\37777777657\37777777671\37777777750\37777777661\37777777641\37777777750\37777777656\37777777641\37777777746\37777777625\37777777660\37777777744\37777777670\37777777655: 33% (1/3)\15\37777777745\37777777657\37777777671\37777777750\37777777661\37777777641\37777777750\37777777656\37777777641\37777777746\37777777625\37777777660\37777777744\37777777670\37777777655: 66% (2/3)\15\37777777745\37777777657\37777777671\37777777750\37777777661\37777777641\37777777750\37777777656\37777777641\37777777746\37777777625\37777777660\37777777744\37777777670\37777777655: 100% (3/3)\15\37777777745\37777777657\37777777671\37777777750\37777777661\37777777641\37777777750\37777777656\37777777641\37777777746\37777777625\37777777660\37777777744\37777777670\37777777655: 100% (3/3), \37777777745\37777777656\37777777614\37777777746\37777777610\37777777620. remote: 对象计数中: 100% (3/3), 完成. 11:45:42.058028 pkt-line.c:80 packet: sideband< \2\37777777746\37777777600\37777777673\37777777745\37777777605\37777777661 3\37777777757\37777777674\37777777610\37777777745\37777777667\37777777656\37777777745\37777777674\37777777602 0\37777777757\37777777674\37777777611\37777777757\37777777674\37777777614\37777777745\37777777644\37777777615\37777777747\37777777624\37777777650 0\37777777757\37777777674\37777777610\37777777745\37777777667\37777777656\37777777745\37777777674\37777777602 0\37777777757\37777777674\37777777611\37777777757\37777777674\37777777614\37777777745\37777777614\37777777605\37777777745\37777777644\37777777615\37777777747\37777777624\37777777650 0 remote: 总共 3(差异 0),复用 0(差异 0),包复用 0 11:45:42.058041 pkt-line.c:80 packet: sideband< PACK ... 11:45:42.058065 pkt-line.c:80 packet: sideband< 0000 11:45:42.058688 git.c:444 trace: built-in: git index-pack --stdin -v --fix-thin '--keep=fetch-pack 97854 on Yesterday17-PC' 接收对象中: 100% (3/3), 完成. 11:45:42.077544 pkt-line.c:80 packet: clone< 0002 11:45:42.078348 run-command.c:664 trace: run_command: git rev-list --objects --stdin --not --all --quiet --alternate-refs '--progress=正在检查连通性' 11:45:42.079311 git.c:444 trace: built-in: git rev-list --objects --stdin --not --all --quiet --alternate-refs '--progress=正在检查连通性'
过程很清晰,这里就不多赘述了。
结语
这篇文章诞生之初想讲的是 git clone
的流程,但众所周知 git clone
包括 git init
、git fetch
和 git checkout
三个步骤,内容还是有点太多了。况且目前的应用场景也不需要了解太多,于是就只研究了最核心的 git fetch
。
Git
的文档只能说是「存在」,其中说得不清楚的地方太多,个人实现的过程中也是磕磕绊绊的。这篇文章也算是艰难 Debug
路上的慰藉吧(笑