Skip to content

Git 仓库组织与依赖策略

项目需要引用另一个代码库时,首先要决定所有权、版本和发布边界,再选择 Submodule、Subtree、Monorepo、多仓库或包管理器。工具没有统一优胜者,关键是变更是否需要原子提交、团队是否共享发布周期、依赖是否有稳定制品。

Git 仓库组织策略

先明确四个边界

选型前回答:

  1. 两部分代码是否经常需要在一次变更中同时修改?
  2. 是否由同一团队维护并遵守相同权限规则?
  3. 是否共享构建、测试和发布周期?
  4. 依赖方需要源码,还是只需要稳定版本的制品?

如果依赖能够形成稳定包,优先通过 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 integration

Subtree 的优点是使用者无需理解嵌套仓库;代价是同步命令更复杂,外部历史可能进入主仓库,双向修改容易形成所有权模糊。

包管理器与制品仓库

如果依赖方只需要编译结果或稳定接口,应优先发布制品:

text
源码仓库 -> 构建与测试 -> 版本化制品 -> 依赖锁定 -> 部署

可靠依赖记录至少包含:

  • 精确版本或不可变摘要。
  • 来源仓库和对应提交。
  • 许可证与安全扫描结果。
  • 升级说明和兼容范围。
  • 回滚到上一个版本的路径。

分支名和浮动标签会变化,不适合作为可复现交付的唯一依据。

真实选型场景

多个业务服务共享协议定义

协议定义变化需要多个服务同时升级,但服务独立部署。可以把协议定义发布成版本化包,消费者按兼容范围升级;只有经常要求原子修改时,再考虑 Monorepo。

固件仓库引用芯片厂商 SDK

需要精确固定上游提交、保留上游历史且本地很少修改时,可选 Submodule。交付脚本必须递归拉取并验证提交存在。

应用需要内置一个小型第三方工具

希望普通 Clone 立即可用,又需要偶尔同步上游时,可选 Subtree。必须明确本地是否允许修改,以及修改怎样回流。

同一产品的前端、后端和公共库高频联动

如果改一个契约经常要同时修改三处,并且共享 CI 和发布门禁,Monorepo 能降低跨仓协调成本;需要配套影响分析和模块负责人制度。

决策流程

text
依赖是否有稳定制品?
├─ 有 -> 优先包管理器和制品仓库
└─ 没有
   ├─ 是否高频原子修改? -> 是:评估 Monorepo
   └─ 是否必须引用外部源码?
      ├─ 精确固定外部提交 -> Submodule
      ├─ 普通 Clone 必须完整 -> Subtree
      └─ 团队和发布独立 -> 多仓库 + 版本契约

选型后要用一次真实变更验证:修改、评审、CI、发布、升级和回滚是否都能被追踪。只比较目录外观无法判断长期维护成本。

别急,先让缓存热一下。