Git Submodule 完整指南

Git Submodule 允许你把一个独立 Git 仓库,作为“子目录引用”放进主仓库(superproject)中。主仓库记录的不是子模块完整内容,而是子模块仓库的一个 commit 指针

它常用于:

  • 多项目共享公共模块(SDK、UI 组件、基础配置)
  • 主项目需要精确锁定依赖版本
  • 希望保留子项目独立历史和权限管理

核心概念

Superproject(主仓库)

包含子模块引用的仓库。

Submodule(子模块)

被嵌入的独立 Git 仓库,仍有自己的分支、标签、提交历史。

.gitmodules

主仓库根目录下的配置文件,记录每个子模块的 pathurl(可选 branchupdate)。

关键点:主仓库提交里保存的是“子模块路径 + 子模块 commit SHA”,不是子模块文件的普通变更。

快速开始

1) 添加子模块

# 基本添加
git submodule add <repository-url> <path>

# 指定跟踪分支(写入 .gitmodules)
git submodule add -b main <repository-url> <path>

# 提交主仓库变更
git add .gitmodules <path>
git commit -m "Add submodule <path>"

2) 克隆包含子模块的仓库

# 推荐:一次性拉取主仓库与子模块
git clone --recurse-submodules <repository-url>

# 已经 clone 后补拉子模块
git submodule update --init --recursive

3) 查看子模块状态

git submodule status
git submodule summary
git status

日常操作

更新到主仓库记录的子模块提交(最安全、最常见)

git submodule update --init --recursive

该命令会把子模块检出到主仓库锁定的 commit(常见为 detached HEAD,属于正常现象)。

拉取子模块远端最新提交(用于“升级依赖版本”)

# 按 .gitmodules 里 branch 配置更新
git submodule update --remote --recursive

# 更新后在主仓库提交新的子模块指针
git add <submodule-path>
git commit -m "chore: bump submodule <submodule-path>"

仅更新某个子模块

git submodule update --remote <submodule-path>
git add <submodule-path>
git commit -m "chore: update <submodule-path>"

同步 URL 变更(远端地址调整后常用)

git submodule sync --recursive
git submodule update --init --recursive

移动子模块路径(重构目录结构时常用)

推荐流程(安全且直观):

# 1) 移动目录(会移动 gitlink)
git mv <old-path> <new-path>

# 2) 确认 .gitmodules 中 path 已更新
git config -f .gitmodules --get-regexp '^submodule\..*\.path$'

# 3) 同步并重新初始化,避免本地缓存路径不一致
git submodule sync --recursive
git submodule update --init --recursive

# 4) 提交
git add .gitmodules <new-path>
git commit -m "chore: move submodule from <old-path> to <new-path>"

如果 git mv.gitmodules 没有自动改对,可手动修正:

git config -f .gitmodules submodule.<name>.path <new-path>
git add .gitmodules

注意:移动路径不会改变子模块所指向的 commit,只是主仓库里子模块的挂载位置发生变化。

正确删除子模块

# 1) 从索引和工作区删除子模块目录(同时更新 .gitmodules)
git rm -f <submodule-path>

# 2) 清理本地缓存目录(可选但推荐)
rm -rf .git/modules/<submodule-path>

# 3) 若 .git/config 仍有残留,移除之(可选)
git config -f .git/config --remove-section submodule.<submodule-name> || true

# 4) 提交
git commit -m "chore: remove submodule <submodule-path>"

团队协作建议

给团队一个固定初始化命令

git submodule update --init --recursive

在 CI 中确保子模块已拉取

git submodule sync --recursive
git submodule update --init --recursive

不要把子模块路径写进 .gitignore

错误示例(不要这样做):

lib/

原因:子模块路径本身是主仓库受控条目,忽略目录会掩盖真实状态、干扰协作。

如果只是想忽略“子模块工作区本地改动噪音”,请用:

# 仅忽略 dirty 提示(不会忽略指针变更)
git config -f .gitmodules submodule.<name>.ignore dirty
git add .gitmodules
git commit -m "chore: set submodule ignore policy"

常见问题排查

1) 子模块目录为空 / 未初始化

git submodule update --init --recursive

2) git status 显示子模块 modified

可能原因:

  • 子模块 HEAD 与主仓库记录的 SHA 不一致
  • 子模块里有本地未提交改动

排查命令:

git submodule status
git -C <submodule-path> status

恢复到主仓库锁定版本:

git submodule update -- <submodule-path>

3) 切分支后子模块状态混乱

git submodule sync --recursive
git submodule update --init --recursive

4) 子模块远端地址变更后拉取失败

git submodule sync --recursive
git submodule update --init --recursive

进阶技巧

批量执行命令

# 显示每个子模块当前分支与状态
git submodule foreach 'echo "== $name =="; git branch --show-current; git status --short'

浅克隆子模块(节省 CI 时间)

git submodule update --init --recursive --depth 1

让子模块使用独立 gitdir 布局(历史仓库迁移时可用)

git submodule absorbgitdirs

与 Git Subtree 对比

维度 Submodule Subtree
主仓库存储 子仓库 commit 指针 子仓库完整代码
历史关系 完全独立 合并进主仓库
使用门槛 略高(需初始化/更新) 略低(像普通目录)
版本锁定 非常精确 也可控制,但流程不同
典型场景 独立团队维护的共享仓库 需要深度融合、减少协作心智负担

.gitmodules 示例

[submodule "third_party/googletest"]
    path = third_party/googletest
    url = https://github.com/google/googletest.git
    branch = main
    update = checkout

字段说明:

  • path:子模块在主仓库中的目录
  • url:子模块远端地址
  • branchupdate --remote 时跟踪的分支
  • update:更新策略(常见 checkoutmergerebase

推荐操作清单(可直接放进项目 README)

# 初次拉取
git clone --recurse-submodules <repo>

# 日常同步(切分支、拉新代码后)
git submodule sync --recursive
git submodule update --init --recursive

# 升级某个子模块并提交指针
git submodule update --remote <submodule-path>
git add <submodule-path>
git commit -m "chore: bump <submodule-path>"

总结

用一句话概括:Submodule 的本质是“主仓库锁定子仓库某个提交”。

只要团队统一下面三件事,Submodule 就会非常稳定:

  • 克隆后必须执行 git submodule update --init --recursive
  • 升级子模块后必须在主仓库提交“指针变更”
  • 远端地址变化后执行 git submodule sync --recursive

参考资源: