bigywater.tech
Git · 25

prune 与宽限期:unreachable 对象什么时候真被删

丢掉的 commit 什么时候真的从磁盘上消失?这篇造出不可达对象,用改文件时间的办法验证 2 周宽限期,再看暂存区、reflog 和 cruft pack 对它们各自的保护,以及 --prune=now 能删到什么程度。

13 / 15 天
13 天前的不可达 blob 被 gc 保留,15 天前的被删除(宽限期 2 周)
reflog
reset --hard 之后 gc --prune=now 也删不掉旧提交,因为 reflog 还引用着它
.mtimes
pack 里的不可达对象靠 cruft pack 的 .mtimes 计算过期时间

不可达不等于已删

实测 · 本机执行输出
  $ git reset -q --hard HEAD~1; git reflog expire --expire=now --all; git log --oneline
  dc41a9c 第一次
  $ git fsck --unreachable --no-reflogs 2>&1 | sed "s/ [0-9a-f]\{40\}/ <sha>/"
  unreachable blob <sha>
  unreachable commit <sha>
  unreachable tree <sha>
  $ git prune -n | sed "s/^[0-9a-f]\{40\}/<sha>/"
  <sha> blob
  <sha> commit
  <sha> tree
  不可达的提交 30f6be9c4f… 此刻是否存在: True

commit 被 reset 掉、reflog 也清空之后,对象仍留在 .git/objects 里,git fsck --unreachable 能列出它们,git prune -n 能预览会删什么。只有 prune 才真正删除。

2 周宽限期

实测 · 本机执行输出
  A(13 天前): 1e193c82ba…  mtime 距今 13.0 天
  B(15 天前): 5737b388ab…  mtime 距今 15.0 天
  $ git prune --expire=2.weeks.ago -n -v | sed "s/^[0-9a-f]\{40\}/<sha>/"
  <sha> blob
  对照:不带过期时间的 git prune -n 会列出所有不可达对象(包括当前的提交 c2 及它的 tree、blob):
  $ git prune -n | wc -l
         5
  $ git gc -q
  git gc 之后(默认 gc.pruneExpire):A 存在 True,B 存在 False,c2 存在 True

git gc 调用 prune 时带 gc.pruneExpire(默认 2 周),只删修改时间早于它的不可达松散对象。把文件时间改成 13 天前和 15 天前,gc 之后只有后者消失。

为什么要宽限

实测 · 本机执行输出
  $ git prune --expire=now -v | sed "s/^[0-9a-f]\{40\}/<sha>/"
  <sha> blob
  git prune --expire=now 之后:刚写入的 blob 存在 False;暂存区的 blob 存在 True

别的进程可能刚写入一个对象、还没挂上引用。prune --expire=now 会立刻删掉这种对象,但暂存区里的对象算可达,不会被删。宽限期给并发操作留出时间。

reflog 的保护

实测 · 本机执行输出
  $ git reset -q --hard HEAD~1; git reflog | cat | sed -E "s/^([0-9a-f]{7}) /<sha> /"
  <sha> HEAD@{0}: reset: moving to HEAD~1
  <sha> HEAD@{1}: commit: 第二次
  <sha> HEAD@{2}: commit (initial): 第一次
  $ git gc -q --prune=now
  gc --prune=now 之后,被 reset 丢掉的提交存在: True(reflog 还引用着它)
  $ git reflog expire --expire=now --expire-unreachable=now --all; git gc -q --prune=now
  清空 reflog 再 gc --prune=now 之后,这个提交存在: False

reflog 也是引用:reset --hard 之后旧提交仍被 reflog 引用,gc --prune=now 也不会删它。想彻底丢掉要先 reflog expire --expire=now --expire-unreachable=now --all。实验里的提交要用真实时间,否则 reflog 条目一开始就已经过期。

pack 里的不可达对象

实测 · 本机执行输出
  $ git reset -q --hard HEAD~1; git reflog expire --expire=now --all
  $ git gc -q; ls .git/objects/pack | sed -E "s/pack-[0-9a-f]{40}/pack-<hash>/"
  pack-<hash>.idx
  pack-<hash>.pack
  pack-<hash>.rev
  pack-<hash>.idx
  pack-<hash>.mtimes
  pack-<hash>.pack
  pack-<hash>.rev
  gc 之后(默认过期 2 周):不可达的提交存在 True
  cruft pack 的 .mtimes 文件: 有
  $ git gc -q --prune=now; ls .git/objects/pack | sed -E "s/pack-[0-9a-f]{40}/pack-<hash>/"
  pack-<hash>.idx
  pack-<hash>.pack
  pack-<hash>.rev
  gc --prune=now 之后:不可达的提交存在 False

已经进了 pack 的不可达对象,git gc 会把它们放进 cruft pack,.mtimes 文件记录每个对象变成不可达的时间,过期后才删除;--prune=now 则立即清掉。

分步演示

prune 与宽限期
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / git 2.54.0(Apple Git-157);每个实验在临时目录里新建仓库,HOME 和全局配置指向实验专用的空目录(不读用户自己的 git 配置),提交的作者与时间固定以便复现;需要网络协议的实验用本机的 file:// 或 git daemon 回环端口(9800 以上),不碰任何远程仓库;计时用 time.perf_counter,数字每次运行会有小幅抖动。“13 天前 / 15 天前”是用 os.utime 把松散对象文件的修改时间改旧得到的,没有真的等待;第四节的提交使用真实时间(其余实验的提交时间固定为 2026-01-01,在当前日期下 reflog 条目早已过期,会让 reflog 看起来不起保护作用——这是我在调试时发现的);gc 的子命令序列来自 GIT_TRACE
    • 没有验证:gc.pruneExpire 设为其它值、gc.cruftExpire 与 --cruft-expiration;松散对象之外(比如 worktree、rerere)的过期;prune 对多个 worktree 里的暂存区与 HEAD 的保护;并发场景下宽限期真的救了一个对象(只验证了 --expire=now 会删除没有引用的新对象);reflog 默认的 90 天 / 30 天过期(只验证了清除之后的结果)
    京ICP备2026067502号-1京公网安备11010502063487号