
Git 推送失败不是新鲜事但文件超过 100MB 限制这个报错几乎是每个团队迟早会撞上一次的。我遇到最典型的场景是本地编译出一个 release 安装包顺手git add、git commit、git push然后屏幕被一段红色 error 打断——远程仓库直接拒绝接收点名的就是某个具体文件。这个报错看起来只是文件太大四个字但真正的问题往往藏在后面这个文件到底该不该进仓库如果该怎么用 Git LFS 合法托管如果不该怎么把已经混入历史的大文件彻底清扫干净这篇文章把整条处理链路完整梳理一遍从报错解读到 BFG 清理历史再到 Git LFS 的实操接入都会给出可以直接照做的命令和步骤。1. 报错背后的限制逻辑先搞清楚这100MB卡在哪1.1 一条典型的报错长什么样假设你往远程仓库推送代码进度条走到一半远程返回这样一段话remote: error: File build/libs/sim-release.zip is 152.7 MB; this exceeds the file size limit of 100.00 MB remote: status: exit 1看到这段就基本明确了单文件超限推送被远程端拒收。注意报错点名的是sim-release.zip这个具体文件而不是整个提交包说明问题不是一次推了太多而是这个文件本身超过了平台的硬性上限。有些场景下报错会不一样。比如只显示error: RPC failed; HTTP 413 curl 22 The requested URL returned error: 413或者remote: fatal: pack exceeds maximum allowed size这两类报错和单文件 100MB 不是一回事。413 往往代表单次推送的总包体超过了服务器的 HTTP 接收限制可能由大量小文件或一个超大的 pack 引起而 pack exceeds maximum allowed size 则更倾向于仓库整体体积已经大到平台阈值附近。把报错读准是第一步因为单文件超限和单次推送总量超限的解法完全不同。1.2 单文件限制和单次推送体积限制是两回事远程托管平台通常做两层限制限制类型常见阈值报错特征单个文件大小上限100MB部分平台 50MB 或 25MB报错明确点名某个文件给出文件大小单次 push 包体上限取决于平台 HTTP 配置通常是数百 MB 到 2GB报错表现为 413、RPC failed 等平时我们撞上的是第一层解法很直接要么让这个文件永远不进仓库要么交给 Git LFS 管理。但如果是第二层说明仓库本身已经进入亚健康状态可能需要做历史瘦身那比单文件超限麻烦得多。1.3 为什么平台要设这个限制站在托管平台的角度想这个限制不是纯属刁难。仓库里的每个对象都会在每次clone、fetch时被完整拉取一次。一个 100MB 的二进制文件一旦进入 Git 历史全队每个人每次克隆都要下载它而且永远是同一份不会再变的数据——这对存储、带宽、校验都是巨大的浪费。平台设限制的本质是逼你在提交前想清楚这个文件真的需要长期躺在版本历史里吗我自己的体会是这个限制更像一个安全闸门它挡住的不只是一个大文件更是后面一连串仓库体积失控的隐患。2. 动手前的关键决策这个文件到底该不该进仓库2.1 三种典型场景的判断依据收到报错后的第一件事不是急着清历史而是判断这个文件的身份。我一般把场景分成三类场景一编译产物、打包产物、临时数据比如build/目录下的安装包、dist/下的压缩包、运行时生成的缓存文件。这类文件的共同特征是每次构建都会变、别人从源码能重新生成、本身不需要版本管理。它们就不该出现在 Git 仓库里写进.gitignore就好。场景二运行所必需但不需要历史版本比如下载好的模型权重、初始数据集、第三方提供的二进制依赖。这类文件项目运行要用但开发过程中基本不变也不需要每次提交都追踪它的版本变化。更合理的做法是放在外部对象存储或单独的资源服务器仓库里只保留下载脚本和版本号。场景三项目自身产物且需要版本管理比如游戏的美术资源、固件镜像、测试用的音视频素材、自己产出的数据集。这些文件会迭代、需要回溯历史版本、团队成员要协作修改走 Git LFS 是正路。2.2 不同选择的后续技术路径决策直接影响后面的操作方向。如果用一句话概括判定为不该进仓库 → 走第三章移除文件、清理历史、加入忽略规则判定为必须进仓库且要版本管理 → 走第四章引入 Git LFS正规托管大文件两种方案都需要重写历史或强推不同的是 LFS 保留了文件本身而清理方案是彻底让文件消失最忌讳的是不假思索地搜怎么绕过远程仓库限制然后找到一个git push --force的粗暴解法直接把大文件硬塞进远端。下面详细说说为什么不能这么干。2.3 先别急着绕过硬推的后患有一种操作是本地把报错压下去或者改掉平台限制强行把超限文件推到远端。就算真的推上去了后患也很明显仓库体积直接膨胀全队每次clone都要拉这份巨大的二进制慢到怀疑人生后续分支合并、服务器备份、CI 构建全都跟着变慢部分托管平台有后台扫描机制检测到历史中存在超限文件后会直接屏蔽该文件在网页端的下载甚至把整个仓库标记为不可用我见过有团队因为硬塞了几百 MB 的资源文件进仓库最后平台侧直接拒绝继续扩容折腾了一周才把历史清理干净。限制本身不是用来绕的而是用来理解并遵守的。3. 如果它不该进仓库移除文件并清理Git历史3.1 文件尚在暂存区或最近提交快速撤离如果大文件只是被git add了但还没 commit处理最简单git rm --cached path/to/large.zip echo path/to/large.zip .gitignore git add .gitignore git commit -m chore: 移除误提交的大文件并加入忽略规则 git push注意git rm --cached只会把文件从暂存区移除本地工作区的文件还在不会被删除。这一步的目的是撤回即将被提交的内容同时用.gitignore防止它下次再被误加入。如果已经 commit 了但还没 push 成功处理方式也类似只是多了一个删除提交git rm --cached path/to/large.zip echo path/to/large.zip .gitignore git add .gitignore git commit -m chore: 移除误提交的大文件 git push这种情况下本地历史里那个大文件对象仍然存在但因为远端从来没收过它所以不影响这次推送。本地历史里的残留如果你看着别扭可以在以后仓库相对空闲时再做历史清理也可以放着不管——它不影响别人只占你自己本地.git的空间。3.2 已混入历史提交用BFG快速清扫真正麻烦的场景是这个大文件已经成功推送到远端并且在历史提交里扎根了。单纯在最新提交里删掉它不够因为历史提交里还躺着那份大对象别人clone时依旧要拉取整个历史。这时候必须重写历史让这个对象从所有提交中消失。官方自带的方式是git filter-branch功能上能做到但速度很慢而且命令繁琐git filter-branch --force --index-filter \ git rm --cached --ignore-unmatch path/to/large.zip \ --prune-empty --tag-name-filter cat -- --all这个命令会逐条重放所有提交遇到指定文件就从索引里删掉。仓库如果有上万条提交跑上几个小时都不奇怪。相比而言社区工具 BFG Repo-Cleaner 要快得多它直接针对.git对象库操作不需要一条条重放提交历史。推荐步骤如下# 1. 先备份整个仓库务必做这一步 cp -r my-repo my-repo-backup # 2. 删除工作区中的大文件并提交 git rm --cached path/to/large.zip echo path/to/large.zip .gitignore git commit -m chore: 移除大文件 # 3. 使用 BFG 清理历史二选一 # 按文件名删除 java -jar bfg.jar --delete-files large.zip .git # 或者按大小删除清理所有超过 100MB 的 blob java -jar bfg.jar --strip-blobs-bigger-than 100M .gitBFG 的--strip-blobs-bigger-than是更省心的选项按大小扫不用精确到文件名。运行完 BFG 后工作区文件不会自动变化需要回到当前分支看下状态确认没异常。3.3 清理后的收尾reflog、gc与强制推送BFG 删掉对象后如果不做收尾那些被删掉的对象其实还留在.git对象库里因为 reflog 默认会保留对它们的引用。必须执行这三条命令git reflog expire --expirenow --all git gc --prunenow --aggressive git push --forcereflog expire让所有 reflog 条目立即过期gc --prunenow立刻回收无引用的对象aggressive重新压缩打包。这三步不跑历史瘦身等于白做。强推之前务必确认当前分支。我有一个很痛的教训当时没注意当前分支把git push --force打在了一个不该推的分支上结果全队花了一下午对齐分支状态。强推前先看git branch再想清楚影响范围。如果遇到受保护分支可能还得先临时取消保护推完再恢复。另外历史重写之后远程分支结构已经和本地不一致。协作者那端最干净的方案是重新 clone 仓库如果不想重新 clone至少要让他们git fetch origin git checkout 自己的分支 git reset --hard origin/自己的分支reset --hard会丢弃本地未提交改动操作前一定要先stash或备份。4. 如果它必须进仓库用Git LFS正确托管大文件4.1 LFS的工作原理指针文件替你占座如果判断结果是这个大文件属于项目自身产物必须要版本管理那正确的出路是 Git LFS。它的核心机制很简单Git 仓库里不再保存那 152.7MB 的二进制本体而是保存一个只有几 KB 的指针文件真正的二进制对象放在 LFS 存储服务器上。指针文件内容长这样version https://git-lfs.github.com/spec/v1 oid sha256:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx size 152700000普通 Git 只认这个指针文件不关心二进制本体在哪。当你checkout或pull时LFS 客户端会读取指针里的oid自动去 LFS 服务器拉回真正的文件。整个流程对开发者几乎是透明的你看到、用到的还是那个 152.7MB 的文件。为什么 LFS 能绕开 100MB 限制因为平台限制的是仓库内的单个对象大小而 LFS 把大对象移出了 Git 仓库本体仓库里只剩一个指针当然不会触顶。4.2 从0开始接入LFS的完整步骤先确认本机是否安装 LFSgit lfs --version没有安装的话Windows 下官方提供安装包macOS 上可以直接brew install git-lfsLinux 用发行版包管理器安装。然后初始化git lfs install指定要跟踪的文件类型git lfs track *.zip *.tar.gz这条命令会创建或修改.gitattributes文件。务必检查一下内容cat .gitattributes如果.gitattributes是新增的提交它git add .gitattributes git commit -m chore: 添加 Git LFS 跟踪规则接下来正常添加文件、正常提交、正常推送git add build/libs/sim-release.zip git commit -m feat: 加入 release 包 git push一个非常常见的误区是git lfs track之后已经用普通 Git 提交过、并且 push 失败过的文件不会自动变成 LFS 管理。它已经在本地对象库和索引历史里作为普通 blob 存在了光有跟踪规则不够需要做历史迁移。4.3 历史大文件迁移lfs migrate import如果你的大文件已经在之前的历史提交中想要完整迁移成 LFS 对象用这个命令git lfs migrate import --everything --include*.zip,*.tar.gz这个命令会重写整个提交历史把符合 pattern 的文件从普通 blob 改成 LFS 指针。旧的大对象变成可回收对象之后同样需要强推才能同步到远端。执行前注意几点在干净仓库上做迁移。有大量未提交改动的工作区迁移容易出意外先备份仓库。历史重写一旦中断恢复成本极高迁移完跑一遍git reflog expire --expirenow --all git gc --prunenow --aggressive和第三章的收尾逻辑一样如果只想迁移最新提交、不想动历史表面上能省事但旧对象还留在历史中仓库体积并没有瘦身。治标不治本反正迟早要还的。4.4 关于LFS配额和团队协作的提醒LFS 不是无限容量的免费仓库托管平台通常只给一个基础的存储空间和流量配额。几个几百 MB 的模型文件配合团队反复拉取、构建可能一两周就把免费配额用完。超出部分要么付费扩容要么直接卡住推送。团队接入 LFS 后最好在项目文档里固定几条约定明确哪些文件类型走 LFS统一维护.gitattributes新成员 clone 之后确认 LFS 拉取是否正常删除大分支或清理仓库时注意 LFS 对象不会因为删分支自动释放还有个实用技巧如果项目里有超大数据集git clone默认会把 LFS 文件一起拉下来。新成员只想看代码、不急着要资源文件可以这样GIT_LFS_SKIP_SMUDGE1 git clone 仓库地址需要资源文件时再手动执行git lfs pull。对超大仓库的体验提升非常明显。5. 推送之后的验证与防复发措施5.1 推送成功后的三件事第一验证本地 LFS 管理列表git lfs ls-files看到输出中列出了刚才提交的大文件说明 LFS 正在接管。再到远端仓库页面确认文件是否标记为 LFS 类型。第二重新 clone 一份验证git clone 仓库地址 repo-bak确认repo-bak里大文件能正常 checkout。这一步能排除本地依赖 LFS 缓存但远端没存好的情况很多问题恰恰出在本地缓存正常、远端对象缺失。第三把.gitattributes的规则同步到全队。文件本身会随提交传到远端但要提醒团队成员别手动绕过它——有人为了方便会直接git add -f强加一个 LFS 类型文件这会破坏整个约定。5.2 用pre-commit hook拦住下一次误提交处理完一次报错最怕的是同样的问题过几天再来一遍。装一个pre-commithook 是最直接的防复发手段。在.git/hooks/pre-commit写入#!/bin/sh threshold$((50 * 1024 * 1024)) # 50MB files$(git diff --cached --name-only --diff-filterACM) for f in $files; do size$(stat -c%s $f 2/dev/null || stat -f%z $f 2/dev/null) if [ -n $size ] [ $size -gt $threshold ]; then echo 拦截$f 大小为 $size 字节超过 ${threshold} 字节限制 exit 1 fi done exit 0注意代码里两个stat写法分别兼容 Linux 和 macOS用||连接是因为不同系统的stat参数不兼容。然后chmod x .git/hooks/pre-commit之后只要暂存区里有超过阈值的文件git commit会被直接拦下来。这个脚本我建议团队里人手一份或者用 Node 项目的 husky 工具把它纳入版本管理让全队自动生效。5.3 协作者分支处理与force推送的姿势凡是经过filter-branch、BFG、lfs migrate这几类历史重写操作远端和本地已经不再是一条线。协作者最省心的处理方式是重新 clone。如果不想重新 clone我也给出了fetch reset --hard的指令但务必记住reset --hard会丢本地未提交改动操作前stash是底线。最后强调一个细节强推时优先用git push --force-with-lease它会先检查远端在你上次fetch之后是否被别人更新过如果远端有变动就直接拒绝推送。这个保险栓比裸--force安全得多历史清理类操作通常在自己的专属分支上进行force-with-lease完全够用。我在实际项目里最深的体会是这类文件超限问题八成的根源不在远程仓库限制太死而在提交前没有把大文件的分流方案定下来。把一个 100MB 的二进制文件放进 Git 历史就像把一箱砖头塞进随身行李箱——能塞进去但每次出行都要多付一份超额行李费。把.gitignore、Git LFS、提交前检查这三件事做成项目的默认姿势这个报错基本就和你告别了。