bigywater.tech
Git · 62

protocol v2 与 v0 的区别

git 协议 v2 到底改进了什么?为什么 2.26 起默认启用?这篇用 GIT_TRACE_PACKET 对比 v0 和 v2:一次 clone 的交互、ls-refs 与 fetch 两个命令、有 5000 个 tag 的仓库里 fetch 一个分支的开销,以及什么也没变的 fetch 的开销。

5007 → 15
5000 个 tag 的远程,fetch 一个分支时 v0 与 v2 下服务端发出的 pkt-line 行数
10008 → 48
什么也没变的 fetch,v0 与 v2 的 packet 跟踪行数
ls-refs + fetch
v2 把“列 ref”和“取对象”拆成两条命令

v0 的问题

实测 · 本机执行输出
  $ git config --get protocol.version || echo "protocol.version 未设置(2.26 起默认 v2)"
  protocol.version 未设置(2.26 起默认 v2)
  packet: upload-pack> version 2
  packet: upload-pack> agent=git/2.54.0-Darwin
  packet: upload-pack> ls-refs=unborn
  packet: upload-pack> fetch=shallow wait-for-done
  packet: upload-pack> server-option
  packet: upload-pack> object-format=sha1
  packet: upload-pack> 0000
  packet: clone< version 2
  packet: clone< agent=git/2.54.0-Darwin
  packet: clone< ls-refs=unborn
  packet: clone< fetch=shallow wait-for-done
  packet: clone< server-option
  packet: clone< object-format=sha1
  packet: clone< 0000
  packet: clone> command=ls-refs
  packet: clone> agent=git/2.54.0-Darwin
  packet: upload-pack< command=ls-refs
  packet: clone> object-format=sha1
  packet: upload-pack< agent=git/2.54.0-Darwin
  packet: clone> 0001
  packet: clone> peel
  packet: upload-pack< object-format=sha1
  packet: clone> symrefs
  packet: upload-pack< 0001
  packet: clone> unborn
  packet: upload-pack< peel
  packet: clone> ref-prefix refs/heads/
  packet: upload-pack< symrefs
  packet: clone> ref-prefix refs/tags/
  packet: upload-pack< unborn
  packet: clone> ref-prefix HEAD
  packet: clone> 0000
  packet: upload-pack< ref-prefix refs/heads/
  packet: upload-pack< ref-prefix refs/tags/
  packet: upload-pack< ref-prefix HEAD
  packet: upload-pack< 0000
  packet: upload-pack> <sha> HEAD symref-target:refs/heads/main
  packet: upload-pack> <sha> refs/heads/main
  (共 69 行 packet 跟踪)

v0 服务端一连上就把所有 ref 倒出来,客户端要的也许只是一个分支。仓库有几千个 tag 或 PR 分支时,每次 fetch 都要先收这一大堆,哪怕什么都没变。

v2 的做法

实测 · 本机执行输出
  packet: upload-pack> ls-refs=unborn
  packet: upload-pack> 0000
  packet: fetch< ls-refs=unborn
  packet: fetch< 0000
  packet: fetch> command=ls-refs
  packet: upload-pack< command=ls-refs
  packet: fetch> agent=git/2.54.0-Darwin
  packet: fetch> object-format=sha1
  packet: fetch> 0001
  packet: fetch> peel
  packet: fetch> symrefs
  packet: fetch> unborn
  packet: fetch> ref-prefix main
  packet: upload-pack< 0001
  packet: fetch> ref-prefix refs/main
  packet: fetch> ref-prefix refs/tags/main
  packet: fetch> ref-prefix refs/heads/main
  packet: fetch> ref-prefix refs/remotes/main
  packet: fetch> ref-prefix refs/remotes/main/HEAD
  packet: fetch> ref-prefix refs/tags/
  packet: upload-pack< ref-prefix main
  packet: fetch> 0000
  packet: upload-pack< ref-prefix refs/main
  packet: upload-pack< ref-prefix refs/tags/main
  packet: upload-pack< ref-prefix refs/heads/main
  packet: upload-pack< ref-prefix refs/remotes/main
  packet: upload-pack< ref-prefix refs/remotes/main/HEAD
  packet: upload-pack< ref-prefix refs/tags/
  packet: upload-pack< 0000
  packet: upload-pack> 0000
  packet: fetch< 0000
  packet: fetch> command=fetch
  packet: fetch> agent=git/2.54.0-Darwin
  packet: fetch> object-format=sha1
  packet: fetch> 0001
  packet: fetch> thin-pack
  packet: fetch> no-progress
  packet: fetch> ofs-delta
  packet: fetch> want <sha>
  packet: fetch> have <sha>
  packet: fetch> 0000
  packet: upload-pack< command=fetch
  packet: upload-pack< 0001
  packet: upload-pack< 0000
  packet: upload-pack> 0001
  packet: fetch< acknowledgments
  packet: fetch< ACK <sha>
  packet: fetch< ready
  packet: fetch< 0001
  packet: fetch< packfile
  packet: upload-pack> 0000
  packet: sideband< 0000

v2 先广告能力(没有 ref),客户端再发 command=ls-refs,并用 ref-prefix 说明只要哪些前缀(git fetch origin main 会展开成 main、refs/heads/main、refs/tags/main 等候选),服务端只返回这些。

fetch 命令

实测 · 本机执行输出
  v0:服务端发出(upload-pack>)5007 行,其中 ref 行 5002 行;客户端收到(fetch<)5006 行
  v2:服务端发出(upload-pack>)15 行,其中 ref 行 1 行;客户端收到(fetch<)14 行

取对象由 command=fetch 完成:参数里是 want、have(和 v0 一样),服务端回 acknowledgments / ready,然后是 packfile。命令之间用 0001(delim-pkt)分隔参数,0000 结束。

省多少

实测 · 本机执行输出
  v0:跟踪 10008 行;客户端发出的 want 0 条;v2 命令:ls-refs 0 次、fetch 0 次
  v2:跟踪 48 行;客户端发出的 want 0 条;v2 命令:ls-refs 1 次、fetch 0 次

5000 个 tag 的远程上 fetch 一个分支:v0 服务端发出 5007 行(5002 个 ref),v2 只发 15 行(1 个 ref);无变化的 fetch 跟踪行数从 10008 降到 48。

服务端广告

实测 · 本机执行输出
  version 2
  agent=git/2.54.0-Darwin
  ls-refs=unborn
  fetch=shallow wait-for-done
  server-option
  object-format=sha1
  0000 (flush)

v2 服务端的广告很短:version、agent、ls-refs=unborn、fetch=shallow wait-for-done、server-option、object-format=sha1,然后是 flush。

分步演示

协议 v2 与 v0
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / git 2.54.0(Apple Git-157);每个实验在临时目录里新建仓库,HOME 和全局配置指向实验专用的空目录(不读用户自己的 git 配置),提交的作者与时间固定以便复现;需要网络协议的实验用本机的 file:// 或 git daemon 回环端口(9800 以上),不碰任何远程仓库;计时用 time.perf_counter,数字每次运行会有小幅抖动。服务端是本机裸仓库(file://,没有走网络);v0 用 -c protocol.version=0 强制;5000 个 tag 由 git update-ref --stdin 批量创建;“省多少”以 pkt-line 行数衡量,不是网络字节数;跟踪里的 hash 做了替换
    • 没有验证:真实网络(ssh、smart HTTP)上的字节数与耗时;客户端对 v2 服务端的降级(服务器只支持 v0 时);protocol.version=1;server-option 的使用;object-info 命令;partial clone 下 v2 的 filter 参数(见 partial clone 一篇)
    京ICP备2026067502号-1京公网安备11010502063487号