Git Subtree vs Submodule — Complete Guide
In this tutorial, you'll learn about Git Subtree vs Submodule. We cover key concepts, practical examples, and best practices.
Git submodules and subtrees are two built-in mechanisms for including external repositories in your project — submodules reference external commits, while subtrees copy their content into your history.
In this tutorial, you'll learn the differences between Git submodules and subtrees — how each works, their tradeoffs in team workflows, clone and maintenance costs, and when to choose one over the other. Understanding this comparison saves your team from the pain of choosing the wrong dependency strategy. By the end, you'll evaluate both approaches and pick the right one for your project.
Real-world use: DodaTech uses both approaches — submodules for shared protocol definitions that change independently, and subtrees for vendored dependencies where the team needs to modify the source. Doda Browser uses submodules for its rendering engine dependency; DodaZIP uses subtrees for its compression library.
flowchart TD
A[External dependency needed] --> B{Chosen approach}
B --> C[Submodule]
B --> D[Subtree]
C --> E[Reference to external repo]
C --> F[Locked to specific commit]
C --> G[Submodule init required]
D --> H[Content copied into repo]
D --> I[Full history included or squashed]
D --> J[Transparent to team]
E --> K[+ Smaller parent repo]
E --> L[- Team must learn commands]
H --> M[+ No extra commands needed]
H --> N[- Larger parent repo]
Adding a Submodule
Submodules embed a pointer to a specific commit in another repository.
# Add a submodule
git submodule add https://github.com/dodatech/shared-protocols.git libs/protocols
git commit -m "Add shared-protocols as submodule"
# Clone a repo with submodules
git clone --recursive https://github.com/dodatech/app.git
# Or after a regular clone:
git submodule update --init --recursive
The .gitmodules file tracks the submodule configuration:
[submodule "libs/protocols"]
path = libs/protocols
url = https://github.com/dodatech/shared-protocols.git
branch = main
Adding a Subtree
Subtrees copy the external repository content directly into your repository.
# Add a subtree (squash history — recommended)
git subtree add --prefix=libs/compression \
https://github.com/dodatech/compression-lib.git main --squash
# Add a subtree with full history
git subtree add --prefix=libs/compression \
https://github.com/dodatech/compression-lib.git main
# See the squashed merge commit
$ git log --oneline -3
a1b2c3d Squashed 'libs/compression/' content from commit e5f6a7b
b2c3d4e Merge commit 'subtree-libs/compression'
c3d4e5f Previous commit
Expected output:
$ ls libs/compression/
README.md src/ package.json tests/
$ ls -la libs/compression/.git # No .git — it's part of the parent repo
ls: libs/compression/.git: No such file or directory
Updating Dependencies
How each approach handles updates.
# Update submodule to latest
git submodule update --remote libs/protocols
git add libs/protocols
git commit -m "Update protocols submodule to latest"
# Pull subtree updates
git subtree pull --prefix=libs/compression \
https://github.com/dodatech/compression-lib.git main --squash
# This creates a new merge commit
Pushing Changes Back
Submodules and subtrees differ significantly in pushing modifications.
# Submodule: push from inside the submodule directory
cd libs/protocols
git checkout -b my-fix
# Make changes...
git commit -am "Fix protocol parsing bug"
git push origin my-fix
# Then create a PR in the external repo
cd ../..
git add libs/protocols
git commit -m "Update submodule to latest fix"
# Subtree: push directly from the parent repo
git subtree push --prefix=libs/compression \
https://github.com/dodatech/compression-lib.git my-fix-branch
# This splits out only the subtree commits and pushes them
Team Friction Comparison
The most important practical difference is how each approach affects your team.
## Submodule Team Friction
- Developers must learn: `git submodule update --init --recursive`
- CI needs extra steps: checkout with `--recursive` or explicit `submodule update`
- Mistakes: committing without updating submodules, forgetting `--recursive`
- `detached HEAD` state: submodules default to detached HEAD, confusing beginners
- Cross-submodule changes: need commits in multiple repositories
- GUI tool support: inconsistent across editors
## Subtree Team Friction
- Zero additional commands: clone and work like a normal repo
- CI is transparent: no extra checkout steps needed
- No detached HEAD: subtree files are just part of your repo
- Cross-repo changes: single commit in your repo, push split upstream
- GUI tool support: works with any Git client
Performance and Size
Measure the real-world impact.
# Compare repository sizes
# Submodule approach (parent repo is small)
$ du -sh .
12M . # Excluding submodule content
# Subtree approach (parent repo includes subtree history)
$ du -sh .
48M . # Includes compression library history
# Clone time comparison
$ time git clone https://github.com/dodatech/app-submodule.git
# 5.2 seconds (small parent, submodules fetched lazily)
$ time git clone --recursive https://github.com/dodatech/app-submodule.git
# 18.7 seconds (includes all submodule content)
$ time git clone https://github.com/dodatech/app-subtree.git
# 15.3 seconds (single larger clone)
Decision Matrix
Choose based on your specific needs.
| Factor | Choose Submodule | Choose Subtree |
|---|---|---|
| Dependency changes frequency | Frequently | Rarely |
| Team needs to modify dependency | Occasionally | Yes |
| Clone size is critical | Yes | No |
| Team is Git-experienced | Yes | No |
| CI speed is critical | Use partial clone | No extra cost |
| Multiple nested dependencies | Yes | Can be complex |
| Need to pin exact version | Yes | Yes |
| Want to audit dependency history | No | Yes |
Migrating Between Approaches
Convert submodules to subtrees or vice versa.
# Submodule → Subtree migration
# 1. Remove the submodule
git submodule deinit -f libs/protocols
git rm -f libs/protocols
rm -rf .git/modules/libs/protocols
git commit -m "Remove submodule"
# 2. Add the same dependency as subtree
git subtree add --prefix=libs/protocols \
https://github.com/dodatech/protocols.git main --squash
git commit -m "Add protocols as subtree"
# Subtree → Submodule migration
# 1. Remove the subtree content
git rm -r libs/compression
git commit -m "Remove compression subtree"
# 2. Add as submodule
git submodule add https://github.com/dodatech/compression.git libs/compression
git commit -m "Add compression as submodule"
Common Errors
fatal: refusing to merge unrelated historiesin subtree pull — The subtree's remote has diverged from your local copy. Use--squashto avoid history conflicts or usegit subtree pull --squash --allow-unrelated-histories.pathspec 'X' did not match any filesin submodule add — The URL is wrong or the remote doesn't exist. Verify the repository URL and branch name before adding.- Submodule showing as modified without changes — The submodule points to a different commit than what's in the index. Run
git submodule updateto sync, or commit the new submodule reference. git subtree pushfails with "couldn't find unique commit" — Multiple subtree splits can cause ambiguity. Use--annotatewith unique prefixes to identify subtree commits.- Recursive submodules forgotten in CI — CI pipelines must use
git clone --recursiveorgit submodule update --init --recursivefor nested submodules, or the build fails with missing dependencies.
Practice Questions
Challenge
Create a project that depends on a shared library. First, implement the dependency as a submodule: add it, update it to a newer version, modify a file inside the submodule, and push the change. Then remove the submodule and re-add the same library as a subtree. Compare clone times, repository sizes, and the developer experience. Write a decision guide for your team based on your findings.
Real-World Task
Evaluate the DodaTech monorepo's dependency strategy. Doda Browser uses a shared rendering engine from another repository. The team needs to modify the engine while developing features. Should they use submodule or subtree? Consider: team size (20 developers), Git experience (mixed), need to modify the dependency (frequent), clone time targets (under 30 seconds), and CI pipeline overhead. Implement your recommendation and document the migration if needed.
Previous: Git Submodules | Related: Monorepos with Git | Related: Git for Teams
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro