which strategy actually fits which use case
As soon as a project needs to pull in code from another repository, two built in Git mechanisms come into play: submodules and subtree. Both solve the same underlying problem of making external history referenceable inside your own repository, but with very different consequences for checkout, CI pipelines, and who maintains which code where. This article compares both approaches through concrete workflows and explains when each one makes sense in a Magento context.
Table of Contents
- 1. The core problem: external code inside your own repository
- 2. How Git submodule works under the hood
- 3. How Git subtree works under the hood
- 4. Day to day work with submodules
- 5. Day to day work with subtree, including pushing back
- 6. Impact on CI pipelines and checkout time
- 7. Subtree and submodule in Magento projects
- 8. Decision guide: which strategy, when
- 9. Switching from submodule to subtree and back
- 10. Summary
- 11. FAQ
1. The core problem: external code inside your own repository
Almost every larger project eventually reaches a point where it needs to include code from another repository, whether that is a shared library, a theme, or an internal toolset. The naive solution of simply copying the foreign code and checking it into your own repository works in the short term, but immediately loses the connection to the original history and turns later updates into manual busywork.
Git ships with two native mechanisms for exactly this problem: submodule maintains a reference to an exact commit of a foreign repository, while subtree copies the foreign code including its history directly into a subdirectory of your own repository. Both approaches have been stable parts of Git for years, but they differ so fundamentally in how they work that the choice is rarely arbitrary.
2. How Git submodule works under the hood
A submodule is technically not a folder full of files in the usual sense, but a special reference entry pointing to a specific commit hash of a foreign repository. The .gitmodules file at the repository root stores the path and remote URL of every included submodule, while the parent repository itself only carries the referenced commit hash, not the actual content.
This has a decisive advantage: the parent repository stays small because the actual history of the foreign code remains separate. The downside shows up at checkout time, because a plain git clone does not fetch submodule content by default, it only creates an empty folder. Anyone who needs the full state has to explicitly run git submodule update --init --recursive, a step that is easy to forget in CI pipelines and then leads to puzzling missing files.
# Add a submodule
git submodule add https://example.com/shared-library.git vendor/shared-library
# After a fresh clone: pull in submodule content
git clone https://example.com/main-project.git
cd main-project
git submodule update --init --recursive
# Bring the submodule up to the latest commit on its referenced branch
git submodule update --remote vendor/shared-library
3. How Git subtree works under the hood
Subtree takes the opposite approach: instead of a reference, the complete content of the foreign repository is merged directly into a subdirectory of the parent repository. After including it, all files sit right there in the working tree, as if they had been part of the project from the start. There is no separate .gitmodules file and no second checkout step.
The price for that is a larger history in the parent repository, because the full commit history of the foreign project comes along when it is included, unless you use --squash. For most teams the practical benefit outweighs that cost: anyone who clones gets the complete code immediately, no extra commands, and tools that have no concept of submodules simply keep working.
# Add a foreign repository as a subtree, history squashed
git subtree add --prefix=vendor/shared-library \
https://example.com/shared-library.git main --squash
# Pull in changes from the original repository
git subtree pull --prefix=vendor/shared-library \
https://example.com/shared-library.git main --squash
4. Day to day work with submodules
Working with submodules day to day adds an extra mental layer, because a submodule folder is almost always in a so called detached HEAD state: it points at a specific commit, not a branch. Anyone making changes inside a submodule has to explicitly switch to a branch there, commit, and push before the reference in the parent repository can even be updated.
If someone on the team forgets to commit the updated submodule reference in the parent repository, other developers keep working with an outdated state without noticing. In practice, git status in the parent repository helps surface such discrepancies, because Git reports a changed submodule commit as a modified file.
5. Day to day work with subtree, including pushing back
Because subtree code looks like ordinary project code, developers can change it directly without thinking about submodule specific quirks. The real challenge is getting such local changes back into the original repository later on. That is what git subtree push is for, filtering out the relevant commits from the subdirectory and pushing them into the foreign repository.
On repositories with lots of parallel changes, this push back step can noticeably slow down, because Git has to search the entire history for changes relevant to that particular prefix. For projects that only occasionally pull updates from the source and rarely push back themselves, that is not a practical problem, but for very active bidirectional collaboration it is worth looking at dedicated wrapper tooling or alternative monorepo strategies.
# Push local changes from the subtree folder back to the original repository
git subtree push --prefix=vendor/shared-library \
https://example.com/shared-library.git feature/local-change
6. Impact on CI pipelines and checkout time
The difference becomes especially clear in CI environments. A standard checkout without extra configuration is fully sufficient for subtree, because the code physically lives in the repository. For submodules, the pipeline configuration must explicitly know to initialize recursively, whether through GIT_SUBMODULE_STRATEGY: recursive in GitLab CI or an equivalent checkout action option on GitHub Actions.
If that step is forgotten, the build often fails late and with a confusing error about missing files rather than an obvious Git error. Teams that rely on submodules should document this step explicitly in pipeline documentation and onboarding guides for new developers.
7. Subtree and submodule in Magento projects
In the Magento world, this question typically comes up around shared Hyva child themes, modules reused across multiple tenants, or internal Composer packages that are not yet distributed through a private Packagist repository. For a theme shared by several shops with an identical base but small store specific tweaks, subtree fits well, because developers can work directly in the theme folder without worrying about detached HEAD states.
For clearly scoped, rarely changed libraries, say a shared PHP utility class collection referenced by several independent Magento installations, submodule is often the cleaner choice, because the reference to a specific, tested version stays explicitly visible in the parent repository. In practice, for genuine PHP dependencies a private Composer repository usually replaces both approaches, leaving subtree and submodule as tools for edge cases like themes or build configuration.
8. Decision guide: which strategy, when
If you change things frequently, want little history overhead, and want every developer to be productive right after a plain clone, subtree is usually the better fit. If you instead need a clear, versioned separation between your own code and a dependency treated as external, and you are willing to accept the extra checkout step, submodule serves you well.
A third option that makes both tools unnecessary in many cases is a real package manager: for PHP code that is Composer with a private repository, for JavaScript it is npm with a private registry scope. Subtree and submodule then remain reserved for cases a package manager does not fit well, such as entire theme directories or build tooling that should not be modeled as a package.
9. Switching from submodule to subtree and back
An existing submodule can be converted into a subtree by removing the submodule entry and then including the code fresh via git subtree add, though without automatically carrying over the original commit history in the parent repository. The reverse path, from subtree back to submodule, is more work, because the already copied files first need to be removed and replaced with a clean submodule reference.
In either direction it is worth doing the switch in its own commit with a clear description and informing the whole team beforehand, because such a structural change affects existing local working copies and, in the worst case, causes confusion over vanished or duplicated folders.
# Remove an existing submodule before re-adding it as a subtree
git submodule deinit -f vendor/shared-library
git rm -f vendor/shared-library
rm -rf .git/modules/vendor/shared-library
git commit -m "Remove vendor/shared-library submodule"
git subtree add --prefix=vendor/shared-library \
https://example.com/shared-library.git main --squash
| Aspect | Submodule | Subtree |
|---|---|---|
| Stored in parent repository | Only a pointer to a commit hash |
Full code copied in |
| Checkout after git clone | Extra submodule update --init step required |
Complete immediately, no extra step |
| CI configuration | Must explicitly enable recursive checkout | Works with a standard checkout |
| Pushing changes back | Commit and push directly inside the submodule | git subtree push, can get slow on large history |
| History in parent repository | Stays lean | Grows, especially without --squash |
Mironsoft
Git workflows, branching strategies, and CI hooks
Chaotic Git history and unclear branching rules across the team?
We set up clean Git workflows, clarify branching strategies for the team, and automate quality checks via Git hooks and CI pipelines so the history stays traceable.
Workflow Audit
Review the existing branching strategy and merge practice for weak spots.
Hook Automation
Set up pre-commit and pre-push hooks for linting, tests, and commit conventions.
Team Training
Teach rebase, cherry-pick, and conflict resolution hands-on across the team.
10. Summary
Subtree vs. Submodule at a Glance
Recommendation
Subtree for frequently changed shared themes, submodule for stable, versioned dependencies
CI overhead
Submodule needs an explicit recursive checkout, subtree works without extra configuration
History
Submodule keeps the parent repository lean, subtree copies history along with it
Magento practice
For genuine PHP dependencies, Composer usually wins, subtree/submodule stay for themes and edge cases