强曰为道

与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

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 对比

特性SubmoduleSubtree
代码存储独立仓库,只存引用直接合并到主仓库
克隆方式需要 --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

仓库对比表

考量维度MonorepoPolyrepo
代码共享直接引用通过包管理器
原子提交✅ 跨项目原子提交❌ 需要协调
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 - 变基进阶