掌握 Git Submodule 的添加、更新、克隆和移除,学会在一个项目中引用和管理其他 Git 仓库。

Submodule — 在项目中嵌套另一个项目

有时候你的项目需要引用另一个 Git 仓库的代码:公共组件库、配置文件、文档站点……直接复制代码会导致版本同步困难,Submodule 就是解决这个问题的方案。


一、什么是 Submodule?

Submodule 允许你在一个 Git 仓库中嵌入另一个独立的 Git 仓库。子模块有自己的提交历史、分支和远程地址,父仓库只记录它指向的特定提交。

1
2
3
4
5
6
7
8
9
my-project/                    ← 父仓库
├── .gitmodules ← 子模块配置文件
├── src/
├── libs/
│ └── common-lib/ ← 子模块(独立的 Git 仓库)
│ ├── .git/
│ ├── src/
│ └── README.md
└── README.md

父仓库记录了什么?

1
2
git ls-tree HEAD libs/common-lib
# 160000 commit abc1234 libs/common-lib

父仓库只保存子模块的一个 commit hash,不保存子模块的文件内容。


二、添加子模块

1
git submodule add <仓库地址> <本地路径>

示例:

1
2
# 把公共库添加到 libs/common-lib 目录
git submodule add https://gitee.com/team/common-lib.git libs/common-lib

执行后会产生两个变化:

1
2
3
4
5
6
# 1. 生成 .gitmodules 文件(记录子模块映射关系)
# 2. 克隆子模块到指定目录

git status
# new file: .gitmodules
# new file: libs/common-lib

记得提交 .gitmodules:这个文件必须和子模块一起提交到父仓库,否则别人 clone 后不知道子模块的信息。

指定分支

1
2
# -b 指定跟踪的分支(默认是 main/master)
git submodule add -b develop https://gitee.com/team/common-lib.git libs/common-lib

三、克隆包含子模块的项目

方式 1:clone 时递归初始化

1
2
# --recurse-submodules:一步到位
git clone --recurse-submodules https://gitee.com/team/my-project.git

方式 2:先 clone 再初始化

1
2
3
4
5
6
# 普通 clone(子模块目录是空的)
git clone https://gitee.com/team/my-project.git

# 初始化 + 拉取子模块
git submodule init
git submodule update
推荐

--recurse-submodules 一步到位,不容易漏。

嵌套子模块

如果子模块里还有子模块:

1
2
# 递归初始化所有层级的子模块
git submodule update --init --recursive

四、更新子模块

子模块是独立仓库,父仓库不会自动跟踪子模块的新提交。需要手动更新。

拉取子模块的最新代码

1
2
3
4
5
6
7
8
# 进入子模块目录,手动 pull
cd libs/common-lib
git pull origin main

# 回到父仓库,提交子模块引用变更
cd ../..
git add libs/common-lib
git commit -m "chore: 更新 common-lib 到最新版本"

批量更新所有子模块

1
2
# 把所有子模块更新到远程分支的最新提交
git submodule update --remote

两种方式的区别

  • git pull(进入子模块):更新到子模块远程分支的最新提交
  • git submodule update:切换到父仓库记录的特定提交

前者是”拉最新”,后者是”同步到父仓库指定的版本”。


五、查看子模块状态

1
2
3
4
5
6
7
8
# 查看子模块状态
git submodule status

# 输出:
# abc1234 libs/common-lib (v1.2.0)
# +def5678 libs/config (heads/main)
#
# + 表示子模块当前提交与父仓库记录的不一致
1
2
3
4
# 查看子模块摘要
git submodule summary

# 输出子模块与父仓库记录之间的提交差异

六、在子模块中开发

子模块本身就是一个完整的 Git 仓库,你可以在里面正常开发:

1
2
3
4
5
6
7
8
9
10
11
12
13
cd libs/common-lib

# 创建分支、修改代码、提交
git checkout -b feature/new-api
vim src/api.js
git add .
git commit -m "feat: 添加新接口"
git push origin feature/new-api

# 回到父仓库,更新子模块引用
cd ../..
git add libs/common-lib
git commit -m "chore: 更新 common-lib 接口"

子模块默认在游离头指针状态git submodule update 后,子模块处于 detached HEAD。如果要在子模块中开发,先 git checkout main 切到一个分支上。


七、移除子模块

Git 没有提供一步删除子模块的命令,需要手动操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 1. 从索引中移除
git rm -r libs/common-lib

# 2. 清理 .gitmodules 中的对应条目
# (手动编辑或删除文件)
vim .gitmodules

# 3. 清理 .git/config 中的对应条目
git config --remove-section submodule.libs/common-lib

# 4. 删除子模块的 Git 数据
rm -rf .git/modules/libs/common-lib

# 5. 提交
git commit -m "chore: 移除 common-lib 子模块"
注意

第 1 步 git rm 会同时删除子模块目录和更新索引,但 .git/modules/ 下的缓存需要手动清理。


八、常见问题

问题 1:clone 后子模块目录为空

1
2
3
# 解决:初始化并拉取
git submodule init
git submodule update

问题 2:子模块提交后父仓库显示 modified

1
2
3
4
5
6
7
# 子模块有新的提交但父仓库还没更新引用
git submodule status
# +abc1234 libs/common-lib

# 解决:在父仓库中更新引用
git add libs/common-lib
git commit -m "chore: 更新子模块引用"

问题 3:切换父仓库分支后子模块不对

1
2
# 解决:切换到父仓库的目标提交
git submodule update

总结:Submodule 速查卡

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
┌──────────────────────────────────────────────────────────┐
│ Git Submodule 速查 │
├────────────────┬─────────────────────────────────────────┤
│ 添加子模块 │ git submodule add <url> <path> │
│ 指定分支添加 │ git submodule add -b <branch> <url> │
├────────────────┼─────────────────────────────────────────┤
│ 克隆含子模块 │ git clone --recurse-submodules <url> │
│ 初始化+拉取 │ git submodule init + update │
│ 递归拉取 │ git submodule update --init --recursive │
├────────────────┼─────────────────────────────────────────┤
│ 更新到远程最新 │ git submodule update --remote │
│ 查看状态 │ git submodule status │
├────────────────┼─────────────────────────────────────────┤
│ 移除子模块 │ git rm + 清理 .gitmodules + .git/modules│
└────────────────┴─────────────────────────────────────────┘