你在某个 commit 里不小心提交了一个 200 MB 的数据文件,或者一个编译产物 .jar。虽然后来已经删除并重新提交,但 git push 依然被 GitHub 拒绝:「remote: error: File path/to/bigfile.bin is 200 MB; this exceeds GitHub’s file size limit of 100 MB」。删除文件的 commit 并没有从 Git 对象库里移除那个大对象——只要历史里有那次提交,对象就还在,push 就会把它带上去。唯一的修复方式是重写历史,把那个对象从所有 commit 中彻底清除。
常见原因
1. 误提交了编译产物或数据文件
最常见。node_modules/、.jar、.war、dist/、大型 CSV 或 SQL dump 误加到了 staging area 并提交。即使后续 commit 删除了这些文件,对象依然在历史里。
怎么判断:git rev-list --objects --all | git cat-file --batch-check='%(objecttype) %(objectname) %(objectsize) %(rest)' | sort -k3 -n -r | head -20 列出历史中最大的对象。
2. .gitignore 添加时机晚了
应该被忽略的文件在 .gitignore 加入前已经被 git add 追踪,后来即使加了忽略规则也不会从历史中删除。
怎么判断:git log --all --oneline -- path/to/bigfile 若有提交记录,说明文件曾被追踪。
3. LFS 迁移时漏掉了某些历史 commit
从普通 Git 迁移到 Git LFS 时,git lfs migrate import 的 --include-ref 或时间范围参数没覆盖到所有分支,部分 commit 里的大文件没被转换成 LFS 指针,原始对象还留在历史里。
怎么判断:git lfs migrate info --include-ref=refs/heads/main --top=10 查看还有哪些大文件没被 LFS 追踪。
4. 合并了包含大文件的外部 PR
合并了一个 fork 提交的 PR,该 PR 里包含大文件,合并后这个大文件就进了主仓库历史。
怎么判断:git log --all --oneline -- path/to/bigfile 找到第一次引入该文件的 commit,查看其来源分支。
5. Squash merge 时把大文件压缩进了一个 commit
多个 commit squash 合并时,其中某个 commit 包含大文件,squash 后大文件进了合并后的单个 commit 历史。
怎么判断:同上,git log --all --oneline -- path/to/bigfile 定位 commit。
最短修复路径
Step 1:找出历史中所有超大对象
git rev-list --objects --all \
| git cat-file --batch-check='%(objecttype) %(objectname) %(objectsize) %(rest)' \
| awk '/^blob/ {print $3, $4}' \
| sort -rn \
| head -20
记录需要清除的文件路径。
Step 2:安装 git-filter-repo(比 filter-branch 快 10-100 倍)
pip install git-filter-repo
# 或者 brew install git-filter-repo(macOS)
Step 3:备份整个仓库
cp -r . ../repo-backup-$(date +%Y%m%d)
git tag backup/before-large-file-removal HEAD
Step 4:用 git-filter-repo 彻底清除大文件
# 清除单个文件
git filter-repo --path path/to/bigfile.bin --invert-paths
# 清除多个路径
git filter-repo --path dist/ --path data/dump.sql --invert-paths
# 按文件大小过滤(清除所有超过 50 MB 的 blob)
git filter-repo --strip-blobs-bigger-than 50M
Step 5:清理残留对象
git reflog expire --expire=now --all
git gc --prune=now --aggressive
Step 6:重新设置远端并 force push
git remote add origin <remote-url>
# 或者如果 remote 已存在:
git remote set-url origin <remote-url>
git push --force-with-lease --all
git push --force-with-lease --tags
Step 7:通知所有协作者重新克隆
历史已被重写,所有协作者的本地克隆都和远端不兼容,必须重新 clone:
git clone <remote-url>
预防建议
- 在仓库根目录维护完善的
.gitignore,项目初始化时就把编译产物、数据目录、凭证文件全部加入。 - 配置 pre-commit hook 拦截大文件:使用
pre-commit框架的check-added-large-files钩子,默认阻止超过 500 KB 的文件被提交。 - 使用 Git LFS 管理所有二进制资产、数据文件、视频、模型文件,避免大对象进入 Git 对象库。
- 在 GitHub/GitLab 仓库设置中启用 push 规则,设置文件大小上限(GitHub.com 免费版单文件上限 100 MB,超过直接拒绝)。
- 定期运行
git count-objects -vH检查仓库体积,发现异常增长时及时排查。 - 团队 onboarding 文档中明确:数据文件、模型文件统一存放到 S3/GCS,Git 里只提交路径或版本号引用。
- 使用
git diff --stat --cached在git commit前检查 staging area,若有超大文件会在这一步看到。
常见问答 (FAQ)
Q: 我只想保留最新版本的历史,能不能只清除旧版本的大对象?
A: 可以。git filter-repo --path path/to/file --invert-paths 会从所有历史中删除该文件。若需保留最新版本,先把文件复制出来,执行 filter-repo 后再重新提交最新版本。
Q: 用完 git-filter-repo 后,commit SHA 全变了,CI 里引用的 SHA 怎么办? A: 历史重写后所有 SHA 都会变化,这是不可避免的代价。需要更新 CI 配置、issue 里的 SHA 引用、release tag 等。如果使用 GitHub Releases,重新打 tag 并更新 release。
Q: 协作者已经基于旧历史做了新工作,怎么迁移?
A: 让协作者执行:git fetch origin && git rebase --onto origin/main <旧历史中对应的公共祖先> HEAD,把他们的新 commit 移植到新历史上。或者更简单地让他们备份改动、重新 clone,再手动 cherry-pick 自己的 commit。
Q: GitHub 提示文件超限但我已经删除了,为什么还报错? A: 因为 Git 记录的是每次 commit 的快照,删除文件只是新增了一个「删除」的 commit,旧 commit 里的大对象依然存在于历史中,push 时会把整个历史都传上去。必须用 filter-repo 重写历史才能彻底移除。
相关阅读
- Git LFS pointer 文件没被真实文件替换
- Monorepo partial clone 数据过期
- 找回 Git 历史里的旧版本文件
- Force push 覆盖了队友的 commit
- AI 把 secret 推到公开仓库了
- CRLF 转换让单次 commit 产生几千行 diff
标签: #git #version-control #排查