最近在公司的项目中有一部分的代码需要多个项目共用(其实这部分代码就是我所写的一些工具代码),简单调查了一下发现有 subtree(子树)和 submodule(子模块)这两种方式,于是乎就有了这一篇文章
以下是 Git 子树(Subtree)与子模块(Submodule)的详细对比,涵盖核心概念、使用场景及操作差异,并附对比表格:
一、核心区别
| 对比维度 | Git 子模块(Submodule) | Git 子树(Subtree) |
|---|---|---|
| 存储方式 | 子模块是主仓库中的一个引用,指向子仓库的特定提交,子仓库独立存在。 | 子树是主仓库中的一个完整副本,子仓库的代码和历史直接合并到主仓库中。 |
| 版本控制 | 子模块的提交历史独立于主仓库,主仓库仅记录子模块的提交 ID。 | 子树的提交历史与主仓库共享,子仓库的修改会体现在主仓库的提交记录中。 |
| 依赖管理 | 需要显式维护子模块的版本,主仓库与子模块的更新需分别操作。 | 子树的代码与主仓库代码统一管理,更新和修改可直接通过主仓库完成。 |
| 工作流程 | 克隆主仓库后需额外执行 git submodule init 和 git update 初始化子模块。 | 开发者无需特殊操作,子树被视为普通目录,工作流程与常规 Git 操作一致。 |
| 文件结构 | 生成 .gitmodules 文件记录子模块信息,子模块代码存放在独立目录中。 | 无额外配置文件,子树的代码完全嵌入主仓库目录结构。 |
| 更新与同步 | 需手动执行 git submodule update --remote 拉取子模块更新,并提交主仓库中的子模块引用变更。 | 通过 git subtree pull 合并子仓库更新到主仓库,或直接提交子树的修改到主仓库。 |
| 适用场景 | 适合需要独立维护的组件库(如公共依赖库),且需与主仓库版本解耦的场景。 | 适合紧密集成的子项目,或希望将子项目代码与主项目统一管理的场景。 |
二、详细解析
1. 存储与版本管理
- 子模块
子模块通过.gitmodules文件记录子仓库的 URL 和路径,主仓库仅存储子模块的提交 ID。子模块的代码独立维护,开发者需分别管理主仓库和子模块的提交。例如,更新子模块需进入子模块目录执行git pull,再提交主仓库中的子模块引用变更。 - 子树
子树将子仓库的代码和历史直接合并到主仓库中,无独立配置文件。子树的修改会反映在主仓库的提交记录中,且更新通过git subtree pull或git subtree push完成,无需切换目录。
2. 协作与维护
- 子模块
- 优点:子模块的独立性适合需要多团队协作的场景,例如多个父项目共用同一子模块时,可确保子模块的版本一致性。
- 缺点:克隆主仓库后需手动初始化子模块(
git submodule update --init),且删除子模块步骤繁琐(需清理.gitmodules和缓存)。
- 子树
- 优点:对开发者透明,适合不希望暴露子项目独立性的场景。例如,将第三方库嵌入主项目时,子树可简化依赖管理。
- 缺点:历史记录混杂,难以单独追踪子仓库的修改。
3. 更新与冲突处理
- 子模块
子模块的更新需显式操作,若子仓库有更新,需在主仓库中提交新的子模块提交 ID。若多个开发者同时修改子模块,可能因版本不一致导致冲突。 - 子树
子树的更新通过合并完成,类似常规分支合并。若子仓库和主仓库同时修改同一文件,需手动解决冲突,但整体流程更接近标准 Git 操作。
4. 典型场景
- 子模块适用场景
- 公共工具库(如通用组件、SDK)需要被多个主项目引用。
- 子项目需独立版本控制,且主项目需灵活切换子项目版本。
- 子树适用场景
- 第三方库的直接嵌入(如开源项目依赖的代码片段)。
- 子项目与主项目高度耦合,无需独立维护。
三、总结建议
- 选择子模块:若需子项目独立维护、版本解耦,或需要跨项目共享同一子仓库。
- 选择子树:若希望简化依赖管理、减少协作复杂度,或子项目无需独立存在。
提示:Git 官方自 1.7.11 版本起推荐优先使用子树,因其操作更直观且与常规 Git 流程兼容性更好。
由于子树推送时,无法直观的看出推送了哪些文件,我还是选择 git 仓库里面嵌入 git 仓库,虽然要提交两次,但是至少清晰直观