A Different Philosophy
Where submodules store a pointer to another repository, Git Subtree physically copies the source code and commit history of the external project into your repository. From that point on, the external code lives inside your repo as ordinary files — no special commands needed to access it.
Adding a subtree means pointing at the external repository and merging its history into a chosen subdirectory of the local repo. A squash option collapses the external repo's entire history into a single commit, keeping the local repo's log clean; without it, every commit from the external repo gets imported too, which can add thousands of entries to the log.
Submodule vs. Subtree: A Direct Comparison
| Dimension | Submodules | Subtrees |
|---|---|---|
| Storage | Pointer (SHA only) | Full file copy + history |
| Clone experience | Empty directory; requires --recurse-submodules |
Files present immediately |
| Repo size | Small | Grows with each external project |
| Pinning to a version | Explicit and strict | Possible but less obvious |
| Modifying upstream | Requires branch + push to submodule remote | git subtree push (slow on large repos) |
| Team friction | High (easy to forget submodule update) |
Low (feels like a normal folder) |
| CI complexity | Must add submodule update step |
No special steps |
Pushing Changes Back Upstream
The hardest part of subtrees is contributing changes back to the original repository. Pushing a subtree's changes upstream requires Git to scan the entire commit history to extract only the commits that touched the subtree's prefix — on a large repository with years of history, that scan can take minutes. This is the primary operational cost of subtrees over submodules.
When to Use Each
- Use Submodules when you need strict version pinning of a dependency you rarely modify (e.g., a vendored binary tool or a frozen third-party library).
- Use Subtrees when you want to merge an external project and treat it as your own code, modifying it frequently and accepting the larger repo size.
- Use a Package Manager (npm, pip, Go modules, Maven) in almost every other case. Package managers were invented precisely to solve the dependency management problem that submodules and subtrees address awkwardly. If your language ecosystem has a package manager, prefer it.
A common platform engineering interview question is: "How would you share code between multiple microservice repositories?" The answer has three tiers: (1) package manager with a private registry — best for most cases; (2) git subtree — good when you want the code embedded and modified locally; (3) git submodule — good for strict version pinning of rarely-modified dependencies. Knowing all three and their trade-offs, rather than defaulting to one, signals senior-level thinking.