Decide First: a Submodule Is Not Glue for Everything
A submodule pins another Git repository onto a commit of the parent; parent and child version independently, and the parent stores only a pointer to a commit. The appeal is exact dependency reproduction; the cost is daily sync, branching, auth, and collaboration overhead.
It suits strongly coupled independent repos that release separately, or third-party product repos (a shared docs theme, a shared config repo) you integrate.
If the goal is merely "reuse shared code", a monorepo or copying the code in is often simpler. Test it with one question: would you develop and push that referenced repo on its own? If yes, a submodule earns its keep.
# 添加子模块(记录的是某个具体提交的指针)
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.gitAfter Clone the Submodule Is Empty: The Two Standard Steps
The most common newbie trap: the project you clone has lib/shared as an empty directory, because the parent does not contain submodule content, only pointers.
The standard fix is two steps: git submodule init registers the urls from .gitmodules into your local .git/config, and git submodule update checks out each recorded commit. The one-liner git submodule update --init is equivalent to both.
If nested submodules exist and you want everything at once, add --recursive.
# 标准激活
cd parent
git submodule init
git submodule update
# 或一步到位
git submodule update --init
# 含嵌套子模块一次性拉全
git submodule update --init --recursive
# 最省事的完整 clone 命令
# git clone --recurse-submodules <url>Mistake: git pull Inside a Submodule Appears to Do Nothing
Some devs see a changed submodule, cd into lib/shared and run git pull. That does fetch a newer commit, but the parent pointer stays put; the next person to init/update snaps back to the old pointer, so "I changed it and nothing happened".
The right move is, from the parent, git submodule update --remote (with a branch configured) to pull the latest, or git submodule update --remote --merge to merge the remote branch into the working tree, then git add that pointer and commit the parent.
If you only want a pinned revision, you should not use update --remote at all; keep the pointer at a commit you verified, and let the bump cadence be a deliberate choice.
# 错误示范——只拉子模块不更新父指针
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"Branch Drift and Detached HEAD: How to Stop Feeling Lost
By default git submodule update leaves the submodule in detached HEAD — pinned to a bare commit with no active branch. That is fine by design, but editing while detached feels confusing because the destination of commits is unclear.
To live on a named branch, configure the submodule to track a remote branch: add branch = main (or develop) under the submodule's section in .gitmodules, then git submodule update --remote checks out the branch's tip. Or just git checkout main inside the submodule.
Remember work inside a submodule does not bubble up to the parent automatically; the parent only records the pointer, and uncommitted changes in the submodule need their own commit and push or they remain local dirt.
# 让子模块跟踪远程 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-featureVerify Submodule State So Bad Pointers Never Sneak In
After clone, CI and colleagues should first run a full submodule init so the environment reproduces exactly.
git submodule status tells you whether each submodule points at the recorded commit and whether its worktree is clean; add --recursive to cover nesting. If a URL, path, or directory is off, status flags a -/+ difference.
Before committing the parent, run git diff --submodule to see the exact pointer movement (from which commit to which), so a wrong version never slips in.
git submodule status --recursive
# <sha> lib/shared 正常
# -<sha> lib/tools 未初始化(前面带 -)
# +<sha> lib/docs 有本地改动或指向不同提交(带 +)
# 查看指针将如何变化
git diff --submodule
git submodule update --recursive # 校准所有子模块