
September 11, 2026
12 min read
By Kokil Thapa | Last reviewed: September 2026
You inherit a Laravel monorepo that shares a private package with three client apps. The team asks whether to wire that library with Git submodules or subtrees. Both patterns nest one repository inside another, but they store history differently and break CI in different ways. This guide compares Git Submodules vs Subtrees the way you actually use them on production projects—commands, update flows, and the traps I've hit during enterprise application deployments.
What is the difference between Git submodules and subtrees?
A submodule is a pointer. The parent repo stores a commit SHA for an external repository checked out in a folder. A subtree is a merge. The parent repo contains the child files and their full history under one path.
Clone behaviour shows the gap immediately. With submodules, git clone gives you an empty submodule directory until you run git submodule update --init --recursive. With subtrees, one clone is enough—no extra init step.
Ownership of history differs. Submodule consumers pin an exact commit. They upgrade deliberately. Subtree consumers see child changes as normal parent commits. They can squash or split later, but the default feels like monorepo work.
On sister sites that share a Deployer recipe, I've used submodules when the recipe lives in its own repo with semver tags. Subtrees worked better when every site needed the same hotfix today and nobody wanted to chase submodule SHAs across five pipelines.
| Criteria | Git Submodule | Git Subtree |
|---|---|---|
| Storage model | External repo + SHA pointer in parent | Child files and history inside parent path |
| Clone steps | Needs submodule update --init | Standard git clone only |
| Version pinning | Explicit, per-commit | Implicit via merge commits |
| Push workflow | Two repos, two remotes | Can push subtree splits back upstream |
| CI complexity | Higher—recursive fetch required | Lower—looks like one repo |
| Contributor learning curve | Steep—easy to commit wrong state | Moderate—feels like normal Git |
| Best fit | Shared libraries with own release cycle | Forks, vendored code, small teams |
The official Git book documents submodule mechanics in depth. The Git Tools — Submodules chapter remains the best primary reference for pointer semantics and update flags.
When should you use Git submodules instead of subtrees?
Pick submodules when the child project has its own lifecycle. Open-source dependencies, internal Composer packages, shared themes, or infrastructure modules that tag releases belong here. You want consumers to opt in to v2.4.1, not accidentally pull main.
Pick subtrees when you want one repository for developers and CI. Agency clients, small Nepal teams, and solo maintainers often lack bandwidth for submodule hygiene. A subtree under lib/payments/ keeps Khalti or eSewa integration code reusable without a second remote to configure on every laptop.
Consider your hosting. GitLab CI and GitHub Actions both support submodules, but you must enable recursive checkout and provide deploy keys or tokens. I've seen pipelines pass unit tests locally yet fail in CI because vendor/internal-sdk was an uninitialized submodule directory. That class of bug rarely appears with subtrees.
Security scanning also nudges the choice. Tools like gitleaks run cleanly on a single tree. Submodules sometimes get skipped unless your scanner walks nested repos. See our guide on secrets scanning in Git and CI with gitleaks for pipeline patterns that apply to both models.
- Use submodules when multiple products consume the same library at different versions.
- Use submodules when the child repo already has issue tracking and release notes.
- Use subtrees when new hires must clone once and run
composer install. - Use subtrees when you may fork upstream temporarily and merge back later.
- Prefer neither when Composer, npm workspaces, or a private package registry solves the problem—Git nesting is a last resort for application repos.
How do you add and update a Git submodule?
Adding a submodule records the URL and path in .gitmodules. Git checks out the default branch tip unless you pass a specific commit during setup.
Add a submodule
cd /var/www/my-laravel-app
git submodule add git@gitlab.com:your-org/shared-validators.git packages/shared-validators
git commit -m "Add shared-validators submodule"
The parent commit now stores the submodule SHA. Teammates need the recursive init on first pull.
git clone git@gitlab.com:your-org/my-laravel-app.git
cd my-laravel-app
git submodule update --init --recursive
Shorthand for clones: git clone --recurse-submodules. Document that flag in your README. Onboarding friction drops sharply.
Update to a newer submodule commit
cd packages/shared-validators
git fetch origin
git checkout v1.3.0
cd ../..
git add packages/shared-validators
git commit -m "Bump shared-validators to v1.3.0"
Or from the parent root:
git submodule update --remote --merge packages/shared-validators
git add packages/shared-validators .gitmodules
git commit -m "Track latest shared-validators on main"
The --remote flag moves the pointer to the upstream branch tip. That is convenient and dangerous. Pin tags in production apps unless you truly want floating HEAD.
Detached HEAD is the classic submodule footgun. Developers commit inside the submodule, push the parent pointer, but never push the child branch. The parent points at a commit only their laptop knows. Train the team: always push the submodule remote first, then the parent.
For Laravel apps, wire submodule paths into Composer with a path repository or symlink carefully. Most teams publish the shared code as a private Composer package instead. Submodules then hold deployment configs or themes—not PHP autoload roots—unless you enjoy pain.
How do you add and update a Git subtree?
Subtree commands live in Git contrib but ship with standard Git installs. They merge another repository into a subdirectory while preserving the ability to push changes back with subtree split.
Add a subtree
cd /var/www/my-laravel-app
git subtree add --prefix=packages/shared-validators \
git@gitlab.com:your-org/shared-validators.git main --squash
The --squash flag collapses upstream history into one commit. Your parent log stays readable. You lose granular blame inside the subtree unless you omit squash during the initial add.
Pull upstream changes
git subtree pull --prefix=packages/shared-validators \
git@gitlab.com:your-org/shared-validators.git main --squash
Resolve merge conflicts like any normal pull. The conflict markers appear in subtree files under the prefix path. Our article on resolving Git merge conflicts confidently applies directly here.
Push local subtree changes upstream
git subtree push --prefix=packages/shared-validators \
git@gitlab.com:your-org/shared-validators.git main
This rewrite can be slow on large histories. Some teams split once, push, then delete the split branch locally. The official docs cover flags and edge cases in the git-subtree manual page.
Alias the long commands. Many teams add shell aliases or Makefile targets like make sync-validators. Document them beside your Deployer tasks. Consistency beats memorising prefix paths during a Friday deploy.
Which is easier for CI/CD pipelines: submodules or subtrees?
Subtrees win on simplicity. Your pipeline runs git clone, installs PHP 8.3+ dependencies, and executes tests. No extra credentials for a second repo unless the subtree itself is private—which it already is, inside the parent.
Submodules need explicit CI support. GitLab example:
variables:
GIT_SUBMODULE_STRATEGY: recursive
stages:
- test
phpunit:
stage: test
script:
- composer install --no-interaction --prefer-dist
- php artisan test
GitHub Actions equivalent:
- uses: actions/checkout@v4
with:
submodules: recursive
token: ${{ secrets.PAT_WITH_REPO_ACCESS }}
The token detail matters. HTTPS submodule URLs need a PAT with read access to the child repo. SSH URLs need a deploy key loaded in the job. I've debugged this on shared EC2 runners where the main repo key worked but submodule fetch failed silently until verbose Git logging was enabled.
Deployer 7 releases compound the issue. Your symlink swap updates the parent checkout. Submodule paths inside the release directory must be initialized on every deploy unless you store them in the shared persistent path—which breaks immutable release philosophy. Subtrees deploy like normal code. That alone pushed several legal-tech sister sites toward subtrees for shared Blade components.
Cache layers behave differently too. Docker COPY of a repo with submodules requires multi-stage init. Subtree code copies with the rest of the app. For containerised Laravel 12 or 13 apps, that saves brittle Dockerfile lines.
Pair pipeline hardening with hooks. Pre-push hooks that verify submodule SHAs exist on the remote catch mistakes before CI burns minutes. Read Git hooks to automate checks before commit and push for patterns that work on Ubuntu 22/24 build agents.
How do submodules and subtrees compare for monorepos and large files?
Neither replaces a monorepo tool. If five Laravel apps share migrations, queues, and UI, a single repo with path-based CI filters is usually clearer. Submodules and subtrees help when organisational boundaries force separate remotes—client-owned library, licensed theme, or legacy CodeIgniter module you cannot merge yet.
Large binaries belong in Git LFS or object storage—not nested repos. Submodule plus LFS doubles configuration pain. See Git LFS for large files before stuffing PDF templates or video assets into a subtree.
Branching strategy still applies. GitFlow with long-lived develop branches makes submodule pins drift fast. Trunk-based flows with tagged submodule updates stay saner. Compare approaches in Git branching strategies: GitFlow vs trunk-based.
Migration between models is possible but messy. Converting submodule to subtree means importing history with git subtree add and removing .gitmodules. The reverse—splitting a subtree into a submodule—needs filter-repo surgery. Plan the choice early on greenfield custom software projects rather than refactoring under deadline pressure.
Key Takeaways
- Submodules store a pointer; subtrees store merged files—choose based on whether the child repo has an independent release cycle.
- Document
git clone --recurse-submodulesfor every submodule project; one forgotten init breaks builds. - Subtrees simplify CI/CD because pipelines treat the repo as a single checkout with no recursive fetch.
- Always push submodule child commits before pushing the parent pointer to avoid orphaned SHAs.
- Prefer Composer, npm, or a private registry for PHP and JS shared code; use Git nesting for configs, themes, or org-boundary constraints.
- Run secret scans and tests across all nested paths—submodules are easy to skip accidentally in automation.
People Also Ask
Can you convert a submodule to a subtree later?
Yes, but treat it as a planned migration. Remove the submodule entry, commit, then run git subtree add with the same remote URL. History rewrite may be required if you need a clean log. Communicate a freeze window so teammates do not pull half-migrated state.
Do Git submodules work with GitHub and GitLab?
Both platforms support submodules in clones and CI. You must grant the pipeline access to every nested remote. Relative URLs in .gitmodules help when child repos sit in the same organisation. HTTPS and SSH each need matching credentials on the runner.
Which option is better for a small team in Nepal?
Subtrees usually win. Budget-sensitive teams run fewer moving parts—one clone, one deploy key, one GitLab project to babysit. Submodules pay off when you already maintain a packaged library consumed by several clients at different versions, similar to shared payment modules across eCommerce builds.
Are submodules and subtrees the same as a monorepo?
No. A monorepo is one repository containing many projects with unified tooling. Submodules link separate repositories. Subtrees approximate monorepo ergonomics while keeping an upstream remote. Tools like Nx or Turborepo solve different problems—build graph and task caching—not cross-org access control.
Choose the pattern your team will actually maintain
Git Submodules vs Subtrees is not a purity contest. Submodules reward disciplined teams that tag library releases and pin SHAs in parent apps—think shared validators on a Laravel booking platform where tour rules change on their own schedule. Subtrees reward teams that need one clone, straightforward GitLab CI, and fewer 2 a.m. "empty vendor folder" pages. Before nesting repos, ask whether a private Composer or npm package already solves the problem.
If you are untangling shared code across multiple production apps, audit the deploy pipeline alongside the Git layout. I routinely help teams stabilise Git-based releases, submodule CI, and Deployer workflows through support and maintenance and Linux system administration. For day-to-day JSON config checks while refactoring repo layout, the JSON formatter on our tools page saves a few silly syntax errors.
Related reading: Git rebase vs merge, manage dotfiles and server config with Git, and recover lost commits with Git reflog. When the repo strategy is settled, contact us to review your CI pipeline or plan a clean migration path.
Frequently Asked Questions
0 Comments
Leave a comment
Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

