Git Submodules

Embed external repositories inside a project. Pin versions, update dependencies, manage the detached HEAD gotcha.

Adding Submodules

Add a submodule at a specific path
git submodule add https://github.com/EvanusModestus/domus-antora-ui.git ui-bundle

This clones the repo into ui-bundle/, creates a .gitmodules file tracking the URL and path, and stages both. The submodule is pinned to the current HEAD of the remote’s default branch.

Add a submodule pinned to a specific branch
git submodule add -b main https://github.com/EvanusModestus/domus-antora-ui.git ui-bundle

The -b flag records a branch in .gitmodules. When you later --remote update, it pulls from this branch instead of the default.

Commit after adding a submodule
git add .gitmodules ui-bundle
git commit -m "chore: add domus-antora-ui as submodule"

Cloning Repos with Submodules

Clone a repo and initialize all submodules in one command
git clone --recurse-submodules https://github.com/EvanusModestus/domus-docs.git

Without --recurse-submodules, submodule directories exist but are empty. This is the most common source of "it works on my machine" — the clone is incomplete.

Already cloned without submodules — initialize after the fact
git submodule update --init --recursive

--init registers submodules from .gitmodules. --recursive handles nested submodules (submodules within submodules). This is the fix when you forgot --recurse-submodules during clone.

Updating Submodules

Update submodules to the commit recorded in the parent repo
git submodule update --recursive

This checks out the exact commit the parent repo has pinned. It does NOT pull the latest from the remote — it restores the recorded state.

Update submodule to the latest commit on its tracked remote branch
git submodule update --remote

This fetches from the submodule’s remote and checks out the latest commit on the configured branch. The parent repo now shows the submodule as modified — you need to commit this change.

Update a specific submodule only
git submodule update --remote ui-bundle
After updating to latest remote — commit the new pin
git add ui-bundle
git commit -m "chore: update ui-bundle to latest"

The parent repo tracks submodules by commit hash, not branch. Every "update" is a new commit hash that must be recorded.

Inspecting Submodule State

Check status of all submodules
git submodule status

Output prefixes: ` ` (space) = at recorded commit, + = checked out at different commit, - = not initialized, U = merge conflict.

Show submodule summary — what changed since last commit
git submodule summary
Show registered submodules and their URLs
git config --file .gitmodules --list
See what commit each submodule is pinned to
git submodule status --recursive

Running Commands Across All Submodules

Pull latest in every submodule
git submodule foreach 'git pull origin main'
Check status in every submodule
git submodule foreach 'git status --short'
Run arbitrary commands — stash everything before a big update
git submodule foreach 'git stash'
Nested submodules — use --recursive
git submodule foreach --recursive 'git fetch --all'

Removing a Submodule

There is no git submodule remove command. Removal is a 3-step manual process.

Step 1 — Deinitialize the submodule (removes from working tree)
git submodule deinit -f ui-bundle
Step 2 — Remove from the index and delete the directory
git rm -f ui-bundle
Step 3 — Remove residual git directory data
rm -rf .git/modules/ui-bundle
Step 4 — Commit the removal
git commit -m "chore: remove ui-bundle submodule"

The .gitmodules file is updated automatically by git rm. If the submodule was the only one, .gitmodules becomes empty but still exists — you can git rm .gitmodules to clean it up.

Changing a Submodule’s URL

Update the remote URL for a submodule
git config --file .gitmodules submodule.ui-bundle.url git@github.com:EvanusModestus/domus-antora-ui.git
git submodule sync
git submodule update --init --recursive

sync propagates the .gitmodules URL to .git/config. Without it, the old URL remains cached in the local git config and fetches still hit the old remote.

Submodule Gotchas

Detached HEAD is normal — submodules check out a specific commit
cd ui-bundle
git status
# HEAD detached at abc1234

This is by design. Submodules are pinned to a commit, not a branch. If you need to make changes inside the submodule, create a branch first:

Work inside a submodule — branch first, commit, push, then update parent
cd ui-bundle
git checkout -b fix/footer-alignment
# Make changes
git add -A && git commit -m "fix: footer alignment in dark mode"
git push origin fix/footer-alignment
cd ..
git add ui-bundle
git commit -m "chore: update ui-bundle with footer fix"
Prevent accidental submodule commits — diff shows submodule changes
git diff --submodule

Shows the commit log between the old and new pinned commits, not a raw diff. Much more readable than the default dirty marker.

See Also

  • Remotes — upstream tracking and multi-remote setups

  • Worktrees — alternative to submodules for parallel work