skip to content

What does git rebase --onto main feature-a feature-b do, and when do you need it?

level: middleimportance: should knowfreq 45%

answer

  1. Three arguments, three different jobs
  2. One argument decides which commits, not where
  3. Think about a branch built on another branch
  4. Preview the set with a two-dot range first
  5. Also the trick for excising a run of commits

basics

~20 s

It replays the commits reachable from feature-b but not from feature-a onto main, then moves feature-b to the result. You need it when the commits you want to move and the base you want to cut them from are different things.

solid answer

~50 s

In `git rebase --onto <newbase> <upstream> <branch>`, the three arguments answer three separate questions: `<newbase>` is where the commits should land, `<upstream>` decides **which** commits move (those in `<branch>` but not in `<upstream>`), and `<branch>` is checked out and moved to the result. A plain `git rebase main feature-b` collapses two of those roles — the commits are cut at the merge base with `main` and land on `main`. `--onto` separates them. The classic need is a **stacked branch**: `feature-b` was branched off `feature-a`, and now you want only `feature-b`'s own commits on `main` without dragging `feature-a`'s along. It is equally the tool for transplanting work off a base that was abandoned or already rebased, and — with a commit range — for dropping a run of commits by rebasing the commits after them onto the commit before them.

go deeper

for a junior

Recognise that a three-argument rebase exists and that it separates where commits land from which commits are chosen. Nobody expects fluency here yet.

for a middle

State the role of each argument, and show why the plain form drags a parent branch's commits along when branches are stacked.

for a senior

Show the operational habit of previewing the replayed set with a two-dot range before running it, and how you recover the old base hash from the reflog when restacking.

for a principal

Own the workflow implication: how a team stacks dependent branches, whether that pattern is worth its restacking cost, and what tooling or conventions absorb it.

## The three questions a rebase must answer Every rebase implicitly answers three questions: which commits are we moving, where are they going, and which ref ends up pointing at the result. The two-argument form answers them with only two inputs, because it assumes the first two coincide. `--onto` exists precisely to separate them. In `git rebase --onto <newbase> <upstream> <branch>`: - `<newbase>` — the commit the replayed series will sit on top of. - `<upstream>` — the *exclusion* boundary. The commits replayed are those reachable from `<branch>` but not from `<upstream>`, the same set `git log <upstream>..<branch>` prints. - `<branch>` — optional; if given, Git checks it out first and moves it to the new tip when the replay finishes. Omit it and the currently checked-out branch is used. ## Why the plain form is not enough `git rebase main feature-b` computes the commits in `feature-b` that are not in `main` and lands them on `main`. That is correct whenever `feature-b` branched directly off `main`. It is wrong for a **stack**: if `feature-b` was branched off `feature-a`, which itself is not merged into `main`, then "commits not in `main`" includes all of `feature-a`'s commits too. Rebasing that way drags someone else's in-progress work onto `main` as copies — usually the opposite of the intent. With `--onto main feature-a feature-b`, the exclusion boundary is `feature-a`, so only `feature-b`'s own commits are selected, and they land on `main`. The stack is flattened correctly. ## The situations that call for it - **Restacking after the parent branch moved.** `feature-a` was rebased or squashed, so `feature-b` now hangs off commits that are no longer on `feature-a`. Rebase `feature-b` onto the new `feature-a` tip, cutting at the *old* `feature-a` tip. The old tip is recoverable from the reflog if you did not note it. - **Transplanting off an abandoned base.** Work was started on the wrong branch entirely; `--onto` moves just your commits to the right base. - **Dropping a range of commits.** `git rebase --onto <commit-before-the-range> <last-commit-of-the-range>` replays everything after the range onto the commit before it, excising the range from the branch. - **Splitting a long branch.** Cut a series at a chosen commit and land the tail somewhere else. ## Two-argument --onto `--onto` also works with two arguments: `git rebase --onto <newbase> <upstream>` uses the current branch as `<branch>`. This is the common shorthand once you are already on the branch you intend to move. ## Getting the boundary right The single most common mistake is confusing the exclusion boundary with the destination. A reliable habit is to run `git log --oneline <upstream>..<branch>` *before* the rebase: that command prints exactly the commits that will be replayed. If the list contains commits you did not write or did not intend to move, your `<upstream>` argument is wrong. Doing this check costs seconds and prevents the most confusing rebase outcomes. ## What it shares with every rebase `--onto` changes only *selection*, not *mechanics*. The selected commits are still replayed one at a time, still get new hashes, can still conflict per commit, and are still resumed with `--continue`, dropped with `--skip`, or abandoned with `--abort`. The branch's previous tip is recorded in the reflog, so a wrong `--onto` is recoverable by resetting to the pre-rebase position. ## Why it is asked It separates people who have memorised `git rebase main` from people who understand what a rebase is selecting. A candidate who can articulate the three roles — destination, exclusion boundary, ref to move — can construct the right command for a stacked branch on the spot, which is a genuinely common situation in review-driven workflows.

  • How do you preview exactly which commits a rebase --onto will replay?
    Run `git log --oneline <upstream>..<branch>` with the same two arguments you plan to pass. That two-dot range is precisely the set rebase selects: commits reachable from the branch but not from the upstream. If the list includes commits you did not intend to move, your exclusion boundary is wrong.
  • How would you drop a run of three consecutive commits from the middle of a branch using --onto?
    Rebase the commits after the run onto the commit immediately before it: `git rebase --onto <commit-before-run> <last-commit-of-run>`. Everything after the run is replayed onto the earlier base, so the three commits are excluded from the result. As with any rebase, the previous tip is still in the reflog.
  • Your stacked branch's parent was rebased, so the old base commits are gone from it. What do you cut at?
    At the parent branch's *old* tip — the commit your branch was actually built on. If you no longer have that hash, the parent branch's reflog records its previous positions. Use that as the upstream argument and the parent's new tip as the destination.

saying these in an interview costs you the question

  • Thinks the first argument selects which commits move
  • Uses the plain two-argument form on a stacked branch
  • Believes --onto avoids conflicts or new hashes
  • Cannot state which two-dot range equals the replayed set
  • Assumes the branch argument is where commits land

context