GitHub push 失败:如何扫描并清理 Git 历史中的大文件

GitHub push 失败:如何扫描并清理 Git 历史中的大文件

文章目录

  • [GitHub push 失败:如何扫描并清理 Git 历史中的大文件](#GitHub push 失败:如何扫描并清理 Git 历史中的大文件)
    • [1. GitHub 为什么会拒绝大文件](#1. GitHub 为什么会拒绝大文件)
    • [2. 先判断:大文件是在当前目录,还是已经进入 Git 历史](#2. 先判断:大文件是在当前目录,还是已经进入 Git 历史)
      • [2.1 扫描当前项目中的 100 MiB 以上文件](#2.1 扫描当前项目中的 100 MiB 以上文件)
      • [2.2 扫描整个 Git 历史中的 100 MiB 以上 blob](#2.2 扫描整个 Git 历史中的 100 MiB 以上 blob)
    • [3. 先决定大文件到底应该怎么管理](#3. 先决定大文件到底应该怎么管理)
    • [4. 批量清理所有不需要的 100 MiB 以上历史文件](#4. 批量清理所有不需要的 100 MiB 以上历史文件)
      • [4.1 先确认仓库状态](#4.1 先确认仓库状态)
      • [4.2 备份当前仍存在的大文件](#4.2 备份当前仍存在的大文件)
      • [4.3 安装 `git-filter-repo`](#4.3 安装 git-filter-repo)
      • [4.4 从整个 Git 历史删除超过 100 MiB 的 blob](#4.4 从整个 Git 历史删除超过 100 MiB 的 blob)
    • [5. 恢复本地文件,并用一个新 commit 防止再次提交](#5. 恢复本地文件,并用一个新 commit 防止再次提交)
    • [6. 清理后再次扫描,而不是直接 push](#6. 清理后再次扫描,而不是直接 push)
    • [7. 最后如何重新推送 GitHub](#7. 最后如何重新推送 GitHub)
    • [8. 如果大文件必须保留:使用 Git LFS,而不是删除](#8. 如果大文件必须保留:使用 Git LFS,而不是删除)
    • [9. 一套适合首次推送失败场景的处理顺序](#9. 一套适合首次推送失败场景的处理顺序)
    • [10. 防止下一次再踩同样的问题](#10. 防止下一次再踩同样的问题)
    • 结语
    • 参考资料

摘要:从 GitHub 100 MiB 大文件拒绝问题出发,说明为何删除文件后仍无法推送,并给出 PowerShell 扫描、历史清理、Git LFS 与防复发方案。

在把一个已有 Git 项目首次推送到 GitHub 时,可能会遇到如下错误:

text 复制代码
remote: error: File vendor/example/.git.zip is 151.91 MB;
remote: error: this exceeds GitHub's file size limit of 100.00 MB
remote: error: GH001: Large files detected.
! [remote rejected] master -> master (pre-receive hook declined)

这里最容易产生的误解是:既然问题文件已经不需要了,那么再新增一个 commit 把它删除,然后重新 git push 不就可以了吗?

答案是否定的。

GitHub 检查的不只是当前工作区最终长什么样,还会接收并检查这次 push 所需要传输的 Git 对象。只要超大文件仍存在于待上传的历史提交中,即使后面的 commit 已经把文件删除,那个大文件对应的 Git blob 仍然属于历史的一部分,push 依然可能被拒绝。

这类问题真正需要处理的是"Git 历史中的大对象",而不仅仅是"当前目录里的大文件"。

1. GitHub 为什么会拒绝大文件

截至本文整理时,GitHub 官方文档对普通 Git 仓库的文件大小约束为:

  • 通过浏览器上传时,单文件不能超过 25 MiB;
  • 普通 Git 操作添加或更新超过 50 MiB 的文件时会收到警告;
  • 普通 Git 仓库会阻止超过 100 MiB 的文件;
  • 如果确实需要版本化管理更大的文件,应考虑 Git Large File Storage(Git LFS)。

这里需要区分"仓库当前版本"和"Git 历史"。Git 的 commit 并不是简单地保存一份文件列表,而是通过 tree、blob 等对象构成历史快照。一个 150 MiB 的压缩包一旦进入某个 commit,即使后续删除,它对应的 blob 仍可能继续保存在历史中。

例如:

text 复制代码
A ---- B ---- C
       |      |
       |      +-- 删除 big-file.zip
       |
       +--------- 提交 big-file.zip(150 MiB)

当前 HEAD 位于 C,工作目录中已经没有 big-file.zip

但是第一次向远端推送 A、B、C 时,B 所引用的 150 MiB blob 仍然必须被传输。GitHub 因此仍会拒绝这次 push。

所以:

powershell 复制代码
git rm --cached "path/to/big-file.zip"
git commit -m "chore: remove large file"

只能表示"从这个新 commit 开始不再跟踪该文件",不能删除旧 commit 中已经存在的 blob。

.gitignore 也是同样的道理。它只能阻止后续未跟踪文件被再次加入 Git,不会自动修改历史。

2. 先判断:大文件是在当前目录,还是已经进入 Git 历史

处理之前建议分别扫描两遍。

第一遍检查当前工作区,可以知道哪些超大文件现在还真实存在于项目目录中;第二遍检查 Git 对象历史,可以找出已经删除、移动或改名,但仍残留在历史中的大文件。

以下命令均按 Windows PowerShell 编写。

2.1 扫描当前项目中的 100 MiB 以上文件

在 Git 仓库根目录执行:

powershell 复制代码
$Limit = 100MB
$Root = (Get-Location).Path

$LargeFiles = Get-ChildItem -File -Recurse -Force |
    Where-Object {
        $_.FullName -notmatch '[\\/]\.git[\\/]' -and
        $_.Length -ge $Limit
    }

$LargeFiles |
    Sort-Object Length -Descending |
    ForEach-Object {
        $RelativePath = $_.FullName.Substring($Root.Length + 1)
        "{0:N2} MiB`t{1}" -f ($_.Length / 1MB), $RelativePath
    }

示例输出:

text 复制代码
151.91 MiB    vendor/example/.git.zip
126.35 MiB    output/archive.bin

这里显式排除了仓库自己的 .git 目录,否则 Git pack 文件本身也可能被扫描出来,造成干扰。

2.2 扫描整个 Git 历史中的 100 MiB 以上 blob

仅扫描工作目录是不够的。真正决定 GitHub 是否会因为旧提交而拒绝 push 的,是历史对象。

执行:

powershell 复制代码
$Limit = 100MB

git rev-list --objects --all |
    git cat-file --batch-check="%(objecttype) %(objectname) %(objectsize) %(rest)" |
    ForEach-Object {
        $Fields = $_ -split ' ', 4

        if (
            $Fields.Count -ge 4 -and
            $Fields[0] -eq "blob" -and
            [int64]$Fields[2] -ge $Limit
        ) {
            "{0:N2} MiB`t{1}" -f ([int64]$Fields[2] / 1MB), $Fields[3]
        }
    }

这组命令的逻辑是:

  1. git rev-list --objects --all 枚举所有可达 Git 对象及路径;
  2. git cat-file --batch-check 查询对象类型和实际大小;
  3. 只保留 blob
  4. 再筛选大小达到 100 MiB 的对象。

如果当前目录已经没有某个文件,但这里仍然能看到它,说明问题就在 Git 历史中。

3. 先决定大文件到底应该怎么管理

发现大文件后,不要马上批量删除。先判断它属于哪一种情况。

文件类型或用途 推荐处理
临时压缩包、构建产物、缓存、日志、备份 从 Git 历史删除,并加入 .gitignore
.git.zip、嵌套仓库备份等 Git 元数据 通常直接排除,不建议放进 Git LFS
必须随项目版本化的大型模型、资源、媒体或二进制文件 使用 Git LFS
发布给用户下载的固件包、安装包、大型归档 更适合 GitHub Release 或独立制品存储

尤其是类似:

text 复制代码
vendor/example/.git.zip

这种文件通常只是另一个 Git 仓库元数据的压缩备份。把它继续放进主仓库,无论普通 Git 还是 Git LFS,通常都没有必要,而且容易让仓库体积不断膨胀。

本文后面的批量清理方案适用于"这些大文件本来就不应该进入普通 Git 历史"的情况。

如果扫描结果中存在必须保留版本的大文件,应先跳到"Git LFS"一节,不要直接用 --strip-blobs-bigger-than 一刀切。

4. 批量清理所有不需要的 100 MiB 以上历史文件

历史重写属于破坏性操作。对于个人仓库、首次推送且 GitHub 已经整体拒绝的仓库,风险通常较低;对于已经多人协作、远端已有分支和 tag 的仓库,则必须先协调,因为历史重写会改变相关 commit 的 SHA。

4.1 先确认仓库状态

powershell 复制代码
git status
git branch --show-current
git remote -v

如果当前还有未提交修改,建议先处理干净再进行历史重写。

还可以额外创建一个仓库级备份:

powershell 复制代码
git bundle create "../repository-before-large-file-cleanup.bundle" --all

这个 bundle 位于当前仓库目录之外,可以在误操作时作为恢复来源。

4.2 备份当前仍存在的大文件

历史清理可能导致这些文件从当前工作树消失。如果目标是"文件继续留在电脑上,只是不进入 Git",可以先复制到仓库外的临时目录。

powershell 复制代码
$Limit = 100MB
$Root = (Get-Location).Path
$Backup = Join-Path $env:TEMP "git-large-files-backup"

$LargeFiles = Get-ChildItem -File -Recurse -Force |
    Where-Object {
        $_.FullName -notmatch '[\\/]\.git[\\/]' -and
        $_.Length -ge $Limit
    }

Remove-Item $Backup -Recurse -Force -ErrorAction SilentlyContinue
New-Item -ItemType Directory -Path $Backup -Force | Out-Null

$Paths = @()

foreach ($File in $LargeFiles) {
    $Path = $File.FullName.Substring($Root.Length + 1)
    $Paths += $Path

    $Destination = Join-Path $Backup $Path
    New-Item -ItemType Directory -Path (Split-Path $Destination) -Force | Out-Null
    Copy-Item -LiteralPath $File.FullName -Destination $Destination -Force
}

这里记录的是当前工作区中的大文件。历史中已经不存在于工作区的旧文件不需要恢复到本地。

4.3 安装 git-filter-repo

GitHub 官方文档在处理需要重写历史的数据时也使用 git-filter-repo。Windows + Python 环境下可以通过 pip 安装:

powershell 复制代码
python -m pip install git-filter-repo

安装后确认:

powershell 复制代码
git filter-repo --version

如果 PowerShell 找不到该命令,需要检查 Python 的 Scripts 目录是否已经加入 PATH

4.4 从整个 Git 历史删除超过 100 MiB 的 blob

确认扫描结果中的所有超大 blob 都可以删除后,执行:

powershell 复制代码
git filter-repo --strip-blobs-bigger-than 100M --force

这条命令的关键不是删除"当前文件",而是重写 Git 历史,把所有超过阈值的 blob 从可达历史中剔除。

因此它有两个重要影响:

  • 相关历史 commit 的 SHA 会变化;
  • 如果远端已经存在这些旧 commit,后续必须以历史重写的方式更新远端。

这也是为什么它不能被简单理解为"新增一个删除 commit"。

5. 恢复本地文件,并用一个新 commit 防止再次提交

历史清理完成以后,如果这些大文件仍需要留在电脑上,可以从临时目录恢复,并把对应路径加入 .gitignore

powershell 复制代码
if (!(Test-Path ".gitignore")) {
    New-Item -ItemType File -Path ".gitignore" | Out-Null
}

foreach ($Path in $Paths) {
    $Source = Join-Path $Backup $Path
    $Directory = Split-Path $Path

    if ($Directory) {
        New-Item -ItemType Directory -Path $Directory -Force | Out-Null
    }

    Copy-Item -LiteralPath $Source -Destination $Path -Force

    $IgnorePath = "/" + $Path.Replace("\", "/")

    if ((Get-Content ".gitignore") -notcontains $IgnorePath) {
        Add-Content -Path ".gitignore" -Value $IgnorePath
    }
}

然后只提交 .gitignore

powershell 复制代码
git add -- .gitignore
git commit -m "chore: exclude oversized files"

从使用者视角看,这样可以把"以后不要再提交这些本地大文件"的策略集中放在一个新的 commit 中。

但需要准确理解:

.gitignore 可以集中成一个新 commit;历史中的大 blob 则是通过 git filter-repo 被移除的,历史重写会改变旧 commit 的对象关系和 SHA。

不存在一种"完全不改旧历史、只新增一个 commit,同时又让 GitHub 看不到旧大文件"的普通 Git 操作。

6. 清理后再次扫描,而不是直接 push

先重新执行历史扫描:

powershell 复制代码
$Limit = 100MB

git rev-list --objects --all |
    git cat-file --batch-check="%(objecttype) %(objectname) %(objectsize) %(rest)" |
    ForEach-Object {
        $Fields = $_ -split ' ', 4

        if (
            $Fields.Count -ge 4 -and
            $Fields[0] -eq "blob" -and
            [int64]$Fields[2] -ge $Limit
        ) {
            "{0:N2} MiB`t{1}" -f ([int64]$Fields[2] / 1MB), $Fields[3]
        }
    }

如果没有任何输出,说明当前所有可达 Git 历史中已经没有达到 100 MiB 的普通 blob。

再检查本地文件是否确实被忽略:

powershell 复制代码
git status --ignored

以及确认最终提交:

powershell 复制代码
git log --oneline -5

7. 最后如何重新推送 GitHub

先检查远端:

powershell 复制代码
git remote -v

历史清理工具可能会调整远端配置,因此不要直接假设原来的远端名称还存在。

如果这是第一次向 GitHub 推送,而且之前的 push 已经整体被拒绝、远端目标分支实际上还没有建立,那么重新添加或确认远端后,通常直接正常 push 即可:

powershell 复制代码
git push -u origin master

如果你的远端名称不是 origin,例如叫 github

powershell 复制代码
git push -u github master

如果远端已经存在旧历史,并且你明确要用清理后的本地历史替换它,应先与其他协作者确认,再使用:

powershell 复制代码
git push -u origin master --force-with-lease

相比无条件的 --force--force-with-lease 会额外检查远端引用是否仍符合本地预期,可以降低覆盖他人新提交的风险。

多人共享仓库中,历史重写之后其他开发者已有的 clone 也需要同步处理,不能把它当成一次普通的 fast-forward push。

8. 如果大文件必须保留:使用 Git LFS,而不是删除

Git LFS 的机制不是把超大二进制直接作为普通 Git blob 存入仓库,而是在 Git 历史里保存较小的 pointer,由 LFS 存储实际对象。

对于以后新增的大文件,可以先配置跟踪规则:

powershell 复制代码
git lfs install
git lfs track "*.bin"
git lfs track "*.zip"

git add -- .gitattributes
git commit -m "chore: track large binaries with Git LFS"

之后再正常 git addgit commit

但有一个非常重要的限制:

powershell 复制代码
git lfs track "*.zip"

不会把旧 commit 中已经存在的普通 Git blob 自动转换成 LFS 对象。

如果大文件已经进入历史,需要使用 git lfs migrate。例如先查看整个历史的情况:

powershell 复制代码
git lfs migrate info --everything

再针对确实应该进入 LFS 的类型执行迁移,例如:

powershell 复制代码
git lfs migrate import --everything --include="*.bin,*.zip"

Git LFS 官方文档明确说明,这类历史迁移同样会重写历史并改变 Git 对象 ID,所以如果远端已有对应历史,仍然需要协调并进行强制更新。

因此,Git LFS 解决的是"这个大文件应该继续版本化,但不适合普通 Git blob"的问题;git filter-repo 删除历史解决的是"这个大文件根本不应该进入仓库"的问题。两者不能混为一谈。

9. 一套适合首次推送失败场景的处理顺序

对于下面这种典型情况:

  • 本地已有较长 Git 历史;
  • 第一次推送 GitHub;
  • GitHub 因历史中存在 100 MiB 以上文件而整体拒绝;
  • 大文件属于备份、缓存、构建产物或嵌套 .git 压缩包;
  • 希望文件继续留在本机,但不进入远端仓库;

推荐流程可以归纳为:

text 复制代码
扫描当前工作区大文件
        ↓
扫描完整 Git 历史中的大 blob
        ↓
人工确认哪些文件不应进入 Git
        ↓
仓库外备份当前仍存在的大文件
        ↓
git filter-repo 重写历史并删除大 blob
        ↓
恢复本地文件
        ↓
.gitignore 排除这些文件
        ↓
新增一个防复发 commit
        ↓
重新扫描 Git 历史
        ↓
确认无超限对象后再 push

这比反复执行"删除文件 → 新增 commit → 再 push"更可靠,因为它直接处理了 GitHub 真正拒绝的对象:历史中的超大 blob。

10. 防止下一次再踩同样的问题

对于源码仓库,最有效的防复发措施不是等 GitHub 拒绝后再清理,而是在提交之前区分"源码"和"制品"。

通常应该优先排除:

gitignore 复制代码
# Build outputs
/build/
/out/
/dist/

# Temporary archives
*.tmp
*.bak

# Nested Git metadata backups
**/.git.zip

实际项目不能机械复制这些规则,应根据构建目录和交付方式调整。例如某些项目确实需要版本化少量 ZIP 测试资源,就不能直接使用全局 *.zip 忽略规则。

提交前还可以快速检查暂存区:

powershell 复制代码
git diff --cached --name-only

如果项目经常产生大型固件、模型、日志或压缩制品,则应该在仓库层面提前约定:

  • 哪些文件属于源码;
  • 哪些文件用 Git LFS;
  • 哪些文件发布到 GitHub Release;
  • 哪些文件只存放于制品服务器或对象存储;
  • 哪些目录必须进入 .gitignore

这样比事后清洗 Git 历史的成本低得多。

结语

GitHub 的大文件报错表面上是一个"文件超过 100 MiB"的问题,本质上却经常是对 Git 历史模型理解不完整导致的:当前目录中看不到文件,并不意味着 Git 历史中不存在它;新增一个删除 commit,也不等于删除旧 blob。

处理这类问题时,最值得记住的判断顺序只有三步:

  1. 先扫描当前工作区,再扫描完整 Git 历史;
  2. 判断大文件应该"删除历史"还是"迁移到 Git LFS";
  3. 只在确认历史中没有不允许的超大 blob 后再推送。

如果大文件本来就不应该进入仓库,git filter-repo + .gitignore 是直接的处理路线;如果大文件本来就需要版本化,则应该设计 Git LFS 或独立制品管理方案,而不是单纯删除。

参考资料

相关推荐
mqiqe42 分钟前
AgentScope Java 2.0 技能仓库(Skill Repository)完全实战指南
java·开发语言·elasticsearch
Da Zeng2 小时前
快速搭建GitHub Page
github
明月_清风2 小时前
GitHub Actions 从入门到实战:一文搞懂 CI/CD 自动化
后端·ci/cd·github
QUOR2 小时前
Zorv AI 终端:基于 proot 沙箱的 Android 终端模拟器架构全解析
github
其实防守也摸鱼2 小时前
ZLibrary 类项目合规避坑指南:从技术实现到法律风险的全景梳理
运维·服务器·数据库·安全·自动化·github·copilot
大明二代3 小时前
使用 Traefik、cert-manager 和 DNS-01 为内网 Kubernetes 服务配置 HTTPS
git·kubernetes·flux
wangchunyu1143 小时前
Elasticsearch 入门与实战:Spring Boot 3 + ES 8 从零搭建商品搜索服务
大数据·数据库·spring boot·elasticsearch
隔窗听雨眠3 小时前
ARM架构下Logstash与Elasticsearch集群部署完全指南:从环境适配到生产验证
arm开发·elasticsearch·架构
小芒果_014 小时前
git常用命令速查
大数据·git·elasticsearch