Skip to content

浅谈 git fetch 的工作方式

Published: at 12:55

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

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

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

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

ToC

环境准备

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

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

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

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

Wireshark 抓包

我们对仓库进行 clone

Terminal window
1
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 明文如下:

1
001e# service=git-upload-pack
2
0000000eversion 2
3
0015agent=git/2.30.1
4
000cls-refs
5
0012fetch=shallow
6
0012server-option
7
0017object-format=sha1
8
0000

可以看到,返回的格式如下:

1
// 为方便观察,手动增加了部分空格
2
// 之后的 body 均如此
3
001e # service=git-upload-pack // 固定
4
0000 // 此处不存在换行,仅为表示需要
5
// 0000 表示结束
6
0015 agent=git/2.30.1 // 服务端 Git 版本
7
000c ls-refs // 表示服务端支持 ls-refs
8
0012 fetch=shallow // 表示服务端支持的操作
9
0012 server-option // 表示可以接收任意数量的 server option
10
0017 object-format=sha1 // 服务端使用的 hash 算法
11
0000

在每一行之前,有四个有趣的 16 进制数字,表示的是之后的字节数量。比如第一行是 001e,表示之后有 30 个字节,而:

1
# service=git-upload-pack
2
0000

正好是 30 个字符。

2

在获取了服务端的信息之后,我们就可以开始正式请求了。

请求

这里我们发现,首先是 method 变成了 POST;其次是请求的 path 发生了变化,变成了 /git-upload-pack;最后是 Content-Type,为 application/x-git-upload-pack-request。让我们来看看 body

1
0014 command=ls-refs # 命令
2
0014 agent=git/2.30.1 # 本地 Git 版本
3
0016 object-format=sha1 # 使用的 hash 算法
4
0001
5
0009 peel
6
000c symrefs
7
0014 ref-prefix HEAD # 获取 HEAD
8
001b ref-prefix refs/heads/ # 获取 ref/heads
9
001a ref-prefix refs/tags/ # 获取 ref/tags
10
0000

返回

返回结果如下:

1
0052 869bda16bc91c702d812e18fd9a2653dd8c9f461 HEAD symref-target:refs/heads/master
2
003f 869bda16bc91c702d812e18fd9a2653dd8c9f461 refs/heads/master
3
0000

可以看到,从这次请求中,我们得到了 ref/heads/masterhash 数据。

3

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

请求

请求的 body 如下:

1
0016 object-format=sha1
2
0011 command=fetch # 命令为 fetch
3
0014 agent=git/2.30.1
4
0001
5
000d thin-pack
6
000f include-tag
7
000d ofs-delta # Pack 使用 OFS_DELTA(下一篇博客详述)
8
000c deepen 1 # 只 clone 1 层
9
0032 want 869bda16bc91c702d812e18fd9a2653dd8c9f461
10
0032 want 869bda16bc91c702d812e18fd9a2653dd8c9f461
11
0009 done
12
0000

这里的 ofs-deltaGit 的打包有关,下一篇博客再述。其他的部分都比较直观。

返回

返回的 body 如下:

1
0011 shallow-info
2
0034 shallow 869bda16bc91c702d812e18fd9a2653dd8c9f461
3
0001
4
000d packfile
5
0021.枚举对象中: 3, 完成.
6
007e.对象计数中: 33% (1/3)
7
对象计数中: 66% (2/3)
8
对象计数中: 100% (3/3)
9
对象计数中: 100% (3/3), 完成.
10
0043.总共 3(差异 0),复用 0(差异 0),包复用 0
11
00dc.PACK........Պxܕ˻
12
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諊
13
0006.&
14
0000

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

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

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

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

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

日志输出

Git 提供了调试日志,我们可以来看一下:

1
❯ GIT_TRACE=1 GIT_TRACE_PACKET=1 git clone http://localhost:3000/yesterday17/Test.git --depth 1
2
11:45:42.031851 git.c:444 trace: built-in: git clone http://localhost:3000/yesterday17/Test.git --depth 1
3
正克隆到 'Test'...
4
11:45:42.039795 run-command.c:664 trace: run_command: git remote-http origin http://localhost:3000/yesterday17/Test.git
5
11:45:42.040655 git.c:730 trace: exec: git-remote-http origin http://localhost:3000/yesterday17/Test.git
6
11:45:42.040693 run-command.c:664 trace: run_command: git-remote-http origin http://localhost:3000/yesterday17/Test.git
7
11:45:42.047808 pkt-line.c:80 packet: git< # service=git-upload-pack
8
11:45:42.047858 pkt-line.c:80 packet: git< 0000
9
11:45:42.047861 pkt-line.c:80 packet: git< version 2
10
11:45:42.047863 pkt-line.c:80 packet: git< agent=git/2.30.1
11
11:45:42.047865 pkt-line.c:80 packet: git< ls-refs
12
11:45:42.047867 pkt-line.c:80 packet: git< fetch=shallow
13
11:45:42.047869 pkt-line.c:80 packet: git< server-option
14
11:45:42.047871 pkt-line.c:80 packet: git< object-format=sha1
15
11:45:42.047873 pkt-line.c:80 packet: git< 0000
16
11:45:42.047946 pkt-line.c:80 packet: clone< version 2
17
11:45:42.047957 pkt-line.c:80 packet: clone< agent=git/2.30.1
18
11:45:42.047964 pkt-line.c:80 packet: clone< ls-refs
19
11:45:42.047969 pkt-line.c:80 packet: clone< fetch=shallow
20
11:45:42.047974 pkt-line.c:80 packet: clone< server-option
21
11:45:42.047980 pkt-line.c:80 packet: clone< object-format=sha1
22
11:45:42.047985 pkt-line.c:80 packet: clone< 0000
23
11:45:42.047989 pkt-line.c:80 packet: clone> command=ls-refs
24
11:45:42.047997 pkt-line.c:80 packet: clone> agent=git/2.30.1
25
11:45:42.047998 pkt-line.c:80 packet: git< command=ls-refs
26
11:45:42.048001 pkt-line.c:80 packet: clone> object-format=sha1
27
11:45:42.048004 pkt-line.c:80 packet: git< agent=git/2.30.1
28
11:45:42.048006 pkt-line.c:80 packet: clone> 0001
29
11:45:42.048028 pkt-line.c:80 packet: git< object-format=sha1
30
11:45:42.048029 pkt-line.c:80 packet: clone> peel
31
11:45:42.048030 pkt-line.c:80 packet: git< 0001
32
11:45:42.048034 pkt-line.c:80 packet: clone> symrefs
33
11:45:42.048035 pkt-line.c:80 packet: git< peel
34
11:45:42.048039 pkt-line.c:80 packet: clone> ref-prefix HEAD
35
11:45:42.048040 pkt-line.c:80 packet: git< symrefs
36
11:45:42.048044 pkt-line.c:80 packet: git< ref-prefix HEAD
37
11:45:42.048044 pkt-line.c:80 packet: clone> ref-prefix refs/heads/
38
11:45:42.048050 pkt-line.c:80 packet: clone> ref-prefix refs/tags/
39
11:45:42.048053 pkt-line.c:80 packet: git< ref-prefix refs/heads/
40
11:45:42.048054 pkt-line.c:80 packet: clone> 0000
41
11:45:42.048057 pkt-line.c:80 packet: git< ref-prefix refs/tags/
42
11:45:42.048061 pkt-line.c:80 packet: git< 0000
43
11:45:42.050613 pkt-line.c:80 packet: clone< 869bda16bc91c702d812e18fd9a2653dd8c9f461 HEAD symref-target:refs/heads/master
44
11:45:42.050613 pkt-line.c:80 packet: git> 0002
45
11:45:42.050624 pkt-line.c:80 packet: clone< 869bda16bc91c702d812e18fd9a2653dd8c9f461 refs/heads/master
46
11:45:42.050628 pkt-line.c:80 packet: clone< 0000
47
11:45:42.050637 pkt-line.c:80 packet: clone< 0002
48
11:45:42.051301 pkt-line.c:80 packet: clone> command=fetch
49
11:45:42.051308 pkt-line.c:80 packet: clone> agent=git/2.30.1
50
11:45:42.051311 pkt-line.c:80 packet: clone> object-format=sha1
51
11:45:42.051317 pkt-line.c:80 packet: clone> 0001
52
11:45:42.051320 pkt-line.c:80 packet: clone> thin-pack
53
11:45:42.051321 pkt-line.c:80 packet: git< object-format=sha1
54
11:45:42.051323 pkt-line.c:80 packet: clone> include-tag
55
11:45:42.051327 pkt-line.c:80 packet: clone> ofs-delta
56
11:45:42.051335 pkt-line.c:80 packet: clone> deepen 1
57
11:45:42.051339 pkt-line.c:80 packet: clone> want 869bda16bc91c702d812e18fd9a2653dd8c9f461
58
11:45:42.051342 pkt-line.c:80 packet: clone> want 869bda16bc91c702d812e18fd9a2653dd8c9f461
59
11:45:42.051344 pkt-line.c:80 packet: clone> done
60
11:45:42.051347 pkt-line.c:80 packet: clone> 0000
61
11:45:42.051354 pkt-line.c:80 packet: git< command=fetch
62
11:45:42.051359 pkt-line.c:80 packet: git< agent=git/2.30.1
63
11:45:42.051362 pkt-line.c:80 packet: git< 0001
64
11:45:42.051365 pkt-line.c:80 packet: git< thin-pack
65
11:45:42.051369 pkt-line.c:80 packet: git< include-tag
66
11:45:42.051372 pkt-line.c:80 packet: git< ofs-delta
67
11:45:42.051375 pkt-line.c:80 packet: git< deepen 1
68
11:45:42.051379 pkt-line.c:80 packet: git< want 869bda16bc91c702d812e18fd9a2653dd8c9f461
69
11:45:42.051383 pkt-line.c:80 packet: git< want 869bda16bc91c702d812e18fd9a2653dd8c9f461
70
11:45:42.051386 pkt-line.c:80 packet: git< done
71
11:45:42.051389 pkt-line.c:80 packet: git< 0000
72
11:45:42.057628 pkt-line.c:80 packet: git> 0002
73
11:45:42.057664 pkt-line.c:80 packet: clone< shallow-info
74
11:45:42.057672 pkt-line.c:80 packet: clone< shallow 869bda16bc91c702d812e18fd9a2653dd8c9f461
75
11:45:42.057676 pkt-line.c:80 packet: clone< 0001
76
11:45:42.057754 pkt-line.c:80 packet: clone< packfile
77
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'
78
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.
79
remote: 枚举对象中: 3, 完成.
80
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.
81
remote: 对象计数中: 100% (3/3), 完成.
82
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
83
remote: 总共 3(差异 0),复用 0(差异 0),包复用 0
84
11:45:42.058041 pkt-line.c:80 packet: sideband< PACK ...
85
11:45:42.058065 pkt-line.c:80 packet: sideband< 0000
86
11:45:42.058688 git.c:444 trace: built-in: git index-pack --stdin -v --fix-thin '--keep=fetch-pack 97854 on Yesterday17-PC'
87
接收对象中: 100% (3/3), 完成.
88
11:45:42.077544 pkt-line.c:80 packet: clone< 0002
89
11:45:42.078348 run-command.c:664 trace: run_command: git rev-list --objects --stdin --not --all --quiet --alternate-refs '--progress=正在检查连通性'
90
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 路上的慰藉吧(笑