你在某个 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 MiB | Git 给出警告,但 push 仍会成功 | GitHub Docs |
> 100 MiB | push 被直接拒绝,报 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、*.exe、dist/*.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:怎么确认修好了
- push 完成,不再报
GH001/ 「exceeds … file size limit」。 - 重新跑 Step 2 的扫描——出问题的路径已消失,排第一的 blob 现在小于 100 MiB。
git count-objects -vH显示size-pack变小了。- 全新 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 -vH;size-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 100M 或 bfg --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 LFS pointer 文件没被真实文件替换
- 我把 secret 推到公开仓库了
- Force push 覆盖了队友的 commit
- Monorepo partial clone 数据过期
- clone 之后 Git hooks 不运行
标签: #git #version-control #排查