Skip to main content

Git submodule 是什么?

· 6 min read

Git submodule 是 Git 原生的多仓库嵌套机制,父仓库只记 commit hash,不存文件内容。

  1. 是什么:主仓库子目录里嵌入独立 Git 仓库,各自有 commit 历史
  2. 三个核心命令submodule add / init + update / update --remote
  3. 典型场景:第三方库源码嵌入、独立发布周期的子项目
  4. 致命坑 1新 clone 是空目录,必须 init + update 才有内容
  5. 致命坑 2:子模块默认 detached HEAD,commit 容易推不上去
  6. 致命坑 3:主仓库切分支后忘了 update --remote,代码对不上
  7. 替代方案: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 看到的是空目录

刚 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