How do you send changes made inside a git subtree prefix back to the upstream repository?
answer
- Your commits are the wrong shape for upstream
- Extract a prefix-only history
- Paths get the prefix stripped
- split -b, then push the branch
- --rejoin saves re-walking history
basics
~20 sUse git subtree split to synthesise a history containing only the commits that touched that prefix, with the prefix stripped from the paths, then push that synthetic branch upstream — git subtree push does both steps in one command.
solid answer
~40 sYour commits touching the vendored directory are ordinary commits in your repository, so a normal push would send your whole project, which upstream does not want. `git subtree split --prefix=vendor/lib -b lib-fixes` walks your history, keeps only the parts of each commit that touched `vendor/lib`, rewrites the paths so the prefix becomes the root, and puts the result on a new branch. That branch looks like the upstream project and can be pushed to it: `git push liblog lib-fixes:some-branch`, or in one step `git subtree push --prefix=vendor/lib liblog some-branch`. The synthesised commits are new objects with new SHAs, though author and message are preserved. Splitting has to walk history, so it is slow on large repositories; `git subtree split --rejoin` records the split point in your history so later splits start from there.
code
bash · 8 linesgit subtree split --prefix=vendor/liblog -b liblog-fixes
git log --oneline liblog-fixes | head
# push the reconstructed history upstream
git push liblog liblog-fixes:fix-timeout
# or in one step
git subtree push --prefix=vendor/liblog liblog fix-timeoutgo deeper
Recall that contributing back is a separate, deliberate step — the vendored files are yours locally, and Git has to reconstruct a prefix-only branch before upstream can take it.
Explain what split produces: commits containing only prefix changes with the prefix stripped from paths, new hashes, original authors, attached at the last imported upstream commit.
Show the operating judgment — keep vendored fixes in dedicated commits, budget for the walk on a long history, use --rejoin when splitting often, and re-pull after upstream accepts so the recorded split point moves forward.
Own whether contributing back is a workflow you want at all. Two-way subtree traffic adds ongoing maintenance; decide when a maintained fork or an upstream-first policy is cheaper than repeated split-and-push cycles.
## Why a normal push does not work When you vendor a project with `git subtree`, its files become ordinary files in your repository. Editing them produces ordinary commits on your branch — commits that may also touch code outside the prefix, and whose paths are all prefixed by the vendoring directory. The upstream repository has neither your other code nor that prefix. So the fix has to be *extracted* into a shape upstream can accept. ## split: reconstructing a prefix-only history `git subtree split --prefix=<dir> [-b <branch>]` does that extraction. Conceptually it replays your history and, for each commit that touched anything under `<dir>`, emits a corresponding commit whose tree is just that directory's content with the prefix stripped, so files sit at the root exactly as they do upstream. Commits that touched nothing under the prefix are skipped entirely; a commit that touched both the prefix and unrelated code contributes only its prefix-side changes. The emitted commits are **new objects with new hashes** — they must be, because their trees differ — but the original author, date and message are carried across. `-b <branch>` puts the resulting tip on a branch name so you can inspect or push it; without it the command prints the resulting commit id. Split needs to know where the synthesised history should attach to upstream's. It gets that from the subtree metadata your imports left behind: the `git-subtree-dir` and `git-subtree-split` lines in previous subtree commits identify the prefix and the upstream commit last imported, so the new commits build on that rather than starting from nothing. This is one concrete reason not to rewrite or strip those commit messages. ## push: split plus push in one step `git subtree push --prefix=<dir> <repository> <branch>` performs the split and pushes the resulting tip to `<branch>` in `<repository>`. It is a convenience wrapper — if you want to inspect the reconstructed commits first, do the split by hand, look at `git log <branch>`, and push with a normal `git push <repo> <branch>:<upstream-branch>`. How the change is then *accepted* is entirely the upstream project's business: they may take a branch push, or ask for a change request through whatever platform they use, or want patches by mail. That part is outside Git's mechanism; your job ends at producing a branch whose commits apply to their tree. ## Cost and the --rejoin option Splitting walks history from the attach point forward, which is proportional to the number of commits it has to consider. On a repository with a long history this is genuinely slow, and it is re-done on each split. `git subtree split --rejoin` addresses this: after producing the synthetic history it merges that result back into your current branch, leaving a commit that records the new split point. Subsequent splits can start from that recorded point instead of walking everything again. The price is an extra merge commit in your history each time you split, so teams that split rarely often skip `--rejoin` and simply accept the wait. ## Practical cautions - **Keep prefix changes in their own commits.** A commit that mixes a vendored fix with unrelated application changes still splits correctly, but its message will describe work that is invisible upstream, which reads as noise in the reconstructed history. Separate commits produce a clean contribution. - **Expect divergence.** Because split produces new SHAs, the branch you push is not identical to anything upstream has seen. If you split the same range twice with different attach points you can get different commits, so do not treat split output as stable identity. - **Round-trip after acceptance.** Once upstream merges your work, pull it back with `git subtree pull --prefix=<dir>` rather than assuming your local prefix is already equivalent. The upstream may have amended, squashed or rebased your contribution, and the pull is what re-anchors the recorded `git-subtree-split` SHA. - **Squashed imports still split.** The recorded upstream SHA in the squash commit gives split its attach point. What you cannot recover is upstream detail you never imported. - **Run from the top level.** Like every `git subtree` subcommand, split and push must be run from the root of the working tree. ## The short interview answer Say: the vendored code is just files, so contributing back means reconstructing a prefix-only history — `git subtree split --prefix=<dir> -b <branch>` — and pushing that branch upstream, optionally in one step with `git subtree push`. Mention that the resulting commits are new objects, that split is slow on long histories and `--rejoin` amortises it, and that the relationship is anchored by the `git-subtree-split` metadata left in your import commits.
- Why do the commits produced by git subtree split have different hashes from your originals?Because their trees are different: the synthesised commits contain only the prefix's content with the prefix stripped from the paths, and they sit on a different parent chain anchored at the upstream commit. A commit's hash covers its tree, parents and metadata, so any of those changing yields a new object. Author, date and message are preserved.
- What happens to a commit that touched both the vendored prefix and unrelated application code?Only its prefix-side changes are carried into the synthesised history; the rest is dropped. The extraction is correct, but the commit message will describe work upstream cannot see, which reads as noise. Keeping vendored fixes in their own commits produces a much cleaner contribution.
- After upstream accepts the change, is your local prefix automatically in sync?No. Do a `git subtree pull --prefix=<dir>` from upstream afterwards. They may have squashed, amended or rebased your contribution, and the pull is what records a new `git-subtree-split` SHA so the next split attaches at the right place instead of trying to re-contribute work upstream already has.
- What does --rejoin buy you, and what does it cost?It merges the split result back into your branch so the split point is recorded in your history, letting later splits start from there instead of re-walking everything — a real saving on long histories. The cost is an extra merge commit in your own history for each split, which teams that split rarely usually prefer to avoid.
saying these in an interview costs you the question
- Suggests pushing your own branch directly to the upstream repository
- Thinks split preserves the original commit SHAs
- Assumes commits outside the prefix are included in the split
- Believes subtree pushes local changes upstream automatically
- Ignores that split walks history and is slow on big repos