Articles

Git Submodules vs Git Subtree: Which to Use

Git submodules link another repo at a pinned commit; git subtree copies its files and history into yours. How each works and when to choose which.

The Lycoris Team The Lycoris Team · · 5 min read
The Git logo on a dark background

Git submodules and git subtree are two ways to include one Git repository inside another. A submodule stores a pointer: the parent repository records the URL of the other repo and the exact commit to check out, and the files live in a separate nested repository. A subtree copies the other repo’s files, and optionally its history, directly into a folder of the parent, so they become ordinary tracked files. Submodules keep the boundary sharp but add workflow friction; subtrees are invisible to collaborators but make pushing changes back upstream more awkward.

Both solve the same problem: you depend on code that lives in its own repository and want it available at a specific version inside yours. If you’re new to how Git tracks content, start with what Git is.

How submodules work

Adding a submodule:

git submodule add https://example.com/team/shared-ui.git libs/shared-ui
git commit -m "Add shared-ui submodule"

Git does two things. It creates a .gitmodules file mapping the path libs/shared-ui to its URL, and it records a special entry in the parent’s tree called a gitlink: not the files, but the commit hash the submodule should be at.

When someone else clones the parent, the submodule folder is empty until they initialize it:

git clone --recurse-submodules https://example.com/team/app.git
# or, after a normal clone:
git submodule update --init --recursive

To move the submodule to a newer version, you go into it, check out the new commit, and then commit the updated pointer in the parent:

cd libs/shared-ui
git fetch && git checkout v2.1.0
cd ../..
git add libs/shared-ui
git commit -m "Bump shared-ui to v2.1.0"

The parent always pins an exact commit. Nothing changes until someone deliberately updates that pointer, which is a property similar to what a lockfile gives you for packages.

How subtree works

git subtree is a contributed command that ships with most Git installations. Adding a subtree:

git subtree add --prefix=libs/shared-ui https://example.com/team/shared-ui.git main --squash

This fetches the other repository and merges its content into libs/shared-ui as normal files. With --squash, the external history is collapsed into a single commit; without it, the full upstream history is merged into yours.

Pulling upstream changes later:

git subtree pull --prefix=libs/shared-ui https://example.com/team/shared-ui.git main --squash

And contributing local changes back:

git subtree push --prefix=libs/shared-ui https://example.com/team/shared-ui.git my-fix-branch

subtree push walks your history, extracts the commits that touched the prefix, rewrites them as if the prefix were the repository root, and pushes the result. On large histories this can be slow.

Crucially, anyone who clones the parent gets the subtree’s files immediately. There’s no extra command and no .gitmodules. To them it’s just a directory.

Submodules vs subtree at a glance

Git submodulesGit subtree
What the parent storesURL plus pinned commitThe actual files (and optionally history)
Clone experienceNeeds --recurse-submodules or update --initWorks with a plain clone
Repository sizeParent stays smallParent grows with the included code
Updating the dependencyCheck out new commit, commit the pointergit subtree pull
Contributing upstreamCommit and push inside the submodule as usualgit subtree push, which splits history
Local editsEasy to lose on detached HEADJust normal commits
Collaborator awarenessEveryone must know about submodulesMost can ignore it entirely
Access controlCan reference a repo some users can’t readAll code is in the parent

The common submodule pitfalls

Submodules have a reputation for confusing people, and most of it comes from a few behaviors:

  • Forgetting to initialize. A fresh clone without --recurse-submodules leaves empty folders and puzzling build errors.
  • Detached HEAD. git submodule update checks out the pinned commit, not a branch. If you then make changes and commit inside the submodule, those commits aren’t on any branch and are easy to lose. Create or check out a branch before editing.
  • Pointer drift. You switch branches in the parent, but the submodule stays where it was until you run git submodule update. git status shows the submodule as modified, and it’s easy to accidentally commit an unintended pointer change.
  • Two-step pushes. Changes made inside a submodule must be pushed to the submodule’s remote before pushing the parent’s new pointer, or collaborators will get a reference to a commit that doesn’t exist on the server. git push --recurse-submodules=check guards against this.

Setting git config --global submodule.recurse true makes many commands, including checkout and pull, update submodules automatically, which removes a lot of this friction.

The common subtree pitfalls

Subtree trades those problems for different ones:

  • Mixed commits. If a single commit touches both the subtree and the rest of the project, splitting it back out for upstream becomes messier. Keep subtree changes in their own commits.
  • History bloat. Without --squash, the parent inherits the dependency’s entire history.
  • Remembering the remote. Subtree doesn’t store the upstream URL anywhere; you have to supply it on every pull and push, typically by adding a named remote.
  • Merge conflicts. Pulling upstream is a real merge, so heavy local modifications produce conflicts like any other merge.

When to choose each

Choose submodules when:

  • The dependency is developed actively and independently, and you contribute to it often.
  • You need to keep the parent repository small, or the included repo is large.
  • Different consumers must pin different versions explicitly and visibly.
  • Some contributors shouldn’t have access to the included code.

Choose subtree when:

  • You mostly consume the dependency and rarely push changes back.
  • You want a plain git clone to produce a working project for everyone, including CI.
  • You’re vendoring a small library and want it to behave like your own code.

Consider neither when the code is published as a package. A package manager with semantic versioning, covered in what semantic versioning is, usually beats both. And if the repositories are tightly coupled and change together, merging them into a monorepo may be simpler than stitching them together at all.

The takeaway

Submodules embed a pointer to another repository at a pinned commit; subtree embeds a copy of its files. Submodules keep projects separate and the parent small but require every collaborator to learn their workflow. Subtree is invisible to anyone who doesn’t need to care, at the cost of a bigger repository and clunkier upstream contributions. If the shared code can be published as a versioned package, that’s often the cleanest option of all.