git subtree: Alternative to Submodules for Shared Code
The previous lesson's submodules approach — a reference pinned to a specific commit, requiring extra clone steps and a two-level commit workflow — isn't the only way to include external code in a repository. `git subtree` takes a fundamentally different approach: actually merging the external repository's files and history directly into your own project, avoiding the separate-reference model entirely.
Learning Objectives
- Explain how git subtree's approach fundamentally differs from submodules.
- Add an external repository as a subtree.
- Pull updates from the external repository into your subtree.
- Understand the trade-offs between subtree and submodules for a given use case.
Key Terms to Know Before Using git subtree
- git subtree: A Git command that merges an external repository's files and history directly into a subdirectory of your own repository, rather than storing a separate reference.
- Squashed subtree: A common subtree usage pattern that condenses the external repository's entire history into a single commit when merging it in, keeping the main repository's history cleaner.
- git subtree pull: Fetches and merges any new changes from the external repository into your subtree's subdirectory.
- git subtree push: Pushes changes made within your subtree's subdirectory back out to the external repository.
How git subtree Actually Works
Where a submodule (previous lesson) stores only a **reference** to a specific commit of an external repository — requiring extra steps to clone and a separate commit workflow to update — `git subtree` takes a fundamentally different approach: it **actually merges** the external repository's files (and, optionally, its history) directly into a subdirectory of your own repository, as genuine, ordinary commits in your own history.
Adding an external repository as a subtree:
```
git subtree add --prefix=libs/shared-library https://github.com/org/shared-library.git main --squash
```
This fetches the external repository and merges its content into the `libs/shared-library` subdirectory, with `--squash` (a very commonly used option) condensing the external repository's entire history into a single commit in your project, rather than importing every individual commit from its full history — keeping your own project's commit log cleaner, at the cost of not preserving that external history in full detail within your own repository.
The single most significant practical advantage of subtree over submodules: **anyone who clones your repository gets the subtree's content immediately, automatically, with a completely ordinary `git clone`** — no `--recurse-submodules` flag, no separate `init`/`update` steps, no risk of ending up with confusing empty folders. Since the external content is genuinely merged into your history as regular files and commits, it behaves exactly like any other part of your project from a cloning perspective.
To pull in newer changes from the external repository later:
```
git subtree pull --prefix=libs/shared-library https://github.com/org/shared-library.git main --squash
```
And, if you've made local modifications within the subtree's subdirectory that you want to contribute back to the original external repository:
```
git subtree push --prefix=libs/shared-library https://github.com/org/shared-library.git some-branch-name
```
The trade-off compared to submodules: subtree's simplicity for cloning comes at the cost of a **less explicit, harder-to-see boundary** between your own code and the merged-in external code — since it's all just ordinary commits in one unified history, it's not immediately obvious from a casual glance which files came from where, unlike a submodule's clearly separate, distinctly pinned reference. Subtree is generally the better choice when simplicity for anyone cloning your project matters most and you don't need submodule's stricter, more explicit separation; submodules remain preferable when you specifically want that clear, explicit boundary and pinned-version tracking, and are willing to accept the extra clone/update steps in exchange.
git subtree vs Submodules: Visual Walkthrough
Draw two contrasting approaches side by side. LEFT labeled 'Submodule': main repo icon with a DASHED-border subfolder pointing OUTWARD to a separate repository icon, captioned 'Reference only — requires --recurse-submodules to clone; separate commit workflow to update.' RIGHT labeled 'Subtree': main repo icon with a SOLID-border subfolder, containing the actual merged files directly as part of the SAME commit history, captioned 'Fully merged in — a plain git clone gets everything automatically, no extra steps.'
git subtree vs Git Submodules: Key Differences
| Aspect | Submodule | Subtree |
|---|---|---|
| What's stored | A reference pinned to one specific commit | The external repository's actual files, merged into your history |
| Cloning experience | Requires --recurse-submodules or init/update steps | Plain git clone works automatically, no extra steps |
| Boundary clarity | Explicit — clearly a separate, pinned reference | Less explicit — merged in as ordinary commits, boundary less visible |
| Contributing changes back | Update the submodule's own repo directly, then update the pinned reference | git subtree push to send local subtree changes back to the external repo |
git subtree: Command Syntax and Examples
# Add an external repository as a subtree (condensing its history with --squash)
git subtree add --prefix=libs/shared-library \
https://github.com/org/shared-library.git main --squash
# Anyone cloning your project now gets this content automatically — no extra steps:
git clone https://github.com/your-org/my-app.git
# libs/shared-library/ is already fully populated, no init/update needed
# Pull in newer changes from the external repository later
git subtree pull --prefix=libs/shared-library \
https://github.com/org/shared-library.git main --squash
# Push local changes made WITHIN the subtree back to the external repository
git subtree push --prefix=libs/shared-library \
https://github.com/org/shared-library.git contribution-branch
Breaking Down the git subtree Example
`git subtree add` merges the external repository's content directly into `libs/shared-library`, with `--squash` condensing its full history into one commit for cleanliness. The key contrast shown next is the cloning experience: unlike a submodule, a completely plain `git clone` of the main project automatically includes this fully populated subtree content, with no extra flags or follow-up commands required. `git subtree pull` and `git subtree push` mirror ordinary pull/push operations but specifically scoped to keeping the subtree's subdirectory synchronized with (or contributing changes back to) the original external repository.
How git subtree Is Used on Real Engineering Teams
- Some teams specifically choose subtree over submodules precisely because it removes the recurring 'forgot to init submodules' problem for new contributors and CI systems, since a plain clone just works.
- Projects vendoring a specific, occasionally-updated external dependency directly into their source tree (rather than managing it through a package manager) sometimes use subtree specifically for its simpler, fully self-contained cloning experience.
- Teams that need to occasionally contribute fixes back to the original external repository appreciate git subtree push as a relatively direct way to send subtree-local changes upstream.
- Some engineering organizations have documented internal guidance explicitly choosing between subtree and submodules based on whether contributor simplicity (favoring subtree) or explicit version boundary clarity (favoring submodules) matters more for a specific use case.
git subtree Interview Questions and Answers
Q1. What is the fundamental difference between git subtree and git submodules?
A submodule stores only a reference pinned to a specific commit of an external repository. Subtree actually merges that external repository's files (and optionally its history) directly into your own repository's history as ordinary commits, rather than storing a separate reference.
Q2. What is the main practical advantage of subtree over submodules when it comes to cloning?
Since subtree content is genuinely merged into your project's own history as regular commits, anyone cloning your repository gets it automatically with a completely plain git clone — no --recurse-submodules flag or separate init/update steps required, unlike submodules.
Q3. What trade-off does subtree's simpler cloning experience come with, compared to submodules?
Subtree provides a less explicit, harder-to-see boundary between your own code and the merged-in external code, since it all becomes part of one unified commit history. Submodules provide a clearer, more explicit separation and pinned-version tracking, at the cost of requiring extra steps to clone and update.
git subtree Quiz: Test Your Understanding
1. What does git subtree do differently from a submodule?
- It stores only a reference to a specific commit, exactly like a submodule
- It actually merges the external repository's files directly into your own repository's history
- It deletes the external repository entirely
- It requires a paid GitHub feature
Answer: B. It actually merges the external repository's files directly into your own repository's history
Explanation: Unlike a submodule's reference-only approach, subtree genuinely merges the external content in as ordinary commits within your own project's unified history.
2. What is the main advantage of subtree when someone clones a repository that uses it?
- Cloning is impossible with subtree
- A plain git clone automatically includes the subtree's content, with no extra flags or steps needed
- Subtree requires a special paid clone tool
- Subtree content is never actually included in clones
Answer: B. A plain git clone automatically includes the subtree's content, with no extra flags or steps needed
Explanation: Since subtree content is fully merged into the repository's own history, an ordinary clone automatically includes it, unlike submodules which require --recurse-submodules or separate init/update commands.
3. What does the --squash flag do when adding a subtree?
- Deletes the external repository
- Condenses the external repository's entire history into a single commit, rather than importing it in full
- Automatically pushes changes back upstream
- Prevents the subtree from ever being updated
Answer: B. Condenses the external repository's entire history into a single commit, rather than importing it in full
Explanation: --squash keeps the main project's commit log cleaner by merging in the external repository's content as one combined commit, instead of importing every individual commit from its full history.
Common git subtree Mistakes Beginners Make
- Assuming subtree and submodules work identically, when they represent fundamentally different approaches (merged history vs. pinned reference).
- Forgetting the --squash flag when adding a subtree, unexpectedly importing an external repository's entire, potentially large history into the main project.
- Expecting subtree to provide the same explicit, clearly visible version boundary that a submodule's separate reference naturally provides.
- Not realizing git subtree push exists as a way to contribute local subtree changes back to the original external repository.
git subtree: Exam-Ready Quick Notes
- git subtree: merges an external repository's files directly into your own history, unlike a submodule's reference-only approach.
- git subtree add --prefix=<path> <url> <branch> --squash: adds a subtree, condensing external history into one commit.
- Key advantage: a plain git clone automatically includes subtree content, no extra flags or steps needed.
- Trade-off: less explicit separation between your own code and merged-in external code, compared to submodules.
git subtree: Key Takeaways
- git subtree merges external code directly into your project's history, fundamentally different from a submodule's separate, pinned-reference approach.
- Subtree's biggest practical advantage is a completely ordinary cloning experience, with no extra flags or follow-up commands ever required.
- The trade-off is a less explicit boundary between your own code and the merged-in external content, compared to a submodule's clearer separation.
Frequently Asked Questions About git subtree
Q1. What is git subtree, and how is it different from a submodule?
It's a way to include an external repository's code, but unlike a submodule (which only stores a reference to a specific commit), subtree actually merges that external repository's files directly into your own repository's history as ordinary commits.
Q2. What is the main advantage of subtree over submodules?
When someone clones your repository, a subtree's content is automatically included with a completely plain git clone, since it's genuinely part of your project's own history — no extra flags or separate initialization steps are ever needed, unlike with submodules.
Q3. How do I add an external repository as a subtree?
Use git subtree add --prefix=<path> <url> <branch> --squash, which merges the external repository's content into the specified subdirectory, with --squash condensing its history into a single commit to keep your project's log cleaner.
Q4. How do I get newer changes from the external repository into my existing subtree?
Run git subtree pull --prefix=<path> <url> <branch>, which fetches and merges any new changes from the external repository into your subtree's subdirectory.
Q5. What is a downside of using subtree compared to submodules?
Since subtree content becomes part of your project's own unified commit history, there's a less explicit, harder-to-see boundary between your own code and the merged-in external code, compared to a submodule's clearly separate, explicitly pinned reference.
Summary
`git subtree` offers an alternative to submodules for including external code, taking a fundamentally different approach: rather than storing a reference pinned to a specific commit, subtree actually merges the external repository's files (often with `--squash` condensing its history into a single commit) directly into a subdirectory of your own repository's history, as ordinary commits. The most significant practical advantage over submodules is the cloning experience — since subtree content is genuinely merged into your project's own history, anyone cloning your repository gets it automatically with a completely plain `git clone`, with no `--recurse-submodules` flag or separate `init`/`update` steps ever required. `git subtree pull` fetches and merges newer external changes, while `git subtree push` can send local subtree modifications back to the original external repository. This simplicity comes at the cost of a less explicit, harder-to-see boundary between your own code and the merged-in external content, compared to a submodule's clearer, more explicit separation and pinned-version tracking.