Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

Git Submodules vs Subtrees

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.

Git Submodules vs SubtreesParent RepoYour Laravel appstores .gitmodulesSubmoduleSeparate .gitPointer to SHAChild RepoOwn remoteIndependent tagsSubtree in Parentpackages/shared-lib/Child history merged inlineSingle clone, no init step
Git Submodules vs Subtrees: submodules link an external repo; subtrees embed merged history inside the parent tree.

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.

CriteriaGit SubmoduleGit Subtree
Storage modelExternal repo + SHA pointer in parentChild files and history inside parent path
Clone stepsNeeds submodule update --initStandard git clone only
Version pinningExplicit, per-commitImplicit via merge commits
Push workflowTwo repos, two remotesCan push subtree splits back upstream
CI complexityHigher—recursive fetch requiredLower—looks like one repo
Contributor learning curveSteep—easy to commit wrong stateModerate—feels like normal Git
Best fitShared libraries with own release cycleForks, 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.

Submodule Update WorkflowClone parentInit submoduleupdate --initWork insidechild folderCommit SHAin parentCommon Submodule FailuresDetached HEAD commits never pushed upstreamCI clone without --recurse-submodulesEmpty directory after pull (forgot init)Parent shows modified submodule with no diff
Git submodule workflow: every consumer must init submodules, then commit the pinned SHA back to the parent repository.

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.

Subtree Sync FlowParent Repo (single clone)packages/shared-validators/subtree pullmerge upstream insubtree pushsplit commits outExternal Remote
Git subtree sync: pull merges upstream into the prefix; push splits local commits back to the child remote.

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.

Submodule or Subtree?Shared external code?Own releasesSame cadenceSubmodulePin versionsSubtreeSingle cloneMultiple versionsacross appsSmall team CIminimal setupStill unsure? Use Composer or npm package registry
Decision guide for Git Submodules vs Subtrees: independent release cycles favour submodules; unified deploys favour subtrees.

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-submodules for 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

A submodule stores an external repo as a commit SHA pointer inside the parent. A subtree merges the child files and full history into a subdirectory of the parent. Submodules need a separate init step after clone; subtrees work with a standard git clone.

Pick submodules when the child project has its own release cycle—open-source dependencies, internal Composer packages, shared themes, or infrastructure modules with semver tags. You want consumers to opt into v2.4.1 deliberately, not pull main accidentally. Use submodules when multiple products consume the same library at different versions, or when the child repo already has issue tracking and release notes. Subtrees suit teams wanting one clone, simpler CI, and fewer onboarding steps. Prefer neither when Composer, npm workspaces, or a private package registry already solves the sharing problem.

Subtrees win on simplicity.

Adding a submodule records the URL and path in .gitmodules, then commits the pinned SHA to the parent. Teammates must run git submodule update --init --recursive on first pull, or clone with --recurse-submodules. To upgrade, fetch inside the submodule, checkout a tag like v1.3.0, then commit the new pointer from the parent root. The --remote flag moves the pointer to upstream tip—convenient but risky in production unless you truly want floating HEAD. Always push the submodule remote first, then push the parent, or you orphan SHAs only your laptop knows.

Use git subtree add with --prefix to merge another repository into a subdirectory. The --squash flag collapses upstream history into one commit for a readable parent log, though you lose granular blame unless you omit squash on initial add. Pull upstream with git subtree pull using the same prefix and remote, resolving conflicts like any normal merge. Push local changes back with git subtree push, which rewrites history and can be slow on large repos. Alias long commands in a Makefile or shell alias beside your Deployer tasks so Friday deploys do not depend on memorised prefix paths.

Subtrees usually win for budget-sensitive teams running 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 Khalti or eSewa payment modules across eCommerce builds. Agency clients and solo maintainers often lack bandwidth for submodule hygiene. If new hires must clone once and run composer install without extra Git steps, subtrees reduce onboarding friction sharply.

Both platforms support submodules in clones and CI, but you must grant the pipeline access to every nested remote. GitLab needs GIT_SUBMODULE_STRATEGY set to recursive. GitHub Actions needs checkout with submodules: recursive and a PAT with read access for HTTPS URLs, or a deploy key for SSH. Relative URLs in .gitmodules help when child repos sit in the same organisation. I've debugged pipelines on shared EC2 runners where the main repo key worked but submodule fetch failed silently until verbose Git logging was enabled.

No. A monorepo is one repository containing many projects with unified tooling and path-based CI filters. Submodules link separate repositories via SHA pointers. Subtrees approximate monorepo ergonomics while keeping an upstream remote you can push back to. Neither replaces monorepo tools like Nx or Turborepo, which solve build graphs and task caching—not cross-organisation access control. If five Laravel apps share migrations, queues, and UI, a single repo is usually clearer than nesting remotes.

You get empty submodule directories and broken builds. Pipelines pass unit tests locally yet fail in CI because vendor or internal-sdk folders contain nothing. That class of bug rarely appears with subtrees, where one clone includes all code. Document git clone --recurse-submodules in your README for every submodule project. Pre-push hooks that verify submodule SHAs exist on the remote catch mistakes before CI burns minutes. This is the classic reason legal-tech sister sites moved shared Blade components to subtrees.

Usually neither—publish shared code as a private Composer package instead. Submodules then hold deployment configs or themes, not PHP autoload roots, unless you enjoy pain. Wire submodule paths into Composer with a path repository or symlink only if you accept the maintenance overhead. Subtrees under lib/payments/ keep Khalti or eSewa integration code reusable without a second remote on every laptop. For Laravel 12 or 13 apps, Composer path repos or a private registry beats Git nesting for autoloaded PHP shared libraries.

Deployer 7 symlink swaps update the parent checkout, but submodule paths inside each release directory must be initialized on every deploy unless stored in a shared persistent path—which breaks immutable release philosophy. Subtrees deploy like normal code with no extra init step. That alone pushed several legal-tech sister sites toward subtrees for shared components on Deployer 7 plus GitLab CI pipelines. Docker COPY of a repo with submodules also requires multi-stage init, while subtree code copies with the rest of the app.

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. Detached HEAD is the classic submodule footgun. Train the team: always push the submodule remote first, then push the parent. Without that discipline, teammates pull a parent SHA referencing commits that do not exist on the shared child remote, breaking clones and CI fetches across the team.

Yes. Tools like gitleaks run cleanly on a single tree, but submodules sometimes get skipped unless your scanner walks nested repos. Run secret scans and tests across all nested paths—submodules are easy to skip accidentally in automation. Pair pipeline hardening with pre-push hooks that verify submodule state. Subtrees keep everything in one checkout, so standard scanning covers all files without extra configuration for nested remotes.

Prefer Composer for PHP, npm workspaces for JavaScript, or a private package registry before nesting repos in application codebases. Git submodules and subtrees are a last resort when organisational boundaries force separate remotes—a 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. Audit whether a private Composer or npm package already solves the problem before choosing either Git nesting pattern.

Yes, but treat it as a planned migration with a freeze window.

Share this article

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.

Quick Contact Options
Choose how you want to connect me: