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.
What you'll learn
- ✓How submodules pin external repos by commit
- ✓How subtrees embed history into the parent
- ✓Workflows for cloning and updating each
- ✓Trade-offs in CI, contributors, and tooling
- ✓When to prefer one over the other
Prerequisites
- •Familiar with shell
- •Basic Git branching and remotes
What and Why
Real projects rarely live in isolation. You may need a shared design system, a vendor SDK, or a private utility library used by several services. Git offers two built-in mechanisms for referencing another repository inside yours: submodules and subtrees. They solve the same problem—composing code from multiple repos—but with very different mechanics.
A submodule is a pointer: the parent repo records the URL and the exact commit SHA of the child repo at a path. The child stays a separate repo on disk. A subtree is a merge: the child’s history is folded into the parent so files live as ordinary tracked content with no special metadata.
Mental Model
Think of submodules as symbolic links with version pinning. The parent says “at libs/sdk you’ll find commit abc123 from git@github.com:org/sdk.git.” Anyone cloning must initialize submodules to actually fetch the files.
Subtrees are more like vendoring: the SDK’s files and (optionally) its history become part of the parent. A clone of the parent gets everything in one shot, with no extra commands.
This single difference cascades into every other trade-off: clone UX, contributor experience, CI configuration, and how you push fixes upstream.
Hands-on Example
Let’s add a shared library common-utils to a parent app using both approaches so you can see the contrast.
# Submodule workflow
git submodule add git@github.com:org/common-utils.git libs/utils
git commit -m "Add common-utils submodule"
# Clone later
git clone git@github.com:org/app.git
cd app
git submodule update --init --recursive
# Update the pinned commit
cd libs/utils
git fetch && git checkout v1.4.0
cd ../..
git add libs/utils
git commit -m "Bump common-utils to v1.4.0"
# Subtree workflow
git remote add utils-remote git@github.com:org/common-utils.git
git subtree add --prefix=libs/utils utils-remote main --squash
# Pull updates
git subtree pull --prefix=libs/utils utils-remote main --squash
# Push local fixes back upstream
git subtree push --prefix=libs/utils utils-remote feature/fix-bug
Submodule layout Subtree layout
----------------- -----------------
app/ app/
.gitmodules libs/
libs/ utils/
utils/ --> commit abc123 index.js
(separate .git) package.json
(tracked files)
Parent records: URL + SHA Parent records: real blobs In the submodule case, libs/utils has its own .git directory and behaves as an independent repo. In the subtree case, the files are indistinguishable from any other directory.
Common Pitfalls
- Forgetting
--init --recursive. New contributors clone the parent and see empty submodule directories, then file confused bug reports. - Detached HEAD inside submodules.
git submodule updatechecks out a specific SHA, not a branch. Editing without first runninggit checkout mainloses commits. - Subtree merge noise. Without
--squash, subtree pulls bring in every upstream commit and can produce confusing merge graphs. - Pushing subtree changes.
git subtree pushrewrites history from the prefix; it can be slow on large repos and surprises contributors who expect a normalgit push. - Mixing both in one repo. This is technically possible but multiplies cognitive overhead—pick one.
Practical Tips
- Use submodules when the child repo is large, evolves independently, or has its own release cadence. Pinning by SHA gives you reproducibility and a clean separation of ownership.
- Use subtrees when contributors should treat the embedded code as part of the project, when you want a single clone command, or when CI shouldn’t need extra steps.
- Add a
make bootstraporscripts/setup.shtarget that runsgit submodule update --init --recursive. Newcomers will thank you. - For subtrees, document the remote name and prefix in
CONTRIBUTING.md. Without that note, future maintainers won’t know how to pull updates. - Consider package managers (Go modules, npm, pip) before reaching for either. They solve the same problem with better tooling for most cases.
Wrap-up
Submodules and subtrees both let you compose repos, but they make opposite trade-offs. Submodules favor isolation and precise pinning at the cost of contributor friction. Subtrees favor a seamless clone and edit experience at the cost of history complexity and harder upstream pushes. Pick based on who edits the embedded code most often and how independently the projects evolve.
Related articles
- Git Git Cherry-pick and Revert Tutorial
Learn how to copy specific commits across branches with cherry-pick and how to safely undo merged changes with revert, including conflict handling and recovery.
- Git Git Large File Storage (LFS) Tutorial
Set up Git LFS to version large binaries like images, models, and datasets without bloating your repository, including tracking, migration, and CI tips.
- Git Git Rebase vs Merge: When to Use Which
A clear, practical guide to choosing between git rebase and git merge, with safe workflows for feature branches, shared branches, and pull requests.
- Git Git reflog Recovery Tutorial
Use git reflog to recover lost commits, branches, and stashes after rebases, resets, and bad merges. A practical walkthrough of how Git remembers where HEAD has been.