浅谈 git fetch 的工作方式

发布日期:分类:Programming

最近写 Anni 的时候遇到一个问题。Anni元数据仓库是以 Git 仓库的形式存在的,但对客户端而言这种形式并不方便交互。

如果客户端只是利用 GitHubAPI 下载 HEAD 的压缩文件,那么仓库和 GitHub 就有了强关联;但如果想要 clone 的话,直接使用 git 命令显然是行不通的,需要一个合适的 Git 实现。

由于客户端需要,我们不仅需要 Anni 编写使用语言的 Rust 版本,还需要一个 Dart 的版本,甚至一个 Web 版本,而后二者目前存在的实现相比 Rust 的实现功能更少,完全不能满足使用需要。

Git 对于很多人,尤其是我,一直都是一个大黑箱。于是正好借着这个机会,我花了一天时间简单地了解了一下这个黑箱中的 fetch 部分,其结果就是这篇文章了。

环境准备

我们以 gitea 作为实验环境,通过 pacman -S gitea 直接安装即可,安装完成后通过 systemctl start gitea 启动。

默认监听的端口是 3000,我们可用通过 localhost:3000 进行访问。

我们任意新建一个仓库。方便起见,我们直接使用网页自带自带的初始化。初始化完成后如下图所示:

基于对 Git 的简单认识,我们知道这个仓库目前存在 3Object:一个 COMMIT、一个 TREE 和一个 BLOB

Wireshark 抓包

我们对仓库进行 clone

git clone http://localhost:3000/yesterday17/Test.git --depth=1

同时打开 wiresharkLoopback: lo 进行抓包:

可以看到,总共有三个 HTTP 请求,我们一个一个来看。

1

请求

我们的请求如下:

其中值得注意的是 GETPathGit-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/masterhash 数据。

3

在得到了 commithash 之后,我们就可以向服务器请求数据了。

请求

请求的 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-deltaGit 的打包有关,下一篇博客再述。其他的部分都比较直观。

返回

返回的 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.rut񵕋Ma蘙.v:wc󲝓9ޱ𜻻T..݁..8xܓV.I-.ᢂ.	7...%..ïۭ̊؍6諊
0006.&
0000

可以发现其中有一些乱码和点(.),这就是实际的二进制部分了。这部分的结构也很简单,在 packfile 之后 就是实际的 Pack 部分了,分为三种类型。

第一种类型就是实际的 Pack 内容,为 0x01

第二种类型为进度消息,由服务端发送以告知客户端 fetch 的进度,类型为 0x02

最后一种是错误信息,当服务端出现致命错误时发出,类型为 0x03

类型信息存储在四位 16 进制数之后的一个字节中,也就是上文中诸如 0021007e00dc 后面的点(.)。

日志输出

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 initgit fetchgit checkout 三个步骤,内容还是有点太多了。况且目前的应用场景也不需要了解太多,于是就只研究了最核心的 git fetch

Git 的文档只能说是「存在」,其中说得不清楚的地方太多,个人实现的过程中也是磕磕绊绊的。这篇文章也算是艰难 Debug 路上的慰藉吧(笑

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注