历史里的大文件让 push 失败

某次 commit 误提交了大文件,之后每次 push 都被 GitHub 或 GitLab 拒绝,提示文件超过 100 MB 限制。本文给出用 git-filter-repo 彻底清除历史大文件的完整修复方案。

你在某个 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.wardist/、大型 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 --cachedgit 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 #version-control #排查