Back to Blog

How I Finally Understood git rebase --onto and Stopped Recreating Commits Manually

How I Finally Understood git rebase --onto and Stopped Recreating Commits Manually

How I Finally Understood git rebase --onto and Stopped Recreating Commits Manually

Most developers treat Git like a haunted house—they know which rooms are safe, but they’re terrified of opening the door labeled rebase --onto. I spent years manually cherry-picking commits like a digital assembly line worker because I thought the --onto flag was reserved for people with PhDs in Graph Theory. It isn’t. It’s actually the most precise tool in the Git toolbox for fixing messy branch histories, and once you get the mental model right, you’ll stop fearing complex branch migrations.

The "Feature-on-a-Feature" Nightmare

We've all been there. You’re working on a big project. You create feature-a, but while waiting for a code review, you realize you need to start feature-b, which depends on code in feature-a.

Your history looks like this:

main -> feature-a -> feature-b

Then, reality hits. Your lead dev asks you to completely rewrite feature-a or, worse, they squash-merge feature-a into main. Now, feature-b is pointing to a version of feature-a that technically no longer exists in the history. If you try a standard git rebase main, Git will try to re-apply all the commits from feature-a (which are already in main but with different hashes) alongside your feature-b changes.

You get a hundred merge conflicts. You panic. You rebase --abort and start manually copying code.

The Three-Argument Logic

The secret to git rebase --onto is realizing it takes three arguments, not just one. Most of us are used to git rebase <branch>, which is shorthand. When you use --onto, you are being surgically specific.

The syntax is:
git rebase --onto <new-parent> <old-parent> <branch-to-move>

Think of it as a "Cut and Paste" operation for your commit graph:
1. Where am I going? (new-parent)
2. Where am I cutting from? (old-parent)
3. What am I moving? (branch-to-move)

A Real-World Example

Let's say your branches look like this:

* main: Commit M1, M2
* feature-a: Commit A1, A2 (off main)
* feature-b: Commit B1, B2 (off feature-a)

feature-a gets merged into main via a squash commit. Now main has a new commit A_SQUASHED. Your feature-b is still hanging off the "old" A1 and A2.

To fix this, you don't want to rebase everything. You just want B1 and B2.

git rebase --onto main feature-a feature-b

In plain English, this command says: *"Find the commits that are in feature-b but NOT in feature-a, and put them right on top of main."*

Git looks at the range between feature-a and feature-b, grabs those specific commits, and transplants them. It ignores all the mess in feature-a that would have caused your merge conflicts.

Why not just use cherry-pick?

You could. You could git cherry-pick B1 and then git cherry-pick B2. But if feature-b has 15 commits, you’re going to hate your life. Plus, rebase --onto keeps your branch context intact. It moves the entire branch pointer for you. Cherry-picking requires you to create a new branch and manually bring things over.

--onto is the "I'm a professional" way to handle branch debt.

The Gotcha: The "From" Argument is Exclusive

One thing that tripped me up for months: the <old-parent> (the second argument) is exclusive.

If you say git rebase --onto main head~3 head, it starts moving commits *after* head~3. If you want to include a specific commit in your move, you have to point to its parent.

When to reach for this tool

I find myself using this most often in two scenarios:
1. The "Orphaned Child": When the parent branch was deleted or squashed and I need to move the child branch to main.
2. The "Branch Splitting": When I realize I’ve accidentally done two different tasks on one branch. I can create a new branch starting from the middle of my current one and "transplant" the latter half onto main.

It feels dangerous the first time you do it. You’ll probably keep a backup branch (git branch feature-b-backup) just in case. That’s fine. I still do that sometimes. But once the command finishes and you see a perfectly clean, conflict-free history, you’ll realize you’ve finally leveled up your Git game.