删掉不等于消失
一句话总结
删除文件并提交,只表示从今以后没有这个文件,历史中留下的对象原封不动。要真正清除,必须重写历史(filter-branch)、移除备份引用、清空 reflog,并执行 gc --prune=now。即便如此,已经扩散出去的副本仍然存在。
为什么需要它
有两种事故,形态相同。
一种是机密。误提交了含有生产令牌的 .env,发现后又删掉了。打开仓库一看,这个文件已经不在了。但 git log -- conf/prod.env 仍然会列出两个提交,而 git cat-file -p <커밋>:conf/prod.env(占位符为提交)会把令牌原文原样打印出来。git 是内容寻址存储,一个 blob 一旦进入仓库,只要还有提交指向它,就不会消失。
另一种是体积。只要提交过一次几百 MB 的数据文件或构建产物,之后即使删除,每个 clone 的人仍要继续下载这些对象。“为什么我们的仓库 clone 要 10 分钟”,答案通常就在这里。
这两种情况,都要修改的不是今后,而是过去的历史,才能解决。
工作原理
git filter-branch 会逐个重新生成指定 ref 下的提交,并对每个提交应用过滤器。移除文件时,只修改索引的 --index-filter 最快——因为它不会展开工作树。
FILTER_BRANCH_SQUELCH_WARNING=1 git filter-branch --index-filter \
'git rm -r --cached --ignore-unmatch conf/prod.env assets/blob.bin' \
--prune-empty --tag-name-filter cat -- --all
每个选项都有其理由。
| 选项 | 为什么需要 |
|---|---|
--ignore-unmatch |
避免在没有该文件的提交上 git rm 失败而使整个过程中断 |
--prune-empty |
避免只添加过该文件的提交变成空提交留下来 |
--tag-name-filter cat |
避免标签仍指向旧提交而残留 |
-- --all |
重写所有 ref,而不只是一个分支 |
FILTER_BRANCH_SQUELCH_WARNING |
跳过 git 弹出的 10 秒警告 |
大多数人会卡在这里。命令结束后查看 git log,文件已经没有了,但 git rev-list --objects --all 里它仍然会出现。原因在于 refs/original/。filter-branch 为了能够撤销,会把原来的 ref 全部备份在这个位置之下,而这些备份本身也是 ref,所以仍然抓着旧提交和旧 blob。
因此有固定的顺序。
- 删除
refs/original/——用git for-each-ref --format='%(refname)' refs/original列出清单,再逐个执行git update-ref -d。 - 清空 reflog——
git reflog expire --expire=now --expire-unreachable=now --all。reflog 也是引用,它会记住 HEAD 走过的所有位置。 - 用
git gc --prune=now删除不可达对象。没有--prune=now的话,要等默认宽限期(2 周)过后才会被删除。
用 git count-objects -vH 在前后各测一次,就能用数字看出这个过程究竟做了什么。
在现场相遇的样子
重写结束后,所有提交哈希都会改变。提交哈希是连同父提交和树一起计算出来的,所以只要在历史最前面改动一处,后面的全都会变。于是接下来会发生这些事。
- 全体成员都必须获取被强制推送的分支。如果各自执行
git pull,旧历史和新历史会混在一起,提交就变成两份。重写之后,“各自重新 clone”是最安全的通知方式。 - 所有打开的 PR 和 CI 流水线都会失效,因为它们指向的提交已经不存在了。
- 如果有父仓库把这个仓库作为子模块钉住了,那么它的“图钉”也会断掉。
还有最重要的一点——已经 clone 走的副本里,那些对象依然存在。 同事的笔记本电脑、CI 缓存、fork 出去的仓库、备份中都还留着。所以机密进入历史之后,第一件事不是清理历史,而是撤销这个机密并重新签发。清理历史排在第二位。如果顺序颠倒,在清理的这几天里,仍然有效的凭据就一直暴露在外。
在大型仓库中 git filter-branch 很慢。官方文档也推荐使用 git filter-repo 或 BFG 之类的工具。不过这些工具需要另外安装,而它们所做的事和善后的顺序完全一样——重写、移除备份引用、让 reflog 过期、gc。
下一项实验要做什么
创建一段混有令牌配置文件和 900KB 大块数据的历史,先确认即使在工作树中删除了,git cat-file 仍能取出原文。然后运行重写,亲自找出对象仍然残留的原因(refs/original)并清除,再用 git count-objects -vH 测量体积确实缩小了。最后展示预先备好的同事副本里那个机密依然存在,并以此说明究竟应该先做什么作为结尾。
官方文档是 git-filter-branch、git-reflog、git-gc。