GitHub·7 min read·

Git Submodules vs Subtrees: GitHub and the Workflow

Submodules pin an external commit; subtrees vendor the code wholesale. Here is how both behave on GitHub, their sharp edges, and when to use each one.

NB

Netbay Infrastructure Team

Netbay Engineering

On this page

When one repository needs to bring in another, git offers two opposing designs. Submodules store a pointer: a tiny record of "repo X at commit Y," with the actual code fetched on demand from the external repository. Subtrees copy: the foreign code is pulled into your tree as ordinary files, and your repository becomes the source of truth for vendored content. Both work with GitHub; they behave very differently there, and the differences decide which one your project should use.

Submodules: a pointer, not a copy

Adding a submodule records a URL and a commit hash in ".gitmodules" plus a gitlink entry in the directory tree. The code is not part of your repository — cloning your repo does not bring it along unless the clone is recursive.

bash
# link the sdk repo into lib/sdk at its current commit
git submodule add https://github.com/netbay/sdk.git lib/sdk

# a full clone needs --recursive to pull submodules in
git clone --recursive https://github.com/netbay/app.git

# pull the pointer and the code in one step
git submodule update --init --recursive
git submodule update --init --recursive --remote

The promise is precision: every checkout of your repo sees exactly the pinned version of the dependency, and bumping the submodule is an explicit commit. The price is a second repository's worth of friction — forget "--recursive" and the build fails on a missing directory, the dependency's own branches and CI are opaque to your workflow, and a submodule change cannot be previewed in your PR because your repo does not contain the code.

Subtrees: the code lives here

A subtree merge splices the foreign history (or, with --squash, just the final tree) directly into a directory. From then on the code is ordinary files: searchable, diffable, reviewable in the same PR, CI-tested with your own test suite.

bash
# vendor the sdk tree into lib/sdk, squashed
git subtree add --prefix=lib/sdk https://github.com/netbay/sdk.git main --squash

# pull upstream changes later
git subtree pull --prefix=lib/sdk https://github.com/netbay/sdk.git main --squash

# push local changes back upstream (needs push access)
git subtree push --prefix=lib/sdk https://github.com/netbay/sdk.git main

The sharp edges are the reverse of submodules: vendored code inflates every clone, upstream updates arrive as real merges that can conflict with your local modifications, and the "pin an exact version" control that submodules give you is looser — your tree just has whatever tree you pulled.

How GitHub treats each

This is the part most comparisons skip. Because submodules are pointers, GitHub's handling is special-cased and has limits:

  • A submodule shows as a link with the pinned commit, and the pull request shows only the gitlink change, not the dependency's diff.
  • Code search does not index submodule contents; you cannot grep what your repo does not hold.
  • Forks and PR merges cannot preview submodule code, and a PR that bumps a submodule is merged blind into your branch.

Subtrees have none of those limits, because they are just files — search, diffs, reviews, and forks all work naturally. The trade is that subtree merges operate on what is literally in your tree, so version discipline lives in your history rather than in git's pointer machinery.

vendoring, two designs submodule pointer to repo + commit code fetched on demand precise pins, --recursive needed no search, no diff preview pins shared libs tightly subtree code copied into your tree search, diff, review work updates are real merges full visibility everywhere vendored history stays cold pick by what GitHub must see submodules hide content; subtrees own it for vendor code, consider a package registry instead

Choosing honestly

Use the sharp edges as the decision rule:

  • If consumers must see and search vendored code in pull requests, subtrees win — the content is literally present.
  • If a precise, CI-pinned dependency version across many consumers matters more than visibility, submodules win despite the friction.
  • If the "dependency" is stable historical code that rarely moves, subtrees are cheap forever and remove clone recursion entirely.
  • If the thing is a live library, neither vendoring model is usually right the long way — a package registry is the honest end state.

The fork-shaped reality

One more GitHub reality shapes the choice: forks. Forking a repo with subtrees gives the fork a full working copy, because everything is in-tree; forking a repo with submodules gives a fork whose submodule pointers still fetch from the original upstream. For the many small projects that live in forks and PRs, subtree's "it is just files" quality is frequently the deciding one.

Takeaway: submodules are precision pointers with poor visibility on GitHub; subtrees are full copies with full visibility but merge-shaped updates. Choose what your PR and search workflows demand — and run the mirror that tests vendored code on a Lucknow DC01 VPS from Netbay, ready in under 60 seconds at netbayhosts.in.

Keep reading

Follow along on a real VPS

Deploy Linux in under 60 seconds

These guides are written against Ubuntu, Debian, and RHEL-family images — the same ones on NetBay.

Deploy an instance