🧩 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 SHA

160000 是 gitlink 的文件模式,只指向 submodule 当前的 commit,不含分支信息。

由此推出两条重要规则:

  1. 父仓库的 commit 锁定了 submodule 的精确版本。别人 clone 你的父仓库,拿到的 submodule 是那个 commit,不是最新代码
  2. 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/lib

5. 嵌套 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,用包管理器

🔗 相关


最后更新:2026-10-05