🧩 Git Submodule 完整指南
Submodule 让一个仓库引用另一个仓库,被引用的仓库以独立 git 目录存在于父仓库内,父仓库只记录「指向哪个 commit」的一个指针。
🎯 解决什么问题
需要把另一个仓库的代码放进当前仓库一起管理时:
- 引入第三方库,但不想 vendor 源码(不想自己维护 patch)
- 公司有公共库,多个仓库都要用同一份
- monorepo 想拆成多个独立仓库,但又要保证原子提交
⚖️ 先想清楚要不要用
| 方案 | 何时选 |
|---|---|
| Submodule | 需要引用外部仓库、且要跟着主仓库版本锁定 |
| Copy 代码进仓库 | 库很小、很少更新、能接受手工同步 |
| 包管理器(npm/pip 等) | 消费的是「发布物」而非「源码仓库」 |
| Monorepo | 多个仓库需要频繁一起改动、需要原子提交 |
| Git subtree | 同上,但不想要 .gitmodules 目录,想让代码看起来是本仓库的 |
Submodule 有明确的负面体验(下面「坑」一节),能用 monorepo 就别用 submodule。
🛠️ 基本操作
添加 submodule
# 添加(会 clone 到 ./libs/xxx)
git submodule add https://github.com/org/lib.git libs/lib
# 也可以指向本地路径
git submodule add ../lib libs/lib
# 添加后自动做了两件事:
# 1. 在 .gitmodules 写入配置
# 2. 在索引里加了一个 gitlink(mode 160000),指向 lib 的某个 commit.gitmodules 内容:
[submodule "libs/lib"]
path = libs/lib
url = https://github.com/org/lib.git日常使用
# 初始化(clone 带 submodule 的仓库后,第一件事)
git submodule update --init --recursive
# 拉取 submodule 的更新
git submodule update --remote
# 进入 submodule 改代码
cd libs/lib
git checkout -b fix-bug
# ...改...
git commit -am "fix: xxx"
git push origin fix-bug
# 回到父仓库,提交指针更新
cd ../..
git add libs/lib
git commit -m "chore: update lib submodule to fix-bug"克隆带 submodule 的仓库
# 方式 1:clone 后初始化
git clone https://github.com/me/repo.git
cd repo
git submodule update --init --recursive
# 方式 2:clone 时直接带上(更常用)
git clone --recurse-submodules https://github.com/me/repo.git
# 已经 clone 过了,之后补
git submodule update --init --recursive🔖 指针机制
Submodule 在父仓库里存的不是代码,而是一个指针:
$ git ls-tree HEAD libs/
160000 commit 8f4a2b1c3d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a libs/lib
# ^^^^^^ ↑ 这就是「gitlink」:mode 160000 + 一个 commit SHA160000 是 gitlink 的文件模式,只指向 submodule 当前的 commit,不含分支信息。
由此推出两条重要规则:
- 父仓库的 commit 锁定了 submodule 的精确版本。别人 clone 你的父仓库,拿到的 submodule 是那个 commit,不是最新代码
- submodule 自己的 commit 不会自动被父仓库记录。你在 submodule 里 commit 了,父仓库
git status仍然是干净的(除非指针真的变了)
查看指针状态
git diff --submodule=log # 看 submodule 指针变了哪些 commit
git submodule status # 前缀 + 表示未同步到父仓库记录的版本📌 分支跟踪
submodule 指针不带分支信息,需要显式告诉它「我关心哪个分支」:
# 手动指定要跟的分支
git submodule set-branch --branch main libs/lib
git submodule update --remote
# 或者在 .gitmodules 里固定
[submodule "libs/lib"]
path = libs/lib
url = https://github.com/org/lib.git
branch = main⚠️ 常见坑
1. clone 后没 --init,目录是空的
症状:libs/lib/ 目录存在但是空的。
原因:submodule 的 git 目录存在 .git/modules/ 下,工作区只放工作文件,不自动 checkout。
解决:
git submodule update --init --recursive预防:clone 时始终加 --recurse-submodules,写进项目 README。
2. 父仓库 git status 干净,但 submodule 有未提交改动
反过来也是:改了 submodule 的代码没 commit,父仓库完全不知道。父仓库只关心指针(commit SHA),工作区脏不脏它不查。
解决:提交顺序永远是「先在 submodule 提交并 push,再在父仓库更新指针」。反过来(先提交父仓库指针,后推 submodule)会导致别人拉不到那个 commit。
3. 指向了本地绝对路径
git submodule add ../lib libs/lib
git submodule add /Users/me/projects/lib libs/lib # 别人肯定拉不了解决:用相对路径或 HTTPS URL,提交前检查 .gitmodules 里的 url。
4. --remote 拉到了破坏性更新
git submodule update --remote 会拉到上游最新 commit,可能是不兼容的大版本。
解决:
# 先进 submodule 看 diff
cd libs/lib && git fetch && git log --oneline HEAD..origin/main
# 确认后再拉
git submodule update --remote libs/lib5. 嵌套 submodule 忘了 --recursive
# 只初始化一层
git submodule update --init
# 初始化所有层级
git submodule update --init --recursive.gitmodules 缺失时,submodule 会被当成普通目录,git add 之后变成一堆散乱文件。
6. CI 上没有凭证
私有 submodule 在 CI 上 clone 会失败(需要 SSH key 或 token)。这是用 submodule 的隐性成本。
7. diff 难读
git diff 对 submodule 只显示一行 SHA 变化,看不出代码改了什么。
解决:
git diff --submodule=diff # 显示实际代码 diff
git diff --submodule=log # 显示 commit log🔄 常用命令速查
# 添加
git submodule add <url> <path>
# 初始化(clone 后必做)
git submodule update --init --recursive
# 查看状态
git submodule status # 每个 submodule 的状态和 SHA
git submodule foreach 'git status' # 逐个看工作区状态
git submodule foreach 'git pull' # 逐个拉取更新
# 更新到上游最新
git submodule update --remote [path]
# 切换 submodule 分支
git submodule set-branch --branch <branch> <path>
# 提交前检查
git diff --submodule=log
git diff --submodule=diff
# 临时禁用/启用
git submodule deinit libs/lib # 禁用(清空工作区但保留配置)
git submodule init libs/lib # 重新启用
# 删除
git submodule deinit -f libs/lib
git rm -f libs/lib
rm -rf .git/modules/libs/lib💡 使用建议
- 能在 monorepo 里解决就别用 submodule。Submodule 的版本管理、CI 配置、协作摩擦成本都高于 monorepo
- 必须使用时,约定好流程:先 push submodule 的 commit,再更新父仓库指针
- README 里写清 clone 步骤,包括
--recurse-submodules - CI 凭证提前配好,不要等到流水线挂了才排查
- 消费发布物而不是源码的,就别用 submodule,用包管理器
🔗 相关
- Git Worktree 完整指南 —— 同样是「多工作目录」但机制不同
- Git Subtree 替代方案
- Monorepo 与多仓库的取舍
- Turborepo:Monorepo 构建编排与缓存
- GitHub 仓库复用:Fork、Template 与 Import
最后更新:2026-10-05