bigywater.tech
Git · 69

filter-branch 改写历史

误提交了密码,想从全部历史里抹掉:git filter-branch 能做到,但它做了什么、留下什么、为什么“删了”之后密码还能读出来?这篇用一个有密码的小历史走一遍,包括备份引用、注解标签的陷阱,以及怎样才算真正清干净。

6 → 4
--prune-empty 之后的提交数;只改了 secret.txt 的 C3、C5 消失
refs/original/
filter-branch 保存的改写前引用,旧历史(包括密码)仍然可达
11.7 vs 18.4 秒
300 个提交上 --index-filter 与 --tree-filter 的耗时

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 要把每个提交检出到磁盘。

hash

实测 · 本机执行输出
  $ 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 才真正从本地对象库消失。别人已经拿到的副本不受影响。

--tag-name-filter cat

实测 · 本机执行输出
  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 就用它。

分步演示

filter-branch
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / git 2.54.0(Apple Git-157);每个实验在临时目录里新建仓库,HOME 和全局配置指向实验专用的空目录(不读用户自己的 git 配置),提交的作者与时间固定以便复现;需要网络协议的实验用本机的 file:// 或 git daemon 回环端口(9800 以上),不碰任何远程仓库;计时用 time.perf_counter,数字每次运行会有小幅抖动。过滤命令都加了 FILTER_BRANCH_SQUELCH_WARNING=1;小历史用于演示(6 个提交);速度对比用 300 个提交、50 个文件的仓库,各运行一次;git filter-repo 没有安装,只引用了 git 自己的警告文字
    • 没有验证:git filter-repo 的行为与速度;--subdirectory-filter、--msg-filter、--env-filter;远程仓库上强推改写后的历史、协作者怎么恢复;GitHub 等平台缓存的旧提交;git replace 作为替代方案;git rebase --root 改写
    京ICP备2026067502号-1京公网安备11010502063487号