Skip to content

背景

最近被一个 3GB 的 Git 仓库折腾了一通,顺手把过程中学到的"巨型仓库生存策略"系统整理一下:

  • git clone 拉到一半超时,重试 3 次都没成功
  • 改成 --depth=1 倒是快了,但其他分支全没了,连 main 之外的所有远程分支都拉不下来
  • --depth=100 也不顶用——3GB 里大头是 LFS/二进制大文件,和 commit 数没关系
  • git fetch --unshallow 根本不敢跑(一旦执行就要把全量历史再下一遍,3GB 又得跑一次)
  • 本地提交也卡——git status 都要转好几秒

所以问题就变成了四个:

  1. clone:3GB 的仓库,怎么才能"够用就行"地拉下来?
  2. 删大文件:已经误提交的大文件,怎么才能永久从仓库里抹掉?
  3. 迁仓库:想把一个巨大仓库"搬到"新地方,历史还要保留,怎么做?
  4. 管依赖:项目里那些 DLL/二进制依赖,到底该不该进仓库?业界怎么搞?

本文一次性把这四个问题讲清楚 🎯


问题一:3GB 仓库怎么 clone

1.1 先认识"3GB 到底是什么"

3GB 这个数字本身就值得拆开看,因为它和 commit 数量、文件数量都不是线性关系

类型占比缓解手段
历史 commit通常 < 50MB--depth=1 就能砍掉 90%
文本代码通常 < 50MB没办法砍
大文件 / 二进制通常占 90%+--filter=blob:noneLFS、从历史里清掉
远程分支 / Tag几十~几百 MB--single-branch 只取一个分支

经验法则:如果 3GB 仓库里大头是 DLL/PSD/MP4/ISO 这种二进制,先别想 clone 的事,先去跑 git lfs ls-files 或者 du -sh .git/objects/pack 看看大头在哪——很可能你 clone 的不是"代码",是"二进制资产"。

1.2 git clone --depth=1 解决了什么,没解决什么

bash
git clone --depth=1 https://github.com/xxx/repo.git

这个命令干了什么:

  • ✅ 只下载最近 1 次 commit 的快照
  • ✅ 不下载历史
  • ✅ 速度比全量 clone 快几十倍

没解决什么

  • ❌ 其他远程分支默认不下载(默认只 clone HEAD 指的那个分支)
  • git log 只能看最近 1 次
  • git blame 只能看最近 1 次
  • git fetch --unshallow 才能拿回完整历史(一旦执行又得重下 3GB)

1.3 其他远程分支也拉不下来——怎么破

这是一个被严重低估的坑:--depth=1 默认只下载 HEAD 那个分支,所有其他远程分支都"看不见"

bash
# 看一眼当前能感知到哪些远程分支
git branch -r
# 通常只剩 origin/HEAD -> origin/main

解决办法有三种,按推荐顺序:

方法 1:clone 时一次性带 --no-single-branch

bash
# 浅 clone,但所有分支都下载(每个分支都只到 depth=1)
git clone --depth=1 --no-single-branch https://github.com/xxx/repo.git

方法 2:手动扩展"想知道的分支"列表

bash
# 先正常 clone
git clone --depth=1 https://github.com/xxx/repo.git
cd repo

# 把要关注的分支告诉 Git
git remote set-branches origin 'main' 'develop' 'feature/*'

# 再 fetch 一次(仍是浅的,但只取你列出的分支)
git fetch --depth=1 origin

方法 3:只对某几个分支做"完整下载",其他都保持浅

bash
# 浅 clone
git clone --depth=1 https://github.com/xxx/repo.git
cd repo

# 把 main 分支"加深"到完整历史
git fetch origin main:refs/remotes/origin/main --unshallow
# 其他分支保持浅

💡 重点--unshallow 是按分支维度的,不是只能对整个仓库跑。很多人误以为"unshallow 就得下载所有历史",其实可以只对某一两个分支 unshallow,其他继续保持浅。

1.4 真正救命的 --filter 系列

Git 2.17+ 引入的 partial clone 才是巨型仓库的终极杀器——它不只是"少下 commit",而是**"用到哪个 blob 才下哪个"**:

bash
# 什么 blob 都不下(按需下载)
git clone --filter=blob:none https://github.com/xxx/repo.git

# 只下小于 1MB 的 blob
git clone --filter=blob:limit=1m https://github.com/xxx/repo.git

# 只下 tree(连文件树都按需取)
git clone --filter=tree:0 https://github.com/xxx/repo.git

配合浅 clone 一起用,效果拔群:

bash
# 又浅又按需下二进制
git clone --depth=1 --filter=blob:none --no-single-branch https://github.com/xxx/repo.git

几个真实场景的组合

场景推荐命令
CI 构建,只用 main 分支git clone --depth=1 --filter=blob:none
本地开发,只用 main 分支git clone --depth=1 --filter=blob:none
本地开发,要用多个分支git clone --depth=1 --filter=blob:none --no-single-branch
想看历史 blamegit clone --filter=blob:none不要 shallow)
完整备份不用 --filter 也不用 --depth

1.5 Sparse Checkout——只取我想要的那部分

如果仓库是个 monorepo(几万个子项目),你只需要其中一两个,sparse checkout 是终极方案:

bash
# 1. 浅 clone,连工作区都别 checkout
git clone --depth=1 --filter=blob:none --no-checkout https://github.com/xxx/monorepo.git
cd monorepo

# 2. 开启 sparse checkout
git sparse-checkout init --cone

# 3. 告诉 Git 我只要这几个目录
git sparse-checkout set apps/web packages/utils

# 4. 拉下来
git checkout main

效果:5GB 的 monorepo,实际只下几十 MB

1.6 问题一的速查表

text
3GB 仓库怎么 clone?

├─ 我只看 main、不要历史
│   → git clone --depth=1 --filter=blob:none

├─ 我要看 main + develop 等几个分支,不要历史
│   → git clone --depth=1 --filter=blob:none --no-single-branch

└─ 我是 monorepo 的一员
    → git clone --depth=1 --filter=blob:none --no-checkout
    → + git sparse-checkout set <要的部分>

问题二:永久删除大文件

这是另一个高频问题。两种场景,解决思路完全不同——很多人分不清,最后要么"删不干净",要么"全删了但远程还在"。

2.1 场景 ①:以后不追踪就行,历史可以保留

误提交了一次 build.zip,现在想让 build.zip 回到未跟踪状态,但不打算重写历史(因为仓库已经有协作者了)。

bash
# 1. 把文件从 Git 索引里拿掉(工作区文件保留)
git rm --cached build.zip
git rm --cached -r build/   # 整个目录

# 2. 加进 .gitignore,确保以后不会再被误 add
echo "build.zip" >> .gitignore
echo "build/" >> .gitignore

# 3. 提交这次"移除"
git add .gitignore
git commit -m "stop tracking build artifacts"

做了什么

  • ✅ 文件在最新这次 commit 之后变成"未跟踪"
  • ✅ 旧 commit 里的 build.zip 仍然存在(需要的话 git log --all -- build.zip 还能看到)
  • ✅ 所有协作者 pull 这次 commit 后,本地也不会再追踪 build.zip

适用场景

  • 大文件只占"当前 HEAD"那点空间,旧 commit 不大
  • 团队已经在用,不能动历史
  • 接受"过去 commit 里还有这个文件"这个事实

2.2 场景 ②:一劳永逸,重写历史

仓库里有几百个 commit 都引用了 assets/ 目录下的几十 GB 资源,现在想从所有历史里彻底抹掉,重新打包成"瘦"仓库。

这是危险操作——但也是唯一能让仓库变小的办法。Git 提供了两个工具:git filter-repo(推荐)和 git filter-branch(已废弃,仅作了解)。

2.2.1 推荐工具:git filter-repo

它不是 Git 内置的,需要先装:

bash
# pip install(跨平台)
pip install git-filter-repo

# macOS
brew install git-filter-repo

基本用法——删除指定文件/目录

bash
# 删除特定文件(从所有 commit 中)
git filter-repo --invert-paths --path build.zip

# 删除整个目录
git filter-repo --invert-paths --path-glob 'build/*'

# 删除多个
git filter-repo --invert-paths --path build.zip --path-glob 'assets/*.psd'

几个常见的 filter

bash
# 只保留某个子目录(把 monorepo 的子项目拆出来)
git filter-repo --path subdir/

# 替换文件内容(敏感信息清理)
git filter-repo --replace-text expressions.txt

# 按文件大小过滤(删掉所有 > 5MB 的文件)
git filter-repo --strip-blobs-bigger-than 5M

🌟 git filter-repo --strip-blobs-bigger-than 5M 这一条是 3GB 仓库的"万金油"——一个命令把历史上所有超大 blob 全删了,仓库立刻瘦身到 MB 级。

2.2.2 重写后的善后

filter-repo 改写完,还要做四件事,否则效果没生效:

bash
# 1. 清空 reflog(旧引用不删的话,"被删的"文件其实还在 .git 里)
git reflog expire --expire=now --all

# 2. 彻底 GC,回收空间
git gc --prune=now --aggressive

# 3. 验证
git log --all -- build.zip    # 应该完全找不到

# 4. 强制推送到远程(⚠️ 这步会重写远程历史!)
git push origin --force --all
git push origin --force --tags

强制推送 = 一次"协议级"操作

强制推送会重写远程的所有 commit hash。所有协作者都必须:

  1. 备份自己未推送的工作
  2. rm -rf 重新 clone
  3. 把新改动重新提交

所以这条操作一定要先在团队内同步——把它当成"线上事故预案"对待。

2.2.3 备选工具:BFG Repo-Cleaner

BFG 是一个 Java 写的第三方工具,用法比 filter-repo 还简单,但**只能做"删大文件 / 替换文本"**这两件事:

bash
# 下载 https://rtyley.github.io/bfg-repo-cleaner/

# 删掉所有 > 50MB 的文件(不包括 latest commit)
java -jar bfg.jar --strip-blobs-bigger-than 50M repo.git

# 删掉指定文件夹
java -jar bfg.jar --delete-folders folder1 --delete-folders folder2 repo.git

# 替换密码、邮箱等
java -jar bfg.jar --replace-text passwords.txt repo.git

跑完 BFG 之后,同样要做善后

bash
cd repo.git
git reflog expire --expire=now --all
git gc --prune=now --aggressive
git push --force --all

2.2.4 git filter-branch——知道就行,别用

bash
# 千万别学
git filter-branch --tree-filter 'rm -f build.zip' HEAD

Git 官方文档自己都写了:"git-filter-branch has a plethora of pitfalls that can produce non-obvious rewrites"。它慢、容易出错、对 reflog 处理有 bug。新项目里一律用 filter-repo 或 BFG

2.3 问题二速查表

text
想删大文件?

├─ 接受历史里还有它,团队不能动历史
│   → git rm --cached + 加进 .gitignore(场景 ①)

└─ 想真正"瘦下来"
    → git filter-repo --path / --strip-blobs-bigger-than 5M
    → 团队公告 → 强制推送 → 所有人重新 clone

问题三:仓库迁移与历史保留

大型项目经常会做"仓库搬迁"——比如把 monorepo 拆成多个独立 repo,或者反过来合并。关键就是"历史不能丢"

3.1 场景 A:仓库搬家(remote URL 变了)

最简单:只是远程地址变了。

bash
# 1. 改 remote URL
git remote set-url origin https://github.com/new-org/new-repo.git

# 2. 验证
git remote -v

# 3. 推送
git push --all
git push --tags

历史、commit hash、所有分支完全不变——Git 本来就是去中心化的,"搬家"对仓库本身是 0 损耗。

3.2 场景 B:从大仓库里"挖"一个子目录出来当新仓库

monoreposervices/auth/ 目录要做成独立仓库,带着它自己的历史

bash
git clone https://github.com/old/monorepo.git
cd monorepo

# 把 services/auth/ 以外的全部砍掉
git filter-repo --subdirectory-filter services/auth

filter-repo --subdirectory-filter 会:

  • 保留所有"修改过 services/auth/ 路径下文件"的 commit
  • 把每个 commit 的"被修改文件"剪到只剩 services/auth/
  • 路径前缀也去掉(services/auth/foo.py 变成 foo.py

效果

  • ✅ 新仓库有完整历史
  • ✅ commit 作者、时间、message 全部保留
  • ❌ 老的 commit hash 全部变了(因为改写了 tree)

💡 Chromium、Godot 这些巨型项目都大量使用这个模式:核心组维护 monorepo,各子模块通过这个机制发布独立仓库。

3.3 场景 C:多个仓库合并成一个

三个独立仓库 core / ui / tools,要合并到同一个 monorepo platform 里。

git subtree

bash
git clone https://github.com/company/platform.git
cd platform

# 把 core 仓库以子树形式合进来
git subtree add --prefix=libs/core https://github.com/company/core.git main
git subtree add --prefix=libs/ui   https://github.com/company/ui.git   main
git subtree add --prefix=tools     https://github.com/company/tools.git main

效果

  • ✅ 每个子仓库的完整历史被合并进 monorepo
  • ✅ 后续可以继续用 git subtree pull/push 双向同步
  • ❌ monorepo 的总 commit 数 = 三个仓库 commit 数之和

备选:git submodule 也能合并,但代价不同

submodule 是"指针式"——子仓库独立存在,monorepo 里只存一个 SHA。优点是 monorepo 不会变胖,缺点是 clone 后还要二次拉取子仓库。subtree 是"嵌入式"——子仓库的代码物理合入,monorepo 完整自包含。两种风格大型项目都在用,没有绝对对错。

3.4 场景 D:完整镜像迁移(GitHub → Gitee 等)

想把 GitHub 上的仓库完整搬到 Gitee,所有分支、tags、release 都带上。

bash
# 1. 在 Gitee 上建好空仓库

# 2. 本地做一次 mirror clone(会拉所有分支、tags、refs)
git clone --mirror https://github.com/xxx/repo.git

# 3. 推送到 Gitee
cd repo.git
git push --mirror https://gitee.com/xxx/repo.git

--mirror 拉的是所有 refs(分支、tags、PR refs、release notes),--mirror push 把这些原样推过去——这是"完整搬家"的标准姿势。

3.5 场景 E:超大型项目怎么"重打地基"

Linux 内核、Chromium 这种上千万行代码、几十 GB 仓库的项目,偶尔会做"重构式迁移"——比如从 Mercurial 转到 Git,或者拆 monorepo。它们的策略通常是:

  1. mirror clone 全量历史
  2. 用脚本批量 filter-repo,按目录、按文件类型切片
  3. 把切片后的新仓库逐个推上去
  4. 旧仓库保留只读,"git mv" 通知用户迁移

这套流程动辄几周,是真正的"大型工程"——不是日常操作,但确实是 3GB 仓库"下一阶段"的样子。

3.6 问题三速查表

场景关键命令
只换远程 URLgit remote set-url origin <new>
完整镜像搬家git clone --mirror + git push --mirror
monorepo 拆出子仓库git filter-repo --subdirectory-filter <dir>
多仓库合并成 monorepogit subtree add --prefix=<dir> <repo> <branch>
大型项目重打地基--mirror clone + 脚本化 filter-repo + 批量推送

问题四:依赖管理——DLL 到底该不该进仓库

4.1 先回答"该不该"

绝大部分情况下,依赖的二进制产物都不应该进仓库。

为什么:

  • 仓库被它们撑大(几 MB 的代码 + 几百 MB 的 DLL = 几 GB 的仓库)
  • Git 没法 diff 二进制,每次更新都是一次大 commit
  • 不同人编译出来的 DLL 可能不一致,repo 里放死的 DLL 反而成了"被绑定的版本"
  • clone / fetch / status 全部变慢

4.2 业界通用的几套方案

方案 1:包管理器(最推荐

不同语言有各自的事实标准:

语言包管理器锁定文件
C++vcpkg / Conan / Huntervcpkg.json / conanfile.txt
C#NuGetpackages.lock.json
Pythonpip / poetry / uvrequirements.txt / pyproject.toml / uv.lock
Nodenpm / pnpm / yarnpackage-lock.json / pnpm-lock.yaml
RustCargoCargo.lock
Gogo modgo.sum
JavaMaven / Gradlepom.xml / build.gradle

这些工具的核心思路

  1. 项目里只放清单文件vcpkg.json 之类)——文本,KB 级别
  2. 实际二进制运行时下载到本地缓存或 build 目录(通常 git ignore 掉)
  3. 锁定文件保证所有人下到的是同一份依赖

对 C++ 项目特别说一下

  • vcpkg(微软出品):vcpkg install xxx,产物是源码级别的、和项目一起编译
  • Conan(C++ 社区主流):支持预编译二进制,"下下来直接用"
  • CMake FetchContent:CMake 自带的"下载源码"机制,依赖也是源码级的
cmake
include(FetchContent)
FetchContent_Declare(
    spdlog
    GIT_REPOSITORY https://github.com/gabime/spdlog.git
    GIT_TAG v1.13.0
)
FetchContent_MakeAvailable(spdlog)
target_link_libraries(MyApp PRIVATE spdlog::spdlog)

方案 2:Git LFS(Large File Storage)

适用于确实需要版本化的二进制——比如游戏素材、模型文件、训练数据集:

bash
# 1. 安装 LFS 扩展
git lfs install

# 2. 告诉 Git 哪些后缀走 LFS
git lfs track "*.psd"
git lfs track "assets/**/*.fbx"

# 3. 提交 .gitattributes
git add .gitattributes
git commit -m "track PSD/FBX via LFS"

# 4. 之后正常的 git add / commit,LFS 自动接管大文件

LFS 的本质

  • 仓库里不存真正的二进制
  • 仓库里只存一个指针文件(文本,几 KB)
  • 真正的二进制存在专门的 LFS 服务器(GitHub/Gitee 都自带)
  • clone 时按需下载

适合:美术资产、训练数据、机器学习模型、3D 资源

不适合:常规第三方库依赖(用方案 1 更合适)

方案 3:Submodule / Subtree

适用于"依赖的代码本身想看 git 历史,但不该污染本仓库主历史":

bash
# 添加 submodule
git submodule add https://github.com/gabime/spdlog.git third_party/spdlog
git submodule update --init --recursive

特点

  • 主仓库里只存 submodule 的 SHA 引用
  • clone 后还需要 git submodule update --init
  • 团队里有人忘了 update 会很痛苦

用得越来越少了——大部分场景被包管理器(方案 1)取代。但嵌入式 / 跨语言的"内部代码库" 仍然常用 submodule。

方案 4:CI/CD 制品库(Artifactory / Nexus / GitHub Packages)

适用于"自己编译出来的产物给团队/外部用":

bash
# 内部用:推送到 Artifactory
conan upload MyLib/1.2.3 --all -r=artifactory

这是企业级方案——一般小项目用不到。

方案 5:Pre-built 产物 + 单独仓库

适用于"团队习惯放二进制,但想避开主仓库变大":

my-project/                  # 主仓库(仅代码)
my-project-binaries/         # 二进制专用仓库(LFS / release)
└── releases/
    ├── v1.0.0-windows.zip
    ├── v1.0.0-linux.tar.gz
    └── v1.1.0-windows.zip

主仓库 release 时把二进制推到这个仓库,开发者按需下载。

4.3 我的项目该选哪个

按"项目规模"和"依赖性质"分:

场景推荐
标准 C++ 项目,依赖 Qt / Boost / OpenCV 这种vcpkg / Conan(首选
公司内部开发的库(ABCD),迭代频繁、要保持版本一致网络共享 + 脚本 / 内部 NuGet / Conan(见 4.5)
公司内部有统一的 DLL 出包流程制品库 / Pre-built 仓库
美术 / 3D 资源密集(游戏 / 动画)Git LFS
跨语言子项目submodule(也可用,但需配文档)
只是临时下载的二进制写个脚本 download_deps.ps1,进 .gitignore

⚠️ 不要 DLL 进了仓库还"解释一下"——它就是个反模式。DLL 该去哪去哪,仓库里只留"怎么拿它"的说明。

4.4 问题四速查表

text
项目依赖 DLL / 二进制,该怎么办?

├─ 它是"标准库"(Boost、Qt、OpenCV)
│   → 用 vcpkg / Conan / NuGet
│   → 仓库里只放清单文件

├─ 它是"团队自己产出的 DLL"
│   → 推到制品库 / 独立 binaries 仓库
│   → 主仓库不沾

├─ 它是"美术/3D/数据"资产
│   → Git LFS

└─ 它是"自研子项目"想带历史
    → git submodule(记得配 init 文档)

4.5 补充场景:公司内部频繁迭代的 DLL 怎么管

4.2 节里给的方案 1(包管理器)很标准,但对"公司内部开发的库、迭代频繁"这种场景只是"原则"——落地方式还要再具体

你的情况:ABCD 都是公司内部开发的依赖,每个版本迭代都在别的项目里,所以为了保持一致性,每次把 DLL 变更记录在主项目里。

这种情况和"用 Boost / OpenCV"完全不同的需求——DLL 跟着主项目走,git 仓库必然被撑爆。下面按"上手成本"从低到高给三个方案。

4.5.1 先诊断:为什么 DLL 进 git 一定走偏

现状问题
每次 A/B/C/D 出新版,把新 DLL 覆盖进项目git 把它当成"完整文件变更"——几个 DLL 各几 MB,迭代一年仓库轻松几 GB
团队成员各自编译 A 得到的 DLL 可能不一致实际并未"一致性",只是"看起来一致"
git log 里一堆 update A.dll v1.2.3 → v1.2.4几百条无意义 commit,但 DLL 真正的源码历史反而没记录

核心矛盾

DLL 是"制品"(artifact),但 git 是"源码版本管理工具"——拿源码工具管制品,必然走偏

不论你用 .gitignore、LFS、还是 submodules,它们都没解决"制品应该有独立版本号、应该走独立的包管理"这个根本问题

4.5.2 方案 1:内部网络共享 + 启动脚本(最低成本、立刻能搞

不引入任何工具,靠"约定"——适合起步期、< 10 人团队、纯 Windows 内网

后端结构:

text
\\公司内部\artifacts\
├── A\
│   ├── 1.2.3\A.dll
│   ├── 1.2.4\A.dll
│   └── 2.0.0\A.dll
├── B\
│   ├── 1.5.2\B.dll
│   └── ...
├── C\
│   └── ...
└── D\
    └── ...

每个库按版本号分子目录,永远不覆盖——这才是"版本管理"。

项目里加 manifest + 拉取脚本:

text
A=2.0.0
B=1.5.2
C=3.1.0
D=0.9.0
bash
@echo off
set BASE=\\公司内部\artifacts

for /F "tokens=1,2 delims==" %%a in (deps.manifest.txt) do (
    if exist "%BASE%\%%a\%%b\" (
        copy /Y "%BASE%\%%a\%%b\%%a.dll" "third_party\%%a.dll" >nul
        echo [OK] %%a = %%b
    ) else (
        echo [ERROR] %%a\%%b not found
        exit /b 1
    )
)

全部进 git:只有 deps.manifest.txtfetch_deps.bat(< 1KB)。DLL 一律在 third_party/ 下,gitignore

text
third_party/*.dll
third_party/*.lib

新成员拉完代码,先跑 scripts\fetch_deps.bat 就拿到正确版本。

升级流程——A 团队发版 2.0.0:

bash
# A 团队自己拷贝一份上去(手动 / 自动化都行)
xcopy /Y build\A.dll \\公司内部\artifacts\A\2.0.0\
# 或者 CI 里自动推

所有人下次只需要改一行

diff
-A=1.2.4
+A=2.0.0

优缺点

  • 0 部署成本,任何网络共享 / 内部 SFTP / 哪怕 SVN 都能当后端
  • 📦 git log 干净(脚本和 manifest 都很小)
  • 📉 DLL 体积膨胀的是网络共享,不污染主仓库
  • ❌ 没版本校验,谁手改了网络共享就乱套
  • ❌ 没跨平台(Windows 网络共享 Linux 用不了)
  • ❌ 没"自动解依赖图"——如果 A 依赖 B,得自己注意

🎯 小团队、刚起步、项目数量 5 个以下——这个方案性价比最高。

4.5.3 方案 2:内部 NuGet feed(Windows 友好、强烈推荐

很多人不知道:NuGet 不只是 .NET 的——分发 C++ DLL 它一样专业。微软自家、Windows 生态圈的 C++ 项目大量在用。

搭个内部 NuGet 服务(10 分钟就能跑起来):

工具特点
BaGet.NET 写的,轻量、Apache 2.0 协议
ProGetInedo 出品,企业级功能更多(有免费版)

搭好之后长这样:

text
https://nuget.internal.company.com/v3/index.json
├── Company.A.1.2.3.nupkg
├── Company.A.2.0.0.nupkg
├── Company.B.1.5.2.nupkg
└── ...

A/B/C/D 团队在每次发版时把 DLL 打成 nupkg 推上去

text
<?xml version="1.0"?>
<package>
  <metadata>
    <id>Company.A</id>
    <version>2.0.0</version>
    <authors>A Team</authors>
    <description>Internal A library</description>
  </metadata>
  <files>
    <file src="build\A.dll" target="runtimes\win-x64\native" />
    <file src="build\A.lib" target="build\native" />
    <file src="include\*.h"  target="build\native\include" />
  </files>
</package>
bash
# A 团队发布
nuget pack Company.A.nuspec -Version 2.0.0
nuget push Company.A.2.0.0.nupkg ^
    -Source https://nuget.internal.company.com/v3/index.json ^
    -ApiKey xxx

你项目里加个 NuGet.config

xml
<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <packageSources>
    <clear />
    <add key="internal" value="https://nuget.internal.company.com/v3/index.json" />
  </packageSources>
</configuration>

Visual Studio 里:直接在"NuGet 包管理器"搜 Company.A,点安装——和装 .NET 库一样体验

CMake 项目里

cmake
include(FetchContent)
FetchContent_Declare(
    Company.A
    URL https://nuget.internal.company.com/v3/flatcontainer/company.a/2.0.0/company.a.2.0.0.nupkg
)
FetchContent_MakeAvailable(Company.A)

target_link_libraries(MyApp PRIVATE Company::A)

优缺点

  • ✅ 业界成熟方案,微软自家 / 大量 Windows C++ 项目在用
  • ✅ 自带版本管理、依赖关系、签名校验
  • ✅ Windows + Visual Studio 集成丝滑
  • ✅ 仓库里只放 manifest 文本,几十字节
  • ✅ 团队成员完全无感——和装 .NET 库一样的体验
  • ⚠️ 需要搭个内部 NuGet 服务(一次性 1~2 人天)
  • ⚠️ 跨平台支持有,但不如方案 3

🎯 10 人以上团队、有专职 DevOps / 内部平台组、纯 Windows——这是 Windows C++ 项目的行业标准答案

4.5.4 方案 3:Conan + Artifactory(最专业、最通用

C++ 社区的事实标准。跨平台、支持二进制缓存、支持源码编译两种模式

搭一个 Artifactory / Nexus(或纯 Conan Server):

text
https://conan.internal.company.com/artifactory/api/conan/conan-local
├── CompanyA/1.2.3
├── CompanyA/2.0.0
├── CompanyB/1.5.2
└── ...

A 团队发版时:

python
from conan import ConanFile

class CompanyAConan(ConanFile):
    name = "CompanyA"
    version = "2.0.0"
    settings = "os", "compiler", "build_type", "arch"

    def package(self):
        self.copy("*.dll", dst="bin", keep_path=False)
        self.copy("*.lib", dst="lib", keep_path=False)
        self.copy("*.h",  dst="include", keep_path=False)
bash
# A 团队发布
conan create . CompanyA/2.0.0@                     # 本地构建并打包
conan upload CompanyA/2.0.0 -r=internal            # 推到 Artifactory

你项目里写个 conanfile.txt

text
[requires]
CompanyA/2.0.0
CompanyB/1.5.2
CompanyC/3.1.0
CompanyD/0.9.0

[generators]
CMakeDeps
CMakeToolchain

构建前 conan install . 就把依赖全拉下来了。

Conan 的杀手锏——二进制缓存

A 团队用 VS 2022 编 Windows x64 Release 的 2.0.0,所有 Windows 团队成员直接复用这个二进制不用在本地重新编译 A——Conan 会自动用 os:Windows / compiler:msvc / arch:x86_64 / build_type:Release 这个 hash 找到对应的包。

优缺点

  • 跨平台(Windows/Linux/macOS 同一套)
  • ✅ 支持"二进制包"和"源码编译"两种模式
  • ✅ 自带版本管理、依赖图解析、二进制兼容性检查
  • ✅ Visual Studio / CMake / 命令行都行
  • 二进制缓存——A 编一次,全公司复用
  • ✅ 业界标准,求职简历上写着不亏
  • ❌ 学习曲线最陡(团队需要学 Conan + Python)
  • ❌ 部署 Artifactory 略重
  • ❌ 几小时到 1 天上手成本

🎯 跨平台 / 团队规模大 / 长期演进——这是 C++ 工程的终极答案

4.5.5 三种方案对比

维度方案 1:网络共享方案 2:内部 NuGet方案 3:Conan + Artifactory
上手成本⚡ 1 小时🛠️ 1~2 天🔧 3~7 天
Windows 友好度⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
跨平台⚠️ 凑合
版本管理手动自动自动
依赖关系解析手动自动自动
二进制缓存⚠️ 半自动
适用团队规模< 1010~10030+
仓库大小几 MB几 MB几 MB
推荐场景起步期Windows 团队首选跨平台 / 长期演进

4.5.6 落地三步走

别一上来就上 Artifactory——从"DLL 在 git 里"的现状,我建议分三步迁移:

第一步:把 DLL 立刻挪出 git(今天就做)

bash
# 1. 从 git 索引里删掉(磁盘上的文件保留)
git rm --cached -r third_party/

# 2. 加进 .gitignore
echo "third_party/*.dll" >> .gitignore
echo "third_party/*.lib" >> .gitignore
echo "third_party/*.pdb" >> .gitignore

# 3. 提交
git add .gitignore
git commit -m "remove DLLs from tracking, move to manifest-based fetch"

这一刀切下去,仓库立刻瘦下来——不管你后面用方案 1/2/3,这步是共同的。

第二步:建内部网络共享 + 写 fetch 脚本(1~2 天内)

  • 让运维开一台共享服务器(或用现有文件服务器)
  • 把当前所有 DLL 按"库名/版本"组织上去
  • 项目里加 deps.manifest.txt + fetch_deps.bat
  • 通知团队"以后 DLL 不在 git 里,跑 fetch 脚本拿"

第三步:搭内部 NuGet(2~4 周后,看规模)

  • 让平台组搭个 BaGet
  • 通知各依赖方团队:"下次发版请打成 nupkg 推到内部 NuGet"
  • 你项目从"调 .bat" 改成"调 NuGet"
  • 如果就 Windows、且团队 < 50 人——永远停在 NuGet 就够了

第四步(可选):要不要升级 Conan

  • 如果就 Windows、< 50 人——不需要
  • 如果要跨平台 / 团队很大 / 出现"依赖地狱"——再升级 Conan

4.5.7 容易踩的坑

不要"建一个仓库专门放 DLL"

text
❌ 错误做法:
company-dlls/    # 一个 git 仓库,专门放所有 DLL
├── A.dll
├── B.dll
└── ...

这其实只是把 git 仓库换了个名字——还是会有:仓库随时间膨胀、没有版本号、没有"哪个项目要哪个版本"的信息、二进制文件 git diff 看不到有意义的内容。

正确做法:DLL 走"包管理器 / 网络共享",根本不进 git

几个具体的坑

  • manifest 文件要进 git,脚本要进 git——DLL 一律不进 git
  • 内部源要加访问凭证——别用匿名开放,否则哪天被人误推几个 GB 的垃圾就坑了
  • 版本号一定要严格 SemVer——每次发版递增,永远不要覆盖已发布的版本1.2.3 不能被改成 1.2.4 的内容后还用 1.2.3 这个名字)
  • DLL 之外别忘了配套——头文件 / 导入库 / 调试符号 / 文档。只发 DLL 不发头文件,调试时会非常痛苦

🌐 业界巨型项目怎么"过日子"

最后顺手整理一下几个真正的巨型仓库的运营策略——这些策略 3GB 仓库完全可以参考:

项目仓库大小关键策略
Linux Kernel几 GB--filter=blob:none 通用;发行版厂商用 partial clone + sparse checkout
Chromium几十 GBGoogle 内部用 Piper,普通开发者只能用 Google 维护的、sparse 过的开源镜像
Unreal Engine几十 GB大文件拆到独立 CDN,Git 里只放源码和指向 CDN 的脚本
Godot几百 MB普通 Git + LFS(资产),多数开发者浅 clone
TensorFlow几百 MB子模块(tensorflow/compiler 等)+ 文本依赖
VSCode几百 MB主仓库用 LFS,几千个扩展用独立仓库

共同点

  • 几乎没有人用全量 clone 日常开发
  • partial clone + sparse checkout 是基本功
  • 大文件不进主仓库(独立仓库 / LFS / 制品库 / CDN)

🛠️ 一组实战命令速查

把上面所有用过的命令汇总一下,方便复制:

bash
# ============ clone 优化 ============
# 浅 clone
git clone --depth=1 https://github.com/xxx/repo.git

# 浅 clone + 所有分支
git clone --depth=1 --no-single-branch https://github.com/xxx/repo.git

# partial clone(按需下载 blob)
git clone --filter=blob:none https://github.com/xxx/repo.git

# sparse checkout(只下指定目录)
git clone --depth=1 --filter=blob:none --no-checkout https://github.com/xxx/repo.git
cd repo && git sparse-checkout set path1 path2

# 把某个分支 unshallow(不影响其他分支)
git fetch origin main:refs/remotes/origin/main --unshallow


# ============ 永久删大文件 ============
# 装 filter-repo
pip install git-filter-repo

# 删特定文件 / 目录
git filter-repo --invert-paths --path build.zip
git filter-repo --invert-paths --path-glob 'build/*'

# 删所有 > 5MB 的文件(万金油)
git filter-repo --strip-blobs-bigger-than 5M

# 善后
git reflog expire --expire=now --all
git gc --prune=now --aggressive
git push origin --force --all


# ============ 仓库迁移 ============
# 只换 URL
git remote set-url origin <new-url>

# 完整镜像搬家
git clone --mirror <old-url>
cd repo.git && git push --mirror <new-url>

# 子目录拆成新仓库
git filter-repo --subdirectory-filter path/to/subdir

# 多仓库合并
git subtree add --prefix=libs/core <core-url> main


# ============ 依赖管理 ============
# LFS
git lfs install
git lfs track "*.psd"
git add .gitattributes

📌 容易忽略的细节

  • --depth=1 默认只下 HEAD 那个分支——其他分支要 --no-single-branchremote set-branches
  • --unshallow 是按分支的——git fetch origin <branch> --unshallow 可以只 unshallow 一个分支
  • filter-repo 改写后必须 reflog expire + gc——否则旧 blob 还在 .git/objects/
  • 强制推送 = 团队级操作——必须公告、必须重 clone
  • subdirectory-filter 会改 commit hash——因为 tree 变了
  • 包管理器生成的"锁定文件"一定要入库Cargo.lock / package-lock.json)——它是"100% 复现构建"的关键
  • vcpkg / Conan 默认下载到 build 目录——确认 .gitignore 里覆盖了对应路径

一句话总结

巨型 Git 仓库的 4 个核心问题,对应 4 类工具:clone 用 partial clone + sparse checkout、删大文件用 filter-repo、迁仓库用 filter-repo / subtree / --mirror、管依赖用包管理器 / LFS / 制品库——选对了工具,3GB 也能轻松应付

理解这 4 类工具的适用边界,基本能覆盖日常工程里 99% 的"巨型仓库"需求。

💡 特别补充:如果你的 DLL 来自公司内部"ABCD"这种频繁迭代的依赖——立刻把 DLL 从 git 里挪出去(git rm --cached),短期用网络共享 + 启动脚本过渡,中期上内部 NuGet(Windows 团队强烈推荐),长期规模到了再升级 Conan。那一刀不切,后面所有方案都是空谈