误提交了密码,想从全部历史里抹掉:git filter-branch 能做到,但它做了什么、留下什么、为什么“删了”之后密码还能读出来?这篇用一个有密码的小历史走一遍,包括备份引用、注解标签的陷阱,以及怎样才算真正清干净。
WARNING: git-filter-branch has a glut of gotchas generating mangled history rewrites. Hit Ctrl-C before proceeding to abort, then use an alternative filtering tool such as 'git filter-repo' (https://github.com/newren/git-filter-repo/) instead. See the filter-branch manual page for more details; to squelch this warning, set FILTER_BRANCH_SQUELCH_WARNING=1. Proceeding with filter-branch...
git filter-branch 按你给的过滤器逐个重写提交,随 git 自带,但一运行就警告并推荐 git filter-repo(本机没装,未实测)。--index-filter 只改暂存区(git rm --cached),--tree-filter 要把每个提交检出到磁盘。
$ git log --stat --format='%s' | grep -E '^(C[0-9]|.*secret)' | head -8
C6 收尾
C5 又改了密码
secret.txt | 2 +-
C4 继续开发
C3 误提交了密码
secret.txt | 1 +
C2 加配置
C1 起点
提交数 6 → 4(--prune-empty 把只改了 secret.txt 的提交删掉了:C3 和 C5 里除了密码什么都没改?—— C3 只加了 secret.txt,C5 只改了它,所以消失)
C1 起点 ebd3676
C2 加配置 88cd8a9
C4 继续开发 080c3a2
C6 收尾 6cdc2f8
原来的 hash: ebd3676 88cd8a9 c85ab46 18617fd 42715f0 afc50dd
$ git log --stat --format='%s' | grep -c secret | sed 's/^ */当前历史里提到 secret 的行数: /'; git ls-files | tr '\n' ' '; echo
当前历史里提到 secret 的行数: 0
a.txt cfg.txt被改写的提交及其后代的 hash 全变;--prune-empty 删掉变空的提交。第一个被改动的提交之前的提交(这里的 C1、C2)保持不变。
$ git for-each-ref --format="%(refname)" | head; echo ---; git rev-parse refs/original/refs/heads/main | cut -c1-7 | sed "s/^/备份的旧 main = /"
refs/heads/main
refs/original/refs/heads/main
refs/original/refs/tags/v1
refs/original/refs/tags/v2
refs/tags/v1
refs/tags/v2
---
备份的旧 main = afc50dd
旧历史里的密码 blob 还能读出来: 'password=hunter3'
标签呢?-- --all 把所有 ref 都纳入改写。轻量标签 v1 被重写到新历史;但注解标签 v2 的 tag 对象本身没有被重写(没有 --tag-name-filter),仍指向旧的 C4:
v1(轻量)指向 080c3a2(新 C4 是 080c3a2,旧 C4 是 18617fd):重写了: True
v2(注解)指向 18617fd:仍是旧 C4: True
旧历史里的 C3(第一次提交密码)可以从 v2 到达: True改写前每个 ref 的值被备份到 refs/original/ 下,旧历史(包括被删的文件)通过它们仍然可达;reflog 也还记着。轻量标签会随 --all 被重写,但注解标签的 tag 对象不会,仍指向旧历史,让旧历史继续存活。
只清掉 refs/original 之后:旧 C3 提交还在吗: True(v2 还引用着旧历史);旧密码 blob(hunter3,C5)还在吗: False 再把 v2 重建到新历史上之后:旧 C3 提交还在吗: False;旧密码 blob 还在吗: False (推送到别人已经克隆过的远程之后,别人手里的副本不会因此消失——这一步只清了本地)
删掉 refs/original、没改写的标签,reflog expire --expire=now --all,再 gc --prune=now,旧提交与旧 blob 才真正从本地对象库消失。别人已经拿到的副本不受影响。
v2(注解)指向的提交变了: True;v2 的说明: C4 继续开发
让注解标签对象也被重写,新标签沿用原来的名字,指向新历史上对应的提交。
--index-filter 11.7 秒(重写 300 个提交;f00.txt 还在历史里吗: False) --tree-filter 18.4 秒(重写 300 个提交;f00.txt 还在历史里吗: False)
300 个提交、每个约 50 个文件,删掉同一个文件:--index-filter 11.7 秒,--tree-filter 18.4 秒(约 1.6 倍)。能用 index-filter 就用它。