Stage 2 · Tools
Remote Collaboration
Submodules & Subtrees
When and how to nest Git repos for monorepo-lite workflows.
What are Submodules?
A Git submodule is a separate Git repository nested inside another repository. The parent repo tracks a specific commit of the submodule — not its branches, just a pinned commit hash. This lets you include external code (libraries, shared configs, companion repos) without vendoring the files directly.
Submodules are stored as entries in .gitmodules (configuration) and as commit references in the parent tree. The submodule's working directory is a full Git clone that can be independently committed to and updated.
When you add a submodule, Git records the exact commit hash. If the submodule repo gets new commits, your parent repo doesn't automatically include them. You must explicitly update the submodule reference.
Adding Submodules
git submodule add https://github.com/team/shared-config.git libs/configClones the shared-config repository into libs/config and registers it in .gitmodules. The parent repo now tracks the latest commit of the default branch.
git submodule add -b develop https://github.com/team/shared-config.git libs/configTracks the develop branch of the submodule. When you update, Git fetches and checks out the latest commit on develop instead of the default branch.
cat .gitmodules
git status.gitmodules contains the submodule URL and local path. git status shows the new submodule as a change. Both .gitmodules and the submodule entry must be committed.
The .gitmodules file and the submodule reference (the commit hash in the parent tree) are both tracked. Always commit them together in the same commit.
Cloning with Submodules
A plain git clone does not initialize submodules. The submodule directories will be empty. You must either pass a flag or run a separate command to populate them.
git clone --recurse-submodules https://github.com/team/parent-repo.gitClones the parent repo and automatically runs git submodule update --init --recursive for every submodule. This is the recommended way to clone repos with submodules.
git submodule update --init --recursiveIf you already cloned without --recurse-submodules, run this command to populate all submodule directories. --init initializes .gitmodules entries, --recursive handles nested submodules.
git submodule update --remoteFetches and checks out the latest commit from each submodule's tracked branch. Without --remote, it checks out the commit recorded in the parent tree (which may be stale).
If you see an empty directory where a submodule should be, run git submodule update --init. This is the most common issue when working with submodules for the first time.
Updating Submodules
Updating a submodule means moving the parent repo's reference to a newer commit. This is a two-step process: update the submodule to the desired commit, then stage and commit the change in the parent.
cd libs/config
git pull origin develop
cd ../..
git add libs/config
git commit -m "chore: update shared-config to latest"Enters the submodule, pulls the latest changes, goes back to the parent, and commits the updated reference. The parent now points to the new submodule commit.
git submodule foreach --recursive git pull origin mainRuns git pull origin main inside every submodule directory. The --recursive flag handles nested submodules. After this, commit the parent to record the new references.
git submodule update --init --recursiveResets every submodule to the commit recorded in the parent tree. Discards any local changes in submodules. Useful after a bad update or when submodules are in a dirty state.
git submodule foreach <command> runs a shell command in each submodule directory. It's like a for-loop over all submodules. Combine with --recursive for nested submodules.
Submodules vs Subtrees
Git subtrees are an alternative to submodules. Instead of referencing a separate repository, subtrees merge the external repository's content directly into the parent tree. The choice depends on your workflow needs.
| Aspect | Submodules | Subtrees |
|---|---|---|
| Storage | Separate clone, reference only | Merged into parent tree |
| History | Independent, linked by commit hash | Integrated into parent history |
| Update mechanism | git submodule update | git subtree pull/merge |
| Clone experience | Requires --recurse-submodules | Just works — content is in the tree |
| Contributing back | Push to submodule repo directly | Use git subtree push |
| Complexity | Higher — more commands, more failure modes | Lower — simpler mental model |
Use submodules when you consume external code but rarely modify it. Use subtrees when you actively develop against the external code and want a simpler clone experience for your team.
When to Use Submodules
Submodules solve specific problems but add operational complexity. Use them when the benefits outweigh the costs.
Good use cases for submodules:
- Shared configuration repos used across multiple projects
- Vendor libraries you need to pin to specific versions
- Documentation sites that live in a separate repo
- Companion services (e.g., a mobile app repo inside a monorepo)
- Shared build tooling or CI templates
Avoid submodules when:
- Your team is new to Git — the learning curve is steep
- You frequently modify the submodule code alongside the parent
- You need atomic cross-repo commits (submodules can't do this)
- CI/CD pipelines need simple clone commands
- The submodule has many nested submodules of its own
Shallow clones (--depth 1) don't work well with submodules by default. CI systems using shallow clones need --recurse-submodules and may need git submodule set-url to fix fetch URLs. Test your CI pipeline thoroughly.
Mark this lesson complete to store local progress and unlock a cleaner resume path the next time you visit.