先说结论:submodule 和 repo 解决的是同一类问题——多仓库协作——但它们的出发点和适用边界完全不同。submodule 是 Git 原生机制,把"子仓库的某一次提交"钉在父仓库里,适合少量依赖关系的场景;repo 是 Google 为 Android 这种超大规模多仓库项目设计的 Python 脚本层,核心是 manifest 清单+批量操作,适合成百上千个仓库的编排。如果你正在纠结自己的项目该用哪个,或者两个都试过但说不清差别,这篇从原理到实操都过一遍,顺便把我踩过的坑也交代清楚。
1. 多仓库管理的本质:你在解决什么问题
先想清楚一个问题:为什么需要多仓库管理?单一仓库不是更简单吗?但实际项目里,多仓库几乎是必然出现的。我见过几种典型情况:
- 组件复用:多个产品共用一套底层库,把库独立成仓库,按需拉取版本。比如你的云平台有十几个微服务,每个服务都依赖同一个 SDK。
- 权限隔离:不同团队维护不同模块,代码权限不能全部放开。A 团队仓库不能让 B 团队随便改。
- 仓库规模控制:一个仓库塞了几十万个文件、几百 G 历史,clone 一次生不如死。拆开之后每个仓库都能独立演进。
- 发布节奏差异:核心库稳定、低频发布,业务库高频迭代,混在一起没法各自打 tag。
多仓库管理工具要解决的,就是"在多个独立 Git 仓库之上,建立一层统一的关联和操作入口"。你需要的核心能力无非三件:
- 版本关联:我当前产品代码,应该配哪个版本的子库?
- 批量操作:几十个仓库我要 clone、切分支、查状态,不能一个个手动操作。
- 原子性保障:一次修改跨多个仓库,怎么确保提交、评审、回滚的一致性?
submodule 和 repo 对这三个问题的回应路径截然不同。理解这一点,后面的选型就不会乱。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Git submodule:原生的"仓库里的指针"
2.1 submodule 的核心机制:提交号才是真相
submodule 的核心机制非常朴素:在父仓库里记录子仓库的 commit SHA-1,而不是某个分支名或 tag。你在父仓库里看到的是一个特殊条目,本质上是一行配置加一个 gitlink 指向。
bash复制# .gitmodules 文件
[submodule "lib/common"]
path = lib/common
url = https://github.com/yourorg/common.git
当你在父仓库执行 git submodule update 时,Git 去子仓库把那个 commit 对应的代码拉下来。注意,submodule 永远不会自动追随子仓库的最新提交——父子之间是一种"钉死"关系。
用生活化的方式理解:父仓库是一份合同,合同里白纸黑字写了"第 3.7 版";子仓库是供应商的货架,你每次拿货只拿合同上写的版本。子仓库的开发者自己怎么改、怎么推,那是他们的事,你的合同不会自动更新。
2.2 常用操作:够用但细节多
把 submodule 用好,核心操作就这几个:
bash复制# 添加一个 submodule(会自动 clone 并记录当前提交)
git submodule add https://github.com/yourorg/common.git lib/common
# 初始化并拉取所有 submodule(新 clone 的仓库必须先 init)
git submodule update --init --recursive
# 子仓库有更新,父仓库想升级关联版本
cd lib/common
git pull origin main
cd ../..
git add lib/common
git commit -m "bump common to latest"
# 批量在所有 submodule 上执行命令(需要自己写循环)
git submodule foreach 'git checkout main && git pull'
# 删除 submodule(容易踩坑,见下文)
git submodule deinit -f lib/common
git rm -f lib/common
rm -rf .git/modules/lib/common
--recursive 表示子仓库里可能还有子仓库,嵌套场景必须带上。
2.3 submodule 的优势:该肯定的地方要肯定
submodule 能在 Git 里存活这么多年,自有它的生态位:
- 零额外依赖:只要装了 Git 就能用,不需要装 Python、不需要拉脚本。对 CI、对同事的机器要求最低。
- 版本粘性极强:父仓库记录的 commit 是精确的,回滚时父子仓库自动保持一致。这一点非常适合"锁定版本发布"的场景。
- 原生支持:GitHub、GitLab、Gitea、Gerrit 全都能正确展示 submodule 指向,Clone 时带
--recurse-submodules就能一次性拉全。 - 适合少量、稳定的依赖:那种一年动不了几次的基础库,用 submodule 记录一下版本。你不需要多仓库编排能力,只需要一个"版本引用"。
2.4 submodule 的痛点:为什么越用越难受
我见过很多团队从 submodule 开始,最后忍无可忍迁走,痛点基本集中在:
- "改哪儿了?"成谜:父仓库的 diff 里只显示
Subproject commit 3f9a...b21c这种一行变化。子仓库内部改了什么完全看不到。代码评审的时候你根本没法判断这次 bump 是否安全。 - 分支漂移是常态:submodule 默认处于 detached HEAD 状态。新人不知情,在 submodule 里改了代码,提交完发现推不上去,因为 HEAD 不指向任何分支。
- 批量操作全要靠 foreach:
git submodule foreach能力太弱,想批量切换分支、批量打 tag、批量看 diff,写 Shell 循环写到怀疑人生。 - 冲突处理反直觉:多人同时 bump 子仓库版本,冲突时不是文本冲突,而是 gitlink 冲突。你得手动
git add,不会处理的人直接懵。 - 删除是灾难:很多教程不写怎么删。只
git rm不够,.git/modules里的残留会让你下次 clone 出问题。
所以 submodule 真正适用的边界是:"少量依赖、低频变更、版本精确可控"。一旦跨到"大量仓库、高频协作、需要统一编排"的场景,submodule 就像用自行车带一火车货——不是不能走,是走得很难受。
3. git-repo:Google 的多仓库"编排大脑"
3.1 repo 的架构:manifest 是所有操作的源头
repo 是 Google 专门为 Android 开发的多仓库管理工具,用 Python 写的,本质是建立在 Git 之上的一层壳。它不替代 Git,而是帮你把几十上百个 Git 仓库当作一个整体来编排。
它的核心心脏是 manifest 仓库——一个只存放 XML 文件的 Git 仓库,描述了你需要哪些子仓库、放到哪个目录、对应哪个分支/提交。
xml复制<?xml version="1.0" encoding="UTF-8"?>
<manifest>
<remote name="origin" fetch="https://git.example.com" />
<default remote="origin" revision="main" />
<project name="platform/frameworks/base" path="frameworks/base" />
<project name="platform/packages/apps/Launcher" path="packages/apps/Launcher" />
<project name="vendor/hal/display" path="vendor/hal/display" revision="refs/tags/v1.2.0" />
</manifest>
name 是仓库路径,path 是放在工作区的哪个目录,revision 是默认分支或 tag。一个 repo sync 会读取这份 manifest,批量克隆/更新所有仓库。你的整个多仓库项目的"物理拓扑"就这一份文件管住。
3.2 repo 的核心能力:sync / upload / forall 三板斧
repo sync 是最常用的命令,相当于批量 git fetch + git merge/reset,把 manifest 里所有项目同步到你指定的版本。第一次运行等价于批量 git clone,后面运行只做增量拉取。
bash复制# 首次拉取:并行 8 个任务
repo init -u https://git.example.com/manifest.git -b main -m default.xml --depth=1
repo sync -c -j8
# 查看整体状态
repo status
# 批量切换分支(所有项目切到 dev 分支)
repo start dev --all
# 批量执行命令
repo forall -c 'git log --oneline -5'
# 跨仓库一起改:所有项目同时创建提交,用 grep 检查
repo forall -c 'git grep -n "TODO_XXX" -- "*.java"'
# 提交到评审系统(Gerrit 场景)
repo upload
repo upload 值得单独说。它把本地所有项目的 pending 提交推送给你配置的代码评审系统(Gerrit),生成独立的评审请求。这在 Android 这种大团队协作里是刚需——每个改动都走评审,而且评审的是多仓库的联合变更。
3.3 repo 的另一个隐藏优势:任务编排与并行
repo 不是简单的"循环执行 git 命令",它内部做了很多优化:
- 并行 clone 时带智能调度,避免过多并发把服务器打挂。
- manifest 支持
include引用其他 XML 文件,不同产品线可以共用一套基线。 - 支持 snapshot(
repo manifest -r),把每个项目的精确 commit 固化成文本,用于复盘和追溯。 - 支持 mirror 模式,服务器端先同步一份全量镜像,客户端从镜像拉取,公司局域网内极其好使。
拿"原子性"来说,submodule 只能在父仓库层面保证"所有 submodule 一起回到某个状态",但 repo 通过 manifest 文件本身就能做到——manifest 是独立仓库,可以走版本管理和评审,整个仓库集合的变更历史本身就是可追溯的。
3.4 repo 的代价:不是免费午餐
repo 也不是银弹,它的前提条件很硬:
- 依赖 Gerrit 生态:repo 的设计和 Gerrit 深度耦合(尤其是 upload 功能)。如果你用的是 GitHub 的 Pull Request,repo 的 upload 基本废掉,只能用
repo upload --cbr这种曲线,或者干脆手动 pull request。 - 学习曲线陡:manifest 的语法、默认分支、
--current-branch标记等一堆概念,新人上手要一个过程。很多第一次用 repo 的人会被repo sync莫名 reset 本地修改吓到。 - Python 依赖:早期版本还要 python2,现在主流 py3 了。Windows 上装各种怪问题,官方支持也弱。
- 操作自由度受限:repo 是"批量控制"思维,你想单仓库搞点个性化操作,反而会觉得被束缚。它的哲学是"所有仓库保持一致",不是"你想怎么玩怎么玩"。
4. 多维度对比:什么时候选谁,一目了然
| 对比项 | Git submodule | git-repo (Repo) |
|---|---|---|
| 定位 | Git 原生功能,记录子仓库"提交号" | Python 工具,管理一批仓库的"清单+编排" |
| 依赖 | 零依赖,Git 自带 | Python3 + Git,需要安装 repo 脚本 |
| 版本记录位置 | 父仓库 .gitmodules + gitlink |
manifest 仓库的 XML 文件 |
| 批量操作能力 | 弱(foreach 基础循环) |
强(sync/start/forall/upload) |
| 代码评审集成 | 无天然集成,依赖你自己处理 | 深度集成 Gerrit |
| 动态追踪分支 | 不支持(必须显式更新提交) | 支持(manifest 指定分支,sync 时自动抓远程) |
| 适合仓库数量 | 几个到十几个 | 几十到上千 |
| 变更可见性 | 父仓库 diff 只有一行提交号 | manifest diff 可评审,多个项目变更可一起评审 |
| Windows 支持 | 好 | 一般(有 make_portable 方案但体验一般) |
| 嵌套结构 | 有递归机制但容易踩坑 | 天然面向嵌套/层次化设计(name 路径即表示层次) |
| 典型场景 | 少量稳定的第三方依赖、简单产品 | 大型平台、多团队、多产品线的整体编排 |
这张表的目标不是让你背结论,而是帮你建立判断框架。先问自己三个问题:
- 仓库规模:超过 20 个仓库,几乎没有悬念选 repo。
- 变更联动频率:如果一次发版常常要改 5 个以上的仓库,submodule 的提交管理会痛苦到你想砸电脑,repo 的"整体 sync + 联合评审"是大杀器。
- 你的协作平台:用 Gerrit 就用 repo,用 GitHub/GitLab 优先 submodule 或者直接别用 submodule(后面说更好的替代)。
5. 实际体验:我在项目中切换的完整过程
5.1 第一阶段:submodule 管理产品依赖
早些年我维护一个嵌入式中间件产品,上层有三个应用模块,共享一个底层协议库。当时选了 submodule:
- 优点:零依赖,GitHub 上 clone 下来就是完整代码,
--recurse-submodules一条命令全拉齐。产品发版打 tag 时,gitlink 保证版本永远一致。 - 痛点:业务团队每周都要升级协议库,每次都要走"进子仓库拉代码、出来父仓库 add gitlink、push、让同事处理冲突"。有新人来实习,在 submodule 里改完代码推不上去,卡了整整一天才搞清 detached HEAD 的问题。这个阶段让我下定决定思考更工程化的方案。
5.2 第二阶段:评估并迁到 repo
后来我们部门重组,要统一管 40 多个仓库,submodule 彻底撑不住了。评估下来最终选了 repo:
- 迁移过程比想象中简单。写个脚本:把所有仓库的 URL 梳理成 manifest,
repo init -u <manifest仓库> -m default.xml,然后repo sync -j16一把拉全。 - 真正提高效率的是
repo start/forall。比如要排查所有服务里某个接口的调用情况,repo forall -c 'grep -rn "old_api" --include="*.py" .',一分钟全局扫完。 - Gerrit 评审也不是问题。一批跨仓库改动,
repo upload生成多个评审,并在 manifest 上做整体标记,回滚的时候有据可依。
5.3 第三阶段:不用 repo 的轻量选择
坦白讲,如果你的团队不在 Gerrit 生态、仓库量也就 5~10 个——submodule 也不是唯一选择。更现代化的方案是 git subtree 或 Android 之外的工具链:
- git subtree:把子仓库的代码"复制"进父仓库的文件夹,有独立的提交历史但也能合并回源。优点是代码对使用者透明、不产生 gitlink(对新人更友好),缺点是子树合并历史比较复杂、团队不熟练时容易冲突。
- 用 Monorepo 思路:仓库规模可控的时候,直接拆包(如 pnpm workspace、Go workspace)比 submodule 更天然。这种"一个仓库管多个模块"的模式在中国互联网公司非常流行,学习曲线最低。
我的体会:选型的关键不是哪个工具更"高级",而是你的团队协作模型、评审平台、仓库数量三个因素的综合匹配。 如果你的仓库数量上 30+、协作平台是 Gerrit、模块间改动频繁联动,repo 几乎是唯一顺手的路径。如果规模很小、平台是 GitHub,我建议你先别想 submodule,考虑 subtree 或 workspace 方案,维护成本更低。
6. 常见问题:都是血泪教训换来的
6.1 submodule 的检测与修复
问题:clone 下来的仓库里 submodule 目录是空的,或者代码不完整。
- 排查:先
git submodule status看每个子模块状态。-前缀表示未初始化,+前缀表示提交不匹配。 - 修复:
git submodule update --init --recursive。如果 .gitmodules 里的 URL 已经失效,手动改 URL 再重试。 - 注意:
git clone --recurse-submodules不是所有同事都会用,写进 README 里比口头强调有效。
问题:在 submodule 里改了东西,切回父仓库时发现"不见了"。
- 原因:submodule 处于 detached HEAD,你的提交挂在临时分支上,父仓库不会记录。
- 避免:进 submodule 前先
git checkout main,改完先推送到远程,再回父仓库 add 新提交号。 - 建议:把"进子仓库必须先 checkout 分支"写进团队的 commit-msg 钩子或文档。
问题:git submodule foreach 在 Windows 上执行 Shell 命令各种怪。
- 经验:Windows 上优先用
git submodule foreach --recursive加简单的git status之类的命令;复杂命令建议写 PowerShell 脚本循环git -C逐个处理。
6.2 repo 的常见坑
问题:repo sync 把本地没提交的修改覆盖了。
- 原因:repo 默认跑
git reset --hard(具体取决于 manifest 的策略),本地未经提交的修改直接丢弃。 - 避免:sync 前先
repo status检查所有仓库是否有未提交的改动,养成习惯。 - 恢复:如果已经被清了,那真的没法找回来。Git 垃圾箱机制不覆盖工作区未提交内容。所以重要改动先 commit。
问题:repo upload 提示没有当前分支或找不到 review。
- 原因:没有
repo start创建本地分支,或者 Gerrit 配置有问题。 - 修复:先
repo start 新分支 --all,再上传。 - 注意:repo 要求每个变更关联到一个 review 分支,这是设计使然,不是 bug。
问题:manifest 里 revision 用分支,导致每次 sync 都是最新代码,产品发版没法稳定复现。
- 经验:发版前要把 manifest 的 revision 锁定为具体的 commit 或 tag。
repo manifest -r可以生成快照,正式发版时提交这份锁定 manifest。 - 另外:团队要约定 --dev 用分支、--release 用 tag 的双 manifest 模式,这是 Android 团队的标准做法。
6.3 选型时的提醒
- 如果你已经有 20+ 仓库,别犹豫,别指望 submodule 加一堆脚本能解决所有问题,那种做法只会让维护负担更重。
- 如果你只有 3 个仓库,用 repo 有点大炮打蚊子。manifest、Gerrit、分支策略都够你折腾的。
- 如果你在 GitHub 上用 submodule 做微服务依赖管理,建议先考虑 git subtree 或者把微服务拆成独立版本号发制品仓库(artifact),不要硬用 submodule。
7. 写在最后:我的真实选择逻辑
我现在的原则很简单:
- 仓库数 > 30、多团队协同、有 Gerrit:直接 repo,没有悬念。
- 仓库数 2~15、依赖关系明确、低频变更:submodule 或将子模块彻底混入主仓库(subtree),看团队 Git 熟练度。
- 仓库数少但业务变化快:优先考虑 Monorepo / workspace 模式。
工具永远是为协作服务的,不要为了"用 repo 很酷"或"submodule 是官方功能"而选。我经历过 submodule 的版本失控,也经历过 repo 刚上手时的批量覆盖,最终沉淀下来的都是流程和习惯,而不仅仅是工具本身。
如果你正在纠结,我的建议是:拿一个周末,把你要管的所有仓库列出来,数一数、看一眼它们之间的关联频率。多数情况下答案自己就出来了。代码世界没有银弹,但一定有一个工具能让你的团队"少疼一次"。
