研究报告 · 约 39,588 字 · 预计推送 4 部分

Git 内部原理:对象模型、引用、packfile 与垃圾回收——二十年发展脉络与横向对照(纵横综合报告·最终稿) 未开始

目录

题记与说明

TL;DR

Git 在诞生头八个月内就打好了沿用至今的地基:内容寻址的不可变对象库(blob/tree/commit、SHA-1、zlib)、索引快照表、可变引用。此后二十年的演化集中在三层:存储层(2005 packfile+delta、2006 idx v2、2019 multi-pack-index)、维护层(2007 git gc、2020 git maintenance、2022 cruft packs)、引用层(files 后端内:松散文件→packed-refs;2024 起新增 reftable 后端),外加 2020 年起可用的 SHA-256 对象格式。横向看,这套"数据—索引—存储—维护"四层结构已成版本控制领域的事实底座:Jujutsu 直接复用 Git 对象库(32.4k stars),Dolt、lakeFS、DVC 把同一思想搬进数据领域;Mercurial 用只追加的 revlog 从设计上取消了 GC,SVN 的集中式架构根本没有需要回收的本地历史,Fossil 让工件永久保存。当下的卡点在平台不在内核:GitHub 至今未公开支持 SHA-256 仓库。对用户的建议:默认选 Git;想要新体验用 jj 平滑过渡;超大仓库用 partial clone/sparse-checkout;SHA-256 与 reftable 等 Git 3.0 和平台支持落地再迁移。

纵轴:发展脉络

纵 0.四层框架:课题四个关键词各指什么

本稿以四层框架为全篇骨架:对象模型是数据层(什么是一条"内容"的唯一身份)、引用是索引层(哪些内容当前"有效")、packfile 是存储层兼传输格式(内容如何落盘与走网络)、垃圾回收是维护层(无效内容何时、以何种安全程度被清除)。纵轴六个阶段,可以读作这四层各自"定形—冻结—加速—重构"的时间线;横轴的九个对比维度按"①②归数据/索引层、③④归存储层、⑤归维护层、⑥–⑨为派生的系统级观察"映射回这副骨架(见横 1 开头)。

纵 1.诞生(2005.4–2005.6):对象模型是先天基因,不是后天补丁

2005 年 4 月,BitMover 收回 BitKeeper 的免费授权,Linux 内核社区失去版本控制工具。Linus Torvalds 于 2005-04-06 在 LKML 发出著名的《Kernel SCM saga..》帖(lkml.org/lkml/2005/4/6/121),约 4 月 3 日前后动工,几天内写出约千行 C 代码;首个提交 e83c5163316 的提交信息是自嘲式的 *"Initial revision of 'git', the information manager from hell"*(GitLab 20 年时间线、GitButler: 20 years of Git)。2005-06-16,内核 2.6.12 成为第一个由 Git 管理发布的内核版本(Wikipedia: Git)。

诞生首周定下的三样东西至今未变:① 内容寻址对象(blob/tree/commit,SHA-1 命名,zlib 压缩,落盘为 .git/objects/ 前两位目录 + 余 38 字符文件名的"松散对象");② 索引(index) 作为工作区与对象库之间的暂存层——它不是"补丁队列"而是"文件名→对象名"的快照表;③ 引用(refs) 是指向 commit 的可变指针,而对象图本身不可变(机制实证见横轴 2.1、2.2)。

驱动因素:内核开发的规模(数千贡献者、数万提交)迫使 Linus 把"完整性、分布式、速度"置于一切之上——SHA-1 命名天然防篡改,对象不可变天然支持任意分支合并,本地即全量仓库。其他同期 SCM 的"增量差值"模型被彻底放弃,转而"每个快照完整命名"。

纵 2.成型(2005.6–2005.12):packfile 诞生与维护者交接

松散对象"一对象一文件"在内核这种海量小对象场景下撑不住:inode 消耗、传输低效、无跨对象压缩。2005-06-16,Linus 在 git 邮件列表发出《Some quick notes on the "packfile" design》(lore.kernel.org,Message-ID: Pine.LNX.4.58.0506160756200.2516@g5.osdl.org;*注:全文抓取受限,日期与标题来自搜索索引*),提出把多个对象打包进单个 .pack、配合 .idx 索引,并引入 delta 压缩——对象可以以"对包内另一对象的差值"形式存储。由此诞生 git-pack-objects/git-unpack-objects(gitformat-pack 文档、Pro Git: Git Internals - Packfiles)。

packfile 的深层意义是"存储模型"与"传输模型"的合流:同一套打包逻辑既用于 .git/objects/pack/ 的本地落盘,也用于 fetch/push 的线上传输(thin pack)。2005-07-26,Linus 将维护权交给 Junio C Hamano(原始邮件:public-inbox;报道:LWN 145123;GitHub Blog 采访中 Hamano 确认是"2005 年 7 月底")。2005-12-21,Junio 发布 Git 1.0.0(LWN 165127),距首提交仅八个多月。维护者交接把项目从"内核子工程"变成独立社区工程——此后 20 年的节奏(每季度特性版、RelNotes 文化、mailing-list 评审)皆由 Junio 时代确立(分析)。

纵 3.巩固与重组(2006–2008):格式定型,"垃圾回收"成为正式命令

驱动因素:用户群从内核黑客扩展到普通开发者,他们不会手动做 housekeeping,于是 gc 必须安全、自动、幂等;同时 Windows 与大仓库开始进入视野,格式需要跨平台与容量上限(idx v2 的 4 GiB 即为此)。

纵 4.优化时代(2009–2015):给"GC 三件事"各配一台加速器

这一阶段对象模型与 pack 格式基本冻结,创新集中在读路径加速——GC 的三个成本中心(对象打包、引用读取、可达性计算)逐一获得专属加速结构:

本阶段之后,"gc"逐渐从清扫工具变成性能工具。

纵 5.规模化(2016–2020):超大仓库把存储模型逼向极限

微软(Windows monorepo)、Google、GitHub 的超大仓库把对象量推到数百万级,内部原理迎来密集升级:

变化的两大驱动:一是安全——2017-02 的 SHAttered 攻击给出首个实用 SHA-1 碰撞(shattered.io、论文),Git 随后在 2.13 起内置 collision detection、GitHub 2017-03 上线拒绝机制(GitHub Blog),为全面换 SHA-256 铺路;二是规模——数百万对象下"定期全量重打包"不可行,于是增量索引(MIDX)、按需取对象(promisor)、后台化维护(maintenance)相继出现。

纵 6.存储格式重构与 Git 3.0 前夜(2021–2026)

纵 7.主线总结:二十年不变的三条不变量

  1. 内容寻址的不可变对象库(2005-04 定型,SHA-1→SHA-256 只换哈希不换架构;横轴 2.1 实验可复现);
  2. 引用 = 可变指针;引用存储的演化分两层说:files(文件)后端内的形态变化——松散文件(2005)与 gc 时生成的 packed-refs(横轴 2.3 观察到)——只是同一后端内的打包,后端本身的首次更换是 2024 年的 reftable(Git 3.0 将默认化,横轴 2.5 实证);
  3. GC = 可达性标记 + 打包 + 过期删除:从 2005 年的手工 repack/prune,到 1.5.3 的 git gc(2007),再到 2.29 的 git maintenance(2020)与 2.37 的 cruft packs(2022)——回收策略越来越保守、越增量化,而"对象不可变、引用可变"的分工从未动摇。

横轴:横向对比

横 1.对比对象与维度

对比对象(2026-11-06 时间截面):

谱系系统入选理由
直接竞品(同期而生)Mercurial2005 年与 Git 同期发布,唯一长期并立的"双雄",存储设计(revlog)与 Git 完全异构
存量替代Subversion (SVN)集中式霸主,企业内仍有大量仓库,FSFS 是与 Git 对象库完全不同的存储范式
一体化小而美Fossil单文件 SQLite 仓库 + 工件(artifact)模型,SQLite 作者 D. R. Hipp 的"另一种答案"
新一代替代(复用 Git)Jujutsu (jj)2025–2026 增长最快的新 VCS,直接以 Git 对象库为存储后端,是"对象模型即底座"的最佳证据
新一代替代(自有存储)Sapling (Meta)Mercurial 血统的百万文件级 monorepo 方案,IndexedLog + Segmented Changelog + Mononoke
理论异端Pijul / Darcs补丁(patch)为第一公民、以合并理论立身,与快照模型形成理论对照
Git 自身的"左右互搏"reftable / SHA-256引用层与哈希层的内部换代,当下 .git 格式最活跃的演化前线

对比维度:① 数据模型与内容寻址;② 引用与分支实现;③ 存储格式与目录布局;④ 压缩与传输;⑤ 垃圾回收与维护;⑥ 超大规模方案;⑦ 生态与互操作;⑧ 成熟度与活跃度;⑨ 适用场景。

这九个维度按纵 0 的四层骨架归位:①②是数据层与索引层的对比,③④是存储层(落盘格式与其网络面)的对比,⑤是维护层的对比;⑥–⑨(超大规模、生态、成熟度、场景)是从四层能力派生的系统级观察。后文表 1 对应①②,表 2 对应③④⑤,表 3 对应⑦⑧⑨,横 4 逐维展开——四层骨架由此贯穿两轴。

横 2.Git 机制锚点:两章实验合并后的定量事实

以下全部来自两章在本次会话中实际执行的命令与输出,是后文横向对比的"锚点"。两章各自在独立临时仓库中实验(隔离 GIT_CONFIG_GLOBAL/SYSTEM,core.autocrlf false),数据互补:横轴章提供了带外部工具复算的对象模型证据与 61 版本的 packfile 定量测量;纵轴章提供了小仓库 gc 对照、悬空对象三件套与 reflog 首行观察。

横 2.1 对象模型:Git 对象 = zlib("<类型> <字节数>\0" + 内容)

$ git init -b main repoA && cd repoA
$ printf 'hello git internals' > a.txt
$ git hash-object a.txt              # 只计算不落盘
54081b983821795fdc61b17e5542bab70d19b7da
$ git hash-object -w a.txt           # 写入对象库:路径 = 前两位目录 + 后 38 位文件
$ find .git/objects -type f
.git/objects/54/081b983821795fdc61b17e5542bab70d19b7da
$ git cat-file -t 54081b98 ; git cat-file -s 54081b98 ; git cat-file -p 54081b98
blob
19
hello git internals

对象头证明(横轴章注:git hash-object --stdin 会把整个 stdin 再当作 blob 内容包一层自己的头再计算,不能用来验证"对象头";正确做法是用外部工具对原始字节流算 SHA-1):

$ printf 'blob 19\000hello git internals' > obj.bin   # 27 字节:头 "blob 19" + NUL + 内容
$ sha1sum obj.bin
54081b983821795fdc61b17e5542bab70d19b7da *obj.bin      # 与 git 的对象 ID 完全一致

再直接解压松散对象文件(压缩后仅 35 字节):

$ python -I -c "import sys,zlib; sys.stdout.buffer.write(zlib.decompress(open(sys.argv[1],'rb').read()))" \
    .git/objects/54/081b983821795fdc61b17e5542bab70d19b7da | od -An -c
   b   l   o   b       1   9  \0   h   e   l   l   o       g   i
   t       i   n   t   e   r   n   a   l   s

纵轴章的另一轮实验从原始字节侧印证了同一件事:松散对象文件以 78 01 4b ca ... 开头——78 01 正是 zlib deflate 流头,证明落盘的就是"zlib 压缩的 <类型> <字节数>\0<内容>"。这一观察对任何松散对象成立,不依赖具体演示文件。

内容寻址的三个推论在实验中一并验证:改一个字节即得到新 ID(横轴实验得 4029f768…);空 blob 的 ID 全世界相同(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391);同一内容天然去重——纵轴章对同一文件连续两次 git hash-object -w,两次返回同一 ID,"写两次"不会产生第二个副本。这是后续一切 GC、去重与增量传输的逻辑基础。

横 2.2 从 index 到 ref:不用 porcelain 搭一条提交链

$ git update-index --add a.txt && git ls-files --stage
100644 54081b983821795fdc61b17e5542bab70d19b7da 0	a.txt
$ od -A x -t x1z .git/index | head -1        # index 二进制格式:魔数 "DIRC" + 版本 2
000000 44 49 52 43 00 00 00 02 ...           >DIRC...........<
$ TREE=$(git write-tree)                     # 00af1606…:把 index 冻结成 tree 对象
$ git cat-file -p $TREE
100644 blob 54081b983821795fdc61b17e5542bab70d19b7da	a.txt
$ C1=$(git commit-tree $TREE -m "c1")        # 3351515b…:手工造 commit
$ git cat-file -p $C1
tree 00af1606ca1cf8210370040067496da3cca3cc11
author Lab <lab@example.com> 1793914232 +0800
committer Lab <lab@example.com> 1793914232 +0800

c1
$ echo 'hello git internals v2' >> a.txt     # ── 第二轮提交:改内容后重走一遍 add → write-tree → commit-tree
$ git update-index a.txt && TREE2=$(git write-tree)   # (中间输出与第一轮同型,省略)
$ C2=$(git commit-tree $TREE2 -p $C1 -m "c2")         # 59fa4949…:挂上父提交 C1,构成链
$ git update-ref refs/heads/main $C2 -m "set main"    # 手工移动分支引用
$ cat .git/refs/heads/main                   # 引用就是一个存 40 位十六进制的文本文件(41 字节)
59fa49491b7ab0ba43643d39efc7e533610fb64b
$ cat .git/HEAD ; git symbolic-ref HEAD      # HEAD 是符号引用
ref: refs/heads/main
refs/heads/main

(转录补记:初稿在此处直接使用了未定义的 $C2,独立审读指出后按原始实验的实际做法补记上面三行命令;补记的 $C2 值 59fa4949… 与下一行 cat .git/refs/heads/main 的实际输出一致,可交叉印证。除这三行外,块内输出均为原始实验转录。)

tree 的原始字节同样眼见为实:<mode ASCII> <文件名> \0 <20 字节二进制 SHA-1>(实验输出 31 30 30 36 34 34 20 61 2e 74 78 74 00 43 …,即 100644 a.txt\0 + 原始哈希)。git init 后 .git 顶层仅有 HEAD config description hooks info objects refs,单条目 index 为 104 字节二进制(纵轴章观察)。reflog 落在 .git/logs/HEAD 与 .git/logs/refs/heads/main,行格式为 旧ID 新ID 作者 时间 说明;纵轴章确认 reflog 从第一次引用变动就开始记录(首行为 0000…0000 <新提交>)——reflog 是后来 GC 的"保护层"。

结论:commit 只是指向 tree 的带署名快照;分支创建 = 写一个 41 字节文件;暂存区是快照表而非补丁队列。这条链就是 git add/git commit 的四步原始操作。

横 2.3 packfile:小仓库与大仓库的双尺度实测

大尺度(横轴章实验 B):构造约 50KB 文本文件,追加一行提交一次,共 61 个版本(61 个版本累计未压缩文本约 3.0MiB ≈ 3070KiB):

指标git gc 前git gc 后
git count-objects -v183 个松散对象,size 718 KiB0 松散,in-pack 183,size-pack 41 KiB
du -sk .git958 KiB162 KiB
$ ls -l .git/objects/pack/
6196  pack-c680ff….idx      # 索引,魔数 ff 74 4f 63(\377tOc)+ 版本 2(即 2006 年 1.4.4 定型的 idx v2)
36259 pack-c680ff….pack     # 包体,头 12 字节:"PACK" + 00 00 00 02 + 00 00 00 b7(=183 个对象)
 784  pack-c680ff….rev      # 反向索引(新版 gc 默认生成)
$ cat .git/packed-refs      # gc 附带 pack-refs,把引用打包
# pack-refs with: peeled fully-peeled sorted
73f8d9e8074116e6e7b2227b47a662e31d9a5fbe refs/heads/main
$ git verify-pack -v … | tail -3
non delta: 124 objects
chain length = 1: 59 objects
$ git verify-pack -v … | awk '$2=="blob"{n++; if($6>0)d++} END{print n" blobs, "d" deltas"}'
61 blobs, 59 deltas
$ git cat-file --batch-all-objects --batch-check='%(objecttype) %(objectname) %(objectsize) %(deltabase)' | tail -2
blob f70d907c… 51647 07040236…      # 该输出只有 objectsize 与 deltabase 两列,能证明的仅是:59/61 为 delta、且 delta 基可跨版本选取

口径注(审读后补):表中 size(gc 前 718 KiB)是全部松散对象的占盘 KiB 数;size-pack 41 KiB 是 .pack(36259 字节)+ .idx(6196 字节)合计 42455 字节 ≈ 41.5KiB 的口径,不含 .rev(784 字节)。

复核重跑(本最终稿修订时执行;git 2.51.0.windows.1,基文件 51647 字节 × 61 版,逐版追加一行):结构与初稿实验完全一致——183 个松散对象、gc 后 1 包 183 对象、non delta: 124 objects、chain length = 1: 59 objects、61 blob 中 59 个 delta;且证实了口径注:该次 gc 后 size-pack 报 19 KiB,恰等于 .pack 13447 + .idx 6196 = 19643 字节(19.2KiB),.rev 784 字节不计入。重跑的绝对值与初稿不同(复核基文件文本重复度更高,zlib 压得更狠:松散对象仅 53 KiB、.pack 仅 13447 字节),但比例结构与 delta 行为一致。

重跑补上了初稿缺的 verify-pack 完整列(格式:SHA-1 类型 大小 包内大小 包内偏移 深度 基对象;对 delta 对象,第 3 列是 delta 指令流展开后的大小,不是对象原始大小——下例对象实际 55707 字节,cat-file -s 可证):

$ git verify-pack -v pack-*.idx | awk '$2=="blob" && NF==7' | head -3
4a851f73b9befaed26f12f8076c16b8310588483 blob   9 21 9103 1 bbecb4915512a98232b2d3d6cb1135005f331910
7ee50e8690d20f45055c4746b5fe6ced6ebf4ea8 blob   9 21 9170 1 bbecb4915512a98232b2d3d6cb1135005f331910
c8a2cdc9c9081d697920c2129e3434bbed446a6d blob   9 20 9237 1 bbecb4915512a98232b2d3d6cb1135005f331910
$ git cat-file -s 4a851f73   # 同一对象的原始大小
55707
$ git verify-pack -v pack-*.idx | awk '$2=="blob"{n++; if(NF==7){d++; s=$4; sum+=s; if(min==""||s<min)min=s; if(s>max)max=s}} END{printf "%d blobs, %d delta blobs, size-in-packfile: min=%d B max=%d B avg=%.1f B\n", n, d, min, max, sum/d}'
61 blobs, 59 delta blobs, size-in-packfile: min=20 B max=21 B avg=20.5 B

解读:183 对象 = 61 commit + 61 tree + 61 blob。zlib 单独已把约 3.0MiB(≈3070KiB)文本压到 718KiB——约 4.3 倍(初稿误作 8.6 倍,审读指出后已按自家数据更正:3070÷718≈4.28),packfile 在此之上再做 delta 链——61 个大 blob 里 59 个以 delta 存储:.pack 本体 36259 字节 ≈ 35.4KiB,对原始文本约 87 倍;加 .idx 后即 size-pack 口径 41KiB,约 74 倍。gc 同时生成 objects/info/commit-graph(魔数 CGPH,加速 commit 遍历)。

小尺度对照(纵轴章实验 D):仅 2 个版本的实验仓库,gc 前 6 个松散对象(2 blob + 2 tree + 2 commit),gc 后 count: 0, in-pack: 6, packs: 1,但 verify-pack -v 显示 non delta: 6 objects——(分析,仅由本实验 61 对象版与 6 对象版两个数据点归纳,不宜过度外推)delta 的收益随对象数量与相似度增长:大仓库 59/61 blob 进 delta 链、3.0MiB 压到 35.4KiB,6 对象小仓库 gc 后全部 non delta、体积几乎不变。同一实验还确认:git cat-file --batch-check --batch-all-objects 可一次性枚举全部对象——对象库对外是"按 SHA 寻址的 KV 库",松散或打包对上层透明。

横 2.4 垃圾回收机制链:fsck 发现 → gc 宽限 → cruft pack → 过期删除

两章分别制造了不被任何引用/reflog 指向的悬空对象:

$ git fsck --dangling
dangling commit b2d4dacdef76afc912e0fbff237ab03e39ec1adf
$ git gc --quiet
$ git fsck --dangling                       # gc 之后依然存在!
dangling commit b2d4dacdef76afc912e0fbff237ab03e39ec1adf
$ ls .git/objects/pack/ | grep -c mtimes    # 出现了带 .mtimes 的"残料包"
1

两个实验合起来正是同一条机制链在不同入口下的行为:git gc 内部调用 prune 时套用 gc.pruneExpire(默认 2.weeks.ago,git-gc 文档原文:"When git gc is run, it will call prune --expire 2.weeks.ago"),且 gc.cruftPacks 默认为 true(本机随版本 2.51 文档核对:"The default is true")——不可达但"还新"的对象被移入带 .mtimes 时间戳文件的 cruft pack(Git 2.37,见纵 6)而不是直接丢弃;reflog 提供恢复窗口(文档默认值:gc.reflogExpire 90 天、不可达 reflog 30 天、gc.auto 6700 个松散对象触发自动打包)。纵轴章还确认 reflog 从第一次引用变动即开始记录,可达历史因此始终受保护。这正是纵 7 第三条不变量的当代形态:回收策略越来越保守。

最后一步:谁删、何时删。 cruft pack 不是终点,删除发生在下一轮 gc 运行时。本机随版本的 git-gc 文档原文给出执行者:*"When git gc is run, it will call prune --expire 2.weeks.ago (and repack --cruft --cruft-expiration 2.weeks.ago if using cruft packs via gc.cruftPacks or --cruft)"*。即每次 gc 时,repack --cruft --cruft-expiration 依据 .mtimes 里记录的对象时间做决策:已过期的不可达对象直接丢弃,未过期的连同原始时间戳搬进新 cruft 包——时间戳不随 gc 重置,宽限期不会因反复 gc 而"续命"(cruft-packs 文档:*.mtimes 文件记录 per-object timestamps,混合版本环境反而会把包时间当对象时间、导致"unreachable objects not getting pruned as a result of repeatedly resetting the objects' mtimes"*);prune --expire 同时清理剩余的过期松散悬空对象。gc.pruneExpire 控制的就是这个截止点("now" 取消宽限、"never" 禁止删除),git gc --prune=now 可立即删。修订时本机实测走完这条链(git 2.51.0.windows.1):

$ git branch -D side                    # 造一个只被 HEAD reflog 挂着的提交 C2
$ git gc --quiet
$ git cat-file -t $C2 ; ls .git/objects/pack/ | grep -c mtimes
commit                                  # 仍在,且 0 个 .mtimes——reflog 条目让它仍算"可达",根本没进不可达集合
$ git reflog expire --expire-unreachable=now --all ; git gc --quiet
$ git cat-file -t $C2 ; ls .git/objects/pack/ | grep -c mtimes
commit                                  # 仍在,但已进 cruft(1 个 .mtimes)——reflog 过期后才转为不可达
$ git gc --prune=now
$ git cat-file -t $C2
fatal: git cat-file: could not get object info   # 过期 + 更严的 expire → 真正删除

两条路径必须分开:普通误删(reset/rebase/amend 丢掉的提交)全程受 reflog 保护——reflog 条目本身就是可达性根,条目未过期(默认可达 90 天、不可达 30 天,引文见上)的对象不会进入"不可达集合",也就不会进 cruft;只有 reflog 过期后才转入"2 周宽限 + cruft"流程,日常恢复窗口是 90/30 天。裸悬空对象是本节实验的特殊路径:commit-tree/hash-object -w 直接造出的对象从未被任何引用指向,reflog(只记录引用变动)里没有它的条目,所以 fsck 一报告就直接进入宽限 + cruft 链。上面的实测演示恰好把两条路径接续展示:同一个提交,先走第一条(reflog 保护),reflog 清空后落入第二条。

横 2.5 两条演化前线:reftable 与 SHA-256(本地可用性实证)

$ git init -b main repoDefault && ls repoDefault/.git    # 2.51 默认仍是 files 引用后端
HEAD config description hooks info objects refs          # (无 reftable/ 目录)
$ git init -b main --ref-format=reftable repoRT && ls repoRT/.git
HEAD config description hooks info objects refs reftable
$ find repoRT/.git/reftable -type f
.git/reftable/tables.list
.git/reftable/0x000000000001-0x000000000001-b53a35ad.ref  # 每次 ref 事务写一个新表文件
$ cat repoRT/.git/HEAD                                     # 注意:HEAD 变成哨兵
ref: refs/heads/.invalid                                   # 真实 HEAD 存在 reftable 表内
$ git init -b main --object-format=sha256 repoSHA && cd repoSHA   # (补记:初稿漏了 cd 这一步)
$ printf 'sha256 demo\n' > f.txt                    # (补记:f.txt 内容任意,原实验文件内容未随输出转录)
$ S=$(git hash-object -w f.txt)   # 212dabee230dab76d408a0c8ed71f368b0567e02a6123608574ccd82ab126b68(64 位 hex)
$ cat repoSHA/.git/config
[extensions]
	objectformat = sha256
[core]
	repositoryformatversion = 1

(转录补记:初稿在 git init --object-format=sha256 与 hash-object 之间漏掉 cd repoSHA 与创建 f.txt 两步,审读指出后补齐;S 的值仍为原始实验转录。)

reftable 解决两大痛点:"海量 refs 下松散文件的 inode/目录项压力"与"packed-refs 每次小更新都要全量重写";Git 2.45(2024-04)引入、仍为 opt-in(GitHub Blog、reftable 文档)。SHA-256 自 Git 2.29 支持并记录于 gitrepository-format,2.42 起去掉实验性标签,Git 3.0 拟设为默认(见纵 5、纵 6)。

横 3.横向对比主表

表 1:数据模型与引用

系统版本单元寻址/哈希分支/引用实现合并模型
Git快照:commit→tree→blobSHA-1(可选 SHA-256,横 2.5 实证)文本引用文件 / packed-refs / reftable;分支=一个 41 字节文件三方合并,冲突手动解决
Mercurial变更集 changeset(附 manifest)SHA-1 node id命名分支写进变更集;bookmark ≈ Git 引用三方合并,DAG 遍历
SVN仓库全局版本号 N(路径树快照)无内容寻址;MD5/SHA-1 校验和(FSFS format 8 可选 SHA-256)分支=仓库内廉价 copy(路径),无本地分支服务器端三方合并
Fossilartifact(一切皆工件:文件/检入/wiki/票据)SHA-1 / SHA3-256(可加固)分支=checkin 的 tag 属性三方合并,内置冲突标记
jjchange(change-id 稳定,自动 rebase)复用 Git 对象库(SHA-1/SHA-256 跟随后端)colocated 模式与 Git refs 双向同步;操作日志 op log三方合并,冲突是一等公民、可以提交
Saplingcommit(Mercurial 血统)hg 系 SHA-1 node提交即分支("无限分支"工作流)三方合并 + 自动 rebase 体验
Pijul补丁 patch 为第一公民补丁哈希寻址(算法见官方手册)channel(数据库 fork 实现)补丁交换理论(合并正确性可证明)

表 2:存储、压缩、传输与垃圾回收

系统仓库布局存储格式传输格式GC / 维护
Git.git/objects(loose zlib + pack)+ refs/reftable + index + logspackfile:zlib + delta 链(实测:61 blob 中 59 个为 delta;6 对象小仓库 gc 后全部 non delta,横 2.3)packfile(smart 协议/bundle/promisor 包)一等公民:gc + 2 周宽限 + cruft pack(.mtimes) + reflog 90/30 天 + commit-graph(gc 默认写)/ bitmap(非裸仓库 gc 默认不写,服务端启用,见常见追问 1)
Mercurial.hg/store:00changelog / 00manifest / data/**.i+.drevlog:index+data,快照点+delta,zlib/lz4/zstdchangegroup / bundle2无自动 GC(只追加设计);strip/obsolete 显式修剪
SVN服务端 FSFS(每版本一文件,1.9+ 打包);客户端 .svn pristinesvndiff 增量svndiff over RA/HTTP无 GC(中央仓库历史永久保留)
Fossil单个 SQLite 数据库文件BLOB 表存工件 + delta + zlib(均为可剥离的优化)Fossil 同步协议无 GC 概念(工件永久保存);rebuild/vacuum
jj.git(共享)+ .jj(元数据)直接复用 Git packfile全兼容 Git 协议委托 Git gc;自有层 jj op abandon + jj util gc --expire now(见常见追问 5)
Sapling.sl 本地仓库 + EdenFS 虚拟工作区IndexedLog(revlog 改造)Mononoke 服务端协议(暂不对第三方开放)服务端策略化维护
Pijul.pijul(Sanakirja KV 数据库)Sanakirja B 树页(支持 fork)补丁推拉通道机制,无 Git 式 GC

表 3:生态与成熟度(检索时点 2026-11-06)

系统首发最新版本(检索所得)平台/生态适用场景
Git20052.56.0(2026-09-28,git-scm.com)SO 2022 调查 93.87% 使用率;SO 2025:GitHub 81.1% / GitLab 35.6%事实标准,全场景默认
Mercurial20057.2(2026-01-29,官网)自托管为主;Facebook 系演进为 Sapling企业内网、偏好简洁 CLI 的团队
SVN20001.14.x LTS(1.14.5,2024-12,发行历史)Apache 官方、企业内网集中管控、二进制/美术资产、遗留仓库
Fossil20072.27(2025-09-30,变更日志)自带 wiki/票据/论坛;SQLite 项目自用个人/小团队一站式、离线优先
jj约 2021 年开源(Martin von Zweigbergk 发起)v0.46.0(Releases);32.4k stars(2026-11-06 实测抓取)与 GitHub/GitLab 原生互操作(复用 Git 协议)想要"更好的 Git 体验"的个人与团队
Sapling2022-11 开源(Meta 工程博客)Meta 内部主力,社区版持续更新Mononoke 服务端暂不对外开放百万文件级 monorepo
Pijul2016 起;1.0 beta 线1.0.0-beta.6(2023-06,crates.io/社区)nest.pijul.com 自建托管研究型场景、对合并理论有洁癖者

横 4.逐维要点

数据模型:快照、快照、路径树与补丁。 Git 与 Mercurial 同属"快照派",但粒度不同:Git 的 commit 指向 tree、tree 递归指向 blob,每个文件一个对象(横 2.2 中 index 一行就是一个 blob ID);Mercurial 的 changeset 指向 manifest、manifest 再指向 filelog 修订,三类 revlog 同构(AOSA Mercurial 章节、Szorc: The Mercurial Revlog)。SVN 是"路径树快照 + 全局递增版本号",无内容寻址。Pijul/Darcs 是唯一的"补丁派":版本 = 补丁及其偏序关系(pijul.org)。Fossil 最特殊:一切皆 artifact,仓库即工件博物馆(tech_overview.wiki)。

内容寻址与哈希:SHA-1 的遗产与 SHA-256 的僵局。 Git/Mercurial/Fossil 最初都押了 SHA-1;2017 年 SHAttered 碰撞后三家分化:Git 一边加碰撞检测一边推进 --object-format=sha256(横 2.5 实证:64 位 hex、repositoryformatversion = 1);Fossil 提供 SHA3-256 与加固版 SHA-1(selfcheck.wiki);SVN 仅在实验性 FSFS format 8 中引入 SHA-256(1.14 发行说明)。当下卡点在托管平台:GitHub 至今未公开支持 SHA-256 仓库(LWN 2025-10、GitHub 社区讨论 #12490),GitLab、Codeberg 已支持。分析:内容寻址模型"换哈希"的成本被平摊到整个生态(平台、CI、工具链),这正是对象模型成为公共底座的代价。

引用与分支:一个目录树、一列字段与一张数据库表。 Git 的引用是"松散文本与 packed-refs(同属 files 后端的两种形态)+ 可选 reftable 后端",分支创建 = 写一个 41 字节文件(横 2.2/2.5);reftable 不是 Git 社区凭空发明,而是 JGit 为 Gerrit 海量 refs 创造、回流核心 Git 的方案(GitHub Blog、GitLab blog)。横 2.5 观察到鲜为人知的细节:reftable 仓库的 .git/HEAD 变成哨兵 ref: refs/heads/.invalid,真实 HEAD 存在表内。对比:Mercurial 命名分支随历史传播、bookmark 才接近 Git 引用;SVN 分支是路径级廉价 copy;Fossil 分支是 checkin 的 tag;Pijul 用 Sanakirja 数据库 fork 实现 channel。分析:Git 把"引用"做成与对象层解耦的独立层,这是它成为平台的关键——jj 的 colocated 模式能直接在这个引用层上双向同步(jj git-compatibility 文档)。

存储格式:packfile vs revlog vs FSFS vs SQLite。 四者都是"zlib + 增量",但增量组织方式不同:Git 在打包时全局搜索 delta 基(横 2.3:59/61 个 blob 有 delta,且 %(deltabase) 显示可跨版本找基;复核重跑中 git 甚至选了最新版本当基、让其余 59 版全变成"复制前缀"单指令 delta);Mercurial revlog 是写入时的固定 delta 链 + 周期性快照点,只追加不重写(ngoldbaum: Storing versioned data with revlogs、hg book);FSFS 按版本存文件、1.9 起打包(SVN Book);Fossil 在 SQLite BLOB 表上做 delta+zlib(官方实测自压缩比 68:1~84:1,stats.wiki),并强调 delta/zlib 只是可剥离的优化(tech_overview.wiki)。分析:Git 的"离线重打包"换来最优压缩率与读取局部性,代价是必须有 gc;revlog 换来 O(1) 追加与永不重写,代价是快照冗余;Sapling 把 revlog 改造成 IndexedLog 以支持懒加载(内部差异文档)。

压缩与传输:packfile 成为"交换格式"。 Git 的传输就是传 packfile(smart 协议协商双方已有对象,只发缺失的 delta 包),因此"存储格式=传输格式"——横 2.3 的 pack 就是网络上的字节流。Git 另有 bundle、partial clone(--filter=blob:none,promisor 远端按需补取 blob,git-scm 文档、GitHub Blog;GitLab 实测大仓库克隆提速约 50%、流量省约 70%,GitLab blog)。分析:存储即传输,让 Git 的每项存储优化(pack、bitmap、cruft、partial clone)自动惠及网络层,这是竞品难以复制的协同效应。

垃圾回收:唯一把 GC 做成一等公民的设计。 这是四个关键词里横向差异最大的维度。Git 因为"引用可变 + 对象不可变 + 松散即时写入"三件事,必然产生无人引用的悬空对象,于是有了横 2.4 的完整机制链:fsck 发现 → gc 默认保留 2 周 → 移入 cruft pack → 过期删除(删除由下一轮 gc 的 repack --cruft --cruft-expiration + prune --expire 执行,含实测演示,见横 2.4 末);reflog(90/30 天)提供恢复窗口;gc.auto=6700 触发自动维护。Mercurial"从设计上取消问题":revlog 只追加、不存在打包悬空物,历史修剪是显式 strip/obsolete 流程(设计依据见 revlog 文献,属分析推论)。SVN 集中式让本地根本没有需要回收的历史。Fossil 立场最激进:工件永久保存。jj 两层各管一段:Git 层照常 git gc,自有 op log 用 jj op abandon + jj util gc --expire now(jj-util-gc 手册、jj CLI 参考)。分析:GC 不是"有没有"的问题,而是"在哪里付代价"——Git 付在运行时维护,revlog 付在写入路径,SVN 付在协作模式,Fossil 付在磁盘。

超大规模:partial clone/sparse-checkout vs EdenFS/Mononoke。 Git 的大仓库方案已并入核心:partial clone + cone 模式 sparse-checkout + commit-graph(横 2.3 实证其生成)+ MIDX 与 bitmap;微软 Scalar 的核心能力即由此路线并入。Sapling 代表另一极:服务端 Mononoke(数百万文件、每日数千提交,暂不开放)+ IndexedLog + Segmented Changelog + EdenFS(Sapling 规模化文档、Mononoke docs、Meta 工程博客)。分析:Git 让"完整历史本地可用"变得便宜,Sapling 让"不完整本地"变得高效。

生态与互操作:jj 证明"对象模型即底座"。 SO 2022 中 Git 使用率 93.87%,"没有其他技术像 Git 一样普及"(SO Blog: Beyond Git);2025 年调查转向平台层:GitHub 81.1%、GitLab 35.6%(survey.stackoverflow.co/2025)。新 VCS 的生存策略因此分化:jj 全盘复用 Git 对象库与传输协议——colocated 模式下 .git 直接暴露,Git 工具、GitHub、GitLab 全部无感(docs),创新只发生在引用/体验层(change-id、自动 rebase、冲突可提交、op log),代价是继承 Git 的 GC 与 packfile 机制(外加自己的 op log GC)。Sapling/Pijul 因自有存储格式,互操作是额外工程。Git 的对象模型还外溢到版本控制之外:Dolt(dolthub.com)、lakeFS(lakefs.io)、DVC(dvc.org)、git-annex(git-annex.branchable.com)——它们是"内容寻址 + 引用 + 打包"这套内部原理的平台化红利。分析:竞品要打败 Git,与其重写存储,不如像 jj 一样"寄生"在 Git 存储之上换体验——2025–2026 年 jj 的增长(32.4k stars)正在验证这条路线。

成熟度与活跃度。 Git 以季度节奏发版,最新 2.56.0(2026-09-28,git-scm.com);两章实验环境为 2.51.0.windows.1(reftable/SHA-256 均可用)。Mercurial 7.2(2026-01-29)活跃迭代;SVN 1.14 LTS 维护态;Fossil 2.27(2025-09-30)稳定半年一版;jj 发布频繁(v0.46.0);Sapling 有 Meta 生产级背书;Pijul 仍在 1.0 beta 线、社区最小(discourse.pijul.org)。分析:按"内部原理的演化活跃度"而非"流行度"排序——引用层(reftable)与哈希层(SHA-256)是 Git 自身最活跃的两条线,存储层(packfile)20 年格局未变,只有外围增强;这层"岩层结构"本身就是对课题的答案。

横 5.实验边界声明

本机未安装 hg/svn/fossil/jj/pijul/darcs,上述其他系统的行为描述全部来自官方文档与技术文章(链接已附),未做本地复现;本地实验仅覆盖 Git 侧。横 4 中标注"分析"的段落为作者基于来源的推论。

交叉洞察:两轴结合说明了什么

洞察 1:不变量解释"底座化"——纵轴的"不变"是横轴的"寄生"得以可能的原因。 纵轴显示,Git 二十年换了引用存储、换了传输协议、加了六七种辅助索引文件,但"内容寻址 + 对象不可变 + 引用可变"三件套从未动过。横轴显示的后果是:jj 不重写存储、只换引用与工作流,就能获得全生态兼容;Dolt/lakeFS/DVC 能把同一套对象模型跨域搬到数据领域;而重写存储的 Sapling/Pijul 必须自己补齐传输、托管、工具链。一个系统的内部格式冻结得越久、越稳定,它就越有可能从"实现细节"变成"公共底座"——2005 年定下的 "<类型> <长度>\0" + 内容 与 packfile 至今未变,变的全是外面两层。

洞察 2:GC 是快照模型的影子成本,纵轴是"记账方式"的演化史,横轴是"账本位置"的比较。 快照模型必然产生无人引用的悬空对象,这是对象不可变 + 引用可变的逻辑代价。纵轴看到的,是 Git 不断降低这笔成本的会计史:手工 repack/prune(2005)→ 带安全默认值的 git gc(2007)→ 后台化 git maintenance(2020)→ cruft packs 的"连龄打包、按龄回收"(2022)——回收越来越保守、越增量化。横轴看到的,是各家把代价记在不同的账本上:Git 付在 gc 运行时(宽限期 + cruft pack 是工程上精巧的"拖延偿付"),revlog 付在写入路径(快照点),SVN 付在协作模式(不能离线改历史),Fossil 付在磁盘(永不删除),jj 两层各付一次。两轴合起来回答了内部原理课程里"为什么 Git 必须有 gc"这一问:GC 不是 Git 的缺陷,而是它选定的快照模型 + 内容寻址组合的必然影子;Mercurial 们不是"解决了 GC",而是把同一笔成本记在了别的账本上。

洞察 3:岩层结构——存储层冻结、引用与哈希层施工中、维护层最活跃。 把纵轴的版本节点按层归类,能看到清晰的地质分层:packfile 格式(2005 定型,2006 idx v2 后冻结)20 年只有外围增强(bitmap、MIDX、cruft、增量 MIDX);引用层在 2024 年迎来首次后端更换(files 后端→reftable;此前的松散文件与 packed-refs 只是同一 files 后端内的两种形态,不算更换后端——与纵 7 口径一致),哈希层在 2020 年起可换(SHA-256),两者都指向 Git 3.0 的默认化;维护层则是发版最密集的施工面(maintenance、is-needed、几何重打包)。版本节点不是随机分布的,而是性能压力与安全压力的时间戳:idx v2/位图/MIDX/commit-graph 源于"仓库变大有小文件",SHA-256/cruft packs 源于"碰撞与误删风险"。同时横 2.5 的细节提醒我们:连"HEAD 是个文件"这样的铁律也在松动(reftable 下变成哨兵 ref: refs/heads/.invalid)——所谓不变量,指的是数据层与对象语义,而不是任何具体的落盘形式。

洞察 4:换哈希的瓶颈在平台不在内核,换引用存储的瓶颈在默认化节奏。 SHA-256 在 Git 里已可用六年(2.29–2.42 转正),Git 3.0 将设为默认,但 GitHub 未公开支持使其仍停留在横 2.5 那样的本地仓库里;GitLab/Codeberg 的先行支持是竞品格局中少见的"平台差异化"变量。reftable 同理:机制已落地(2.45),广泛普及要等 Git 3.0 默认化 + 托管平台跟进。内部格式的换代,"能换"与"换了"之间隔着整个生态的迁移成本——这也是内容寻址模型成为公共底座后必须支付的代价的另一面(与洞察 1 互为表里)。

洞察 5:方法论——plumbing 是最低成本的"原理考古工具",横向对比的正确粒度是"设计取舍层"。 纵轴的实践表明:hash-object/write-tree/commit-tree/update-ref 能把高层命令的黑盒拆回四步原始操作,count-objects -v、verify-pack -v、fsck --dangling 让 gc 的"打包—保护—回收"三段式直接可见,比读文档更快建立 ground truth;验证格式理论要用"绕开该工具自身 API"的独立工具(sha1sum 直接算原始流),因为工具自带的 --stdin 入口可能隐含另一层封装。横轴的实践表明:把 GC 还原为"代价记在哪本账",把 jj/Sapling/Pijul 的差异还原为"是否复用存储层",比罗列特性表更有解释力——四层框架(数据—索引—存储—维护)在 Mercurial(revlog+node+changegroup+strip)、SVN(FSFS+路径+svndiff+无)、Fossil(artifact+tag+SQLite+无)中同样可以逐层辨认。

对用户的判断与建议

  1. 默认选 Git:生态、平台、CI、招聘市场全占,2.56.0 的季度节奏下没有任何"迁移 away"的理性理由。
  2. 想要 Git 的存储、不想要 Git 的体验:用 jj。colocated 模式可随时 git 回退,是当前风险最低的"体验升级"路径;代价是要理解它继承的 Git GC 之外还有一层 op log GC(两层会不会互删,见常见追问 5)。
  3. 超大仓库:Git partial clone + sparse-checkout + commit-graph/MIDX 已够用大多数场景(GitLab 实测克隆提速约 50%、流量省约 70%);百万文件级极端规模再看 Sapling(需接受其服务端不开放)。
  4. SHA-256 与 reftable:观望到平台支持落地,但可提前准备。GitHub 用户暂缓;自建 GitLab/Codeberg 的团队可以试点 SHA-256 仓库,并提前测试 CI/工具链对 repositoryformatversion = 1 的兼容性——Git 3.0 的迁移成本将主要落在工具链而不是 Git 本身。存量 SHA-1 库的迁移路径与代价见常见追问 2:没有一键原地转换,靠跨算法 fetch 边翻译边搬,且旧哈希的全部外部锚点作废;3.0 时间表本身没有官方日期(见常见追问 3)。
  5. 集中管控/二进制资产/遗留系统:SVN 仍有现实位置(检出粒度、路径级授权);个人一站式(代码+wiki+票据+离线)可考虑 Fossil;对合并正确性有理论洁癖的研究型场景看 Pijul(成熟度风险自担)。二进制资产交给 Git 的边界条件见常见追问 4。
  6. 学习内部原理的最短路径:在一个临时仓库里走一遍横 2.1–2.4 的 plumbing 命令链,再看纵轴的版本年表——先有 ground truth,再有历史,顺序不能反。

常见追问

追问 1:本地 git gc 之后为什么没有 .bitmap?表 2 不是把 bitmap 列为 gc 输出吗?

横 2.3 的 pack 目录只有 .pack/.idx/.rev,没有 .bitmap。因为位图是否写出由 repack.writeBitmaps 控制,本机随版本(2.51)的 git-config 文档原文:"Defaults to true on bare repos, false otherwise."——gc 内部走 repack,非裸仓库默认不写位图,裸仓库(服务端形态)默认写。收益面也在服务端:*"This index can speed up the 'counting objects' phase of subsequent packs created for clones and fetches, at the cost of some disk space and extra time spent on the initial repack."* 且要求单包全量打包(*"This has no effect if multiple packfiles are created."*)。表 2 的意思是 Git 的维护工具箱里有 bitmap 这一文件类型(纵 4 的 2013–2014 位图史),不是说每次 gc 都生成——表内已加注区分。本地想手写一次位图:git repack -a -d --write-bitmap-index。

追问 2:存量 SHA-1 仓库到底怎么迁到 SHA-256?要重写历史吗?

对象 ID 遍布整个提交图,换哈希意味着每个 commit/tree 的 ID 都变——官方没有"一条命令原地改哈希"。官方 hash-function-transition 设计的路径是跨算法传输 + 双向映射表:*"Fetches from a SHA-1 based server convert the fetched objects into SHA-256 form and record the mapping in the bidirectional mapping table"*(push 反向翻译;*"Users can use SHA-1 and SHA-256 identifiers for objects interchangeably"*;*"The format allows round-trip conversion between SHA-256 content and SHA-1 content"*)。落到操作:新建 --object-format=sha256 空库,从旧库 fetch 搬运,对象名在传输层翻译,新旧 ID 经映射表短期并存。但同一文档划出明确边界:协议层 SHA-256 在初始设计中 *"out of scope"*,且在协议支持前 *"using SHA-256 based storage on public-facing Git servers is strongly discouraged"*;子模块指针要等子模块仓库自身转换(*"To convert recorded submodule pointers, you need to have the converted submodule repository in place"*),notes 树迁移另行设计。真正的代价不在 git 命令而在生态:所有把 40 位哈希写死在外部的地方——pinned commit 链接、子模块指针、签名验证、文档与 CI 钉扎——都要逐一跟进。这就是建议 4 说"成本落在工具链"的依据。

追问 3:Git 3.0 有时间表吗?

没有官方日期。可考的只有方向性表态:LinuxAdictos 对 2.53 的报道(明确 3.0 方向为 reftable+SHA-256 默认)、LWN 2025-10、GitHub 社区讨论 #12490,均不含发布时间。可作参照的节奏:1.0(2005-12-21)到 2.0(2014-05-28)隔了八年半;2.x 至今保持季度发版,2.56(2026-09-28)刚落地,官方没有任何把 3.0 排进日程的公开信号。分析:把 3.0 理解为"方向已定、时间未定"更准确,任何具体年限都是推测。

追问 4:二进制/大文件下 delta 的表现?横 2.3 实验只测了文本。

先给文档边界:core.bigFileThreshold 默认 512 MiB(本机随版本文档核对),超过阈值的文件 *"Stored deflated in packfiles, without attempting delta compression"*,文档并说明这一默认值的用意正是 *"most projects will have their source code and other text files delta compressed, but not larger binary media files"*。再给修订时补做的三案例实测(各 3 版 × 200KB,git 2.51.0.windows.1):

案例第 2、3 版与第 1 版的关系gc 后 size-packdelta blob
纯文本(原地改 24 字节)字节级高度相似2 KiB(.pack 1356 B)3 中 2
随机二进制(原地改 24 字节)相似区域原样保留202 KiB(≈一份不可压基准 + 两条小 delta)3 中 2
随机二进制(每版全新随机)无字节级相似(模拟重编码的压缩媒体)602 KiB(≈3×200KB 原样堆放)0

解读(分析,基于上述实测与文档):① delta 是字节级差量——相似二进制(逐版本编译产物、微调的图标/字体)能进 delta 链,但包体积由那份不可压缩的完整基准决定;② "每版字节全变"的已压缩媒体(jpg/mp4/zip 重存)与随机数据一样 0 delta、0 zlib 增益,每个版本原样入包;③ 超大文件被 512MiB 阈值直接排除出 delta。工程结论:常规二进制交给 Git 没问题,真正该外挂的是"超大 + 每版全变"的媒体资产——Git LFS / git-annex(横 4 生态段)。

追问 5:jj 的两层 GC 会互相删掉对方的对象吗?

机制上各管各的、互不知晓对方私有存储,且两侧都有防互删设计;官方承认的唯一不确定性是"未充分测试"。Git 侧:git gc 只清 .git/objects 里它认为不可达的对象,而 jj 把自己创建的提交用 refs/jj/ 前缀引用暴露给 Git——git-compatibility 文档写明这些引用的用途就是 "to prevent GC",冲突树的特殊表示同样 "to prevent GC of the relevant trees"。jj 侧:jj util gc 手册写明它做 backend-dependent GC,清理对象是被 abandon 的旧操作及其引用的提交/对象——*"To garbage-collect old operations and the commits/objects referenced by them, run jj op abandon ..<some old operation> before jj util gc"*——且默认只动 2 周前的废弃对象(目前 --expire 只接受 "now")。官方对 colocated 仓库跑 git gc 的口径是:*"It should be safe to run git gc in the Git repo, but it's not tested, so it's probably a good idea to make a backup of the whole workspace first."* 结论:设计目标就是互不删除;对 jj 用户,少手动跑 Git 侧 gc、清理用 jj 自己的命令,是最贴合官方口径的姿势。

附录:两章综合方法、已知局限与修订记录

上游章节(本稿由其综合而成):

两章的综合方法(如何消除矛盾与重复):

  1. 两章各自独立执行过对象模型、gc 机制的实验(不同临时仓库、不同演示数据),本稿将其合并为横 2 的单一定量锚点,按实验规模互补呈现,并标注数据出自哪一章的实验;
  2. 两章表述的"表面矛盾"按条件差异归位:例如纵轴实验"裸 git prune --dry-run 列出全部悬空对象"与横轴实验"git gc 之后悬空提交依然存在"并不冲突——前者是裸调用 prune(无宽限期),后者是 git gc 走默认 gc.pruneExpire=2.weeks.ago 加 cruft pack 保护,本稿在横 2.4 把它们放进同一条机制链说明;
  3. 对象模型演示统一采用横轴一轮(带外部 sha1sum 复算佐证的那组数据);纵轴一轮观察到的 zlib deflate 流头(文件以 78 01 开头)对任何松散对象都成立,作为独立证据保留;
  4. 两章重复引用的版本节点(2.37 cruft packs、2.45 reftable、2.29 SHA-256 等)只在纵轴出现一次,横轴引用时回指。

已知局限(如实继承自两章):2005-06-16 packfile 设计原始邮件的全文在 lore/public-inbox 多次抓取返回 HTTP 300/404,其日期、标题与 Message-ID 来自搜索引擎索引;GitLab《Journey through Git's 20-year history》一文抓取超时,仅作搜索侧佐证;hg/svn/fossil/jj/pijul/darcs 未在本机安装,这些系统的行为描述全部来自官方文档与技术文章,未做本地复现;凡属作者推论而非直接引用处,均标注"分析"。

最终稿修订记录(按独立审读意见逐条落实):最终稿新增实验均于 2026-11-06 在本机执行(git version 2.51.0.windows.1,Windows 10 Git Bash)。

  1. 横 2.3 数字自洽:压缩倍数 8.6 改为 ≈4.3(3070÷718≈4.28);"整包 41KiB"注明为 size-pack 口径 = .pack 36259 + .idx 6196 字节 ≈ 41.5KiB(不含 .rev),并以同规模复核重跑证实口径(重跑 size-pack 19 KiB = 13447+6196 字节)。
  2. 横 2.3 "约9字节":删去无输出支撑的说法,替换为复核重跑 verify-pack 的真实数值(59 个 delta blob 的包内大小 20–21 字节、delta 指令流 9 字节),并写明 verify-pack 对 delta 对象的列语义(第 3 列为指令流大小,用 cat-file -s 交叉核对)。
  3. 转录断链:横 2.2 补记第二轮提交($C2)的缺失命令并显式标注省略;横 2.5 补齐 cd repoSHA 与创建 f.txt 两步,均注明为转录补记。
  4. 内部口径统一:纵 7 与洞察 3 统一为"松散文件与 packed-refs 是 files 后端内的形态,reftable 是首次更换引用后端";TL;DR 同步。
  5. 框架与正文对齐:纵 0 删去上游章节节号引用,直接宣告四层骨架;横 1 开头把 9 个对比维度映射回四层(①②→数据/索引层、③④→存储层、⑤→维护层、⑥–⑨→派生观察)。
  6. "分析"标注贯彻:纵 2"Junio 时代确立"、纵 6"二十年来最大改动"、横 2.3 delta 收益结论分别补"分析"标注或降格表述("仅由两个数据点归纳")。
  7. 机制链补全:横 2.4 补"过期删除"的执行者与时机(git-gc 文档引文:gc 调用 repack --cruft --cruft-expiration + prune --expire;cruft-packs 文档:.mtimes 保留原始时间戳不重置),新增全链条实测演示(reflog 保护→reflog 过期→进 cruft→prune=now 删除),并区分"有 reflog 的普通误删"与"无 reflog 的裸悬空对象"两条路径。
  8. 结构调整:头部压缩为数据与来源说明;"对用户的判断与建议"升为独立 ## 节;新增"常见追问"5 问(本地 gc 无 bitmap、SHA-256 迁移路径、Git 3.0 时间表、二进制 delta 实测、jj 两层 GC);综合方法与已知局限移至本附录。

参考来源

Git 诞生与历史

  1. LKML《Kernel SCM saga..》(2005-04-06)— https://lkml.org/lkml/2005/4/6/121
  2. Pro Git: A Short History of Git — https://git-scm.com/book/en/v2/Getting-Started-A-Short-History-of-Git
  3. Linux Foundation:10 years of Git 访谈 — https://www.linuxfoundation.org/blog/blog/10-years-of-git-an-interview-with-git-creator-linus-torvalds
  4. GitButler: 20 years of Git — https://blog.gitbutler.com/20-years-of-git
  5. GitLab: Journey through Git's 20-year history — https://about.gitlab.com/blog/journey-through-gits-20-year-history
  6. Wikipedia: Git — https://en.wikipedia.org/wiki/Git
  7. Linus packfile 设计说明(2005-06-16,lore.kernel.org)— https://lore.kernel.org/git/Pine.LNX.4.58.0506160756200.2516@g5.osdl.org/
  8. Junio 接任维护者原始邮件(public-inbox)— https://public-inbox.org/git/7vmzo8ss2l.fsf@assigned-by-dhcp.cox.net
  9. LWN 145123(维护者交接)— https://lwn.net/Articles/145123
  10. GitHub Blog:15 年访谈 Junio Hamano — https://github.blog/open-source/git/celebrating-15-years-of-git-an-interview-with-git-maintainer-junio-hamano
  11. LWN 165127(Git 1.0.0)— https://lwn.net/Articles/165127

Git 官方文档与格式规范

  1. Pro Git ch.10 Git Internals: Git Objects — https://git-scm.com/book/en/v2/Git-Internals-Git-Objects
  2. Pro Git: Packfiles — https://git-scm.com/book/en/v2/Git-Internals-Packfiles
  3. gitformat-pack — https://git-scm.com/docs/gitformat-pack
  4. git-gc(默认值引文,含 repack --cruft --cruft-expiration 调用)— https://git-scm.com/docs/git-gc
  5. index-format — https://git-scm.com/docs/index-format
  6. partial-clone — https://git-scm.com/docs/partial-clone (及 2.19.0 版:https://git-scm.com/docs/partial-clone/2.19.0 )
  7. cruft-packs(.mtimes、过期删除、时间戳保留)— https://git-scm.com/docs/cruft-packs
  8. reftable — https://git-scm.com/docs/reftable
  9. gitrepository-format — https://git-scm.com/docs/gitrepository-format
  10. hash-function-transition(跨算法 fetch/push 与映射表)— https://git-scm.com/docs/hash-function-transition
  11. git-scm.com(2.56.0,2026-09-28)— https://git-scm.com
  12. git-config(repack.writeBitmaps / core.bigFileThreshold / gc.writeCommitGraph / gc.cruftPacks 默认值;经本机随版本 2.51 安装文档核对)— https://git-scm.com/docs/git-config
  13. git-verify-pack(-v 输出列;列语义经本机 cat-file -s 交叉核对)— https://git-scm.com/docs/git-verify-pack

版本节点:RelNotes 与发布公告

  1. RelNotes 1.5.0 — https://github.com/git/git/blob/master/Documentation/RelNotes/1.5.0.adoc
  2. RelNotes 1.5.3(git gc 诞生)— https://github.com/git/git/blob/master/Documentation/RelNotes/1.5.3.adoc
  3. RelNotes 1.6.0 — https://github.com/git/git/blob/master/Documentation/RelNotes/1.6.0.txt
  4. LWN 209476(1.4.4 / idx v2)— https://lwn.net/Articles/209476
  5. LWN 521075(1.8.0 / 索引 v4)— https://lwn.net/Articles/521075
  6. GitHub Blog: Google Summer of Git(位图)— https://github.blog/news-insights/the-library/google-summer-of-git
  7. LKML 2.0.0 发布公告 — https://lkml.iu.edu/hypermail/linux/kernel/1405.3/02592.html
  8. kscherer: Git and Bitmaps — https://kscherer.github.io/git/2015/05/15/git-and-bitmaps
  9. Azure DevOps: Supercharging the Git Commit Graph — https://devblogs.microsoft.com/devops/supercharging-the-git-commit-graph-ii-file-format
  10. Azure DevOps: Updates to the commit-graph feature — https://devblogs.microsoft.com/devops/updates-to-the-git-commit-graph-feature
  11. GitHub Blog: Highlights from Git 2.21(MIDX)— https://github.blog/open-source/git/highlights-from-git-2-21
  12. Stack Overflow: clone --filter 语法 — https://stackoverflow.com/questions/49917616/what-is-the-git-clone-filter-options-syntax
  13. GitHub Blog: partial clone and shallow clone — https://github.blog/open-source/git/get-up-to-speed-with-partial-clone-and-shallow-clone/
  14. GitLab blog: partial clone for massive repositories — https://about.gitlab.com/blog/partial-clone-for-massive-repositories
  15. RelNotes 2.26.0(protocol v2 默认)— https://github.com/git/git/blob/master/Documentation/RelNotes/2.26.0.adoc
  16. InfoQ: Git 2.26 wire protocol v2 — https://www.infoq.com/news/2020/03/git-2-26-wire-protocol-2
  17. GitHub Blog: Git 2.29 released(SHA-256 + maintenance)— https://github.blog/open-source/git/git-2-29-released
  18. InfoQ: Git 2.29 SHA-256 — https://www.infoq.com/news/2020/10/git-2-29-sha-256
  19. ZDNet: Git's move away from SHA-1 — https://www.zdnet.com/article/gits-move-away-from-sha-1-version-2-29-brings-experimental-sha-256-support
  20. RelNotes 2.37.0(cruft packs)— https://github.com/git/git/blob/master/Documentation/RelNotes/2.37.0.adoc
  21. GitHub Blog: Highlights from Git 2.37 — https://github.blog/open-source/git/highlights-from-git-2-37
  22. devtoollab: Git SHA-256(2.42 转正)— https://devtoollab.com/blog/git-sha256
  23. RelNotes 2.45.0(reftable)— https://github.com/git/git/blob/master/Documentation/RelNotes/2.45.0.adoc
  24. GitHub Blog: Highlights from Git 2.45 — https://github.blog/open-source/git/highlights-from-git-2-45/
  25. GitLab blog: A beginner's guide to the Git reftable format — https://about.gitlab.com/blog/a-beginners-guide-to-the-git-reftable-format
  26. heise: Git 2.47 增量 MIDX — https://www.heise.de/en/news/Git-2-47-brings-incremental-multi-pack-indexes-and-a-basic-branch-search-9973928.html
  27. GitHub Blog: Highlights from Git 2.52 — https://github.blog/open-source/git/highlights-from-git-2-52/
  28. LWN 1046835(Git 2.52)— https://lwn.net/Articles/1046835
  29. GitLab blog: What's new in Git 2.53.0 — https://about.gitlab.com/blog/whats-new-in-git-2-53-0
  30. LKML 2.53 发布公告 — https://lkml.iu.edu/2602.0/01686.html
  31. LinuxAdictos: Git 2.53 与 3.0 方向 — https://en.linuxadictos.com/git-2-53-reinforces-its-course-towards-git-3-0-and-rust.html
  32. ADTmag: Git 2.54 — https://adtmag.com/articles/2026/04/22/git-2dot54-adds-new-history-editing-tools-and-maintenance-updates.aspx
  33. Collabora: Git 2.54/2.55 — https://www.collabora.com/news-and-blog/news-and-events/git-hooks-upgraded-whats-new-git-254-and-coming-255.html
  34. GitHub Blog: Highlights from Git 2.55 — https://github.blog/open-source/git/highlights-from-git-2-55
  35. GitLab blog: What's new in Git 2.55.0 — https://about.gitlab.com/blog/whats-new-in-git-2-55-0
  36. GitHub 社区讨论:Git 3.0 / SHA-256 — https://github.com/orgs/community/discussions/12490
  37. LWN:SHA-256 与 Git 3.0(2025-10)— https://lwn.net/Articles/1042172/

SHA-1 危机

  1. SHAttered — https://shattered.io
  2. SHAttered 论文 — https://eprint.iacr.org/2017/190
  3. GitHub Blog: SHA-1 collision detection on GitHub.com — https://github.blog/news-insights/company-news/sha-1-collision-detection-on-github-com

Mercurial

  1. Mercurial 官网(7.2,2026-01-29)— https://www.mercurial-scm.org
  2. AOSA: The Mercurial Project — https://aosabook.org/en/v1/mercurial.html
  3. Gregory Szorc: The Mercurial Revlog — https://gregoryszorc.com/blog/2014/02/05/the-mercurial-revlog
  4. ngoldbaum: Storing versioned data with revlogs — https://ngoldbaum.github.io/posts/revlog
  5. hg book: Behind the scenes — https://book.mercurial-scm.org/read/concepts.html

SVN

  1. Subversion 1.14 Release Notes(FSFS 8 / SHA-256)— https://subversion.apache.org/docs/release-notes/1.14
  2. Subversion Release History(1.14.5)— https://subversion.apache.org/docs/release-notes/release-history.html
  3. SVN Book(FSFS 存储后端)— https://svnbook.red-bean.com/en/1.8/svn.reposadmin.create.html

Fossil

  1. Technical Overview — https://fossil-scm.org/home/doc/trunk/www/tech_overview.wiki
  2. Repository Integrity Self-Checks — https://fossil-scm.org/home/doc/tip/www/selfcheck.wiki
  3. Fossil Performance(压缩比)— https://fossil-scm.org/home/doc/tip/www/stats.wiki
  4. Changes Log(2.27)— https://fossil-scm.org/home/doc/tip/www/changes.wiki

jj / Sapling / Pijul

  1. jj GitHub(32.4k stars,2026-11-06 抓取)— https://github.com/jj-vcs/jj
  2. jj Releases — https://github.com/jj-vcs/jj/releases
  3. jj Git compatibility(refs/jj/ 防 GC、colocated 跑 git gc 的官方口径)— https://docs.jj-vcs.dev/latest/git-compatibility/
  4. jj CLI Reference — https://docs.jj-vcs.dev/latest/cli-reference/
  5. jj-util-gc(1)(backend-dependent GC、--expire、op abandon 流程)— https://man.archlinux.org/man/extra/jujutsu/jj-util-gc.1.en
  6. Sapling: Scaling overview — https://sapling-scm.com/docs/scale/overview
  7. Sapling: Internal differences from Mercurial — https://sapling-scm.com/docs/dev/internals/internal-difference-hg
  8. Meta Engineering Blog: Sapling(2022-11)— https://engineering.fb.com/2022/11/15/open-source/sapling-source-control-scalable/
  9. Mononoke docs — https://github.com/facebook/sapling/blob/main/eden/mononoke/docs/README.md
  10. Pijul 官网 — https://pijul.org
  11. Pijul Discourse — https://discourse.pijul.org

调查与同构外溢

  1. SO Developer Survey 2022(93.87%)— https://survey.stackoverflow.co/2022
  2. SO Developer Survey 2025(GitHub 81.1% / GitLab 35.6%)— https://survey.stackoverflow.co/2025
  3. SO Blog: Beyond Git — https://stackoverflow.blog/2023/01/09/beyond-git-the-other-version-control-systems-developers-use/
  4. 同构外溢项目官网:Dolt(https://www.dolthub.com)、lakeFS(https://lakefs.io)、DVC(https://dvc.org)、git-annex(https://git-annex.branchable.com)
  5. Git LFS(大文件外挂方案,见常见追问 4)— https://git-lfs.com
← 资料库今日播报 →