← Back to DevBytes

Git Subtree Merging

What is Git Subtree Merging?

Git subtree merging is a strategy for incorporating the contents of one repository into a subdirectory of another repository while preserving the full commit history of both. Unlike Git submodules, which merely store a reference pointer to an external repository, subtree merging physically copies the files and their entire history into your main project. This gives you a single, unified repository with no external dependencies at clone time.

The technique relies on Git's powerful merging machinery. Git can merge two completely unrelated histories by using a shared "synthetic" common ancestor. During a subtree merge, Git combines the file trees from two branches and places the merged content into a specific subdirectory of the target branch. The result is a clean, self-contained project that includes third-party code as if it had always been part of your repository.

There are two ways to work with subtree merging: the manual merge strategy using core Git plumbing commands, and the higher-level git subtree command that wraps the complexity into simple verbs. Understanding both approaches gives you full control over the process and helps you troubleshoot when things go wrong.

Why Git Subtree Merging Matters

🚀 Deploy your AI agent in 10 minutes

Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.

Try it free →

Subtree merging solves a fundamental problem in software development: managing dependencies that you need to customize or keep tightly integrated with your project. Here are the key reasons why this technique is valuable:

How to Use Git Subtree Merging

Approach 1: Manual Subtree Merge Strategy

The manual approach uses the Git merge strategy subtree. This gives you precise control over every step. The process involves adding the external repository as a remote, fetching its history, and then merging it into a subdirectory using a custom merge strategy.

Step 1: Add the external repository as a remote.

git remote add mylib https://github.com/user/mylibrary.git

This creates a named remote called mylib pointing to the external repository. You can verify it with:

git remote -v

Step 2: Fetch the remote history.

git fetch mylib

Now your local repository knows about all branches and tags from mylib, but they are not yet integrated into your working tree.

Step 3: Perform the subtree merge. Suppose you want to pull the main branch of mylib into the directory libs/mylib/ within your project. You use the subtree merge strategy with the --prefix option:

git merge -s subtree --prefix=libs/mylib/ mylib/main --allow-unrelated-histories

The --allow-unrelated-histories flag is required because the two repositories do not share a common ancestor. Git will merge the entire file tree from mylib/main into libs/mylib/, creating a merge commit that ties the histories together.

Step 4: Pulling upstream updates later. When the upstream repository has new commits, you can merge them into your subtree with the same strategy:

git fetch mylib
git merge -s subtree --prefix=libs/mylib/ mylib/main

Since the histories are now connected, Git knows how to reconcile changes. Any modifications you made inside libs/mylib/ will be preserved, and upstream changes will be merged in intelligently. Conflicts that occur within the subtree are resolved just like any other merge conflict.

Step 5: Pushing changes back to the upstream repository (optional). If you want to contribute changes you made in the subtree back to the original project, you can extract the subtree's history into a separate branch and push that:

git subtree split --prefix=libs/mylib/ --branch=mylib-contrib
git push mylib mylib-contrib:main

The subtree split command creates a new branch that contains only the commits that touched files within libs/mylib/, rewriting the paths so they appear at the root. You can then push this branch to the upstream remote.

Approach 2: The Modern git subtree Command

The git subtree command (available since Git 1.7.11) wraps the manual merge process into a simpler interface. It handles the remote setup, fetching, merging, and splitting for you.

Adding a new subtree (initial import):

git subtree add --prefix=libs/mylib/ https://github.com/user/mylibrary.git main

This single command fetches the remote repository and merges it into libs/mylib/ using the subtree strategy. It automatically creates a merge commit that joins the two histories.

Pulling updates from upstream:

git subtree pull --prefix=libs/mylib/ https://github.com/user/mylibrary.git main

This fetches the latest changes from the remote and merges them into your subtree directory. Git remembers the remote URL and branch if you used git subtree add initially, so you can shorten this to:

git subtree pull --prefix=libs/mylib/ mylib main

Where mylib is the remote name you set up earlier (or that git subtree add created automatically).

Pushing changes back to upstream:

git subtree push --prefix=libs/mylib/ https://github.com/user/mylibrary.git main

This extracts all commits that modified libs/mylib/, rewrites them to root paths, and pushes them to the specified remote branch. This is the cleanest way to contribute back to the upstream project without exposing your entire repository structure.

Viewing the log of a subtree:

git log --oneline -- libs/mylib/

This shows commits that affected files in the subtree directory. For a view that mimics what the upstream project sees, use:

git subtree split --prefix=libs/mylib/ --branch=tmp-view
git log --oneline tmp-view
git branch -D tmp-view

Working with Multiple Subtrees

You can incorporate several external projects into different subdirectories. Each one gets its own prefix and remote:

git subtree add --prefix=libs/mathlib/ https://github.com/math/lib.git main
git subtree add --prefix=libs/ioutils/ https://github.com/io/utils.git main

Pulling updates for each is straightforward:

git subtree pull --prefix=libs/mathlib/ mathlib main
git subtree pull --prefix=libs/ioutils/ ioutils main

Git handles each subtree independently. Their histories are merged into your project's history at the merge commits created during the add and pull operations.

Resolving Merge Conflicts in Subtrees

Conflicts inside a subtree merge are resolved exactly like any other merge conflict. Git will mark the conflicting regions within the files under the prefix directory. After fixing them manually, you stage the resolution and commit:

git add libs/mylib/conflicting_file.c
git commit

If a subtree pull results in massive conflicts, you can preview the changes before merging by fetching and then using diff:

git fetch mylib
git diff --staged mylib/main -- libs/mylib/

This helps you assess the impact of upstream changes before committing to the merge.

Best Practices for Git Subtree Merging

git subtree pull --prefix=libs/mylib/ mylib main -m "Merge mylib v2.3.1 into libs/mylib/"
git subtree add --prefix=libs/mylib/ --squash mylib main

This collapses all upstream commits into a single merge commit while still recording the merge parent for future incremental merges. It keeps your history clean while preserving the ability to pull updates later.

git tag upstream-mylib-v2.3.1 HEAD
git push origin upstream-mylib-v2.3.1

This gives you reference points for knowing exactly which upstream version is integrated at any point in your history.

git checkout -b test-mylib-update
git subtree pull --prefix=libs/mylib/ mylib main
# Run tests, fix issues
git checkout main
git merge test-mylib-update

Conclusion

Git subtree merging gives you the best of both worlds: the independence of a monolithic repository and the modularity of external dependency management. By physically incorporating third-party code with its full history into your project, you eliminate the runtime fragility of submodules while retaining the ability to track upstream changes and contribute back. Whether you use the manual merge strategy for fine-grained control or the streamlined git subtree command for day-to-day operations, the technique scales from small libraries to entire nested projects. With careful prefix organization, disciplined commit practices, and regular upstream synchronization, subtree merging becomes a reliable backbone for managing shared code across teams and projects.

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles