历史里的大文件让 git push 失败

历史中有超过 100 MiB 的文件,GitHub 拒绝 push。用 git-filter-repo 把它从每个 commit 里彻底清除,再 force push 干净推上去。

你在某个 commit 里提交了一个 250 MB 的数据文件,发现后已经删掉、又提交了一次删除,然后跑 git push origin main,结果撞上:

remote: error: File dataset.csv is 250.00 MB; this exceeds GitHub's file size limit of 100.00 MB
remote: error: GH001: Large files detected. You may want to try Git Large File Storage - https://git-lfs.github.com.

最快的修复思路:在后续 commit 里删掉文件,并不会把它从历史中移除——那个 blob 对象仍然被旧 commit 引用,所以每次 push(以及每次 clone)都会把它一起带上。你必须用 git-filter-repo 把这个文件从每一个 commit 中外科手术式地切掉,这会重写所有 commit 的 SHA,然后再 force push。因为重写历史是破坏性操作、会改变每一个 SHA,动手前请先和持有克隆的同事打好招呼。

各项限制的准确数字(截至 2026 年 6 月)

GitHub 用的是 mebibyte(MiB),不是十进制的 MB,而且有三道不同的阈值。判断你踩中的是哪一道,就知道该怎么处理。

阈值会发生什么来源
> 50 MiBGit 给出警告,但 push 仍会成功GitHub Docs
> 100 MiBpush 被直接拒绝,报 GH001(本文这个错误)GitHub Docs
网页「Add file」上传 > 25 MiB浏览器上传被拒绝(这是一道独立且更严的限制)GitHub Docs
仓库总体积 > ~5 GB软限制;GitHub 可能发邮件提醒你瘦身Repository limits

如果你只是需要让今后超限的文件有地方放,Git LFS(Large File Storage)或 GitHub Releases 才是正确归宿——但 LFS 并不会回头清理已经写进历史的 blob,那仍然得靠下面的历史重写。

GitLab、Bitbucket、Azure DevOps 的单文件上限大同小异;重写流程完全相同,只是报错措辞不一样。

常见原因

按命中率从高到低排列。

1. 直接提交了大数据集或模型权重

数据和 ML 团队经常图「省事」把 CSV、HDF5、Parquet,或 .onnx / .safetensors 权重文件直接提交进仓库,没意识到 100 MiB 上限和它对 clone 体积的拖累。

怎么判断:跑下面 Step 2 的 blob 体积扫描;罪魁通常就是排在最上面的某个 .csv.h5.parquet 或权重文件。

2. 误提交了编译产物

一个超过 100 MiB 的编译二进制(*.jar*.war*.exedist/*.js.gz)混进了某次「完整发布」的 commit,于是 CI 在每次全新 clone 时都失败。

怎么判断.gitignore 里缺了对应扩展名。列出工作区里被追踪的大文件:

git ls-files | xargs -I{} du -h {} 2>/dev/null | sort -rh | head -10

3. 提交了数据库 dump 或日志文件

为了一次迁移「临时」提交了 pg_dump production.sql(2 GB),下一次 commit 前忘了清掉。

怎么判断:扩展名是 .sql.log.dump.bak。查看某个已知路径的 blob 体积:

git cat-file -s HEAD:production.sql   # 打印这个 blob 的字节数

4. LFS 迁移开了头却没做完

有人执行了 git lfs track "*.psd" 并提交了 .gitattributes,但历史中已存在.psd blob 从未被转换成 LFS 指针。

怎么判断git lfs ls-files 什么都没列出来,可 .gitattributes 里却有 LFS 规则。大 blob 还是普通 Git 对象。确认还有哪些没迁移:

git lfs migrate info --everything --above=50MB

5. 把子模块或外部仓库整包提交,而不是引用

有人把一个依赖仓库拷进项目目录后整个提交了进来,有时连它 .git 目录里的对象都作为散落文件一起进来了。

怎么判断:Step 2 的扫描会列出某一个目录(如 vendor/third_party/)下一大堆大 blob。

最短修复路径

Step 1:安装 git-filter-repo(优于 filter-branch 和 BFG)

git filter-repo 是 Git 项目官方推荐的工具,比老的 git filter-branch 快,处理边角情况也比 BFG 周全。

pip install git-filter-repo
# macOS:        brew install git-filter-repo
# Debian/Ubuntu:apt install git-filter-repo
git filter-repo --version   # 确认已在 PATH 上

Step 2:找出历史中所有的大 blob

git rev-list --objects --all \
  | git cat-file --batch-check='%(objecttype) %(objectname) %(objectsize) %(rest)' \
  | awk '/^blob/ { print $3, $4 }' \
  | sort -rn \
  | head -20

体积单位是字节,134217728 就是 128 MiB。把所有超过 50 MB 的路径记下来,待会儿要传给 filter-repo。

Step 3:重写前先备份整个仓库

这次重写不可逆,先留一份可以回退的副本。

cd ..
cp -r myrepo myrepo-backup
cd myrepo
git tag backup/before-filter-repo   # 仓库内部再加一道保险

Step 4:从所有历史中移除这些文件

移除单个路径:

git filter-repo --path dataset.csv --invert-paths --force

一次移除多个路径:

git filter-repo \
  --path dataset.csv \
  --path models/weights.onnx \
  --path dumps/production.sql \
  --invert-paths --force

或者不管文件名、按体积一刀切(很多文件都踩线时很省事):

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

这里必须加 --force,因为你是在一个已有的检出(而非全新 clone)上运行;这也正是 Step 3 备份重要的原因。跑完后 git count-objects -vH 应该能看到仓库体积变小。

Step 5:把文件加进 .gitignore,防止它卷土重来

echo "dataset.csv" >> .gitignore
echo "*.onnx" >> .gitignore
git add .gitignore
git commit -m "chore: ignore large files that must not be committed"

Step 6:重新加上 remote 并 force push

git filter-repo故意删除 origin remote,以防你在准备好之前不小心又把改写过的历史推回共享仓库。重新加上它:

git remote add origin <remote-url>

然后 push。重写后的第一次 push 没有任何 remote-tracking ref 可供 --force-with-lease 比对,lease 检查无的放矢,因此真正能用的是普通的 --force

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

后续再次重写时,remote-tracking ref 已经存在,就改用 --force-with-lease,避免覆盖同事的新提交。)

如果某个受保护分支拒绝 force push,临时到 GitHub 的 Settings -> Branches 里关掉该分支的「Allow force pushes」/ 分支保护,推完再开回去。

Step 7:怎么确认修好了

  1. push 完成,不再报 GH001 / 「exceeds … file size limit」。
  2. 重新跑 Step 2 的扫描——出问题的路径已消失,排第一的 blob 现在小于 100 MiB。
  3. git count-objects -vH 显示 size-pack 变小了。
  4. 全新 clone 验证git clone --depth 1 <remote-url> /tmp/verify-clone 很快成功,且那个文件已不存在。

然后通知每一位同事:他们的克隆里还是旧 SHA,一旦 push 就会把大文件再带回来。干净的做法是重新 clone。若他们有本地尚未推送的工作,应改用:

git fetch --all
git rebase --onto origin/main ORIG_HEAD main

预防建议

  • 配一个 pre-commit hook,在大文件进入历史之前就拦下来:
# .git/hooks/pre-commit  (chmod +x)
LIMIT=52428800  # 50 MiB
git diff --cached --name-only --diff-filter=AM | while read f; do
  size=$(git cat-file -s ":$f" 2>/dev/null || echo 0)
  if [ "$size" -gt "$LIMIT" ]; then
    echo "ERROR: $f is $((size/1048576)) MiB — exceeds the 50 MiB limit."
    exit 1
  fi
done

如果团队用 pre-commit 框架,直接启用它内置的 check-added-large-files 钩子(默认上限 500 KB,可用 --maxkb 调整)。hook 是每个 clone 各自一份,所以要把统一的安装步骤提交进仓库——参见 clone 之后 Git hooks 不运行

  • 超过约 10 MB 的二进制资产改用 Git LFS:git lfs track "*.psd" "*.onnx" "*.zip"
  • 数据集存到对象存储(S3、GCS、Azure Blob),仓库里只放 URL 或内容哈希作引用。
  • 建仓时就提交一份 .gitignore,覆盖常见大文件扩展名和构建产物(dist/build/node_modules/*.log*.sql)。
  • 定期跑 git count-objects -vHsize-pack 突然变大就是误提交的早期信号。
  • 对 monorepo,在 CI 里设单 blob 体积预算,任何提交对象超阈值就让构建失败。

常见问答 (FAQ)

Q:我明明已经删掉文件、也提交了删除,为什么 GitHub 还是拒绝 push? A:Git 存的是快照而不是 diff。删除文件只是新增了一个「该文件不存在」的 commit,但旧 commit 仍然引用着那个 blob。一次 push 会把整段历史连同大 blob 一起传上去。只有重写历史(Step 4–6)才能真正移除它。

Q:能不能改用 BFG Repo-Cleaner? A:可以,简单场景下它很快:bfg --strip-blobs-bigger-than 100Mbfg --delete-files dataset.csv,跑完接 git reflog expire --expire=now --all && git gc --prune=now --aggressive。但 git filter-repo 是官方推荐、仍在积极维护、边角情况处理更全,所以优先用它。

Q:filter-repo 之后 push 被拒,或者 --force-with-lease 没反应,怎么办? A:filter-repo 删掉了 origin,先重新加上(git remote add origin <url>)。重写后的首次 push 没有 remote-tracking ref 供 lease 检查,所以改用普通的 git push --force --all,而不是 --force-with-lease

Q:同事在我重写历史之前已经 clone 了,他们该怎么办? A:重新 clone 最稳。如果他们有未推送的本地 commit,可以跑 git fetch --all,再 git rebase --onto origin/main ORIG_HEAD main 把工作移植到重写后的历史上。千万不要 git pull 后合并——那会把旧的大 blob 又拽回来。

Q:那个文件几个月前只加过一次、早就删了,真的要重写全部历史吗? A:是的。即便它在第 50 个 commit 里被「删除」,blob 仍被第 5 个 commit 引用,所以移除它要重写第 5 个及其之后的所有 commit。这也是为什么所有 SHA 都会变。

Q:用 git config http.postBuffer 把限制调大行不行? A:不行。http.postBuffer 只影响 Git 在 HTTP 上推送大数据时的缓冲方式,对服务端「单文件 100 MiB」的规则毫无作用。调它只在推送合法的大文件(已用 LFS 追踪)时才有意义,绝不能用来绕过本就不该提交的文件。

相关阅读

标签: #git #version-control #排查