超详细的Git submodule讲解以及出现分离头指针(detached HEAD)的解决方法
Git submodule 是对父仓库中的子仓库进行管理的命令
git submodule
命令用于管理包含其他 Git 仓库的项目。
git submodule
命令对于大型项目或需要将外部库集成到项目中的情况非常有用。 通过使用子模块,你可以将外部库作为你的项目的一部分来管理,而不必将其直接合并到主仓库中。
使用详解
一:拉取仓库及其子模块
1、初始化子模块
git submodule init
该命令会初始化配置文件中的所有子模块。它会根据 .gitmodules 文件中的信息设置子模块的 URL 和路径,但不会下载子模块的内容。
常见用法:在克隆了一个包含子模块的仓库后,运行此命令来初始化子模块,也就是将子仓库的内容也一克隆下来
git clone <repo-url>
cd <repo-dir>
git submodule init
2、更新子模块
git submodule update
该命令会从子模块的远程仓库中拉取子模块的内容,并将其更新到 .gitmodules 文件中指定的提交。
**常见用法:**在初始化子模块后,或当你需要更新子模块的内容时,运行此命令。
git submodule update
如果要直接拉取仓库的所有模块,可以直接用以下命令直接搞定
git submodule update --init --recursive
# --init:初始化未初始化的子模块(即首次拉取时,根据 .gitmodules 配置克隆子模块代码到指定路径)。
# --recursive:递归处理嵌套的子模块(如果子模块本身还包含其他子模块,也会一并初始化和更新)。
3、添加子模块
git submodule add <repo-url> [<path>]如果想要添加制定分支的子模块,使用git submodule add -b branch_name <repo-url> [<path>]
该命令会将指定的 Git 仓库作为子模块添加到当前仓库中。
<repo-url>
是子模块的仓库地址,<path>
是子模块在主仓库中的路径(可选,如果不指定,默认使用子模块仓库的名称作为路径)。
常见用法:将外部库作为子模块添加到项目中。
git submodule add https://github.com/example/test.git test1# 最后的test就是在你的仓库中对应的子仓库的名称,如果不写,将直接用子仓库的名称命名
# 比如,这里如果不写的最后的test1,子仓库的名称将直接用test命名
4、注意:
-
.gitmodules本质上就是一个文本配置文件,他的内容是可以直接进行修改的,以下是其内容示例:
[submodule "libs/utils"]path = libs/utilsurl = https://github.com/example/utils.gitbranch = dev
其中包含了以下的内容:
- 子模块的名称(
name
):标识子模块的唯一名称(通常与路径一致)。 - 子模块的本地路径(
path
):子模块在主仓库中的存放目录。 - 子模块的远程仓库地址(
url
):子模块对应的远程代码仓库 URL(用于克隆或拉取更新)。使用http或者ssh方式都可以 - 子模块跟踪的分支(
branch
,可选):如果添加子模块时指定了b
选项,这里会记录对应的分支名称。
- 子模块的名称(
二:更新子模块
由于 Git 子模块(submodule)在主仓库中记录的是一个特定的静态提交哈希(commit hash),而非分支引用,因此子模块的更新不会自动同步到主仓库,可能出现 “不及时更新” 的情况。简单来说,就是这是一个静态的commit,子仓库的更新不会同步到主仓库。因此就需要更新子模块,使得主仓库的内容保持最新。
如何让主仓库同步子模块的更新?
需要手动执行两步操作:
-
更新子模块到最新版本:在主仓库中,进入子模块目录拉取最新代码,或直接用命令拉取子模块跟踪分支的最新版本:
git submodule update --remote <子模块路径> # 拉取子模块跟踪分支的最新提交
(如果子模块指定了跟踪分支,
--remote
会拉取该分支的最新代码;否则默认拉取master
/main
分支) -
提交主仓库对子模块的更新:子模块更新后,主仓库会检测到子模块的提交哈希发生了变化,此时需要提交这个变化到主仓库:
git add <子模块路径> # 暂存子模块的新提交哈希 git commit -m "更新子模块 B 到最新版本" git push
这样,其他开发者拉取主仓库后,再执行 git submodule update
就能同步到子模块的最新版本了。
三:删除子模块
1. 查看子模块信息
git submodule
这个命令将直接列出当前仓库中的所有子模块,以及它们的提交哈希和路径。
示例:
setsuna@Shadow:~/RM/AutoAim/auto_aim_2026$ git submodule 13e2a7268a154053b1888c86c758bbbc5f453bd3 Orbit_tracker_general (heads/main)5bce0283751870f3763d50a3ed824ee3235ece59 aimer (heads/main)24080309cf53b9c08a9078590ddfce33b897cdd2 auto_aim_debugger (2408030)793089d4a2fb66ac75ed836ba1697100b02648b3 communicate_2025 (v2.0.0-alpha.1-3-g793089d)f571ed8202827d8fd4b156708342e31e154f2548 detector_26 (heads/dev-mao)4dffb5c53c032be137723f05883ddca08e2836e5 mindvision_camera (remotes/origin/dev)5e4a1667edef8f656ad3350382a5fb9512adc023 tracker_26 (remotes/tracker_26/main)
其中,前面的像13e2a7268a154053b1888c86c758bbbc5f453bd3 就是对应的提交哈希和路径
2.移除子模块
首先,你可以删除子模块的目录。在删除之前,你需要使用 git submodule deinit
命令将其标记为不再被 Git 追踪。例如,如果你想移除 submodule
子模块,你可以运行以下命令:
git submodule deinit submodule
# 这里后一个submodule是你实际的子模块的名称,这里用submodule作为示例
然后,你可以删除子模块的目录:
git rm submodule
# 同样这里submodule是你实际的子模块的名称
接着,你需要提交这些更改:
git commit -m "Remove submodule"
最后,将commit推送到远端
git push
其他命令
1.更新所有子模块
git submodule update --recursive --remote
--recursive
:递归地更新所有子模块(包括子模块的子模块)。--remote
:从子模块的远程仓库拉取最新的更改。
注意:这里是根据.gitmodule中对应的分支进行拉取,如果其没有指定那么remote拉取的将会是main/master分支
2.检查子模块状态
git submodule status
显示子模块的当前状态,包括当前的提交哈希和路径,是否有未提交的更改。
常见用法:查看子模块的当前状态。
git submodule status
疑难杂症:
1.“分离头指针(detached HEAD)且有未跟踪提交”
示例:
setsuna@Shadow:~/RM/AutoAim/auto_aim_2026/tracker_26$ git checkout origin/main
警告:您正丢下 1 个提交,未和任何分支关联:51efa6b 在trackerinfo中添加发布的装甲板类型、数量和编号如果您想要通过创建新分支保存它,这可能是一个好时候。
如下操作:git branch <新分支名> 51efa6bHEAD 目前位于 5e4a166 add old tracker
-
出现原因:
Git 的
HEAD
指针默认指向本地分支(如main
、dev
),而分支又指向具体的提交 —— 这是 “正常工作状态”。但当你做了以下操作时,HEAD
会直接指向某个提交哈希或远程分支(如origin/main
),从而进入分离状态:- 直接 checkout 一个提交哈希:比如
git checkout 5e4a166
(为了查看历史版本); - 直接 checkout 远程分支:比如你执行的
git checkout origin/main
(origin/main
是远程分支的本地镜像,不是本地分支); - 切换到标签(tag):标签本质是固定的提交,比如
git checkout v1.0
。
简单说:只要你
checkout
的目标不是 “本地分支名”,就会进入分离头指针状态。这里我显然就是直接checkout这个提交哈希,因此我进入了分离头指针状态
- 直接 checkout 一个提交哈希:比如
-
如何解决
-
按提示创建一个新分支来跟踪它,避免丢失:
git branch my_new_branch 51efa6b # 创建新分支,指向 51efa6b git checkout my_new_branch # 切换到新分支,此时提交被分支跟踪# 原来51efa6b这个提交哈希中的内容不会被修改,保持原样
之后可以基于这个分支继续开发,或合并到其他分支(如
main
)
-
-
仓库拉取到本地后,git提交后,github远端仓库点击子仓库发现404(且不是网络原因和.gitmodule配置问题)
建议:确认目前工作的分支是远程有的分支,主仓库中子模块的提交哈希为远程存在的版本
排查步骤:
-
本地进入子模块目录,查看当前提交是否存在于远程:
cd 子模块路径 git fetch origin # 拉取远程最新信息 git log --oneline 你的提交哈希 # 检查本地提交是否在远程存在
-
假设你要检查的子模块提交哈希是
51efa6b
(替换成你的实际哈希),执行:git log --oneline 51efa6b
情况 1:提交存在于远程(正常)
如果输出类似以下内容(显示该提交的信息),说明这个提交在远程仓库中存在:
51efa6b 在trackerinfo中添加发布的装甲板类型、数量和编号 # 前面的是提交哈希,后面是对应的commit消息
(即使该提交不在
main
等默认分支,只要远程仓库的任何分支或历史中包含它,就会显示)情况 2:提交不存在于远程(异常)
如果输出以下错误,说明这个提交仅存在于你的本地子模块仓库,远程仓库中没有(可能被删除或从未推送过):
fatal: bad object 51efa6b
若显示 “fatal: bad object”,说明该提交已从远程删除。
-
-
查看主仓库记录的子模块提交哈希:
# 在主仓库根目录执行 git submodule status # 输出格式:<提交哈希> <子模块路径>
若该哈希在子模块远程仓库中不存在,会导致 GitHub 404。
解决办法:
更新主仓库中子模块的提交哈希为远程存在的版本:
-
进入子模块目录,拉取远程最新代码并切换到有效分支:
cd 子模块路径 git checkout 有效的分支名 # 如 main、dev git pull origin 有效的分支名 # 拉取远程最新提交(确保该提交在远程存在)
-
返回主仓库,提交子模块的新哈希:
cd .. git add 子模块路径 git commit -m "更新子模块到远程存在的最新提交" git push origin 你的分支名
现在应该可以解决404的问题了。
-
参考:
- 菜鸟教程git submodule命令
- 极客教程Git如何移除一个子模块