Git 完全指南 / 10 - 子模块:submodule、subtree、monorepo 策略
第十章:子模块与多仓库管理
当项目依赖其他 Git 仓库时,子模块和子树提供了不同的管理方案。
10.1 Git Submodule
子模块允许你将一个 Git 仓库作为另一个仓库的子目录,同时保持独立的版本历史。
10.1.1 添加子模块
# 添加子模块
$ git submodule add https://github.com/lib/library.git libs/library
# 指定分支
$ git submodule add -b main https://github.com/lib/library.git libs/library
# 结果:创建 .gitmodules 文件和 libs/library 目录
$ cat .gitmodules
[submodule "libs/library"]
path = libs/library
url = https://github.com/lib/library.git
branch = main
10.1.2 克隆含子模块的仓库
# 方法 1:克隆后初始化子模块
$ git clone https://github.com/user/project.git
$ cd project
$ git submodule init
$ git submodule update
# 方法 2:一步完成
$ git clone --recursive https://github.com/user/project.git
# 方法 3:克隆后递归初始化
$ git clone https://github.com/user/project.git
$ cd project
$ git submodule update --init --recursive
10.1.3 更新子模块
# 更新所有子模块到最新提交
$ git submodule update --remote
# 更新特定子模块
$ git submodule update --remote libs/library
# 更新并合并
$ git submodule update --remote --merge
# 更新并变基
$ git submodule update --remote --rebase
10.1.4 子模块日常工作流
# 进入子模块目录
$ cd libs/library
# 子模块有独立的 Git 仓库
$ git status
$ git checkout main
$ git pull
# 回到主仓库
$ cd ../..
# 查看子模块状态
$ git submodule status
abc1234 libs/library (heads/main)
# 提交子模块变更
$ git add libs/library
$ git commit -m "Update library submodule to latest"
10.1.5 删除子模块
# 1. 从 .gitmodules 删除配置
$ git config -f .gitmodules --remove-section submodule.libs/library
# 2. 从暂存区删除
$ git rm --cached libs/library
# 3. 删除子模块目录
$ rm -rf libs/library
$ rm -rf .git/modules/libs/library
# 4. 提交变更
$ git commit -m "Remove library submodule"
10.2 Git Subtree
Subtree 将子仓库的代码直接合并到主仓库中,不需要额外的 .gitmodules 文件。
10.2.1 添加子树
# 添加子树(前缀目录)
$ git subtree add --prefix=libs/library https://github.com/lib/library.git main --squash
# --squash 表示压缩子仓库历史为一个提交
10.2.2 更新子树
# 拉取子树最新代码
$ git subtree pull --prefix=libs/library https://github.com/lib/library.git main --squash
# 推送本地修改到上游仓库
$ git subtree push --prefix=libs/library https://github.com/lib/library.git main
10.2.3 分割子目录为独立仓库
# 将某个子目录的历史分离为独立分支
$ git subtree split --prefix=libs/library --branch=library-split
# 推送到独立仓库
$ git push https://github.com/lib/library.git library-split:main
10.3 Submodule vs Subtree 对比
| 特性 | Submodule | Subtree |
|---|
| 代码存储 | 独立仓库,只存引用 | 直接合并到主仓库 |
| 克隆方式 | 需要 --recursive | 无需额外操作 |
| 更新方式 | 进入子目录手动更新 | subtree pull |
| 历史记录 | 保留完整子仓库历史 | 可选择压缩历史 |
| 推送修改 | 在子模块目录推送 | subtree push |
| 删除难度 | 较复杂 | 简单 |
| 适用场景 | 大型依赖库 | 小型共享代码 |
| 学习曲线 | 较陡 | 较平缓 |
10.4 Monorepo vs Polyrepo
架构对比
| 策略 | 说明 | 代表项目 |
|---|
| Monorepo | 所有代码在一个仓库 | Google、Facebook、Babel |
| Polyrepo | 每个服务/库独立仓库 | 大多数开源项目 |
| 混合模式 | 核心代码 Monorepo + 外部依赖 Polyrepo | 很多企业项目 |
Monorepo 管理工具
# Git Sparse Checkout(稀疏检出,只检出部分目录)
$ git clone --no-checkout https://github.com/org/monorepo.git
$ cd monorepo
$ git sparse-checkout init --cone
$ git sparse-checkout set services/api libs/shared
$ git checkout main
# 或使用 Git 2.25+ 的简化方式
$ git clone --filter=blob:none --sparse https://github.com/org/monorepo.git
$ cd monorepo
$ git sparse-checkout set services/api libs/shared
仓库对比表
| 考量维度 | Monorepo | Polyrepo |
|---|
| 代码共享 | 直接引用 | 通过包管理器 |
| 原子提交 | ✅ 跨项目原子提交 | ❌ 需要协调 |
| CI/CD | 需要增量构建 | 每个仓库独立 |
| 权限控制 | 粗粒度 | 细粒度 |
| 仓库大小 | 可能很大 | 相对较小 |
| 开发体验 | 统一工具链 | 独立技术栈 |
| 代码复用 | 容易 | 需要发布包 |
10.5 常见问题
子模块 detached HEAD
# 子模块默认处于 detached HEAD 状态
$ cd libs/library
$ git status
HEAD detached at abc1234
# 切换到分支解决
$ git checkout main
$ git pull
子模块冲突解决
# 更新子模块时遇到冲突
$ git submodule update --remote --merge
CONFLICT (content): Merge conflict in libs/library
# 进入子模块解决冲突
$ cd libs/library
$ git merge --abort # 或手动解决冲突
$ git checkout main # 切换到正确分支
$ cd ../..
$ git add libs/library
$ git commit -m "Resolve submodule conflict"
业务场景
| 场景 | 推荐方案 |
|---|
| 引用外部库且需要固定版本 | Submodule |
| 共享内部工具库 | Subtree 或私有包管理 |
| 微服务单体仓库 | Monorepo + Sparse Checkout |
| 大型单体应用 | Monorepo |
| 多团队协作 | Monorepo + 代码所有权 (CODEOWNERS) |
扩展阅读
🔗 上一章:09 - 标签管理 | 下一章:11 - 变基进阶