git 的分支和标签存在哪里?为什么有的在 .git/refs 下是文件,有的又在 packed-refs 里?两个地方都有的时候谁说了算?这篇用真实命令看清文件结构、优先级、删除的行为,再测 5000 个标签时两种存放的耗时。
$ find .git/refs -type f | sort .git/refs/heads/dev .git/refs/heads/feat/x .git/refs/heads/main .git/refs/tags/v1 .git/refs/tags/v2 $ cat .git/refs/heads/dev | cut -c1-12; ls .git/packed-refs 2>&1 88a540f9c578 ls: .git/packed-refs: No such file or directory (退出码 1)
每个分支或标签是 .git/refs 下的一个文件,内容就是 hash,分支名里的斜杠变成子目录。创建、更新 ref 都是写这个小文件,所以很便宜。
$ git pack-refs --all --prune; find .git/refs -type f | sort; echo "(以上是剩下的松散 ref 文件)"
(以上是剩下的松散 ref 文件)
$ cat .git/packed-refs | sed -E 's/^([0-9a-f]{7})[0-9a-f]{33}/\1…/; s/^\^([0-9a-f]{7})[0-9a-f]{33}/^\1…/'
# pack-refs with: peeled fully-peeled sorted
88a540f… refs/heads/dev
88a540f… refs/heads/feat/x
88a540f… refs/heads/main
88a540f… refs/tags/v1
a5b9e18… refs/tags/v2
^88a540f…
$ git rev-parse dev v2 v2^{} | cut -c1-12
88a540f9c578
a5b9e18fe2ee
88a540f9c578git pack-refs --all --prune 把它们收进 .git/packed-refs:第一行说明特性(peeled fully-peeled sorted),之后每行“hash ref 名”,按名字排序。注解标签的下一行以 ^ 开头,记着剥开后的提交,查 v2^{} 不用再读 tag 对象。
$ ls .git/refs/heads; grep -E ' refs/heads/main$' .git/packed-refs | cut -c1-12; cat .git/refs/heads/main | cut -c1-12 main 88a540f9c578 fc77d20f2047 git rev-parse main = fc77d20f2047(提交 2 是 fc77d20f2047,提交 1 是 88a540f9c578) $ rm .git/refs/heads/main; git rev-parse main | cut -c1-12 88a540f9c578 手工删掉松散文件之后,main “回退”成 packed-refs 里的旧值(提交 1)——松散文件被删就露出下面的旧记录
松散文件优先于 packed-refs。更新 ref 只写松散文件、不碰 packed-refs,所以 packed-refs 里的旧值会一直在那里,直到 pack-refs 再次运行。手工删掉松散文件会让旧值“复活”,这是手工操作 .git 的一个坑。
$ git branch -D dev; grep -c refs/heads/dev .git/packed-refs || true; git branch
Deleted branch dev (was 88a540f).
0
feat/x
* main
$ git branch tmp1; git branch -D tmp1; ls .git/refs/heads
Deleted branch tmp1 (was fc77d20).
main删 packed 的 ref 要重写整个 packed-refs(并和别的更新互斥),删松散的 ref 只是删一个文件。两种都不会留下旧值。
$ find .git/refs/tags -type f | wc -l; ls .git/packed-refs 2>&1 | head -1
5000
ls: .git/packed-refs: No such file or directory
$ git pack-refs --all --prune; find .git/refs/tags -type f | wc -l; wc -l < .git/packed-refs; ls -l .git/packed-refs | awk '{print $5}'
0
5002
290103
git for-each-ref refs/tags | wc -l 松散 184.5 ms → packed 111.4 ms
git rev-parse t04999 松散 11.0 ms → packed 10.8 ms
git tag -l | wc -l 松散 90.9 ms → packed 14.6 ms列举很多 ref 时,读一个文件比打开几千个文件快得多:5000 个 tag 的 git tag -l 快了约 6 倍;只查一个 ref 则几乎没有差别。
$ git gc -q; find .git/refs -type f | sort; grep -c . .git/packed-refs 4 $ git branch b3; find .git/refs -type f | sort .git/refs/heads/b3
git gc 默认会执行 pack-refs,新创建的 ref 又是松散文件,下次 gc 再合并。所以常态是“大部分在 packed-refs,少数新的是松散的”。