背景
最近被一个 3GB 的 Git 仓库折腾了一通,顺手把过程中学到的"巨型仓库生存策略"系统整理一下:
git clone拉到一半超时,重试 3 次都没成功- 改成
--depth=1倒是快了,但其他分支全没了,连main之外的所有远程分支都拉不下来 - 改
--depth=100也不顶用——3GB 里大头是 LFS/二进制大文件,和 commit 数没关系 git fetch --unshallow根本不敢跑(一旦执行就要把全量历史再下一遍,3GB 又得跑一次)- 本地提交也卡——
git status都要转好几秒
所以问题就变成了四个:
- clone:3GB 的仓库,怎么才能"够用就行"地拉下来?
- 删大文件:已经误提交的大文件,怎么才能永久从仓库里抹掉?
- 迁仓库:想把一个巨大仓库"搬到"新地方,历史还要保留,怎么做?
- 管依赖:项目里那些 DLL/二进制依赖,到底该不该进仓库?业界怎么搞?
本文一次性把这四个问题讲清楚 🎯
问题一:3GB 仓库怎么 clone
1.1 先认识"3GB 到底是什么"
3GB 这个数字本身就值得拆开看,因为它和 commit 数量、文件数量都不是线性关系:
| 类型 | 占比 | 缓解手段 |
|---|---|---|
| 历史 commit | 通常 < 50MB | --depth=1 就能砍掉 90% |
| 文本代码 | 通常 < 50MB | 没办法砍 |
| 大文件 / 二进制 | 通常占 90%+ | --filter=blob:none、LFS、从历史里清掉 |
| 远程分支 / 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 解决了什么,没解决什么
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 那个分支,所有其他远程分支都"看不见"。
# 看一眼当前能感知到哪些远程分支
git branch -r
# 通常只剩 origin/HEAD -> origin/main解决办法有三种,按推荐顺序:
方法 1:clone 时一次性带 --no-single-branch
# 浅 clone,但所有分支都下载(每个分支都只到 depth=1)
git clone --depth=1 --no-single-branch https://github.com/xxx/repo.git方法 2:手动扩展"想知道的分支"列表
# 先正常 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:只对某几个分支做"完整下载",其他都保持浅
# 浅 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 才下哪个"**:
# 什么 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 一起用,效果拔群:
# 又浅又按需下二进制
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 |
| 想看历史 blame | git clone --filter=blob:none(不要 shallow) |
| 完整备份 | 不用 --filter 也不用 --depth |
1.5 Sparse Checkout——只取我想要的那部分
如果仓库是个 monorepo(几万个子项目),你只需要其中一两个,sparse checkout 是终极方案:
# 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 问题一的速查表
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回到未跟踪状态,但不打算重写历史(因为仓库已经有协作者了)。
# 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 内置的,需要先装:
# pip install(跨平台)
pip install git-filter-repo
# macOS
brew install git-filter-repo基本用法——删除指定文件/目录:
# 删除特定文件(从所有 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:
# 只保留某个子目录(把 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 改写完,还要做四件事,否则效果没生效:
# 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。所有协作者都必须:
- 备份自己未推送的工作
rm -rf重新 clone- 把新改动重新提交
所以这条操作一定要先在团队内同步——把它当成"线上事故预案"对待。
2.2.3 备选工具:BFG Repo-Cleaner
BFG 是一个 Java 写的第三方工具,用法比 filter-repo 还简单,但**只能做"删大文件 / 替换文本"**这两件事:
# 下载 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 之后,同样要做善后:
cd repo.git
git reflog expire --expire=now --all
git gc --prune=now --aggressive
git push --force --all2.2.4 git filter-branch——知道就行,别用
# 千万别学
git filter-branch --tree-filter 'rm -f build.zip' HEADGit 官方文档自己都写了:"git-filter-branch has a plethora of pitfalls that can produce non-obvious rewrites"。它慢、容易出错、对 reflog 处理有 bug。新项目里一律用 filter-repo 或 BFG。
2.3 问题二速查表
想删大文件?
│
├─ 接受历史里还有它,团队不能动历史
│ → git rm --cached + 加进 .gitignore(场景 ①)
│
└─ 想真正"瘦下来"
→ git filter-repo --path / --strip-blobs-bigger-than 5M
→ 团队公告 → 强制推送 → 所有人重新 clone问题三:仓库迁移与历史保留
大型项目经常会做"仓库搬迁"——比如把 monorepo 拆成多个独立 repo,或者反过来合并。关键就是"历史不能丢"。
3.1 场景 A:仓库搬家(remote URL 变了)
最简单:只是远程地址变了。
# 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:从大仓库里"挖"一个子目录出来当新仓库
monorepo里services/auth/目录要做成独立仓库,带着它自己的历史。
git clone https://github.com/old/monorepo.git
cd monorepo
# 把 services/auth/ 以外的全部砍掉
git filter-repo --subdirectory-filter services/authfilter-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,要合并到同一个 monorepoplatform里。
用 git subtree:
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 都带上。
# 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。它们的策略通常是:
- mirror clone 全量历史
- 用脚本批量
filter-repo,按目录、按文件类型切片 - 把切片后的新仓库逐个推上去
- 旧仓库保留只读,"git mv" 通知用户迁移
这套流程动辄几周,是真正的"大型工程"——不是日常操作,但确实是 3GB 仓库"下一阶段"的样子。
3.6 问题三速查表
| 场景 | 关键命令 |
|---|---|
| 只换远程 URL | git remote set-url origin <new> |
| 完整镜像搬家 | git clone --mirror + git push --mirror |
| monorepo 拆出子仓库 | git filter-repo --subdirectory-filter <dir> |
| 多仓库合并成 monorepo | git 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 / Hunter | vcpkg.json / conanfile.txt |
| C# | NuGet | packages.lock.json |
| Python | pip / poetry / uv | requirements.txt / pyproject.toml / uv.lock |
| Node | npm / pnpm / yarn | package-lock.json / pnpm-lock.yaml |
| Rust | Cargo | Cargo.lock |
| Go | go mod | go.sum |
| Java | Maven / Gradle | pom.xml / build.gradle |
这些工具的核心思路:
- 项目里只放清单文件(
vcpkg.json之类)——文本,KB 级别 - 实际二进制运行时下载到本地缓存或 build 目录(通常
git ignore掉) - 锁定文件保证所有人下到的是同一份依赖
对 C++ 项目特别说一下:
- vcpkg(微软出品):
vcpkg install xxx,产物是源码级别的、和项目一起编译 - Conan(C++ 社区主流):支持预编译二进制,"下下来直接用"
- CMake FetchContent: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)
适用于确实需要版本化的二进制——比如游戏素材、模型文件、训练数据集:
# 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 历史,但不该污染本仓库主历史":
# 添加 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)
适用于"自己编译出来的产物给团队/外部用":
# 内部用:推送到 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 问题四速查表
项目依赖 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 内网。
后端结构:
\\公司内部\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 + 拉取脚本:
A=2.0.0
B=1.5.2
C=3.1.0
D=0.9.0@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.txt 和 fetch_deps.bat(< 1KB)。DLL 一律在 third_party/ 下,gitignore。
third_party/*.dll
third_party/*.lib新成员拉完代码,先跑 scripts\fetch_deps.bat 就拿到正确版本。
升级流程——A 团队发版 2.0.0:
# A 团队自己拷贝一份上去(手动 / 自动化都行)
xcopy /Y build\A.dll \\公司内部\artifacts\A\2.0.0\
# 或者 CI 里自动推所有人下次只需要改一行:
-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 协议 |
| ProGet | Inedo 出品,企业级功能更多(有免费版) |
搭好之后长这样:
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 推上去:
<?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># 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 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 项目里:
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):
https://conan.internal.company.com/artifactory/api/conan/conan-local
├── CompanyA/1.2.3
├── CompanyA/2.0.0
├── CompanyB/1.5.2
└── ...A 团队发版时:
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)# A 团队发布
conan create . CompanyA/2.0.0@ # 本地构建并打包
conan upload CompanyA/2.0.0 -r=internal # 推到 Artifactory你项目里写个 conanfile.txt:
[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 友好度 | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| 跨平台 | ❌ | ⚠️ 凑合 | ✅ |
| 版本管理 | 手动 | 自动 | 自动 |
| 依赖关系解析 | 手动 | 自动 | 自动 |
| 二进制缓存 | ❌ | ⚠️ 半自动 | ✅ |
| 适用团队规模 | < 10 | 10~100 | 30+ |
| 仓库大小 | 几 MB | 几 MB | 几 MB |
| 推荐场景 | 起步期 | Windows 团队首选 | 跨平台 / 长期演进 |
4.5.6 落地三步走
别一上来就上 Artifactory——从"DLL 在 git 里"的现状,我建议分三步迁移:
第一步:把 DLL 立刻挪出 git(今天就做)
# 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"
❌ 错误做法:
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 | 几十 GB | Google 内部用 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)
🛠️ 一组实战命令速查
把上面所有用过的命令汇总一下,方便复制:
# ============ 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-branch或remote 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。那一刀不切,后面所有方案都是空谈。