如何彻底删除Git历史中的大文件?

文章导读
当你发现git clone越来越慢,或者.git目录比工作区还大,先别急着优化.gitignore。大文件往往已经进了历史,每次提交都带着一份快照。这篇文章从一个操作者的角度,讲讲如何把这些对象彻底清掉,以及清理前后需要确认的事情。
📋 目录
  1. 先定位历史中的大文件
  2. 用filter-repo重写历史
  3. 重写哈希后的团队协作问题
  4. 清理后的验证与垃圾回收
  5. 边界情况和替代方案
A A

当你发现git clone越来越慢,或者.git目录比工作区还大,先别急着优化.gitignore。大文件往往已经进了历史,每次提交都带着一份快照。这篇文章从一个操作者的角度,讲讲如何把这些对象彻底清掉,以及清理前后需要确认的事情。

先定位历史中的大文件

如果想把大文件从Git历史中彻底删除,不能只靠git rm——因为之前的提交对象仍然保留着该文件的快照,仓库体积不会缩小。要判断历史中是否存在大文件,可以用git rev-list --objects --all列出所有历史对象,再配合git cat-file --batch-check查看每个对象的大小,排序后就能找出体积异常的路径。当某个文件在多次提交中反复被修改,每次提交都会各存一份,累积的体积可能远超你的预期。

上面这条思路同时适用于普通文件和二进制文件。实际执行时,在仓库根目录运行:

git rev-list --objects --all | git cat-file --batch-check='%(objecttype) %(objectname) %(objectsize) %(rest)' | sort -k3 -n -r | head -20

输出结果会按对象大小倒序排列,重点关注体积最大的几个路径。这里的%(rest)就是文件路径,同名路径可能多次出现,说明它存在于多个提交中。如果想看仓库所有对象的累计大小,可以用git rev-list --all --disk-usage。

用filter-repo重写历史

目前推荐用git filter-repo来清理历史,它是一个较新的工具,比filter-branch更快更安全。操作前先整个仓库备份,比如直接复制仓库目录或使用git clone --mirror生成镜像。然后安装filter-repo,进入仓库执行git filter-repo --path 大文件路径 --invert-paths,这会从全部历史中移除该路径的所有版本。命令完成后,仓库的提交哈希都会改变,需要用git push --force --all强制推送到远程。

如何彻底删除Git历史中的大文件?

备份这件事不能跳过。直接复制工作目录有风险,因为.git目录里的refs可能不完整,建议用git clone --mirror备份一个裸仓库。filter-repo执行时会自动移除origin remote,因为旧远程已经失效。你需要先添加新的远程地址,再决定是否强制推送。特别注意,如果远程仓库有分支保护规则,推送前先确认是否允许force push。

重写哈希后的团队协作问题

使用filter-repo后,所有commit的SHA1都变了,你的本地与远程会彻底分叉。如果之前有多个协作者基于旧历史工作,他们必须删除未推送的旧分支,重新从远程clone。如果其他人再基于旧历史提交,就会把已经清除的大文件再次带回来。所以清理历史不是一个人能完成的事,需要提前和团队沟通好,统一时间点操作,并取消所有旧的pull request。

这里的“取消所有旧的pull request”听起来很绝对,但确实需要这样做。因为PR通常基于旧历史,直接合并会把删掉的对象带回来。你可以先让团队把未合并的改动做成patch,等清理完成后再apply到新仓库。操作时间点要选在提交最少的时候,比如周末或深夜。清理后让所有协作者重新clone,而不是fetch,因为fetch可能保留旧对象。

清理后的验证与垃圾回收

清理完成后,不能只看工作区,要回到开头那条命令再检查一次。

git rev-list --objects --all | git cat-file --batch-check='%(objecttype) %(objectname) %(objectsize) %(rest)' | sort -k3 -n -r | head -20

确认之前定位的大文件路径不再出现在输出里。然后看仓库物理大小:

如何彻底删除Git历史中的大文件?
git count-objects -vH

filter-repo默认会运行垃圾回收,所以本地一般不需要手动gc。如果你用其他工具,可能需要执行git reflog expire --expire=now --all,再执行git gc --prune=now --aggressive。另外,还要检查所有引用是否指向新历史:git for-each-ref --format='%(refname)'。如果发现远端标签或分支仍指向旧提交,需要删除或更新。

边界情况和替代方案

有一种情况要格外小心:如果大文件被多个分支或标签的历史引用,指定路径删除时会把它们全清掉,但如果你写了--path只匹配一个文件,而它存在于不同路径下,需要分别列出。另外,如果大文件曾经被合并到其他文件的提交中,只要该提交对象被引用,就还在仓库里。filter-repo只会重写所有分支和标签,但无法清除未被任何引用但可能留在pack中的孤儿对象,通常需要配合gc。建议操作前先检查所有引用并做好详细记录。

如果你对命令行不熟悉,可以试试BFG Repo-Cleaner,但BFG不会重写最新提交,所以只对历史提交有效。BFG更新较慢,使用前需要确认Git版本,而且同样要做好备份。总体而言,filter-repo仍是当前最直接的选择,但任何工具都不能替代事前的备份和事后的验证。

清理Git历史是一次不可逆的重写操作,建议先在本地镜像仓库上完整试跑一遍,确认结果再影响远程。如果仓库托管在GitHub或GitLab,还要注意保护分支设置,可能需要临时关闭分支保护才能强制推送。