Git 里删了大文件,仓库还是那么大,得用 filter-repo 清历史

举报
phantomxjc 发表于 2026/10/21 08:58:31 2026/10/21
【摘要】 事情起因是我手滑。那天打包一个导出的数据文件,顺手就放在项目根目录了,然后 git add . 一把梭,跟着代码一起推了上去。第二天同事在群里问:"咱这仓库怎么 clone 一次要这么久?" 我第一反应是赶紧删掉再提交一次: 然后 du sh .git,数字一动没动。 删了文件,历史里那份还在 Git 存的不是"差异",每个 commit 都指向一整棵文件树的快照。你删掉文件再提交,只是让最新的快

事情起因是我手滑。那天打包一个导出的数据文件,顺手就放在项目根目录了,然后 git add . 一把梭,跟着代码一起推了上去。第二天同事在群里问:"咱这仓库怎么 clone 一次要这么久?"

我第一反应是赶紧删掉再提交一次:

git rm data/export-2024.zip
git commit -m "remove large file"

然后 du -sh .git,数字一动没动。

删了文件,历史里那份还在

Git 存的不是"差异",每个 commit 都指向一整棵文件树的快照。你删掉文件再提交,只是让最新的快照里没有它了,前面那些 commit 里那个几百 MB 的 blob 还老老实实躺在 .git/objects 里。任何人 clone 下来,历史是要完整拉走的,所以该大还是大。

想验证的话,先看看仓库到底多重:

git count-objects -vH

看 size-pack 那一行,就是 pack 文件占的空间。这时候你会觉得很冤:明明工作区就几十 MB,.git 却几百 MB。

先把"元凶"找出来

清理之前得知道是哪个文件,不然白折腾。这段组合命令我基本每次都直接复制:

git rev-list --objects --all \
  | git cat-file --batch-check='%(objecttype) %(objectname) %(objectsize) %(rest)' \
  | awk '$1=="blob" && $3>1048576 {printf "%.1f MB\t%s\n", $3/1048576, $4}' \
  | sort -rn | head -20

git rev-list --objects --all 列出所有 commit 里的对象,中间那层 cat-file --batch-check 把对象的类型和大小查出来,最后用 awk 只留下大于 1MB 的 blob,按大小倒序。

列出来之后经常能看到一些早就删掉的文件名——这就对了,它们就是从历史里翻出来的。

用 git filter-repo 重写历史

老教程里会让你用 git filter-branch,git 官方自己的文档现在都不推荐了,慢而且容易出错,每次执行还会往 .git/refs/original/ 里塞备份。现在主流的做法是 git filter-repo:

pip install git-filter-repo

第一步一定要先做:新克隆一份裸仓库,在这份上操作。

git clone --mirror git@xxx:team/project.git
cd project.git

--mirror 克隆下来的是一份完整的裸仓库,所有分支和 tag 都在,而且没有工作区——filter-repo 要求仓库干净,工作区有东西它会拒绝执行,这个设计其实是在保护你。

已知文件名,直接把这个路径从全部历史里剔除:

git filter-repo --path data/export-2024.zip --invert-paths

如果不确定有哪些,可以按体积一刀切,比如所有大于 50MB 的 blob 全清掉:

git filter-repo --strip-blobs-bigger-than 50M

想先看清楚会动到哪些文件,可以先跑个分析:

git filter-repo --analyze

它会在 .git/filter-repo/analysis/ 下生成几个 txt,里面按体积排好了序。我一般是先 --analyze 看一遍,确认要删的确实是误提交的产物,再动手,免得把正常的历史资源一起清了。

跑完之后还没完,历史里的旧对象已经没有引用了,但还没物理删除,需要自己 gc 一轮:

git reflog expire --expire=now --all
git gc --prune=now --aggressive

reflog expire 这步不能省。reflog 会记录你本地 HEAD 的每一次移动,默认保留 90 天,只要它还引用着那些旧 commit,gc 就不敢删。我第一次清理的时候就是漏了这句,gc 跑完 .git 纹丝不动,白等了半天。

推上去,以及几个容易翻车的地方

git push --force --all
git push --force --tags

这里几个坑我基本都踩过:

强制推送之后,同事那边的本地仓库是冲突的。 最省事的通知方式就是一句话:"麻烦重新 clone 一份,别 pull。" 真要保留本地改动,得让他们 git fetch --all 之后 git rebase --onto 把新 commit 嫁接到重写后的历史上,操作成本高,小仓库直接重 clone 更划算。

远端仓库不会立刻变小。 服务端只是把引用挪了,旧对象还留在那,得等它自己触发 gc。GitLab 在「设置 - 仓库 - Repository cleanup」里上传 filter-repo 生成的 commit-map 文件,可以立刻触发清理;GitHub 这边我最后是重新建了个仓库推上去,最笨但也最快。

保护分支要先放开。 主分支一般设了禁止强推,得临时到设置里关掉,推完再打开。

别再让它发生第二次

清理是补救,真正的办法是别让大文件进得来。

.gitignore 先写严实一点,*.zip、*.tar.gz、数据目录这些一律不进版本库。但靠自觉不靠谱,我在项目里加了个 pre-commit 钩子,超过 20MB 直接拒绝提交:

#!/bin/sh
limit=$((20 * 1024 * 1024))
for f in $(git diff --cached --name-only --diff-filter=ACM); do
  if [ -f "$f" ]; then
    size=$(wc -c < "$f")
    if [ "$size" -gt "$limit" ]; then
      echo "文件超过 20MB,别往 Git 里塞:$f"
      exit 1
    fi
  fi
done

存到 .githooks/pre-commit 并 chmod +x,然后让全组都生效:

git config core.hooksPath .githooks

这行得每个人都自己执行一次,.githooks 目录本身可以进版本库,但 core.hooksPath 是本地配置,clone 不会自动带上。

真有必须跟代码走的大文件,就上 Git LFS:

git lfs install
git lfs track "*.psd"
git add .gitattributes

LFS 把大文件换成指针存在仓库里,实际内容单独存,clone 的时候可以只拉当前版本的。

折腾完这一圈,我的收获其实就一句:Git 的删除是"记一笔新的",不是"擦掉旧的"。想真正变小,只能重写历史。所以现在每次 git add . 之前,我都会下意识先 git status 扫一眼。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。