Docs Library

Git Subtree: Managing Nested Repositories

A practical comparison of git subtree vs git submodule, and how to use subtree for embedding and synchronizing external projects.

Who This Is For
  • Readers who want the history model before advanced commands
Prerequisites
  • A basic sense that commits are not just a file list
Common Risks
  • Treating a concepts page like a command how-to

What is Git Subtree?

git subtree allows you to embed one repository (the subproject) inside another repository (the main project) as a subdirectory. Unlike submodules, which store only a reference to an external commit, subtree merges the subproject's history directly into your repository.

The git subtree command was originally a contributed script in Git's source tree and has been included in official Git distributions since version 1.7.11.

main-project/
├── src/
├── docs/
├── libs/
│   └── my-library/        ← subtree: external repo embedded here
│       ├── src/
│       ├── tests/
│       └── README.md
└── README.md

Subtree vs Submodule

Before diving into subtree commands, it's important to understand when to use subtree versus submodule:

Featuregit subtreegit submodule
HistoryMerged into main repoSeparate repo with own history
.gitmodulesNot neededRequired
CloningWorks with a simple git cloneRequires git clone --recurse-submodules
Contributor experienceTransparent — files appear as part of the repoRequires submodule awareness
Pushing changes backRequires git subtree pushDone in the submodule directory
Repository sizeLarger (includes subproject history)Smaller (only stores commit reference)
Updating subprojectManual pull/pushgit submodule update
Branch switchingSeamlessCan cause detached HEAD issues
Best forLibraries you modify frequentlyDependencies you rarely modify

When to Choose Subtree

  • You want contributors to interact with the subproject transparently
  • You need to modify the subproject and push changes back upstream
  • You don't want to manage .gitmodules and submodule initialization
  • The subproject is relatively small
  • You want a single-clone experience (no --recurse-submodules)
Subtree WorkflowSubtree embeds an external repository's history as a subdirectory in the main repository. Adding, updating, and pushing back to the subrepo all have dedicated commands.
External Repository
lodash.git (utility)docs-site.git (documentation)shared-lib.git (shared library)
Operation Result
add → subrepo history embedded in main repopull → fetch subrepo updates into subdirectorypush → push subdirectory changes back to subrepo
The --squash flag collapses the subrepo's multiple commits into one, keeping the main repo history clean.

When to Choose Submodule

  • The subproject is large and you don't want to bloat the main repo
  • You rarely need to modify the subproject
  • You want to pin to specific versions of external dependencies
  • The subproject is maintained by a different team

Basic Subtree Commands

Adding a Subtree

To add an external repository as a subtree:

git subtree add --prefix=libs/my-library \
    https://github.com/example/my-library.git \
    main \
    --squash

Parameters:

  • --prefix=libs/my-library: The directory where the subproject will be placed
  • The repository URL: The source repository to embed
  • main: The branch to pull from (can also be a commit hash or tag)
  • --squash: Combines the entire subproject history into a single commit (recommended)

The --squash flag is important — without it, the entire history of the subproject is merged into your repository, which can significantly increase its size.

Pulling Updates

To pull the latest changes from the upstream subproject:

git subtree pull --prefix=libs/my-library \
    https://github.com/example/my-library.git \
    main \
    --squash

This fetches the latest changes from the remote repository and merges them into the subtree directory.

Pushing Changes Back

If you've made changes to the subtree directory and want to push them back to the upstream repository:

git subtree push --prefix=libs/my-library \
    https://github.com/example/my-library.git \
    main

This extracts the commits that affect only the subtree directory and pushes them to the specified remote branch.

Note: git subtree push only pushes commits that are unique to the subtree. If the upstream has new commits, you may need to pull first and resolve conflicts before pushing back.

Working with Subtrees

Making Local Changes

After adding a subtree, you can modify files within the subtree directory just like any other files in your repository:

# Edit files in the subtree
vim libs/my-library/src/main.js

# Commit changes
git add libs/my-library/src/main.js
git commit -m "fix: resolve compatibility issue in my-library"

# Push changes back upstream
git subtree push --prefix=libs/my-library \
    https://github.com/example/my-library.git \
    main

Viewing Subtree History

# View commits that affected the subtree
git log --oneline -- libs/my-library/

# View the merge commit that added the subtree
git log --oneline --grep="git-subtree-dir"

Splitting a Subtree into a Separate Repository

If you decide that a subtree should become its own repository:

# Create a new branch with only the subtree's history
git subtree split --prefix=libs/my-library --branch=my-library-standalone

# This creates a new branch with just the subtree's commits
# You can then push it to a new remote
cd /path/to/new-repo
git remote add origin https://github.com/example/my-library.git
git push origin my-library-standalone:main

Removing a Subtree

To remove a subtree, simply delete its directory and commit:

git rm -r libs/my-library
git commit -m "Remove my-library subtree"

Advanced Patterns

Using a Shared Remote

Instead of specifying the full URL each time, add the subproject as a remote first:

# Add the subproject as a remote
git remote add my-library https://github.com/example/my-library.git

# Then use the remote name
git subtree add --prefix=libs/my-library my-library main --squash
git subtree pull --prefix=libs/my-library my-library main --squash
git subtree push --prefix=libs/my-library my-library main

Multiple Subtrees

You can manage multiple subtrees in a single repository:

git subtree add --prefix=libs/utils https://github.com/example/utils.git main --squash
git subtree add --prefix=libs/logger https://github.com/example/logger.git main --squash
git subtree add --prefix=libs/config https://github.com/example/config.git main --squash

# Update all subtrees
git subtree pull --prefix=libs/utils utils main --squash
git subtree pull --prefix=libs/logger logger main --squash
git subtree pull --prefix=libs/config config main --squash

Automating Subtree Updates

Create a script to update all subtrees:

#!/bin/bash
# scripts/update-subtrees.sh

subtrees=(
    "libs/utils:utils:main"
    "libs/logger:logger:main"
    "libs/config:config:main"
)

for entry in "${subtrees[@]}"; do
    IFS=':' read -r prefix remote branch <<< "$entry"
    echo "🔄 Updating $prefix from $remote/$branch..."
    git subtree pull --prefix="$prefix" "$remote" "$branch" --squash
done

echo "✅ All subtrees updated."

Understanding How Subtree Works

Git subtree works by identifying commits that affect a specific directory path. When you run git subtree add, it:

  1. Fetches the remote repository
  2. Reads the specified branch
  3. Rewrites the commits so that all file paths are prefixed with the --prefix directory
  4. Merges these rewritten commits into your current branch

The --squash flag collapses all these rewritten commits into a single commit, keeping your main repository's history clean.

When you run git subtree push, it:

  1. Identifies all commits that affect the subtree directory
  2. Extracts only the changes within that directory
  3. Rewrites the paths (removing the prefix)
  4. Pushes these commits to the remote repository

Common Issues and Solutions

Merge Conflicts

Subtree merge conflicts can occur when both the main project and the upstream subproject modify the same files. To resolve:

# Standard conflict resolution
git status
vim libs/my-library/src/conflicted-file.js
git add libs/my-library/src/conflicted-file.js
git commit -m "Resolve subtree merge conflict"

Accidentally Pushing Unrelated Changes

When using git subtree push, Git may try to push commits from the main project that also touch the subtree directory. This usually indicates that changes were made directly in the subtree directory without following the proper subtree workflow.

Solution: Make changes to the subproject in its own repository, then pull them into the main project with git subtree pull.

Performance with Large Subtrees

If the subproject is large, consider:

  • Using --squash to reduce history size
  • Using submodules instead for very large dependencies
  • Splitting the subproject into smaller, focused libraries

Summary

OperationCommand
Add subtreegit subtree add --prefix=DIR URL BRANCH --squash
Pull updatesgit subtree pull --prefix=DIR URL BRANCH --squash
Push changesgit subtree push --prefix=DIR URL BRANCH
Split subtreegit subtree split --prefix=DIR --branch=NEW-BRANCH
Remove subtreegit rm -r DIR

git subtree is a powerful tool for embedding external projects while maintaining a clean contributor experience. It shines when you need to modify the subproject and push changes back upstream, and when you want to avoid the complexity of submodule management. For large, rarely-modified dependencies, submodules may still be the better choice.