Skip to content

Git Subtree vs Submodule — Complete Guide

DodaTech Updated 2026-06-24 8 min read

In this tutorial, you'll learn about Git Subtree vs Submodule. We cover key concepts, practical examples, and best practices.

Git submodules and subtrees are two built-in mechanisms for including external repositories in your project — submodules reference external commits, while subtrees copy their content into your history.

In this tutorial, you'll learn the differences between Git submodules and subtrees — how each works, their tradeoffs in team workflows, clone and maintenance costs, and when to choose one over the other. Understanding this comparison saves your team from the pain of choosing the wrong dependency strategy. By the end, you'll evaluate both approaches and pick the right one for your project.

Real-world use: DodaTech uses both approaches — submodules for shared protocol definitions that change independently, and subtrees for vendored dependencies where the team needs to modify the source. Doda Browser uses submodules for its rendering engine dependency; DodaZIP uses subtrees for its compression library.

flowchart TD
  A[External dependency needed] --> B{Chosen approach}
  B --> C[Submodule]
  B --> D[Subtree]
  C --> E[Reference to external repo]
  C --> F[Locked to specific commit]
  C --> G[Submodule init required]
  D --> H[Content copied into repo]
  D --> I[Full history included or squashed]
  D --> J[Transparent to team]
  E --> K[+ Smaller parent repo]
  E --> L[- Team must learn commands]
  H --> M[+ No extra commands needed]
  H --> N[- Larger parent repo]

Adding a Submodule

Submodules embed a pointer to a specific commit in another repository.

# Add a submodule
git submodule add https://github.com/dodatech/shared-protocols.git libs/protocols
git commit -m "Add shared-protocols as submodule"

# Clone a repo with submodules
git clone --recursive https://github.com/dodatech/app.git
# Or after a regular clone:
git submodule update --init --recursive

The .gitmodules file tracks the submodule configuration:

[submodule "libs/protocols"]
  path = libs/protocols
  url = https://github.com/dodatech/shared-protocols.git
  branch = main

Adding a Subtree

Subtrees copy the external repository content directly into your repository.

# Add a subtree (squash history — recommended)
git subtree add --prefix=libs/compression \
  https://github.com/dodatech/compression-lib.git main --squash

# Add a subtree with full history
git subtree add --prefix=libs/compression \
  https://github.com/dodatech/compression-lib.git main

# See the squashed merge commit
$ git log --oneline -3
a1b2c3d Squashed 'libs/compression/' content from commit e5f6a7b
b2c3d4e Merge commit 'subtree-libs/compression'
c3d4e5f Previous commit

Expected output:

$ ls libs/compression/
README.md  src/  package.json  tests/
$ ls -la libs/compression/.git  # No .git — it's part of the parent repo
ls: libs/compression/.git: No such file or directory

Updating Dependencies

How each approach handles updates.

# Update submodule to latest
git submodule update --remote libs/protocols
git add libs/protocols
git commit -m "Update protocols submodule to latest"

# Pull subtree updates
git subtree pull --prefix=libs/compression \
  https://github.com/dodatech/compression-lib.git main --squash
# This creates a new merge commit

Pushing Changes Back

Submodules and subtrees differ significantly in pushing modifications.

# Submodule: push from inside the submodule directory
cd libs/protocols
git checkout -b my-fix
# Make changes...
git commit -am "Fix protocol parsing bug"
git push origin my-fix
# Then create a PR in the external repo
cd ../..
git add libs/protocols
git commit -m "Update submodule to latest fix"

# Subtree: push directly from the parent repo
git subtree push --prefix=libs/compression \
  https://github.com/dodatech/compression-lib.git my-fix-branch
# This splits out only the subtree commits and pushes them

Team Friction Comparison

The most important practical difference is how each approach affects your team.

## Submodule Team Friction

- Developers must learn: `git submodule update --init --recursive`
- CI needs extra steps: checkout with `--recursive` or explicit `submodule update`
- Mistakes: committing without updating submodules, forgetting `--recursive`
- `detached HEAD` state: submodules default to detached HEAD, confusing beginners
- Cross-submodule changes: need commits in multiple repositories
- GUI tool support: inconsistent across editors

## Subtree Team Friction

- Zero additional commands: clone and work like a normal repo
- CI is transparent: no extra checkout steps needed
- No detached HEAD: subtree files are just part of your repo
- Cross-repo changes: single commit in your repo, push split upstream
- GUI tool support: works with any Git client

Performance and Size

Measure the real-world impact.

# Compare repository sizes

# Submodule approach (parent repo is small)
$ du -sh .
12M     .  # Excluding submodule content

# Subtree approach (parent repo includes subtree history)
$ du -sh .
48M     .  # Includes compression library history

# Clone time comparison
$ time git clone https://github.com/dodatech/app-submodule.git
# 5.2 seconds (small parent, submodules fetched lazily)

$ time git clone --recursive https://github.com/dodatech/app-submodule.git
# 18.7 seconds (includes all submodule content)

$ time git clone https://github.com/dodatech/app-subtree.git
# 15.3 seconds (single larger clone)

Decision Matrix

Choose based on your specific needs.

Factor Choose Submodule Choose Subtree
Dependency changes frequency Frequently Rarely
Team needs to modify dependency Occasionally Yes
Clone size is critical Yes No
Team is Git-experienced Yes No
CI speed is critical Use partial clone No extra cost
Multiple nested dependencies Yes Can be complex
Need to pin exact version Yes Yes
Want to audit dependency history No Yes

Migrating Between Approaches

Convert submodules to subtrees or vice versa.

# Submodule → Subtree migration
# 1. Remove the submodule
git submodule deinit -f libs/protocols
git rm -f libs/protocols
rm -rf .git/modules/libs/protocols
git commit -m "Remove submodule"

# 2. Add the same dependency as subtree
git subtree add --prefix=libs/protocols \
  https://github.com/dodatech/protocols.git main --squash
git commit -m "Add protocols as subtree"

# Subtree → Submodule migration
# 1. Remove the subtree content
git rm -r libs/compression
git commit -m "Remove compression subtree"

# 2. Add as submodule
git submodule add https://github.com/dodatech/compression.git libs/compression
git commit -m "Add compression as submodule"

Common Errors

  1. fatal: refusing to merge unrelated histories in subtree pull — The subtree's remote has diverged from your local copy. Use --squash to avoid history conflicts or use git subtree pull --squash --allow-unrelated-histories.
  2. pathspec 'X' did not match any files in submodule add — The URL is wrong or the remote doesn't exist. Verify the repository URL and branch name before adding.
  3. Submodule showing as modified without changes — The submodule points to a different commit than what's in the index. Run git submodule update to sync, or commit the new submodule reference.
  4. git subtree push fails with "couldn't find unique commit" — Multiple subtree splits can cause ambiguity. Use --annotate with unique prefixes to identify subtree commits.
  5. Recursive submodules forgotten in CI — CI pipelines must use git clone --recursive or git submodule update --init --recursive for nested submodules, or the build fails with missing dependencies.

Practice Questions

What is the main difference between submodule and subtree?

A submodule is a pointer to a specific commit in another repository — the actual content stays in the external repo. A subtree copies the external repository content directly into your repository. Submodules create a dependency link; subtrees merge content.

Which approach is easier for a team new to Git?

Subtree is significantly easier. Developers clone the repo and everything works — no special commands, no detached HEAD state, no CI changes. Submodules require learning extra commands and handling edge cases like detached HEAD.

Can I modify code in a submodule and push changes?

Yes. Enter the submodule directory, create a branch, make changes, commit, and push to the external repository. Then update the parent's submodule reference to point to the new commit. Subtree lets you modify and push from the parent directly.

Does subtree increase repository size?

Yes. Every file in the subtree becomes part of your repository permanently. Even with --squash, the content exists in your history. Submodules only store a pointer (40 bytes), not the actual content. Subtree repos are typically 3-5x larger.

When should I use subtree instead of a package manager?

Use subtree when you need to modify the vendored dependency, when the dependency is internal and not published to a package registry, or when you want to audit every line of the dependency's history. For published third-party libraries, use a package manager

Challenge

Create a project that depends on a shared library. First, implement the dependency as a submodule: add it, update it to a newer version, modify a file inside the submodule, and push the change. Then remove the submodule and re-add the same library as a subtree. Compare clone times, repository sizes, and the developer experience. Write a decision guide for your team based on your findings.

Real-World Task

Evaluate the DodaTech monorepo's dependency strategy. Doda Browser uses a shared rendering engine from another repository. The team needs to modify the engine while developing features. Should they use submodule or subtree? Consider: team size (20 developers), Git experience (mixed), need to modify the dependency (frequent), clone time targets (under 30 seconds), and CI pipeline overhead. Implement your recommendation and document the migration if needed.


Previous: Git Submodules | Related: Monorepos with Git | Related: Git for Teams

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro