Git Workflow

Git Submodule vs. Git Subtree: Which Should You Use?

Punyapal Shah 4 min read
edit this tip
A practical comparison of Git Submodules and Git Subtrees to help you choose the right strategy for embedding one repository inside another.

When managing projects that share common libraries, documentation sets, microservices, or reusable UI components, you frequently need to embed one Git repository inside another.

Git provides two distinct mechanisms to handle this:

  • Git Submodules: Keep the external repository strictly independent; the parent repository only stores a pointer to a specific commit SHA.
  • Git Subtrees: Merge the external repository files and commit history directly into a subdirectory of the parent repository.

1. Git Submodules: Independent Pointer

With submodules, the external project remains an entirely separate repository. The parent repository does not store the files directly; it only tracks the submodule URL and a commit hash in .gitmodules.

Main Repository
├── app/
├── config/
└── packages/shared-lib/  → [Pointer to Commit abc1234 in external repo]

Adding a Submodule

# Add external repo into packages/shared-lib
git submodule add https://github.com/example/shared-lib.git packages/shared-lib

# Stage and commit the .gitmodules and submodule reference
git commit -m "Add shared-lib submodule"

Cloning a Repository with Submodules

When team members clone a repository containing submodules, standard git clone leaves submodule directories empty. They must pass the --recurse-submodules flag:

# Clone parent and initialize all nested submodules
git clone --recurse-submodules https://github.com/example/main-app.git

# Or initialize submodules in an existing clone
git submodule update --init --recursive

Updating a Submodule

To pull the latest changes from the submodule remote branch and update the parent commit pointer:

git submodule update --remote packages/shared-lib
git add packages/shared-lib
git commit -m "Update shared-lib to latest commit"

2. Git Subtrees: In-Tree Merged History

With subtrees, the external repository's files are committed directly into your repository. Collaborators can clone, pull, and edit files normally without needing extra Git flags or submodule commands.

Main Repository
├── app/
├── config/
└── packages/shared-lib/  → [Real files + merged commit history]

Adding a Subtree

# 1. Add the external repository as a remote
git remote add shared-lib https://github.com/example/shared-lib.git

# 2. Add the subtree into packages/shared-lib using --squash
git subtree add --prefix=packages/shared-lib shared-lib main --squash

Pulling Upstream Updates

When the external repository publishes new commits, pull them into the parent project:

git subtree pull --prefix=packages/shared-lib shared-lib main --squash

Pushing Local Changes Upstream

If you make modifications inside packages/shared-lib within the parent repo and want to push those commits back to the original repository:

git subtree push --prefix=packages/shared-lib shared-lib main

3. Side-by-Side Comparison

FeatureGit SubmoduleGit Subtree
Storage in Parent RepoOnly commit SHA reference (.gitmodules)Full file contents and history
Cloning ExperienceRequires --recurse-submodulesNormal git clone (no extra flags)
Developer ComplexityHigher (requires submodule workflow knowledge)Lower (files behave like normal code)
History IsolationCompletely separate Git treesMerged into the parent repository tree
Locking Exact VersionsStrict (pinned to exact commit hash)Flexible (updated on demand via pull)
Pushing Changes BackDirect commits inside submodule directoryRequires git subtree push command
Third-Party DependenciesIdeal for external code you rarely modifyIdeal for shared code you actively edit

4. Which One Should You Choose?

Choose Git Submodules if:

  • The embedded repository has its own strict release cycle and version tags.
  • You want the parent repository to lock onto an exact commit hash.
  • Team members and CI pipelines are comfortable initializing submodules (--recurse-submodules).
  • The embedded project is large and you want to keep repository clone sizes small.

Choose Git Subtrees if:

  • You want a friction-free clone experience for team members and contributors.
  • You want the external code to behave like ordinary files inside the repository.
  • You frequently edit the embedded files directly alongside your application code.
  • You want to avoid detached HEAD states and submodule initialization errors in CI.

5. Common Gotchas

Submodule Gotchas:

  • Empty Folders on Clone: Forgetting --recurse-submodules leaves submodule folders blank.
  • Uncommitted Pointer Updates: Making changes inside the submodule folder without committing the updated reference in the parent repository causes inconsistent states between team members.
  • Detached HEAD: By default, submodules check out detached commits rather than tracking branches.

Subtree Gotchas:

  • Longer Commands: Subtree syntax (--prefix, --squash, remote names) requires precise terminal commands.
  • Repo Bloat: Importing large repositories with long commit histories increases the size of your parent repository.

Summary

  • Use Git Submodules when you need strict commit pinning and want the dependency to remain an isolated external project.
  • Use Git Subtrees when you want shared code to live directly in your repository with seamless cloning for collaborators.
Tags: Git Version Control Workflow DevOps Monorepo
Share on X

// Found an issue or want to contribute a tip? github.com/MrPunyapal/tips