Git Subtree vs. Submodule: Choosing the Right Strategy for Each Use Case
AI generated
git
HEAD
Git · Repository Structure
Git Subtree vs. Submodule
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.

9 min read Git Repository Structure

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

11. FAQ: Subtree vs. Submodule at a Glance

1What is the main difference between Git subtree and Git submodule?
Submodule only stores a reference to a specific commit of a foreign repository, while subtree copies the full code directly into a subdirectory of the parent repository. Submodule therefore needs an extra checkout step, subtree does not.
2Why is a submodule folder empty right after cloning?
Git only stores the reference to the submodule's commit hash in the parent repository, not its content. Only the command git submodule update --init --recursive downloads the actual files and places them in the referenced folder.
3Can I make changes directly inside a subtree folder?
Yes, that is one of the main advantages of subtree. The code behaves like ordinary project code, changes can be committed normally and later pushed back into the original repository via git subtree push.
4Why doesn't a plain git clone fully work for submodules?
A plain git clone does not initialize submodules automatically for compatibility and performance reasons. Without the --recurse-submodules flag or a subsequent submodule update command, the referenced folders stay empty.
5How do I update a submodule to the latest state of its remote branch?
git submodule update --remote brings the submodule up to the current commit of the branch stored in .gitmodules. The new reference then needs to be committed in the parent repository so other developers pick it up too.
6Does subtree pull in the entire history of the foreign repository?
By default yes. The --squash flag compresses the incoming history into a single commit both when including it and on every subsequent pull, keeping the parent repository considerably leaner.
7Which solution makes more sense for a shared Hyva theme across multiple Magento shops?
In practice subtree usually fits better, because developers can work directly in the theme folder without dealing with detached HEAD states, and a plain clone immediately delivers the full code.
8Do submodules need anything special configured in CI pipelines?
Yes, without explicit configuration such as GIT_SUBMODULE_STRATEGY in GitLab CI or an equivalent checkout action option on GitHub Actions, submodule folders stay empty during pipeline checkout, which leads to confusing build failures.
9Isn't a private Composer repository the better alternative to both?
For genuine versioned PHP code dependencies, a private Composer repository is usually the cleanest solution. Subtree and submodule remain relevant mainly for cases that do not model well as a package, such as complete theme directories.
10Can I switch from submodule to subtree without redoing the whole project?
Yes, the submodule is removed with git submodule deinit and git rm, then git subtree add includes the same code fresh as a subtree. The original submodule history in the parent repository is lost in the process, however.