- git rebase –onto <new-base> <old-base> <branch> transplants commits after <old-base> onto <new-base>, excluding intermediate history
- Use –onto to drop broken commits mid-branch, rebase onto merge points without replaying all history, or fix multi-team branch dependencies
- The three-pointer model: where you want to end up (new-base), what to exclude (old-base), and what to move (branch)—the middle argument is the exclusion boundary
- Prefer –onto over cherry-pick when moving 5+ commits or when batch conflict resolution matters; use merge commits if history cleanliness isn't critical
When Interactive Rebase Isn’t Enough
Most developers know git rebase -i for cleaning up commit history. But there’s a scenario that trips up even senior engineers in interviews: what happens when you need to move a feature branch from one base to another, and the original base has commits you explicitly don’t want?
git rebase --onto solves this. It’s the surgical tool for branch transplants, and understanding it separates people who’ve shipped messy production fixes from those who’ve only worked in pristine feature-branch workflows.
Here’s the syntax that makes people’s eyes glaze over:
git rebase --onto <new-base> <old-base> <branch>
The mental model: take the commits after <old-base> on <branch>, and replay them on top of <new-base>. The <old-base> parameter is what throws people off — it’s not where the branch currently points, it’s the exclusion boundary. Everything after that commit gets moved.
I’m going to walk through four scenarios where --onto is the only clean solution. Each one is based on interview questions I’ve seen at companies like Stripe and Databricks, where Git fluency is a signal of production experience.

Scenario 1: Abandoned Feature Branch Recovery
You branched feature-A off develop two weeks ago. Since then, develop accumulated 40 commits, including a database migration that breaks your feature. Your teammate just merged feature-B into develop, which includes the schema changes you actually need.
The naive approach is git rebase develop, but that replays all 40 commits, including the breaking migration and unrelated work. You only want commits after the point where feature-B merged.
Here’s the move:
# Find the merge commit of feature-B into develop
git log develop --oneline --merges -n 5
# Let's say it's abc1234
# Rebase your feature onto that merge point, excluding everything before it
git checkout feature-A
git rebase --onto abc1234 <original-branch-point> feature-A
The <original-branch-point> is where you originally branched off — you can find it with:
git merge-base feature-A develop
This command finds the common ancestor. So the full operation becomes:
ORIGIN=$(git merge-base feature-A develop)
git rebase --onto abc1234 $ORIGIN feature-A
What this does: Git takes every commit you made on feature-A (everything after $ORIGIN) and replays them on top of abc1234, skipping all the intermediate commits on develop.
Scenario 2: Dropping a Broken Commit Mid-Branch
This one comes up in production firefighting. You have a branch with 8 commits. Commit #4 introduced a memory leak, but commits #5-8 are solid bug fixes you need to ship now. You can’t afford to cherry-pick each one individually.
Interactive rebase can drop commits, but what if the broken commit is also the branch point? Or what if dropping it would create conflicts that interactive rebase can’t auto-resolve?
--onto gives you precision:
# Say commit 4 is deadbeef, commit 3 is cafe123
git rebase --onto cafe123 deadbeef feature-branch
This says: take everything after deadbeef and put it on top of cafe123. Commit deadbeef vanishes. The rebase conflict resolution happens in one pass, and you can inspect the result before force-pushing.
If you’re debugging why this works, try visualizing with:
git log --oneline --graph --all
Before the rebase, your branch looks like:
* commit8
* commit7
* commit6
* commit5
* deadbeef (commit 4 - the broken one)
* cafe123 (commit 3)
* commit2
* commit1
After git rebase --onto cafe123 deadbeef feature-branch:
* commit8'
* commit7'
* commit6'
* commit5'
* cafe123
* commit2
* commit1
The prime marks (') indicate those commits got new SHA hashes because their parent changed. But their diffs are identical to the originals.
Scenario 3: Emergency Hotfix on the Wrong Base
You cut a hotfix branch hotfix-auth off main at commit aaa1111. You made 3 commits fixing a critical auth bug. Before you could merge, someone force-pushed main with a revert (commit bbb2222) that undoes a recent feature.
Your hotfix now has a bad parent — if you merge it, you’ll re-introduce the reverted code. But you don’t want to lose your 3 commits.
Solution: rebase your hotfix onto the current main, excluding everything between your original branch point and now:
# Current main is at ccc3333 (which includes bbb2222, the revert)
git fetch origin
git rebase --onto origin/main aaa1111 hotfix-auth
This transplants your 3 commits onto the new main, as if you’d originally branched from ccc3333. The intermediate history (including the reverted code) is ignored.
Why --onto beats cherry-pick here: cherry-pick would work for 3 commits, but what if you had 20? Or what if some of them conflict? Rebase lets you resolve conflicts incrementally, and Git preserves authorship metadata automatically.
Scenario 4: Multi-Team Branch Dependency Hell
This is the interview killer. Your team maintains backend-api branch. Another team has frontend-integration branched off your work. You realize commits 5-7 in backend-api are a dead end and need to be removed, but frontend-integration depends on commit 8+.
If you force-push backend-api with those commits removed, the frontend team’s branch becomes orphaned. Here’s how to fix it for them without breaking their work:
# On backend-api, identify the bad commits (say they're between commit4 and commit8)
# You want to keep commit1-4 and commit8+, dropping 5-7
# Create a new branch without the bad commits
git checkout -b backend-api-clean commit4
git cherry-pick commit8..backend-api # This picks commit8 through HEAD
# Now the frontend team can rebase onto your clean branch
git checkout frontend-integration
git rebase --onto backend-api-clean commit4 frontend-integration
The --onto here is critical: it takes all commits the frontend made after commit4 and replays them on backend-api-clean, which no longer has commits 5-7.
Alternatively, if you control both branches:
# Remove commits 5-7 from backend-api in place
git checkout backend-api
git rebase --onto commit4 commit7 backend-api
# Then fix frontend-integration
git checkout frontend-integration
git rebase backend-api # Standard rebase now works
But the --onto approach gives you more control if the frontend team has already pushed their branch — you can create the clean rebase and send them a PR instead of force-pushing their work.
The Three-Pointer Mental Model
Here’s the pattern I wish someone had told me earlier: git rebase --onto always takes three pointers:
- Where you want to end up (
<new-base>) - What to exclude (
<old-base>— everything up to and including this) - What to move (
<branch>— the commits after<old-base>on this branch)
The mistake people make is thinking <old-base> is “where the branch currently starts.” It’s not. It’s the exclusion boundary. Git takes everything after that commit and moves it.
If you run git rebase --onto A B C, Git does this:
# Pseudocode
commits_to_move = commits_between(B, C) # Exclusive of B, inclusive of C
for commit in commits_to_move:
replay(commit, on_top_of=A)
The range is (B, C] in interval notation — open on the left, closed on the right.

When –onto Breaks (and How to Recover)
I’ve seen two common failure modes:
1. Rebasing onto a commit that’s already in the branch’s history
If <new-base> is an ancestor of <branch>, Git will try to replay commits that already exist. You’ll get:
fatal: refusing to rebase onto a base that is a descendant of the current tip
Fix: double-check your commit SHAs. Use git log --oneline --graph to visualize the tree.
2. The dreaded “empty commit” avalanche
If the commits you’re moving have already been applied to <new-base> (maybe via a previous cherry-pick), Git will try to replay them and realize they produce no diff. You’ll see:
The following commits are empty:
abc1234 Fix auth bug
def5678 Update config
Git gives you three choices:
– --keep-empty: preserve them as empty commits (rarely what you want)
– --skip: drop them (usually correct)
– Abort and cherry-pick manually
If you’re mid-rebase and this happens:
git rebase --skip # Skips the current empty commit, continues
If it happens repeatedly, abort and rethink:
git rebase --abort
# Then manually inspect which commits are truly unique
git log <new-base>..<branch> --oneline
Interview Red Flags (Things That Make Me Cringe)
If you’re explaining --onto in an interview and say any of these, expect follow-up grilling:
- “It’s like interactive rebase but more advanced” — No. They solve different problems.
- “You use it to move branches” — Vague. How does it move them? What gets excluded?
- “It’s for fixing mistakes” — True but not insightful. Every Git command is for fixing mistakes.
What impresses me:
- “It’s for replaying a commit range onto a new base while excluding intermediate history”
- “The middle argument is the exclusion boundary — Git ignores everything up to that commit”
- “I use it when cherry-picking would require manual tracking of 5+ commits”
The Math Behind the Magic
Git’s rebase is fundamentally a sequence of cherry-picks with conflict resolution. When you run:
git rebase --onto A B C
Git computes the commit range and applies the transformation:
for each commit in the range. The “parent” of the first moved commit changes from its original parent to . Subsequent commits maintain their relative parentage within the moved sequence.
If any apply operation produces a conflict, Git pauses and asks you to resolve it, then resumes with:
git rebase --continue
The complexity is where is the number of commits being moved and is the average diff size, because each commit requires a three-way merge:
For a rebase, “base” is the original parent, “ours” is the commit’s changes, and “theirs” is the new base. Git uses the same merge machinery as git merge, which is why conflict markers look identical.
Real Output from a Failed –onto
Here’s what you see when things go wrong (this is from a production incident where we tried to rebase a 3-month-old feature branch):
$ git rebase --onto main feature-old-base feature-new
Auto-merging src/api/handler.py
CONFLICT (content): Merge conflict in src/api/handler.py
Auto-merging tests/test_api.py
CONFLICT (content): Merge conflict in tests/test_api.py
error: could not apply a1b2c3d... Add new endpoint
Resolve all conflicts manually, mark them as resolved with
"git add/rm <conflicted_files>", then run "git rebase --continue".
You can instead skip this commit: run "git rebase --skip".
To abort and get back to the state before "git rebase", run "git rebase --abort".
Could not apply a1b2c3d... Add new endpoint
The temptation is to spam git rebase --skip, but that discards your work. Instead:
# See what the conflict actually is
git diff
# Fix the conflicts in your editor (look for <<<<<<< markers)
vim src/api/handler.py
# Stage the resolution
git add src/api/handler.py tests/test_api.py
# Continue the rebase
git rebase --continue
Git will then try to apply the next commit. If you have 15 commits and 10 conflict, you’ll resolve 10 times. This is why I said “make sure you actually need --onto” — sometimes cherry-picking 2-3 commits is faster than resolving 10 conflicts.
Edge Case: Rebasing Onto the Same Branch
What happens if you do:
git rebase --onto feature-A commit3 feature-A
This drops all commits between commit3 and the current tip of feature-A. It’s equivalent to:
git reset --hard commit3
But with --onto, Git actually replays the commits, so you get a chance to resolve conflicts if commit3 and HEAD have diverged. I’ve used this exactly once, to drop a contaminated commit from a branch where git reset would’ve lost work (teammate had already pulled the branch).
FAQ
Q: When should I use git rebase --onto instead of git cherry-pick?
If you’re moving 5+ commits, --onto is cleaner. Cherry-pick requires you to manually list each SHA or use a range, and you lose the ability to batch conflict resolution. Use cherry-pick for 1-3 specific commits when you know exactly which ones you need and the order doesn’t matter.
Q: Can I use --onto to rebase a branch onto itself to drop commits in the middle?
Yes, and it’s sometimes safer than interactive rebase if you’re worried about accidentally reordering commits. The syntax is git rebase --onto <commit-before-bad> <commit-after-bad> <branch>, which drops everything between the two pointers. But interactive rebase with drop is usually more intuitive for this use case.
Q: What happens if I specify the wrong <old-base> — say, a commit that’s not actually in the branch history?
Git will try to compute the range (old-base, branch] and likely find zero commits, giving you Current branch <branch> is up to date. If the commit you specify is after the branch tip, you’ll get fatal: invalid upstream because the range is nonsensical. Always verify with git log --oneline <old-base>..<branch> before running the rebase.
When to Just Merge Instead
Not every branch surgery needs --onto. If your commit history is already a mess and you’re not publishing a library, a merge commit might be fine. I covered when merge commits actually make more sense in a previous post, but the TL;DR: if your team doesn’t enforce linear history, don’t waste time rebasing.
Use --onto when:
- You’re preparing a clean PR for open-source review
- You need to drop specific commits that would break trunk if merged
- You’re fixing a branch dependency hell scenario like Scenario 4 above
Don’t use it when:
- You’d need to resolve 20+ conflicts (just start over)
- The branch is already merged (you’d be rewriting history post-merge, which is almost always wrong)
- You don’t understand the three-pointer model yet (practice on a throwaway branch first)
And if you’re doing late-night hotfixes fueled by caffeine, maybe grab some Dark Chocolate Espresso Beans before you start messing with --onto on a shared branch. The last thing you need is to force-push the wrong ref at 2am.
What I Still Don’t Fully Trust
I’ve used --onto to salvage production branches at least a dozen times, but I’m still not 100% confident in how it interacts with Git submodules. The official docs claim it works, but I’ve seen cases where submodule pointers get orphaned after a complex rebase, and you have to manually git submodule update --remote afterward.
My rule: if your repo has submodules and you’re doing --onto surgery, test the result in a fresh clone before force-pushing. I’ve wasted hours debugging “submodule not initialized” errors that were caused by a rebase inadvertently changing the submodule commit reference without updating .gitmodules.
For straightforward branch-on-branch rebases without submodules, though? --onto is the cleanest tool Git offers for this class of problem. Just make sure you visualize the tree first and triple-check your three pointers.
Did you find this helpful?
Your support keeps this blog running and ad-free content coming.
☕ Buy me a coffeeMost Popular Posts
- Custom Metaclass in Python: 43% Faster Validation (12,883 views)
- Python match-case: 7 Patterns That Beat if-elif Chains (969 views)
- yfinance Alternatives 2026: 7 Free APIs Compared (889 views)
- YOLOv8 INT8 Quantization: 4x Faster on Jetson Orin (828 views)
- PaddleOCR vs EasyOCR vs Tesseract: Why PaddleOCR Is Slower (636 views)