Git Submodules vs Subtrees: Choosing the Right Approach
Compare git submodules and subtrees for managing shared code and nested repositories. Learn workflows, trade-offs, and when to use each approach in real projects.
What you'll learn
- ✓How git submodules work and their core workflow
- ✓How git subtrees work and when they shine
- ✓Key differences in cloning, updating, and contributing back
- ✓Decision framework for choosing between them
- ✓Advanced patterns for managing shared libraries
Prerequisites
- •Comfortable with Git branching and remotes
- •Understanding of repository structure
The Shared Code Problem
Most projects eventually need to share code across repositories. A design system used by three frontend apps. A protobuf schema shared between services. A utility library maintained by a platform team. Git gives you two built-in mechanisms for this: submodules and subtrees. Both solve the problem, but they make fundamentally different trade-offs.
Git Submodules: Linked Repositories
A submodule is a pointer to a specific commit in another repository. The parent repo stores the URL and the pinned commit hash. The actual submodule code lives in its own directory but is managed as a separate repository.
parent-repo/
.gitmodules (URL + path mapping)
.git/ (parent repo data)
src/ (parent code)
libs/shared-ui/ (submodule: separate .git)
-> pinned to commit abc1234 of github.com/org/shared-ui Adding a submodule
git submodule add https://github.com/org/shared-ui.git libs/shared-ui
git commit -m "feat: add shared-ui submodule"
This creates two things:
- A
.gitmodulesfile mapping the path to the URL. - A special entry in the Git tree pointing to the exact commit of the submodule.
Cloning a repo with submodules
When someone clones your repo, submodule directories are empty by default:
# Clone without submodule content
git clone https://github.com/org/parent-repo.git
cd parent-repo
ls libs/shared-ui/ # Empty!
# Initialize and fetch submodules
git submodule update --init --recursive
Or clone everything at once:
git clone --recurse-submodules https://github.com/org/parent-repo.git
Updating a submodule
Submodules are pinned to a specific commit. To update to the latest version:
cd libs/shared-ui
git fetch origin
git checkout main
git pull
cd ..
git add libs/shared-ui
git commit -m "chore: update shared-ui to latest"
Or update all submodules at once:
git submodule update --remote --merge
git add .
git commit -m "chore: update all submodules"
Contributing back to a submodule
Since submodules are full repositories, you can make changes and push them upstream:
cd libs/shared-ui
git checkout -b feature/new-button
# Make changes
git add . && git commit -m "feat: add primary button variant"
git push origin feature/new-button
# Create PR in the shared-ui repo
cd ..
git add libs/shared-ui
git commit -m "chore: point shared-ui to new-button branch"
Git Subtrees: Merged History
A subtree merges another repository’s code directly into your repository. There is no separate .git directory, no pointer — the code is part of your repo’s history. Contributors do not need to know a subtree exists.
parent-repo/
.git/ (single repo, contains all history)
src/ (parent code)
libs/shared-ui/ (subtree: regular directory, full code)
-> code merged from github.com/org/shared-ui Adding a subtree
git subtree add --prefix=libs/shared-ui https://github.com/org/shared-ui.git main --squash
The --squash flag collapses the subtree’s history into a single merge commit, keeping your log clean.
Without --squash, the full commit history of the subtree is merged into your repo.
Cloning a repo with subtrees
There is nothing special to do. Subtree code is part of the repository:
git clone https://github.com/org/parent-repo.git
# libs/shared-ui/ is already populated with code
This is the biggest advantage of subtrees: zero setup for contributors.
Updating a subtree
Pull the latest changes from the upstream repository:
git subtree pull --prefix=libs/shared-ui https://github.com/org/shared-ui.git main --squash
To avoid typing the URL every time, add it as a remote:
git remote add shared-ui https://github.com/org/shared-ui.git
git subtree pull --prefix=libs/shared-ui shared-ui main --squash
Contributing back from a subtree
Push changes made in the subtree directory back to the upstream repository:
git subtree push --prefix=libs/shared-ui shared-ui feature/new-button
This extracts commits that touched libs/shared-ui/ and pushes them to the upstream repo. It works, but it can be slow on repos with long histories because Git must filter every commit.
Head-to-Head Comparison
Cloning experience
Submodules: Requires --recurse-submodules or a separate git submodule update --init step. New developers frequently forget this and end up with empty directories.
Subtrees: Just git clone. Code is already there. No extra steps.
Repository size
Submodules: The parent repo stays small. Submodule code is fetched separately.
Subtrees: The parent repo contains all the subtree code and potentially its full history. Repository size grows.
Updating shared code
Submodules: Update the submodule commit pointer. Explicit and versioned — you always know which version you are using.
Subtrees: git subtree pull merges upstream changes. Less explicit about versioning.
CI/CD complexity
Submodules: CI pipelines need --recurse-submodules in clone steps. Most CI platforms support this, but it is an extra configuration step that gets forgotten.
# GitHub Actions
- uses: actions/checkout@v4
with:
submodules: recursive
Subtrees: No special CI configuration needed.
Contributing changes back
Submodules: Natural. cd into the submodule, work like any repo, push, create PR.
Subtrees: git subtree push works but is slower and less intuitive. Extracting the right commits from mixed history is tricky.
Mental model
Submodules: Two separate repositories linked together. Developers must understand the two-repo model.
Subtrees: One repository with code imported from elsewhere. Developers can ignore the subtree origin entirely.
Decision Framework
Use submodules when:
- The shared code is actively developed as a standalone project. If
shared-uihas its own release cycle, CI, and team, submodules respect that independence. - You need strict version pinning. Submodules always point to an exact commit. You update intentionally.
- Repository size matters. Submodules keep the parent repo lean.
- Contributors frequently work on the shared code. The submodule workflow (cd, commit, push, update pointer) is natural for two-way development.
Use subtrees when:
- Contributors should not need to know about the dependency. Subtrees are invisible to developers who do not manage them.
- The shared code changes infrequently. Occasional
subtree pullis simpler than ongoing submodule management. - You want simple cloning and CI. No extra flags, no initialization steps.
- Contributing back upstream is rare. If changes mostly flow from the upstream into your repo, subtrees work well.
- You want offline access to everything. All code is in one repo.
Avoid both when:
- A package manager can handle it. If the shared code can be published as an npm package, a PyPI package, or a Go module, use the language’s dependency management. It is purpose-built for this.
- You need independent versioning and semver. Package managers handle version constraints better than either submodules or subtrees.
Advanced Patterns
Submodule with branch tracking
Instead of pinning to a commit, track a branch:
# Set the submodule to track the 'main' branch
git config -f .gitmodules submodule.libs/shared-ui.branch main
# Update to the latest on the tracked branch
git submodule update --remote libs/shared-ui
Subtree with a split workflow
For large repos where subtree push is slow, use subtree split to extract the subtree history into a separate branch, then push that:
# Split subtree history into a branch
git subtree split --prefix=libs/shared-ui -b shared-ui-split
# Push the split branch to the upstream remote
git push shared-ui shared-ui-split:main
Multiple submodules with foreach
Run commands across all submodules:
# Update all submodules to latest
git submodule foreach 'git pull origin main'
# Check status of all submodules
git submodule foreach 'git status'
# Run tests in each submodule
git submodule foreach 'npm test || true'
Sparse submodule checkout
If you only need part of a submodule, combine with sparse checkout:
cd libs/shared-ui
git sparse-checkout init
git sparse-checkout set src/components
Common Pitfalls
Submodule pitfalls:
- Forgetting to push submodule changes before pushing the parent. The parent now points to a commit that does not exist on the remote.
- Not running
git submodule updateafter switching branches, leaving submodules out of sync. - Detached HEAD in submodules confuses developers who are not familiar with the model.
Subtree pitfalls:
- Forgetting the
--prefixflag leads to merging code into the wrong directory. - Using
--squashinconsistently causes merge conflicts on subsequent pulls. - History bloat when importing large repositories without
--squash.
Wrap-Up
Submodules and subtrees solve the same problem with opposite philosophies. Submodules keep repositories separate and linked — explicit, versioned, but requiring more setup and awareness. Subtrees merge everything into one repository — simple to clone and work with, but harder to contribute back upstream. Choose submodules when the shared code is a first-class project with its own lifecycle. Choose subtrees when the shared code is a dependency that most developers never need to think about. And always consider whether a proper package manager might be the better answer.
Related articles
- Git Git Submodules vs Subtrees Explained
Compare git submodules and subtrees for managing nested repositories, including workflows, trade-offs, and when to choose each approach in practice.
- Git Git Monorepo Strategies: Sparse Checkout, Shallow Clones, and Tooling
Scale Git monorepos with sparse checkout, partial clones, shallow clones, and modern tooling like Turborepo and Nx. Practical strategies for keeping large repos manageable.
- Git Git Monorepo Management at Scale
Manage large Git monorepos with sparse checkout, partial clone, subtrees, CODEOWNERS, and scaling strategies that keep performance acceptable as the repo grows.
- Git Advanced Git Rebase Techniques: Interactive, Autosquash, and Rebase Onto
Master interactive rebase, autosquash, fixup commits, and rebase --onto for clean Git history. Advanced techniques for rewriting, reorganizing, and cleaning up commits.