| name | git-subtree |
| description | Vendor external dependencies into a monorepo using git subtree. Use when adding, updating, or managing vendored third-party code, soft forks (pull-only), or push-pull subtrees for self-owned repos. Covers interaction with jujutsu (jj) colocated workflows. |
Git Subtree Vendoring
Vendor dependencies as plain directories — no submodule indirection, works with any tool that reads files.
Two Modes
| Mode | Use case | Operations | Example |
|---|
| Pull-only (soft fork) | Third-party libs you patch locally | add, pull | vendoring an OSS library |
| Push-pull | Repos you own / can push to | add, pull, push | shared internal library |
Choose pull-only by default. Upgrade to push-pull only when you actually need to upstream changes.
Setup: Register a Remote
Always register a named remote so the URL isn't repeated:
git remote add <name> <url>
git fetch <name>
Convention: prefix vendor remotes with vendor/ or v- to distinguish them from origin/upstream (e.g. vendor/foo, v-foo).
Add a Subtree
git subtree add --prefix=<path> <remote> <branch> --squash
--prefix — destination directory (e.g. vendor/foo, lib/bar)
--squash — collapse the upstream history into a single merge commit (always use for pull-only; recommended for push-pull unless you need full history)
Example:
git remote add vendor/arrow https://github.com/apache/arrow.git
git fetch vendor/arrow
git subtree add --prefix=vendor/arrow vendor/arrow main --squash
Pull (Update) a Subtree
git fetch <remote>
git subtree pull --prefix=<path> <remote> <branch> --squash
--squash keeps each pull as one merge commit — clean, bisectable history.
Push Changes Upstream (push-pull only)
git subtree push --prefix=<path> <remote> <branch>
This extracts commits touching <path>, rewrites them relative to the subtree root, and pushes. Only meaningful for repos you control.
Keeping a Clean History
Always squash on pull
--squash is the single most important flag. Without it every upstream commit lands in your log. With it each add/pull is one merge commit.
Commit discipline
- Separate vendoring from feature work. Do subtree add/pull in their own commits — never mix with unrelated changes.
- Descriptive messages. The auto-generated squash message is fine; append context if needed:
Squashed 'vendor/foo/' changes from abc123..def456
Pulls in fix for CVE-2025-XXXX
- Avoid rebasing subtree merge commits. Rebase will flatten the merge and break future
subtree pull. If you must reorder history, keep subtree merges intact.
Periodic re-add (nuclear option)
If subtree tracking state gets corrupted (pull fails with cryptic merge errors):
git rm -r <path>
git commit -m "Remove vendor/foo for re-add"
git subtree add --prefix=<path> <remote> <branch> --squash
Jujutsu Interaction
jj colocated repos share the .git directory, but git subtree is a git-porcelain command. Run subtree operations through git directly — jj sees the result as normal file changes.
Workflow
jj new -m "Pull vendor/foo update"
git fetch vendor/foo
git subtree pull --prefix=vendor/foo vendor/foo main --squash
jj st
jj log --limit 5
jj squash
jj describe -m "Update vendor/foo to <version/date>"
jj new
Caveats
- Do not
jj rebase across subtree merge commits — same as with git rebase, this destroys the merge structure subtree relies on.
git subtree split/push creates temporary refs; run jj git import afterwards if jj doesn't pick them up automatically.
- Conflicts during
subtree pull are resolved in git's working tree. After resolution, git commit to complete the merge, then let jj import.
Quick Reference
| Task | Command |
|---|
| Register remote | git remote add vendor/X <url> && git fetch vendor/X |
| Add subtree | git subtree add --prefix=vendor/X vendor/X main --squash |
| Update subtree | git fetch vendor/X && git subtree pull --prefix=vendor/X vendor/X main --squash |
| Push upstream | git subtree push --prefix=vendor/X vendor/X main |
| Check remotes | git remote -v |
| Re-add (broken state) | git rm -r vendor/X && git commit -m "rm" && git subtree add ... |