Subtree merges for embedded projects Jump to heading

A subtree merge combines a project whose files live at the root of its own repository with your repository, where those same files live under a subdirectory. Git normally matches paths exactly, so merging vendor/parser’s upstream would put its README.md at your root and conflict with yours. The subtree strategy shifts one side’s paths by a prefix so they line up. This is the mechanism under the git subtree command, and you can also use it directly — useful when you want to understand what git subtree pull did, when the command is not installed, or when you need finer control over a one-off import. This page covers the initial import, later updates, and the prefix mistakes that cause confusing results, within merge strategies and gitattributes.

When to use this approach Jump to heading

Step 1 — Fetch the other project as a remote Jump to heading

Add the embedded project’s repository as a remote and fetch it. Its branches become available as remote-tracking refs, with the files at their root.

git remote add parser https://github.com/example/fast-parser.git
git fetch parser --no-tags
git ls-tree --name-only parser/main | head        # README.md, src/, tests/ … at the root

Step 2 — Import it under a prefix with read-tree Jump to heading

The initial import needs two steps: start a merge without committing, then read the other project’s tree into the index under the prefix. The commit records the other project’s branch as a parent, so its history is joined to yours.

git merge -s ours --no-commit --allow-unrelated-histories parser/main
git read-tree --prefix=vendor/parser/ -u parser/main
git commit -m "Import fast-parser into vendor/parser"
ls vendor/parser
The initial subtree importAn ours-strategy merge with no commit joins the unrelated histories while keeping your tree. read-tree with a prefix then places the other project's files under the subdirectory and stages them. Committing records a merge whose second parent is the other project's history.fetchparser/mainmerge -s ours--no-commit--allow-unrelatedread-tree--prefix=vendor/parser/commithistory joinedthe ours merge supplies the second parent; read-tree supplies the files
# Verification: the second parent is the other project, and files sit under the prefix
git log -1 --format='%p'
git log --oneline -3 HEAD^2
git diff --stat HEAD^1 HEAD | tail -1

Step 3 — Pull later updates with the subtree strategy Jump to heading

After the initial import, updates are ordinary merges with path shifting. -X subtree=<prefix> tells the default strategy exactly where the other side’s root belongs.

git fetch parser
git merge -X subtree=vendor/parser/ --no-edit parser/main
git log --oneline -1
git diff --stat HEAD^1 HEAD                 # changes only under vendor/parser/

The older form, git merge -s subtree parser/main, guesses the prefix by comparing trees. The guess is usually right, and occasionally wrong when the embedded project’s layout resembles another directory. The explicit -X subtree= option removes the guess.

-s subtree against -X subtree=prefixThe subtree strategy guesses which subdirectory the other project belongs in by comparing trees, which can choose the wrong directory when layouts are similar. The subtree option to the default strategy takes the prefix explicitly, so the shift is always where you intend.git merge -s subtreegit merge -X subtree=vendor/parser/prefixguessedexplicitwrong directory riskwhen layouts look alikenonemerge enginesubtree strategydefault (ort) + shiftalways give the prefix — guessing saves nothing and occasionally costs a lot

Step 4 — Avoid the common prefix mistakes Jump to heading

Most subtree trouble comes from the prefix: a missing trailing slash, a different prefix in the update than in the import, or files moved inside the prefix locally.

# Check what the last import or update used — the prefix should be identical every time
git log --merges --format='%h %s' -- vendor/parser | head
# A wrong prefix shows up as files appearing outside the subdirectory
git diff --name-only HEAD^1 HEAD | grep -v '^vendor/parser/' && echo "WARNING: changes outside the prefix"

⚠️ SAFETY WARNING: A subtree merge with the wrong prefix can scatter the embedded project’s files across your repository root, sometimes overwriting your own files with the same name. Check git diff --name-only HEAD^1 HEAD right after every subtree merge. If files landed outside the prefix, undo the merge immediately with git reset --hard ORIG_HEAD (before pushing) and repeat it with an explicit, correct prefix.

Step 5 — Send local changes back upstream Jump to heading

Changes you make under the prefix are ordinary commits in your repository. To contribute them upstream, extract the subdirectory’s history into a branch with paths shifted back to the root.

git subtree split --prefix=vendor/parser -b parser-upstream-changes
git log --oneline parser-upstream-changes -5
git push parser parser-upstream-changes:refs/heads/from-acme

git subtree split walks your history and produces commits containing only the prefix’s content, re-rooted. Without the git subtree command, the same can be done with git filter-repo --subdirectory-filter on a separate clone, as described in rewriting history with git filter-repo.

The round trip with an embedded projectThe upstream project is imported under a prefix and later updates are merged with the explicit subtree option. Local fixes made under the prefix are split back out into a re-rooted branch and pushed to the upstream repository for review.upstream repoyour reposplit branchimport under vendor/parser/-X subtree= updatessubtree split --prefixpush fixes upstreamthe same prefix must be used for every step, in both directions

Validation checklist Jump to heading

Frequently Asked Questions Jump to heading

How is this different from a submodule? Jump to heading

A subtree merge copies the other project’s files and history into your repository; a submodule stores only a pointer. Subtrees make cloning and building simpler and history larger; submodules keep repositories separate at the cost of extra clone steps.

Can I import with squashed history instead? Jump to heading

Yes — merge a squashed copy rather than the full branch. git subtree add --squash does exactly that, producing one commit per update instead of importing every upstream commit.

What if the upstream project rewrites its history? Jump to heading

Your previous merges reference the old commits, and the next subtree merge sees unrelated or diverged history. Merge with --allow-unrelated-histories and expect conflicts once, or switch to squashed imports, which are not affected by upstream rewrites.