首页 / Git 入门教程 / 子模块(Submodule)

Git 入门教程

子模块(Submodule)

本教程共 26 篇 · 第 21 篇 · 更新于 2026-07-29 · 约 6 分钟阅读

GitGit 入门教程submodule子模块嵌套仓库依赖管理

21. 子模块(Submodule)

本节目标:学会在一个 Git 仓库中嵌入另一个 Git 仓库,理解子模块的”快照”本质,避免协作时最常见的坑。

为什么需要子模块

你正在开发一个网站,需要用到一个第三方库。这个库有自己的 Git 仓库,自己发版、自己更新。你想把它引入你的项目,但又想保持两个仓库的獨立。

直接拷贝代码进去?可以,但下次上游更新了,你得手动重新拷贝。

子模块解决了这个问题。它把一个 Git 仓库嵌到另一个仓库里当子目录。两个仓库的提交历史保持独立。

添加子模块

在项目根目录执行:

git submodule add https://github.com/example/lib.git libs/lib

这做了三件事:

  1. lib 仓库克隆到 libs/lib 目录
  2. 创建(或修改).gitmodules 文件,记录映射关系
  3. 把子模块注册到暂存区

查看 .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 记录

下一章,我们学习如何让同一个仓库拥有多个并行工作区。