先判断该不该用:submodule 不是万能胶水
submodule 把另一个 Git 仓库钉在每个父仓库的提交上,父子各自版本化,父仓库只记录对哪个提交的“指针”。好处是精确复现依赖版本,坏处是每次都要处理同步、分支、认证和协作心智成本。
适合的场景:多个独立仓库之间强依赖、需要各自独立发版,或集成第三方产品仓库(如同一个文档主题、一个共享配置仓库)。
如果只是想“复用一段公共代码”,monorepo(同仓多包)或把代码直接复制进来往往更省事。判断一句话:你会不会单独开发并推送那个被引用仓库?会,才有 submodule 的意义。
# 添加子模块(记录的是某个具体提交的指针)
git submodule add https://github.com/you/shared-lib.git lib/shared
# 提交父仓库时把指针一起提交
git commit -m "add shared-lib submodule"
cat .gitmodules
# [submodule "lib/shared"]
# path = lib/shared
# url = https://github.com/you/shared-lib.gitclone 后子模块目录是空的:标准激活两步
最常见的新手坑:git clone 出来的项目里 lib/shared 是空目录。因为父仓库不包含子模块内容,只记录指针。
标准解法分两步:git submodule init 写入 .git/config(把 .gitmodules 的 url 登记到本地配置),再 git submodule update 按父记录指针 checkout。一步式 git submodule update --init 等价于两者。
如果你还需要嵌套子模块且想一次性拿全,用 --recursive。
# 标准激活
cd parent
git submodule init
git submodule update
# 或一步到位
git submodule update --init
# 含嵌套子模块一次性拉全
git submodule update --init --recursive
# 最省事的完整 clone 命令
# git clone --recurse-submodules <url>错误示范:在子模块里 git pull 却“没生效”
有的同学看到子模块变了就进到 lib/shared 里 git pull,这样确实拉到了新提交,但父仓库记录的指针没动。下次任何人重新 init/update,又回退到旧的指针,于是“怎么我改了没效果”。
正确姿势:在父仓库执行 git submodule update --remote(配合 branch 配置)拉取子模块最新,或用 git submodule update --remote --merge 把远端分支合并进子模块工作区,然后 git add 那个指针再提交父仓库。
如果只想要“固定版本”,甚至不该使用 update --remote,保持指针指在你验证过的提交上,更新频率由你刻意决定。
# 错误示范——只拉子模块不更新父指针
git -C lib/shared pull
# 修复:从父仓库更新子模块到远端最新并提交
git submodule update --remote lib/shared
git status # lib/shared 显示为 modified(指针变了)
git add lib/shared
git commit -m "bump shared-lib to latest main"分支漂移与 detached HEAD:怎么避免“我到底在哪个分支”
默认 git submodule update 会让子模块处于 detached HEAD——只钉在某个提交,没有活动分支。这本身没问题(它就是为固定版本设计的),但很多改动的行为在 detached 下让人困惑(提交去向不明)。
要进入命名分支做日常开发,配置子模块跟踪一个远程分支:在 .gitmodules 的 submodule 段加 branch = main(或 develop),然后 git submodule update --remote 会 checkout 到该分支最新。也可以在子模块里手动 git checkout main。
记住子模块工作区的改动不会自动进入父仓库,父仓库只关心“指针”,子模块里的未提交改动需要单独在子模块内提交并推送,否则只是本地脏状态。
# 让子模块跟踪远程 develop 分支
git config -f .gitmodules submodule.lib/shared.branch develop
git submodule update --remote lib/shared
# 当使用默认固定指针(detached HEAD)时,开发分支另建:
git -C lib/shared checkout -b my-feature main
# 推子模块自己的提交
git -C lib/shared push origin my-feature验证子模块状态,别让坏指针悄悄进仓库
CI 或同事拉到项目后,第一步就是跑一次完整的子模块初始化,保证环境可复现。
用 git submodule status 看每个子模块是否都指着记录值、工作区干不干净;带 --recursive 检查嵌套层。若某个子模块的 URL/路径对不上或目录缺失,status 会标 -/+ 提示差异。
在提交父仓库前跑 git diff --submodule,能看到指针的确切变化(从哪个提交到哪个提交),防止手滑提交错误版本。
git submodule status --recursive
# <sha> lib/shared 正常
# -<sha> lib/tools 未初始化(前面带 -)
# +<sha> lib/docs 有本地改动或指向不同提交(带 +)
# 查看指针将如何变化
git diff --submodule
git submodule update --recursive # 校准所有子模块