Chapter 10-7 — Git Subtree☕ ☕ 18 min read

Git Subtree — Alternative

Submodules giving you a headache? Subtree puts code directly in your repo. No empty folders, no detached HEAD — just working code.

01Subtree vs Submodule: The Better Alternative

Git Subtree merges another repository's code DIRECTLY into your repository. Unlike submodules that store a pointer to an external repo, subtree copies the actual files into your project.

Why this matters: When you clone a repo with submodules, you get empty folders. When you clone a repo with subtrees, you get the actual code. No initialization needed, no detached HEAD, no .gitmodules file to manage.

Advantages over submodules:

  • No empty folders — code exists in your repo after clone
  • No detached HEAD — you work on a regular branch, not a detached state
  • No .gitmodules — one less config file to manage
  • Simpler clone — just git clone, no --recurse-submodules needed
  • Easy removal — just git rm -r libs/shared, no multi-step deinit

Disadvantages:

  • Larger repo size — the subtree code is stored in your repo
  • Slower push — pushing changes back upstream requires history splitting
  • Complex merges — pulling updates can create merge conflicts

The --squash option is your best friend: it combines the subtree's entire history into one commit before merging, keeping your repo's history clean and size manageable.

# Submodule vs Subtree comparison

# SUBMODULE: Stores a POINTER to external repo
git submodule add https://github.com/company/shared.git libs/shared
# Result: .gitmodules file + pointer (libs/shared is empty until initialized)

# SUBTREE: Stores the ACTUAL CODE in your repo
git subtree add --prefix=libs/shared https://github.com/company/shared.git main --squash
# Result: libs/shared/ has all the files IMMEDIATELY
# No .gitmodules, no empty folders, no detached HEAD

# Key differences:
# | Feature        | Submodule    | Subtree      |
# |----------------|--------------|--------------|
# | Code storage   | Pointer      | Actual code  |
# | Empty folders  | YES (bad!)   | NO           |
# | Detached HEAD  | YES (bad!)   | NO           |
# | Clone simple   | Need --recurse| Works normally|
# | Update process | Two-step     | One command  |
# | Repo size      | Small        | Larger       |
# | Remove easy    | NO           | YES          |
Git Subtree eliminates the submodule nightmare. Code comes DIRECTLY into your repo. No empty folders, no detached HEAD, no pointer tracking. Simple and stable. Merges can be tricky, but the daily workflow is 10x easier than submodules.
02Adding a Subtree

The git subtree add command brings another repository's code into your project. The basic syntax is:

git subtree add --prefix=<path> <repo-url> <branch> --squash

Let's break down each part:

  • --prefix=libs/shared — the directory where the subtree code will live inside your repo
  • <repo-url> — the URL of the upstream repository you want to include
  • <branch> — which branch of the upstream repo to pull (usually main)
  • --squash — combines the subtree's entire history into one commit (recommended)

With --squash (recommended): Git fetches the upstream repo, squashes all its commits into one, then merges that single commit into your repo. Two commits are created: a squashed content commit and a merge commit. Your repo stays clean.

Without --squash: Git imports the ENTIRE commit history of the upstream repo into your repository. This makes your repo larger but allows you to use git log --follow across subtree boundaries.

After adding, the files exist in your repo just like any other files. No special treatment needed. Edit them, commit them, push them — they're YOUR files now.

# Add a subtree (with squash — recommended)
git subtree add --prefix=libs/shared \
  https://github.com/company/shared.git main --squash

# What happened:
# 1. Git fetched the shared repo
# 2. Squashed its history into one commit
# 3. Merged the files into libs/shared/
# 4. Created two commits: fetch commit + merge commit

# Verify
ls libs/shared/
# (all files from shared repo — IMMEDIATELY available!)
cat libs/shared/utils.js
# (code is right there, no initialization needed)

# Add WITHOUT squash (imports full history)
git subtree add --prefix=libs/shared \
  https://github.com/company/shared.git main
# Imports ALL commit history from shared repo
# Useful if you need git log --follow across subtree boundaries

# Commit message
git log --oneline -2
# abc1234 Merge commit 'sq-shared' into libs/shared
# def5678 Squash 'shared/' content from commit xyz...
03Pulling Updates from Upstream

When the upstream (shared) repository gets updated, you need to pull those changes into your subtree. The command is similar to the add command:

git subtree pull --prefix=libs/shared <repo-url> main --squash

Important: Always use the same --squash option you used when adding the subtree. Mixing squash and non-squash operations creates conflicting histories.

The pull process:

  • Git fetches new commits from the upstream repository
  • Squashes them (if --squash is used) into one commit
  • Merges that commit into your subtree directory

Merge conflicts can occur if you modified the subtree files in your repo AND the upstream also modified the same files. Resolve them like any other merge conflict — edit the files, mark as resolved, and commit.

Speed tip: Add the upstream repo as a named remote to avoid typing the full URL every time and to let Git cache the fetch data.

# Pull updates from the upstream shared repo
git subtree pull --prefix=libs/shared \
  https://github.com/company/shared.git main --squash

# If there are no conflicts, this creates a merge commit
# libs/shared/ now has the latest upstream code

# If there ARE conflicts:
# CONFLICT (content): Merge conflict in libs/shared/utils.js
# Resolve like any merge conflict
# Then commit the resolution

# Speed up: save the remote URL
git remote add shared https://github.com/company/shared.git
git subtree pull --prefix=libs/shared shared main --squash
# Shorter command, and Git caches the fetch

# Pull WITHOUT squash (if you added without squash)
git subtree pull --prefix=libs/shared shared main
# Preserves upstream commit history
04Pushing Changes Back to Upstream

If you have push access to the upstream repository, you can push your subtree changes back. This is useful when you fix a bug or add a feature in the shared code and want to share it with other projects.

git subtree push --prefix=libs/shared <repo-url> main

WARNING: git subtree push can be VERY SLOW. Git needs to analyze your entire repository history, extract only the commits that affected the subtree directory, create a separate branch with just those commits, and push that branch upstream. For large repositories, this can take 10+ minutes.

This is the main disadvantage of subtrees. While adding and pulling are fast, pushing back is slow because of the history-splitting process.

Workaround: Use git subtree split to create a dedicated branch with just the subtree history, then push that branch. This separates the slow split operation from the push.

Alternative workflow: Make changes directly in the shared repository and pull them into your main project. This avoids git subtree push entirely.

# Make changes in the subtree directory
cd libs/shared
echo "new feature" >> utils.js
cd ../..

# Commit in your main repo
git add libs/shared
git commit -m "add new feature to shared lib"

#// Push subtree changes back to upstream
git subtree push --prefix=libs/shared shared main
# This is SLOW — Git must split the subtree commits
# For a large repo, this can take 10+ minutes

# Speed up: use a dedicated branch for splitting
git subtree split --prefix=libs/shared -b shared-split
# Creates a branch with just the subtree history
git push shared shared-split:main
# Much faster than subtree push

# Alternative workflow: make changes directly in shared repo
# and pull them into main repo (avoids subtree push entirely)
05When to Use Subtree vs Submodule vs Package

Not every shared code problem needs subtrees or submodules. Choose the right tool for the job:

Subtree is best for: internal shared code that changes frequently, when you need to edit the shared code in-place, when you want a simple clone experience for your team.

Submodule is best for: third-party code you track but rarely modify, when you need to pin exact versions, when repo size matters more than workflow simplicity.

Package registry (npm, Maven, PyPI) is best for: versioned libraries with stable releases, when you don't need to edit the dependency's source, when you want semver versioning.

Monorepo is best for: tightly coupled projects that change together, when you need atomic commits across projects, when the same team works on all projects.

# Decision matrix:

# Use PACKAGE REGISTRY when:
# - Library has stable releases
# - You don't edit the dependency source
# - You want semver versioning
npm install shared-lib@2.3.1

# Use SUBTREE when:
# - You need to edit shared code in your project
# - You want simple clone/build workflow
# - Shared code changes frequently
git subtree add --prefix=libs/shared <url> main --squash

# Use SUBMODULE when:
# - You rarely update the dependency
# - You need to pin exact versions
# - Repo size is critical
# - You're tracking third-party code
git submodule add <url> libs/shared

# Use MONOREPO when:
# - Projects are tightly coupled
# - You need atomic commits
# - Same team works on both projects
# (No submodules, no subtrees, just one repo)
💡 Pro Tip: The simplest workflow is to avoid both subtrees and submodules. Publish your shared library to a package registry (npm, Artifactory) and install it as a dependency. This gives you versioning, easy updates, and no Git complexity. Use subtrees only when you need to edit the shared code in-place within your project.
Git Subtree is a pragmatic choice for internal shared code. It eliminates the empty-folder and detached-HEAD problems of submodules while keeping everything in one repository. The trade-off is slower push operations and larger repo size. For most teams, subtree + squash provides the best balance of simplicity and functionality.

Lo kar liya — Key Points:

  • ✅ Git Subtree merges another repository's code directly into your repo, avoiding the submodule pointer model
  • ✅ Unlike submodules, subtree code exists in your repository — no empty folders, no detached HEAD, no .gitmodules
  • ✅ Add a subtree with git subtree add --prefix=<path> <url> <branch> --squash
  • ✅ Pull upstream updates with git subtree pull --prefix=<path> <url> <branch> --squash
  • ✅ Push changes back to upstream with git subtree push — but this is SLOW due to history splitting
  • ✅ Use --squash to keep the subtree's history out of your main repo, reducing repo size
  • ✅ Subtree is better for frequently-updated internal code; submodules are better for rarely-updated third-party code
Course Search
Search across all chapters & stages
📖

Search the course

Type any topic — branching, stash, rebase, hooks — and jump straight to that chapter.

merge branchesgit stashundo commitrebase