Appearance
Git 仓库组织与依赖策略
项目需要引用另一个代码库时,首先要决定所有权、版本和发布边界,再选择 Submodule、Subtree、Monorepo、多仓库或包管理器。工具没有统一优胜者,关键是变更是否需要原子提交、团队是否共享发布周期、依赖是否有稳定制品。
先明确四个边界
选型前回答:
- 两部分代码是否经常需要在一次变更中同时修改?
- 是否由同一团队维护并遵守相同权限规则?
- 是否共享构建、测试和发布周期?
- 依赖方需要源码,还是只需要稳定版本的制品?
如果依赖能够形成稳定包,优先通过 Maven、npm、PyPI、Go Module、容器镜像或内部制品库发布版本。把源码仓库直接嵌套通常意味着依赖尚未形成清晰的发布契约。
方案对比
| 方案 | 版本边界 | 跨模块原子修改 | 克隆体验 | 适合场景 |
|---|---|---|---|---|
| Monorepo | 一个仓库统一提交 | 强 | 一次 Clone,规模可能很大 | 共享工具链、频繁联动修改 |
| 多仓库 | 每个组件独立历史 | 弱,需要协调版本 | 每个仓库独立 | 团队、权限、发布周期独立 |
| Submodule | 主仓库记录子仓库提交 | 主从仓库分别提交 | 需要额外初始化 | 精确引用外部源码,不合并历史 |
| Subtree | 外部代码进入主仓库目录 | 主仓库内可原子提交 | 普通 Clone 即可 | 希望内置源码并保留同步通道 |
| 包管理器 | 依赖制品版本 | 通过版本发布协调 | 普通依赖安装 | 有稳定接口和发布流程的组件 |
Monorepo
Monorepo 把多个应用或库放在一个仓库中。它的核心收益不是目录统一,而是跨模块修改可以形成一个提交,并由同一套 CI 验证。
适合:
- 多个模块经常同时修改。
- 共享构建、格式化、测试和依赖升级工具。
- 权限边界相近。
- 能建设增量构建、影响分析和 CODEOWNERS。
风险:
- 文件和历史规模持续增长。
- 全量 CI 成本高。
- 细粒度权限和发布流程更复杂。
- 目录耦合可能掩盖接口边界。
Monorepo 仍应为每个模块定义负责人、公共接口、依赖方向和独立测试。仓库统一不等于架构可以互相穿透。
多仓库
多仓库让每个组件拥有独立权限、历史和发布周期。它适合边界稳定、团队自治、需要独立交付的系统。
需要补齐的工程能力:
- 接口契约和兼容策略。
- 制品版本、来源和校验信息。
- 跨仓变更的发布顺序。
- 自动依赖升级和回滚。
- 集成测试环境。
如果一次业务变更经常需要同时创建多个 PR、按固定顺序发布并人工追踪版本,多仓协调成本可能已经超过隔离收益。
Submodule
Submodule 让主仓库记录另一个仓库的某个提交。主仓库保存的是 Gitlink,不会复制子仓库的完整历史。
添加:
bash
git submodule add https://example.com/platform/sdk.git third_party/sdk
git commit -m "chore: add sdk submodule"克隆:
bash
git clone --recurse-submodules <repository-url>已有工作区初始化:
bash
git submodule update --init --recursive更新依赖:
bash
git -C third_party/sdk fetch
git -C third_party/sdk switch --detach <target-commit>
git add third_party/sdk
git commit -m "chore: update sdk revision"判断重点:
- 主仓库记录的是提交,不是“始终跟随某分支”。
- 子模块目录可能处于 Detached HEAD,这是精确版本引用的正常结果。
- 子模块的新提交必须先推送到它自己的远端,再提交主仓库指针。
- CI、归档和发布脚本必须显式初始化递归子模块。
Subtree
Subtree 把外部项目内容合入主仓库的某个目录。普通 Clone 可以直接得到全部文件,不需要额外初始化。
添加外部项目:
bash
git subtree add \
--prefix=third_party/sdk \
https://example.com/platform/sdk.git main \
--squash拉取更新:
bash
git subtree pull \
--prefix=third_party/sdk \
https://example.com/platform/sdk.git main \
--squash把本地修改推回外部仓库:
bash
git subtree push \
--prefix=third_party/sdk \
https://example.com/platform/sdk.git integrationSubtree 的优点是使用者无需理解嵌套仓库;代价是同步命令更复杂,外部历史可能进入主仓库,双向修改容易形成所有权模糊。
包管理器与制品仓库
如果依赖方只需要编译结果或稳定接口,应优先发布制品:
text
源码仓库 -> 构建与测试 -> 版本化制品 -> 依赖锁定 -> 部署可靠依赖记录至少包含:
- 精确版本或不可变摘要。
- 来源仓库和对应提交。
- 许可证与安全扫描结果。
- 升级说明和兼容范围。
- 回滚到上一个版本的路径。
分支名和浮动标签会变化,不适合作为可复现交付的唯一依据。
真实选型场景
多个业务服务共享协议定义
协议定义变化需要多个服务同时升级,但服务独立部署。可以把协议定义发布成版本化包,消费者按兼容范围升级;只有经常要求原子修改时,再考虑 Monorepo。
固件仓库引用芯片厂商 SDK
需要精确固定上游提交、保留上游历史且本地很少修改时,可选 Submodule。交付脚本必须递归拉取并验证提交存在。
应用需要内置一个小型第三方工具
希望普通 Clone 立即可用,又需要偶尔同步上游时,可选 Subtree。必须明确本地是否允许修改,以及修改怎样回流。
同一产品的前端、后端和公共库高频联动
如果改一个契约经常要同时修改三处,并且共享 CI 和发布门禁,Monorepo 能降低跨仓协调成本;需要配套影响分析和模块负责人制度。
决策流程
text
依赖是否有稳定制品?
├─ 有 -> 优先包管理器和制品仓库
└─ 没有
├─ 是否高频原子修改? -> 是:评估 Monorepo
└─ 是否必须引用外部源码?
├─ 精确固定外部提交 -> Submodule
├─ 普通 Clone 必须完整 -> Subtree
└─ 团队和发布独立 -> 多仓库 + 版本契约选型后要用一次真实变更验证:修改、评审、CI、发布、升级和回滚是否都能被追踪。只比较目录外观无法判断长期维护成本。
