Version Control

Git Submodule Management and Its Common Traps

A field guide to when a submodule actually makes sense, how to add, clone, and update it, and the standard escape hatches for empty dirs after clone, branch drift, and nesting.

By LaoHand Team·7 min read·Updated 2026-09-06

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.git

After 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-feature

Verify 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   # 校准所有子模块

Official References

Each command links to its official documentation below, so you can verify the latest usage and read deeper.