整体说明
- submodule 允许 git 灵活地将其他项目嵌入到当前项目,同时自由地切换 sumodule 分支和内容管理,且保持各自的版本独立
- 本质上: submodule 还是作为一个独立的项目存在的,主项目管理子模块靠
.gitmodules文件 - 子模块默认跟踪的是固定 commit ID ,而非分支:
- 若子仓库更新,主仓库需手动更新子模块的 commit ID 并提交;
.gitmodules是子项目的核心文件- 这个文件必须纳入版本控制,否则他人克隆仓库时无法识别子模块;
- 避免嵌套过深的子模块,否则会增加版本管理和协作的复杂度
添加子模块
基础语法
1
2
3git submodule add <子仓库URL> <本地存放路径(可选)>
# 上述命令会自动添加 submodule 相关的必要文件,立刻直接 commit 即可添加 submodule 完成,注意这里如果先执行 checkout 等可能导致 submodule 信息无法对齐,建议立即 commit
git commit "Add: submodule xxx"<子仓库URL>:子模块的 Git 仓库地址(HTTPS/SSH 均可)<本地存放路径>:子模块在当前仓库中的存放目录(省略则默认用仓库名)
若需让子模块跟踪分支,可添加
-b <分支名>参数:1
git submodule add -b <branch> <子仓库URL> <路径>
submodule 的分支可以切换绑定(或新增绑定)
1
2
3
4git config -f .gitmodules submodule.<本地存放路径>.branch <branch>
# 比如将 dev 分支绑定到名为 libs/sub-repo 的 submodule 下:
git config -f .gitmodules submodule.libs/sub-repo.branch dev
示例
示例:比如将
https://github.com/example/sub-repo.git添加到当前仓库的libs/sub-repo目录:1
2
3
4# 进入主仓库根目录
cd /path/to/your/main-repo
# 添加子模块
git submodule add https://github.com/example/sub-repo.git libs/sub-repo执行上述命令后,Git 会自动完成以下操作:
1)在指定路径(如
libs/sub-repo)拉取子仓库的代码;- 就像是普通的 git 项目一样,还包括
.git目录
- 就像是普通的 git 项目一样,还包括
2)在主仓库根目录生成
.gitmodules文件(记录子模块配置),内容示例:1
2
3[submodule "libs/sub-repo"]
path = libs/sub-repo
url = https://github.com/example/sub-repo.git3)主仓库的暂存区会新增
.gitmodules和libs/sub-repo两个条目(子模块条目是一个“链接”,记录子仓库的 commit ID)
添加后续提交子模块到主仓库
- 添加子模块后,需要将
.gitmodules和子模块条目提交到主仓库:1
2
3
4# 提交变更
git commit -m "添加子模块 sub-repo 到 libs 目录"
# 推送到远程
git push
克隆包含子模块的仓库
- 直接
git clone只会拉取子模块目录,但不会拉取子模块的代码,需执行:1
2
3
4
5
6
7
8# 方式1:克隆时直接拉取子模块
git clone --recurse-submodules <主仓库URL>
# 方式2:先克隆主仓库,再初始化+更新子模块
git clone <主仓库URL>
cd main-repo
git submodule init # 初始化子模块配置(读取.gitmodules)
git submodule update # 拉取子模块的代码
git submodule update vs git submodule update --remote
git submodule update命令还有个特殊参数--remote,两者有很多容易犯错的区别git submodule update- 用于 对齐主项目固定的子模块版本
- 仅将子模块切换到主项目记录的哈希值(即
.gitmodules/.gitmodules中固定的子模块版本); - 不会主动从子模块的远程仓库拉取新代码;
- 若子模块本地无该哈希值的代码,会从远程克隆,但仅克隆该版本
- 同步主项目指定的子模块版本(比如团队协作时,确保所有人用相同版本的子模块)
- 执行
git submodule update后:- 子模块处于「分离头指针(detached HEAD)」状态,且版本与主项目记录完全一致;
git submodule update --remote- 用于 更新子模块到远程最新版本
- 先从子模块的远程仓库拉取最新代码(更新子模块的远程追踪分支);
- 再将子模块切换到该远程分支的最新哈希值;
- 会修改主项目中记录的子模块版本(需手动提交主项目的修改)
- 主动更新子模块到远程最新版本(比如子模块有新功能/修复,需要同步到主项目)
- 执行
git submodule update --remote后:- 子模块仍处于「分离头指针」状态(仅指向远程最新哈希),主项目的 git 状态会显示子模块版本已修改
- 需执行
git add <子模块路径> && git commit才能将新的子模块版本记录到主项目
- 慎用这个命令
git submodule update vs git submodule update --remote 示例
假设主项目
main-proj包含子模块sub-proj:同步主项目指定的子模块版本:
1
git submodule update
更新子模块到远程最新版本,并提交主项目的版本修改:
1
2
3
4git submodule update --remote sub-proj # 拉取sub-proj远程最新代码,切到最新哈希
# 提交主项目的修改
git add sub-proj
git commit -m "update sub-proj to latest remote version"
附录:初始化带有 submodule 的仓库详细理解
正常拉取外层项目
1
git pull origin master:master
- 此时除了
./.gitmodules文件包含关于子模块的信息外,其他的文件都不包含,包括./.git/中
- 此时除了
初始化子模块
1
git submodule init
- 将
.gitmodules中的所有子模块注册到外层项目中 - 注册方式:添加子模块信息(文件夹路径和子模块项目地址)到
.git/config文件中并指明子模块对应的active = true - 注:如果子模块之前存在于
.git/config中 且active = false,这个初始化操作会修改为active = true
- 将
初始化指定子模块(其他子模块可以不初始化,也不会影响,未初始化的子模块会是一个空文件夹)
1
git submodule init <path_to_sub_module_name>
- 仅初始化
<path_to_sub_module_name>这个模块- 测试发现:注意初始化时
<path_to_sub_module_name>是 submodule 的文件夹路径 - 可以是相对路径或绝对路径,执行这个命令时需要在 submodule 的外面
- 测试发现:注意初始化时
- 仅初始化
更新子模块
1
2
3
4# 更新所有子模块('.git/config' 和 '.gitmodules' 中的所有子模块)
git submodule update
# 更新单个路径下对应的模块
git submodule update <path_to_sub_module_name>- 具体含义:根据主仓库中记录的 子模块 commit ID ,从子模块的远程仓库拉取对应版本的代码,并存放到主仓库指定的子模块路径中
- 这行代码执行下面的操作:
- 如果还没有下载,则所有子模块的链接地址项目下载到
.git/modules/中 - 将对应的 commit ID
checkout到 submodule 文件夹(工作目录)中 - 常常用来在切换分支后同步子模块数据
- 如果还没有下载,则所有子模块的链接地址项目下载到
<path_to_sub_module_name>模块参数的使用方法同上
特别注意:
git submodule init后,.git/config和.gitmodules应该是一致的.git/config和.gitmodules中都有,且在.git/config中active = true的 submodule 才能被update操作下载和checkout
特别说明:解耦初始化 deinit
1
2
3
4
5# 将 <path_to_sub_module_name> 初始化,后续执行 git submodule update 等命令时自动更新 <path_to_sub_module_name> 这个 submodule
git submodule init <path_to_sub_module_name>
# 将 <path_to_sub_module_name> 解耦初始化,后续执行 git submodule update 等命令时不会再自动更新 <path_to_sub_module_name> 这个 submodule
git submodule deinit <path_to_sub_module_name>
更新子模块到自己的最新 commit
- 更新逻辑:
1
2
3
4
5
6
7# 进入子模块目录
cd libs/sub-repo
git pull origin master # 拉取子仓库最新代码
cd ../.. # 回到主仓库
git add libs/sub-repo # 提交子模块的新 commit ID
git commit -m "update: 子模块 sub-repo 同步到最新版本"
git push
删除子模块
- 若需移除子模块,步骤稍多(Git 无直接
git submodule delete命令):1
2
3
4
5
6
7
8# 1. 解除子模块关联
git submodule deinit -f libs/sub-repo
# 2. 删除 .git 中的子模块缓存
rm -rf .git/modules/libs/sub-repo
# 3. 删除工作区的子模块目录,这一步后会看到 .gitmodules 中的相关 submodule 也被删除了
git rm -f libs/sub-repo
# 4. 提交删除操作
git commit -m "remove: 移除子模块 sub-repo"
附录:关于 Git submodule 的理解
submodule 自己知道自己被当做 submodule
一个项目被作为 submodule 后,他的
./submodule_name/.git将不再是一个文件夹,而是一个指明.git/文件夹路径的配置文件1
cat ./submodule_name/.git
gitdir: ../.git/modules/submodule_name
.git/文件夹可以在./.git/modules/submodule_name/.git/中找到
submodule 相关信息都在外层项目中显示出来
在 submodule 文件夹
./submodule_name/下, submodule 的更新,提交等操作正常按照一般项目进行即可- 这里操作时虽然仓库在外层项目的
./.git/modules/submodule_name/.git/中,但是在 submodule 的目录下我们可以正常访问 submodule 的仓库 - 也就是说在 submodule 文件夹下的
git操作(add,commit)实际上不修改当前文件夹下的任何文件,修改都在外层项目的./.git/modules/submodule_name/.git/仓库中
- 这里操作时虽然仓库在外层项目的
外层项目只存储
- submodule 文件夹
- 在
./.gitmodules中存储 submodule 相关信息(文件夹路径与 submodule 远程地址) - 在GitHub中,直接用网页打开项目可以看到 submodule 会被自动解析远程地址和最近提交的ID信息,点击 submodule 对应的文件夹链接即可跳转到 submodule 远程仓库地址中
递归 submodule
- 递归时记住项目的库都在父项目的库中即可
- 这句话等价于所有项目的库都在根项目的
.git/中
- 这句话等价于所有项目的库都在根项目的
特别说明
- 非必要不建议使用 submodule
附录:移除 submodule .git 但内容保留到主项目
- 如果 Git 项目下面有个 submodule 也是包含 Git 的(可能是 git clone 命令下载的)的,往往不能正常的提交和管理项目,这是因为项目变成了 Git submodule 了
- 现象:如果 submodule 是
git clone别人的项目,我们将 submodule 提交到整个大项目中时- 会提示:
modified:xxx(modified content, untracked content) - 此时如果直接提交,那么远程仓库里面 submodule 将是空的
- 会提示:
- 若不想再保留 submodule 的 git 仓库,则需要删除 submodule 相关的所有信息
第一步:需要先删除子模块
- 移除子模块:
1
2
3
4# 1. 解除子模块关联
git submodule deinit -f libs/sub-repo
# 2. 删除 .git 中的子模块缓存
rm -rf .git/modules/libs/sub-repo
第二步:重新添加文件路径(当做普通的文件)
- 重新添加 submodule 文件夹
1
git add xxx
git submodule 是否跟踪分支的区别
- 在 Git 中使用子模块(Submodule)时,“不跟踪分支(默认行为/锁定特定提交)” 和 “跟踪分支” 的核心区别在于父项目(Superproject)如何决定子模块应该停留在哪个版本 ,以及 更新子模块时的流程
不跟踪分支(默认行为 / 锁定特定 Commit)
- 这是 Git 子模块最原始也是最常用的工作方式
- 父项目只关心:“子模块必须是
a1b2c3d这个提交”,不在乎这个提交属于哪个分支,也不在乎这个提交是不是最新的 - 当克隆父项目并运行
git submodule update时,Git 会进入子模块目录,强制将其 checkout 到父项目记录的那个 SHA-1 哈希值- 注:此时,子模块处于 Detached HEAD(游离指针)状态
- 不跟踪分支时,如果要切换到某个分支的最新 commit,需要执行如下操作:
- 1)进入子模块目录:
cd submodule_dir - 2)手动拉取或切换:
git checkout master && git pull - 3)回到父项目目录:
cd .. - 4)提交变更:
git add submodule_dir->git commit
- 1)进入子模块目录:
- 建议使用这种方式, 团队所有成员拉取代码后,得到的子模块代码完全一致,不会因为子模块远程仓库更新了代码而导致父项目构建失败
跟踪分支
可通过配置
.gitmodules文件来实现的一种更动态的模式在
.gitmodules中明确告诉 Git:“这个子模块应该跟随main(或dev) 分支”也可以通过命令行绑定,如添加 submodule 时,或之后直接修改:
1
2
3
4
5
6
7
8# # 新建 submodule:
git submodule add -b submodule_master https://github.com/xxx/lib.git
# 此时 .gitmodules 中会添加 branch = submodule_master
# # 已有 submodule 绑定某个 分支:
git config -f .gitmodules submodule.<本地存放路径>.branch <branch>
# 比如将 dev 分支绑定到名为 libs/sub-repo 的 submodule 下:
git config -f .gitmodules submodule.libs/sub-repo.branch dev.gitmodules文件中会多一行配置:1
2
3
4[submodule "my-lib"]
path = my-lib
url = https://github.com/example/my-lib.git
branch = main # 多出来的配置
虽然父项目在数据库中依然存储的是 SHA-1 哈希值,但当你使用特定参数更新时(update 时添加
--remote),Git 会忽略本地记录的哈希值,直接去拉取远程分支的最新代码跟踪分支时,如果要切换到某个分支的最新 commit,只需要执行如下操作:
不需要进入子模块目录,只需在父项目根目录运行:
1
git submodule update --remote
- Git 会自动去子模块的远程仓库抓取
branch字段指定分支的最新提交,并将子模块更新到该提交 - 注意:如果不添加
--remote则只是切换到当前 commit_id 而不会拉取最新分支(这与不绑定分支的git submodule update执行含义完全相同)
- Git 会自动去子模块的远程仓库抓取
注:运行完上述命令后,父项目的状态会显示子模块有变化(指向了新的 Hash),仍然需要 在父项目中执行
git add和git commit来固化这个变更
如果开发的项目依赖另一个正在快速迭代的库,且总是希望使用该库的最新版本,这种方式可以简化更新流程,但是要小心使用
一些实操及理解
新增 submodule 时,默认(不跟踪):
1
2git submodule add https://github.com/xxx/lib.git
# 此时 .gitmodules 中没有 branch 字段新增 submodule 时,跟踪分支:
1
2git submodule add -b main https://github.com/xxx/lib.git
# 此时 .gitmodules 中会添加 branch = main更新 submodule 默认(不跟踪):
- 如果运行
git submodule update,什么都不会发生 ,因为它只会把子模块恢复到父项目当前记录的旧 Hash 值
- 如果运行
更新 submodule (若
.gitmodules中已经跟踪分支):- 如果运行
git submodule update,什么都不会发生 ,因为它只会把子模块恢复到父项目当前记录的旧 Hash 值,与.gitmodules中是否已经跟踪分支无关 - 如果运行
git submodule update --remote,Git 会检测到配置了分支,于是去拉取远程最新代码,并更新本地子模块的指针
- 如果运行
特别说明:
- “跟踪分支” 不意味着自动更新:即使配置了跟踪分支,当
git pull父项目时,子模块不会自动更新到远程最新- 必须显式执行
git submodule update --remote
- 必须显式执行
- “跟踪分支” 后父项目仍然会记录 Hash 值,Git 的底层数据结构决定了父项目永远 只记录子模块的 Commit Hash
- 所谓“跟踪分支”,只是提供了一个快捷命令(
--remote)来帮你自动找到那个最新的 Hash 值并 checkout 过去,省去了手动进入子目录 pull 的过程
- 所谓“跟踪分支”,只是提供了一个快捷命令(
- 如果是引用第三方稳定的开源库 ,或者要求构建环境绝对可复现,使用默认(不跟踪) 模式
- 如果是迭代很快的开发,且父项目需要时刻集成子模块的最新开发成果,使用跟踪分支 模式
- “跟踪分支” 不意味着自动更新:即使配置了跟踪分支,当