Git Submodules
Embed external repositories inside a project. Pin versions, update dependencies, manage the detached HEAD gotcha.
Adding Submodules
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.
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.
git add .gitmodules ui-bundle
git commit -m "chore: add domus-antora-ui as submodule"
Cloning Repos with Submodules
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.
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
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.
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.
git submodule update --remote ui-bundle
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
git submodule status
Output prefixes: ` ` (space) = at recorded commit, + = checked out at different commit, - = not initialized, U = merge conflict.
git submodule summary
git config --file .gitmodules --list
git submodule status --recursive
Running Commands Across All Submodules
git submodule foreach 'git pull origin main'
git submodule foreach 'git status --short'
git submodule foreach 'git stash'
git submodule foreach --recursive 'git fetch --all'
Removing a Submodule
There is no git submodule remove command. Removal is a 3-step manual process.
git submodule deinit -f ui-bundle
git rm -f ui-bundle
rm -rf .git/modules/ui-bundle
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
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
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:
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"
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.