08-Git 工程化专题
前面七章讲的都是"单仓库"场景。随着团队和项目变大,会遇到三个工程化问题:怎么组织多服务代码、怎么管大文件、怎么复用跨项目代码。本章讲这三块。
Monorepo vs Multirepo#
这是工程组织层面的根本选择——所有代码放一个仓库(Monorepo),还是每个服务一个仓库(Multirepo)。
Monorepo#
text
my-company/
├── services/
│ ├── user-service/
│ ├── order-service/
│ └── payment-service/
├── libs/
│ ├── auth-lib/
│ └── common-types/
├── infra/
│ ├── terraform/
│ └── k8s-manifests/
└── docs/优势:
- 原子提交:一个 PR 同时改服务和依赖库,不会出现"库改了但服务没跟上"的中间状态
- 代码共享简单:所有服务共享同一份 libs,不用发包
- 统一工具链:一份 lint 配置、一份 CI 模板,全员复用
- 跨服务重构容易:IDE 全局搜索 + 改动一次性提交
劣势:
- CI 复杂:一个仓库的 CI 要识别"这次改了哪些服务,只构建这些"
- 权限粒度粗:所有人能看所有代码(对某些场景是问题)
- 仓库膨胀:10 年的代码历史,clone 一次几个 GB
- 需要工具支持:Bazel、Buck、Turborepo、Nx 这类构建工具是 Monorepo 的标配
Multirepo#
text
user-service/ ← 独立仓库
order-service/ ← 独立仓库
payment-service/ ← 独立仓库
auth-lib/ ← 独立仓库(发包给别人用)优势:
- CI 简单:每个仓库的 CI 只管自己
- 权限清晰:按仓库分配权限
- 仓库轻量:每个仓库只关心自己的代码
劣势:
- 跨服务改动难:改一个 lib 接口,要发版、所有依赖方升级,周期长
- 代码复用靠发包:lib 改了要发布到 npm/PyPI/Maven,下游再升级
- 工具链漂移:每个仓库的 ESLint、tsconfig 各自演化,时间久了不一致
- 版本地狱:服务 A 用 lib v1.2,服务 B 用 v1.3,某天要统一时发现 API 不兼容
对比#
| 维度 | Monorepo | Multirepo |
|---|---|---|
| 跨服务改动 | 原子提交,简单 | 多 PR + 发版 + 升级,复杂 |
| CI 配置 | 复杂(需要按路径触发) | 简单(每仓库独立) |
| 权限管理 | 粗(全仓库可见) | 细(按仓库分配) |
| 代码复用 | 直接引用源码 | 靠发包 |
| 工具链一致性 | 天然统一 | 需要额外约束 |
| 仓库体积 | 大(需浅克隆) | 小 |
| 适合规模 | 中大型(50+ 人) | 小到中(< 50 人) |
选型建议#
- 5 个以下服务、小团队:Multirepo,简单直接
- 10+ 服务、中大型团队、强工程文化:Monorepo,配合 Bazel/Nx/Turborepo
- 混合模式:核心服务 Monorepo + 边缘服务独立仓库,是常见的折中
Google、Meta、Microsoft 内部都是 Monorepo,但他们的工具链(自研构建系统、PB 级代码仓库支持)不是普通团队能复制的。普通团队上 Monorepo 前要先评估工具链能力——没有 Bazel/Nx 这类增量构建工具,Monorepo 在 50+ 服务时会变慢到无法接受。
Git LFS#
Git 设计上不适合存大文件(>50MB 的 blob 性能开始下降)。Git LFS(Large File Storage)是补丁方案——把大文件存在 LFS 服务器,仓库里只保留指针。
工作原理#
text
工作区有大文件 video.mp4 (500MB)
│ git add
▼
LFS filter 把文件内容上传到 LFS 服务器
仓库里只存一个指针文件(几百字节)
│ git commit
▼
commit 里是这个指针(小)
clone 时只拉指针,需要时才拉实际内容配置#
bash
# 安装 Git LFS
git lfs install
# 指定哪些文件用 LFS
git lfs track "*.psd"
git lfs track "*.mp4"
git lfs track "/assets/big-files/*"
# 提交 .gitattributes(LFS 配置进版本控制)
git add .gitattributes
git commit -m "chore: configure Git LFS"
# 正常 add/commit,LFS 自动处理
git add design.psd
git commit -m "feat: add design file"适用场景#
- 设计文件(PSD、AI、Sketch)
- 视频和音频
- 大型数据集(机器学习训练数据)
- 二进制依赖(游戏资源)
注意事项#
- LFS 服务器是单点:GitHub/GitLab 的 LFS 服务有配额和带宽限制
- clone 不会自动拉 LFS 内容:需要
git lfs pull,浅克隆时尤其注意 - LFS 不是免费的:GitHub LFS 配额 1GB 存储 + 1GB 带宽/月,超出要付费
- LFS 迁移困难:历史里已经存的大文件不会自动迁到 LFS,要用
git lfs migrate
Submodule vs Subtree#
跨仓库复用代码的两种方式。
Submodule#
Submodule 把另一个仓库作为子目录嵌入当前仓库:
bash
# 添加 submodule
git submodule add https://github.com/org/shared-lib.git libs/shared
# clone 带 submodule 的仓库
git clone --recurse-submodules https://github.com/org/main-repo.git
# 已 clone 的仓库初始化 submodule
git submodule update --init --recursive
# 更新 submodule 到最新
git submodule update --remote特点:
- 子仓库独立,有自己的 commit 历史
- 主仓库只存子仓库的 commit hash(指针)
- 切分支、checkout 时 submodule 状态可能不一致,要手动同步
痛点:
- detached HEAD:submodule 默认 checkout 到具体 commit,不在任何分支上
- 同步复杂:主仓库切分支,submodule 不会自动跟着切
- recursive 操作多:
--recurse-submodules参数容易忘 - CI 复杂:CI 配置要处理 submodule 拉取
Subtree#
Subtree 把另一个仓库合并进当前仓库的子目录,不再保留独立的仓库引用:
bash
# 添加 subtree
git subtree add --prefix=libs/shared https://github.com/org/shared-lib.git main --squash
# 从上游拉取更新
git subtree pull --prefix=libs/shared https://github.com/org/shared-lib.git main --squash
# 把改动推回上游
git subtree push --prefix=libs/shared https://github.com/org/shared-lib.git feature/change特点:
- 子仓库的代码完全合并进主仓库,历史也合并了
- 不需要特殊命令就能 clone——所有内容都在主仓库里
- 主仓库会变大(合并了子仓库历史)
优势:比 submodule 简单,没有"detached HEAD"和同步问题 劣势:操作命令较慢(要拉子仓库全部历史);推回上游不直观
对比#
| 维度 | Submodule | Subtree |
|---|---|---|
| 子仓库独立性 | 独立(保留引用) | 合并进主仓库 |
| clone 复杂度 | 需要 --recurse-submodules | 普通 clone 即可 |
| 历史保留 | 子仓库独立历史 | 合并进主仓库历史 |
| 同步更新 | 简单(update --remote) | 较慢(subtree pull) |
| 推回上游 | 在子仓库内 commit + push | subtree push,不直观 |
| 主仓库体积 | 小(只存指针) | 大(合并历史) |
| 学习曲线 | 陡(detached HEAD 等坑) | 平缓 |
选型建议#
- 跨团队共享、强独立演进:Submodule(但要团队理解它的状态机)
- 同团队、轻量共享:Subtree(更简单)
- 能 Monorepo 就别用这两个:Submodule 和 Subtree 都是在 Multirepo 下"模拟 Monorepo"的妥协方案。如果共享代码量较大,直接 Monorepo 是更优解。
实战要点#
- Monorepo 不是"先进",Multirepo 不是"落后"。两者都是合理的工程选择,看团队规模和工具链能力。
- Monorepo 必须配构建工具。没有 Bazel/Nx/Turborepo 的 Monorepo,CI 会变成"每次改一行,全仓库构建一遍"。
- Git LFS 是补丁不是银弹。能在源码层面避免大文件就避免(比如把数据集放对象存储,仓库里只存 URL)。
- Submodule 用之前三思。它的状态机复杂,团队成员都要培训才能用对。能 Subtree 就别 Submodule。
- 混合模式很常见。核心业务 Monorepo + 基础设施代码独立仓库 + 文档站独立仓库,是很多公司的现实结构。
小结#
Git 工程化的三个专题——Monorepo/LFS/Submodule——都是"在 Git 基础模型上打补丁"。Monorepo 解决代码组织问题但需要构建工具支持;LFS 解决大文件问题但有单点和配额风险;Submodule/Subtree 解决跨仓库复用但状态机复杂。
理解这些工具的边界比会用更重要——知道什么时候不该用,比知道怎么用更值钱。第一阶段(理念与协作)到此结束,下一阶段进入持续集成 CI。