bigywater.tech
Git · 28

packed-refs 与松散 ref 谁优先

git 的分支和标签存在哪里?为什么有的在 .git/refs 下是文件,有的又在 packed-refs 里?两个地方都有的时候谁说了算?这篇用真实命令看清文件结构、优先级、删除的行为,再测 5000 个标签时两种存放的耗时。

松散优先
同一个 ref 两处都有时以 .git/refs 下的松散文件为准;删掉松散文件旧的 packed 值会重新生效
90.9 → 14.6 ms
5000 个 tag 时 git tag -l 在松散与 packed 下的耗时
^ 行
packed-refs 里注解标签的下一行记着它剥开之后的提交

松散 ref

实测 · 本机执行输出
  $ 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 都是写这个小文件,所以很便宜。

packed-refs

实测 · 本机执行输出
  $ 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
  88a540f9c578

git 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 则几乎没有差别。

gc

实测 · 本机执行输出
  $ 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,少数新的是松散的”。

分步演示

packed-refs 与松散 ref
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / git 2.54.0(Apple Git-157);每个实验在临时目录里新建仓库,HOME 和全局配置指向实验专用的空目录(不读用户自己的 git 配置),提交的作者与时间固定以便复现;需要网络协议的实验用本机的 file:// 或 git daemon 回环端口(9800 以上),不碰任何远程仓库;计时用 time.perf_counter,数字每次运行会有小幅抖动。5000 个 tag 用 git update-ref --stdin 批量创建;耗时是 5 次中位数,含进程启动;输出里 hash 缩写为前几位
    • 没有验证:reftable 后端(下一篇单独讲);ref 总数到十万级时 packed-refs 的行为;并发更新 packed-refs 时的锁(packed-refs.lock);git clone 下来的 ref 是否直接放进 packed-refs(没有查看);for-each-ref 在 packed 下仍要 111 ms 的原因(没有深挖)
    京ICP备2026067502号-1京公网安备11010502063487号