Git 里删了大文件,仓库还是那么大,得用 filter-repo 清历史
事情起因是我手滑。那天打包一个导出的数据文件,顺手就放在项目根目录了,然后 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 扫一眼。
- 点赞
- 收藏
- 关注作者
评论(0)