子模块(Submodule)
本教程共 26 篇 · 第 21 篇 · 更新于 2026-07-29 · 约 6 分钟阅读
21. 子模块(Submodule)
本节目标:学会在一个 Git 仓库中嵌入另一个 Git 仓库,理解子模块的”快照”本质,避免协作时最常见的坑。
为什么需要子模块
你正在开发一个网站,需要用到一个第三方库。这个库有自己的 Git 仓库,自己发版、自己更新。你想把它引入你的项目,但又想保持两个仓库的獨立。
直接拷贝代码进去?可以,但下次上游更新了,你得手动重新拷贝。
子模块解决了这个问题。它把一个 Git 仓库嵌到另一个仓库里当子目录。两个仓库的提交历史保持独立。
添加子模块
在项目根目录执行:
git submodule add https://github.com/example/lib.git libs/lib
这做了三件事:
- 把
lib仓库克隆到libs/lib目录 - 创建(或修改)
.gitmodules文件,记录映射关系 - 把子模块注册到暂存区
查看 .gitmodules 文件:
[submodule "libs/lib"]
path = libs/lib
url = https://github.com/example/lib.git
提交这个变更:
git commit -m "添加 lib 子模块"
Note子模块目录在 Git 眼里是一个特殊的”指针”,指向子仓库的某个具体提交。它的文件模式是
160000,不是普通的文件或目录。
克隆含子模块的项目
这是最容易踩坑的地方。普通 git clone 之后,子模块目录是空的。
错误示范
git clone https://github.com/example/myproject.git
cd myproject
ls libs/lib # 空的!
正确方式一:clone 时带上参数
git clone --recurse-submodules https://github.com/example/myproject.git
这会自动初始化并拉取所有子模块,包括嵌套的子模块。
正确方式二:先 clone 再初始化
如果 clone 时忘了加 --recurse-submodules:
git submodule update --init
如果有嵌套子模块(子模块里还有子模块):
git submodule update --init --recursive
也可以分开执行:
git submodule init # 注册子模块
git submodule update # 拉取子模块内容
子模块的工作原理
关键点:主项目不跟踪子模块的文件内容,只跟踪子模块的提交哈希。
当你查看主项目的 diff 时:
git diff --submodule
Submodule libs/lib c3f01dc..d0354fc:
> 修复内存泄漏
> 优化查询性能
主项目只记录”子模块当前指向提交 d0354fc”。至于这个提交里改了哪些文件,主项目不关心。
这意味着:子模块的作者更新了仓库,你的主项目不会自动跟着变。你得主动去拉取。
更新子模块
方法一:手动进入子模块拉取
cd libs/lib
git fetch
git merge origin/master
cd ../..
git add libs/lib
git commit -m "更新 lib 子模块到最新版本"
方法二:用 update —remote
git submodule update --remote libs/lib
git add libs/lib
git commit -m "更新 lib 子模块"
--remote 让 Git 进入子模块,拉取最新代码,然后检出主项目记录的那个分支的最新提交。
如果想指定子模块跟踪哪个分支:
git config -f .gitmodules submodule.libs/lib.branch stable
常见坑
坑一:子模块有未提交的修改
你在子模块目录改了文件但没提交。回到主项目,git status 会提示:
modified: libs/lib (modified content)
但主项目不会帮你提交这些改动。你得先进子模块提交,再回主项目提交指针变更。
坑二:子模块处于”分离 HEAD”状态
git submodule update 默认检出的是具体的提交哈希,而不是分支。这会让子模块处于分离 HEAD 状态。
如果你在子模块里直接修改并提交,这个提交会悬空。切到别的分支就找不到了。
正确做法:先切到子模块的某个分支,再修改提交。
cd libs/lib
git checkout main
# 修改文件...
git add .
git commit -m "修复 bug"
git push
cd ../..
git add libs/lib
git commit -m "更新子模块"
坑三:协作者更新了子模块指针
别人更新了子模块并推送了。你拉取主项目后:
git pull
git status
modified: libs/lib (new commits)
你需要执行 git submodule update 来同步子模块到主项目记录的那个提交。
坑四:删除子模块
Git 没有提供 git submodule remove 命令。删除子模块需要手动几步:
# 1. 从配置中移除
git submodule deinit -f libs/lib
# 2. 删除子模块目录
git rm -f libs/lib
# 3. 清理 .git/modules 中的记录
rm -rf .git/modules/libs/lib
子模块 vs 其他方案
| 方案 | 优点 | 缺点 |
|---|---|---|
| 子模块 | 保持独立仓库,精确控制版本 | 操作复杂,容易出错 |
| 直接拷贝 | 简单直接 | 无法跟踪上游更新 |
| 包管理器(npm/pip) | 自动管理依赖 | 需要构建系统支持 |
| 子树(subtree) | 对协作者透明 | 仓库体积大,历史混杂 |
Note子模块适合”你明确需要锁定第三方库某个精确版本”的场景。如果只是简单引入依赖,包管理器通常是更好的选择。
实用技巧
查看子模块状态:
git submodule status
c3f01dc8862123d317dd46284b05b6892c7b29bc libs/lib (v1.2)
开头的空格表示已同步,- 表示未初始化,+ 表示子模块有本地修改。
批量更新所有子模块:
git submodule update --remote --recursive
一句话总结:子模块是 Git 的”引用”而非”拷贝”。主项目只记住一个哈希值,更新需要你主动拉取。
这章学到了什么
- 子模块把一个 Git 仓库嵌到另一个仓库里,两个仓库的提交历史保持独立
- 添加子模块用
git submodule add,克隆含子模块项目用--recurse-submodules - 主项目只跟踪子模块的提交哈希,不跟踪文件内容,更新需要主动拉取
- 常见坑:分离 HEAD 状态、子模块有未提交修改、协作者更新了子模块指针
- 删除子模块需要三步:deinit、git rm、清理 .git/modules 记录
下一章,我们学习如何让同一个仓库拥有多个并行工作区。