Git Subtree
git subtree is an alternative to submodules for nesting one repository inside another. Instead of recording a pointer to a separate repo, subtree merges the entire history of the external repo into a subdirectory of the parent repo. The result is a single, unified history with no hidden metadata files, no detached HEADs, and no extra commands needed after a plain git clone.
Mental model
What subtree looks like in your repo
myproject/ ├── src/ │ └── app.js ├── lib/ ← external repo merged here │ ├── components/ │ └── index.js └── package.json The entire "lib/" directory lives in YOUR repo's history. No .gitmodules, no .git/modules/, no special clone flags needed.
Adding a subtree
# Add a remote for the library (optional but recommended — makes pulls/pushes easier) git remote add ui-lib https://github.com/example/ui-components.git git fetch ui-lib # Merge the entire history of ui-lib/main into the prefix "libs/ui" git subtree add --prefix=libs/ui ui-lib main --squash
Terminal output
git fetch ui-lib From https://github.com/example/ui-components * [new branch] main -> ui-lib/main git subtree add --prefix=libs/ui ui-lib main --squash git fetch ui-lib main From https://github.com/example/ui-components * branch main -> FETCH_HEAD Added dir 'libs/ui'
The --squash flag condenses the entire history of the external repo into a single merge commit, keeping your main log clean. Without it, every commit from the external repo is replayed into your history — useful for full attribution, but often noisy.
Pulling updates from the upstream repo
# Fetch the latest commits from the external repo and merge them in git subtree pull --prefix=libs/ui ui-lib main --squash
Output after a successful pull
From https://github.com/example/ui-components * branch main -> FETCH_HEAD Squash commit -- not updating HEAD Merge made by the 'ort' strategy. libs/ui/src/Button.tsx | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-)
Pushing changes back to the upstream repo
If you fix a bug or add a feature inside the subtree directory, you can push those changes back to the upstream repo — a major advantage over simply copying files.
# Extract commits that touched libs/ui and push them to ui-lib/main git subtree push --prefix=libs/ui ui-lib main
Push output
git push using: ui-lib main Counting objects: 5, done. Delta compression using up to 8 threads. Compressing objects: 100% (4/4), done. Writing objects: 100% (5/5), 521 bytes | 521.00 KiB/s, done. To https://github.com/example/ui-components b3c4d5e..f6a7b8c f6a7b8c -> main
The --rejoin flag (performance optimisation)
# Add a synthetic merge commit that marks the split point # Subsequent splits will start from here instead of re-scanning everything git subtree push --prefix=libs/ui ui-lib main --rejoin # Pull also benefits from --rejoin git subtree pull --prefix=libs/ui ui-lib main --squash --rejoin
Full workflow example
Day-to-day subtree workflow
# --- Initial setup (once) --- git remote add ui-lib https://github.com/example/ui-components.git git fetch ui-lib git subtree add --prefix=libs/ui ui-lib main --squash git push origin main # push the merge commit to your team # --- Regular updates --- # Fetch latest and merge it in git subtree pull --prefix=libs/ui ui-lib main --squash # --- Editing files in the subtree --- vim libs/ui/src/Modal.tsx git add libs/ui/src/Modal.tsx git commit -m "fix: close Modal on Escape key" # --- Contribute the fix back upstream --- git subtree push --prefix=libs/ui ui-lib fix/modal-escape # Open a PR on github.com/example/ui-components from fix/modal-escape -> main
Splitting a subdirectory into its own repo
git subtree split is also useful for extracting part of a monorepo into a standalone repo, keeping the relevant history.
# Extract all commits that touched "src/payments/" into a new branch git subtree split --prefix=src/payments -b payments-history # Create a new bare repo and push the extracted history to it git init --bare /tmp/payments-repo.git git push /tmp/payments-repo.git payments-history:main # Clean up the split branch git branch -D payments-history
git subtree vs git submodules
Aspect | git subtree | git submodules |
|---|---|---|
Clone experience | Plain | Requires |
History | Merged into the parent repo (one unified history) | Separate history in an independent repo |
Pinning | Squash commit = implicit pin; explicit SHAs also possible | Explicit commit SHA stored in superproject index |
Contributing back upstream |
| Push from inside the submodule directory |
Detached HEAD risk | None | Constant — must checkout a branch before committing |
.gitmodules file | None | Required |
Offline access | Always available (it's in your repo) | Always available (already cloned) |
Repo size | Larger — external history squashed in | Smaller — only a pointer stored |
Team learning curve | Low — standard Git commands | Medium — extra mental model required |
Updating upstream | One command ( | One command ( |
Use cases
Vendoring a library: lock a specific version of a third-party lib into your repo for reproducible builds, with no package-registry dependency.
Shared component libraries: a design-system repo that multiple product repos embed and occasionally contribute back to.
Documentation sites: embed a shared docs theme or layout repo without separate install steps in CI.
Migrating a monorepo to microrepos: use
git subtree splitto carve out subdirectories with their full history.Bootstrapping a new project: start from a template repo by adding it as a subtree, then diverge freely.
Common errors
Error | Cause | Fix |
|---|---|---|
| You have uncommitted changes when running | Commit or stash your changes first. |
| The target directory already exists in the repo. | Use a different prefix path, or remove the existing directory first. |
Push is extremely slow |
| Use |
| Upstream has commits you have not yet pulled. | Run |