Git submodule 是什么?
Git submodule 是 Git 原生的多仓库嵌套机制,父仓库只记 commit hash,不存文件内容。
- 是什么:主仓库子目录里嵌 入独立 Git 仓库,各自有 commit 历史
- 三个核心命令:
submodule add/init + update/update --remote - 典型场景:第三方库源码嵌入、独立发布周期的子项目
- 致命坑 1:新 clone 是空目录,必须
init + update才有内容 - 致命坑 2:子模块默认 detached HEAD,commit 容易推不上去
- 致命坑 3:主仓库切分支后忘了
update --remote,代码对不上 - 替代方案:monorepo 处理强耦合,subtree 处理简单嵌入
一个仓库嵌一个仓库
git submodule 是 Git 原生的"仓库套仓库"机制。主仓库里有一个子目录,这个子目录不是普通文件,而是另一个完整的 Git 仓库。主仓库不存这个仓库的文件内容——它只存一个 commit hash 指针。
# 在主仓库里嵌入一个第三方仓库
git submodule add https://github.com/example/lib.git vendor/lib
执行后:
- 主仓库多出
.gitmodules文件(记录 submodule 的 URL 和路径) - 主仓库多出
vendor/lib/目录(空,但有 .git 引用) - 主仓库 index 里,
vendor/lib对应一条 gitlink(mode 160000),不是普通文件
也就是说:你提交到主仓库的,只是"子仓库当前指向哪个 commit"。子仓库的文件本身仍在那个独立仓库里。
三个核心命令
submodule 的工作流就是这三步的循环。1. 添加:git submodule add
git submodule add <url> <path>
把外部仓库钉到主仓库的 <path>。自动写入 .gitmodules + 暂存 gitlink。
2. 克隆后初始化:git submodule init && git submodule update
git clone https://github.com/me/main-repo.git
cd main-repo
git submodule init # 注册 .gitmodules 里的配置
git submodule update # 按 commit hash 拉取子仓库内容
或者一步到位:
git clone --recurse-submodules https://github.com/me/main-repo.git
--recurse-submodules 是新 clone 时一次拉全部子仓库的命令。
3. 更新子仓库:git submodule update --remote
# 把子仓库切到上游最新 commit,并更新主仓库的 gitlink
git submodule update --remote vendor/lib
跑完后,主仓库的 gitlink 也变了——你还得 git add . && git commit 才能把这个变化推到主仓库。
典型场景
submodule 不是为"代码复用"设计的,是为"独立项目嵌入"设计的。适合用 submodule 的情况:
- 第三方库的源码嵌入:你想跟某个库的特定 commit 一起发版,而不是用 npm 装
- 多个独立发布周期的项目:子项目有自己的版本号、主项目有自己的版本号
- 权限隔离:子仓库是另一个 team 维护,主仓库只读某个 commit
- 构建期拉取:CI 里按 commit 锁定,避免上游 breaking change 突然进入
不适合 submodule 的情况(这是大多数项目):
- 多个项目共享代码、要频繁互相改动 → monorepo
- 只是想把别人的 仓库复制一份进来用 → 直接 clone + 复制粘贴
- 想"git pull 一次把所有依赖都更新了" → 不要用 submodule
4 个常见坑
刚 clone 完主仓库,vendor/lib 是个空文件夹。这是因为 submodule 的文件不存主仓库——你必须 git submodule update --init --recursive 才会拉取内容。
坑 1:clone 完看不到文件。 上面这个。其他开发者第一次拉你的项目时,100% 会遇到。要么文档里写清楚,要么用 --recurse-submodules 兜底。
坑 2:submodule 里的 commit 推不上去。 submodule 默认 checkout 在 detached HEAD 状态(钉在某个 commit hash 上)。你在里面改代码 commit 后,必须切回分支再 push:
cd vendor/lib
# 当前 detached HEAD,commit 完会丢失
git checkout main
git pull # 把刚才的 commit rebase 上去
坑 3:主仓库 commit 忘了更新 submodule。 你在主仓库切了分支,但 submodule 没 update --remote,主仓库代码和子仓库代码版本错位——本地能跑,CI 炸。
坑 4:删除 submodule 很麻烦。 Git 没有 git submodule rm,标准流程是:
git submodule deinit -f vendor/lib
git rm -f vendor/lib
rm -rf .git/modules/vendor-lib
git commit -m "remove submodule vendor/lib"
少一步都会留下垃圾。
现代替代方案
submodule 不是银弹。多数人想要的是"代码复用 + 同步",monorepo / subtree 更合适。| 方案 | 隔离性 | 同步成本 | 适用场景 |
|---|---|---|---|
| submodule | 强(独立仓库) | 高(手动 update) | 第三方嵌入、独立发布周期 |
| subtree | 弱(同仓库) | 低(merge 操作) | 简单嵌入、偶尔同步 |
| monorepo | 无(一个仓库) | 极低(同一 commit) | 强耦合、频繁跨项目改 |
subtree 怎么用(官方文档):
# 把外部仓库合并成主仓库的子树
git subtree add --prefix=vendor/lib https://github.com/example/lib.git main --squash
# 后续拉上游更新
git subtree pull --prefix=vendor/lib https://github.com/example/lib.git main --squash
subtree 看起来像普通目录,没有 detach HEAD 这种坑,但失去了子项目的独立历史。
一句话总结
submodule 解决的是"独立项目嵌入",不是"代码复用"——如果你想要后者,monorepo 几乎是更优解。
记住三件事:
- 父仓库只存 commit hash,不存文件
- 新 clone 必须
init + update才有内容 - detach HEAD / 同步错位是 90% 的坑的来源
不需要的时候别强行用。
Read More
- Pro Git Book - Submodules 章节
- Git 官方文档 - git-submodule
- Atlassian: Git submodules vs subtrees
- JS Monorepo 怎么起步? —— 同站 monorepo 详解,可作为 submodule 的对照方案