📚 Git 子模块完全指南

一、什么是 Git 子模块?

子模块(Submodule) 是 Git 提供的一个功能,让你在一个 Git 仓库(称为”主仓库”或”父仓库”)中嵌入另一个独立的 Git 仓库作为子目录。子模块保持自己独立的提交历史,主仓库只记录子模块当前指向的具体 commit 哈希

简单来说:仓库里套仓库,各自独立版本控制。

适用场景

场景说明
共用库多个项目依赖同一个内部库,想在主项目中固定其版本
第三方依赖引入某个开源项目,需要锁定其特定版本
微服务/模块化一个大型项目拆分为多个独立仓库,又需要统一管理
共享配置或脚本在不同项目间共享一套工具脚本

二、核心命令速查表

操作命令
添加子模块`git submodule add <仓库URL> <路径>`
初始化(拉取)`git submodule update —init —recursive`
克隆含子模块的项目`git clone —recurse-submodules `
更新子模块到最新`git submodule update —remote`
查看子模块状态`git submodule status`
删除子模块手动移除配置 + `git rm <路径>`
遍历执行命令`git submodule foreach ‘<命令>‘`

三、详细操作指南

1️⃣ 添加子模块

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

执行后会发生:

  • 克隆 my-liblibs/my-lib 目录
  • 在仓库根目录生成一个 .gitmodules 文件(记录子模块信息)
  • 主仓库会记录该子模块当前的 commit 指向

.gitmodules 文件内容示例:

[submodule "libs/my-lib"]
    path = libs/my-lib
    url = https://github.com/example/my-lib.git

.gitmodules 和子模块当前指向的 commit 都提交到主仓库!


2️⃣ 克隆含有子模块的项目

方法一:一步到位(推荐)

git clone --recurse-submodules https://github.com/your/project.git

方法二:分步操作

git clone https://github.com/your/project.git
cd project
git submodule init        # 初始化子模块配置
git submodule update      # 拉取子模块到指定 commit

简化合并:

git submodule update --init --recursive

3️⃣ 更新子模块

将子模块更新到远程的最新 commit:

git submodule update --remote

如果想更新特定子模块:

git submodule update --remote libs/my-lib

更新后,主仓库会看到子模块指向变了,需要再提交一次:

git add libs/my-lib
git commit -m "chore: update my-lib to latest"

4️⃣ 在子模块内部工作

cd libs/my-lib
git checkout main        # 切到主分支(默认是 detached HEAD)
# 做修改...
git add .
git commit -m "feat: add new feature"
git push origin main
 
cd ../..                 # 回到主仓库根目录
git add libs/my-lib
git commit -m "chore: update submodule reference"

⚠️ 重点:子模块默认处于 detached HEAD 状态,想在上面开发要先 git checkout 一个分支。

5️⃣ 删除子模块

Git 没有一键删除命令,需要手动清理:

# 1. 移除子模块目录
git submodule deinit -f libs/my-lib
 
# 2. 从 Git 中移除
git rm -f libs/my-lib
 
# 3. 删除 .gitmodules 中对应的条目
# 4. 删除 .git/config 中的子模块配置
# 5. 提交变更
git commit -m "remove submodule my-lib"

6️⃣ 遍历所有子模块执行命令

git submodule foreach 'git status'
git submodule foreach 'git pull origin main'

四、进阶技巧

🔹 子模块使用相对路径

如果子模块和主仓库在同一个 Git 服务器上,推荐用相对路径:

git submodule add ../shared-lib.git libs/shared-lib

这样 fork 或迁移仓库时不用改 URL。

🔹 指定子模块的分支

.gitmodules 或命令行中指定子模块跟踪的分支:

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

或在 .gitmodules 中手动加:

[submodule "libs/lib"]
    path = libs/lib
    url = https://github.com/example/lib.git
    branch = develop

🔹 --recurse-submodules 魔法

很多 Git 命令都支持这个参数:

# 递归拉取
git pull --recurse-submodules
 
# 递归推送
git push --recurse-submodules=on-demand
 
# 递归 diff
git diff --recurse-submodules

🔹 别名简化操作

git config --global alias.sync '!git pull && git submodule update --init --recursive'

五、常见问题与坑

❌ 子模块指针落后

现象:子模块代码是旧的,明明远程有新提交。 原因:主仓库记录的是指向子模块的 特定 commit,不是分支最新。 解决:执行 git submodule update --remote 后提交更新。

❌ 拉取后子模块目录为空

原因:忘了加 --recurse-submodules解决:补执行 git submodule update --init --recursive

❌ 子模块修改未提交导致主仓库无法切换分支

cd libs/my-lib
git stash
cd ..
git checkout other-branch

❌ 合并冲突

git add libs/my-lib
git commit

六、子模块 vs 其他方案对比

方案优点缺点
Submodule独立版本控制,依赖清晰操作稍复杂,新手易踩坑
Subtree合入主仓库,操作透明历史混乱,容易冲突
Monorepo统一管理,原子提交仓库巨大,权限难管控
包管理器(npm/pip)生态成熟,语义化版本非 Git 原生,需额外配置

七、子模块内部的分支与主仓库的分支

核心区别一句话

主仓库的分支 —— 是你项目级别的开发线 子模块内部的分支 —— 是子模块自己独立的分支 主仓库记录的不是子模块的分支,而是子模块的某个具体 commit

它们是完全独立的两套分支体系

主仓库                               子模块
┌──────────────────┐                ┌──────────────────┐
│ main  ←──────┐   │                │ main  ←──────┐   │
│ develop ←─┐  │   │                │ develop ←─┐  │   │
│ feature-x │  │   │                │ v1.x   ←┐ │  │   │
└───────────┴──┘   │                └─────────┴─┴──┘   │
                    │                                    │
主仓库记录的是:     │               子模块内部:
子模块 → commit     │               可以自由切换分支
(一个固定的哈希值)   │               各自独立演进

关键点: 主仓库记录的是一个固定 commit,而不是”子模块的 main 分支”。

用例子看区别

场景假设

主仓库 my-project  → 分支 main
子模块 libs/utils   → 有自己的 main、develop、v1.x 分支

主仓库分支切换的影响

# 在主仓库 main 分支下
git submodule status
# 输出: abc1234 libs/utils  (记录的是 commit abc1234)
 
# 切换到主仓库的 develop 分支
git checkout develop
git submodule status
# 输出: def5678 libs/utils  (develop 分支记录的可能是另一个 commit)

👉 主仓库不同分支,可以”记住”子模块的不同 commit 版本

子模块内部的分支切换

cd libs/utils
git branch
# * main     ← 当前是 main 分支
#   develop
#   v1.x
 
# 切换到 develop
git checkout develop
 
cd ../..
git submodule status
# 输出: abc1234 libs/utils (带 + 号,表示有修改)
# 因为主仓库记录的是 abc1234,但子模块当前 HEAD 变了

👉 子模块内部切分支,主仓库视其为”未提交的修改”

对比表

维度主仓库的分支子模块内部的分支
是否独立✅ 完全独立✅ 完全独立
相互影响切换主仓库分支 → 子模块指向的 commit 可能变子模块内部切换分支 → 主仓库视为”未提交修改”
记录方式主仓库的每个 commit 记录子模块的 commit 哈希子模块有自己的 Git 对象和引用
默认状态正常在分支上工作update 后通常是 detached HEAD
分支名持久性分支名是 Git 历史的一部分分支名不会传递到其他开发者那里

实际开发中的常见困惑

❓ 主仓库 main 分支,子模块也”在 main”吗?

不一定。 默认情况下子模块处于 detached HEAD,指向的是某个固定 commit。

❓ 为什么别人拉下来,子模块不在任何分支上?

因为 Git 传递的是 commit 哈希,不是分支名。这确保了所有人拿到完全相同的子模块版本。

❓ 如何在子模块里长期开发?

cd libs/utils
git checkout main      # 切到分支,脱离 detached HEAD
# 做开发、提交、推送
cd ../..
git add libs/utils
git commit -m "update submodule reference"

八、一句话总结

添加时用 add,克隆时用 --recurse-submodules,更新时用 update --remote,提交时别忘了 git add 子模块目录。

主仓库的分支管理”我要用子模块的哪个版本”;子模块的分支管理”子模块自己怎么演进”。两者通过 commit 哈希桥接,完全不耦合。