reftable 是 git 新的引用存储格式:不再一个 ref 一个文件,而是一组有序的表。这篇创建 reftable 仓库,看目录结构、每次更新产生的表文件与自动合并、表的头尾字节、5000 个 tag 时的体积和耗时,以及与 files 后端互相迁移。
$ cat .git/HEAD; echo "---"; git config --local --list | grep -E "refstorage|formatversion"; echo "---"; ls -l .git/refs | awk 'NR>1 {print $5, $9}'; cat .git/refs/heads; echo; echo "---"; ls -l .git/reftable | awk 'NR>1 {print $5, $9}'; cat .git/reftable/tables.list
ref: refs/heads/.invalid
---
extensions.refstorage=reftable
core.repositoryformatversion=1
---
41 heads
this repository uses the reftable format
---
124 0x000000000001-0x000000000001-0b921ef5.ref
43 tables.list
0x000000000001-0x000000000001-0b921ef5.ref
(git 自己仍能正常工作:)
$ git symbolic-ref HEAD; git show-ref 2>&1 | head -2; echo "(新仓库没有任何 ref,show-ref 无输出)"
refs/heads/main
(新仓库没有任何 ref,show-ref 无输出)reftable 仓库的 .git/HEAD 是占位值 ref: refs/heads/.invalid,.git/refs/heads 是一个说明文件而不是目录——用来挡住不认识 reftable 的旧版 git。配置里 extensions.refstorage=reftable,版本升到 1。真正的 ref(和 reflog)存在 .git/reftable/ 下。
提交 1 之后: $ ls .git/reftable | grep -v tables.list 0x000000000001-0x000000000002-40602fbf.ref git branch b2 之后:1 个表 1-3(355B) git branch b3 之后:1 个表 1-4(388B) git branch b4 之后:1 个表 1-5(419B) git branch b5 之后:2 个表 1-5(419B) 6-6(255B) git branch b6 之后:1 个表 1-7(481B) git branch b7 之后:2 个表 1-7(481B) 8-8(255B) git branch b8 之后:1 个表 1-9(543B)
每次更新写一个新表,文件名里是它覆盖的更新序号范围。git 自动把相邻、大小相近的表合并,所以表的数量保持很小,不像松散 ref 那样随 ref 数线性增加文件。
(先 git pack-refs --all 合并成 1 个表) 表文件 0x000000000001-0x000000000009-bd6423a5.ref,543 字节 0000 52 45 46 54 01 00 10 00 00 00 00 00 00 00 00 01 REFT............ 0010 00 00 00 00 00 00 00 09 72 00 01 0a 00 23 48 45 ........r....#HE …(共 543 字节) 头部:魔数 b'REFT' 版本 1 块大小 4096 最小更新序号 1 最大更新序号 9 尾部(最后 68 字节):魔数 b'REFT' 与头部相同: True ref_index_offset=0 obj_offset(含长度字段)=0 obj_index_offset=0 log_offset=266 log_index_offset=0 CRC-32(尾部前 64 字节)= e12b8a26;文件里记的 e12b8a26;一致: True 表里有多少个 ref(用 git 数): 8
表以 24 字节头开头:魔数 REFT、版本、块大小、最小与最大更新序号;末尾 68 字节是头的副本加各区偏移(ref 索引、对象反查、reflog 等)与 CRC-32。小表里没有的区域偏移为 0;reflog 也存在表里。
后端 文件数 占用字节 git tag -l rev-parse 单个 files(松散) 5001 205,041 91.1 ms 11.0 ms files(packed-refs) 1 290,103 14.6 ms 10.6 ms reftable 1 135,479 14.8 ms 11.0 ms
5000 个 tag 时,reftable 只有 packed-refs 的一半左右(ref 名前缀压缩);列举所有 ref 与 packed-refs 一样快,远快于松散文件;只查一个 ref 三者没有差别。
一次 update-ref --stdin 事务写入 5000 个:表的个数 = 1 逐个 git tag(第 N 个之后的表数):1→2 2→2 3→1 4→2 5→2 8→1 16→2 32→2 64→2 100→3 150→2 200→4
一次事务里的所有更新属于同一个更新序号,只写一个表;逐个更新则产生许多小表,靠合并控制数量(200 次后只有 4 个)。
$ git refs migrate --ref-format=reftable; cat .git/HEAD; ls .git | grep -E "^(logs|packed-refs|reftable)$"; git for-each-ref --format="%(refname)"; git config --local --list | grep -E "refstorage|formatversion" ref: refs/heads/.invalid reftable refs/heads/b1 refs/heads/main refs/tags/t1 core.repositoryformatversion=1 extensions.refstorage=reftable $ git refs migrate --ref-format=files; cat .git/HEAD; ls .git | grep -E "^(logs|packed-refs|reftable)$"; git for-each-ref --format="%(refname)"; git config --local --list | grep -E "refstorage|formatversion" || echo "(没有 refstorage 了;formatversion 回到 0)" ref: refs/heads/main logs packed-refs refs/heads/b1 refs/heads/main refs/tags/t1 core.repositoryformatversion=0
git refs migrate --ref-format=reftable|files 在两种后端之间转换,ref 内容保持不变。reftable 没有 logs/ 与 packed-refs,迁回 files 后它们重新出现。