Skip to content

Git rebase without losing work

What rebase actually does to your commits, how to drive the interactive form, and the recovery moves that keep a bad rebase from becoming a lost-work incident.

Updated May 26, 2026

Rebase replays your commits on top of a new base. The replay creates new commits with new hashes — the originals don’t move, they’re orphaned and eventually garbage-collected. Everything else about rebase follows from that one fact: why it’s great for cleaning up a feature branch, why it’s a foot-cannon on a shared one, and why the reflog is your safety net.

The mental model

Before rebase
main:    A---B---C
              \
feature:       D---E
After `git rebase origin/main`
main:    A---B---C
                  \
feature:           D'---E'

D' and E' carry the same content as D and E but have different commit IDs. The branch label feature moves to the new tip; the old D---E chain is now unreachable from any ref.

This is why rebase is great for private branches (the feature looks like it was always built on the latest main) and dangerous for shared ones (teammates who already pulled D---E will now see history diverge from the remote).

The basic workflow

git switch feature
git fetch origin
git rebase origin/main

If there are no conflicts, you’re done. If a commit fails to apply cleanly, rebase pauses:

Applying commit D ... OK
Applying commit E ... CONFLICT (stops here)
  1. Inspect the conflict

    git status shows the unmerged files. The working tree has the conflict markers; the index holds the three conflict stages (base/ours/theirs) and is waiting for you to resolve and stage a single clean version.

  2. Resolve and stage

    Edit the files, then git add <file> for each one. Do not run git commit — rebase does that for you.

  3. Continue

    git rebase --continue applies the resolved commit and moves on to the next.

  4. Escape hatches

    git rebase --skip drops the current commit. git rebase --abort returns to the state before you started — the cleanest undo Git offers mid-rebase.

If the same conflicts keep reappearing (long-lived branch, repeated merges with the same line ranges), git config rerere.enabled true tells Git to record resolutions and replay them automatically next time.

Interactive rebase for history cleanup

git rebase -i origin/main

Git opens an editor with a pick line per commit. Replace pick with one of:

VerbEffect
pickKeep the commit as-is (the default).
rewordApply, then open the editor on the commit message.
editApply, then pause so you can git commit --amend or split the commit.
squashCombine into the previous commit, opening the editor to merge messages.
fixupCombine into the previous commit, discarding this message.
dropSkip the commit entirely.

For splitting an edit-paused commit: git reset HEAD^, then add and commit in pieces, then git rebase --continue. See 0026 Three ways to squash, and when each is right · Git for the squash-specific workflows and when to use which.

Rebasing onto a different base

git rebase --onto <newbase> <upstream> [<branch>] is the surgical form. Read it as: take the commits in <branch> after <upstream>, and replay them onto <newbase>.

Before
main:      A---B---C---H
              \
old-base:      D---E---F   feature
After `git rebase --onto H C feature`
main:      A---B---C---H
                       \
feature:                D'---E'---F'

The common use: you branched off the wrong base, or the branch you depended on got squashed/rewritten and you want to move your work onto its new tip without dragging the old base along.

Pushing rewritten history

After rebase, the remote branch and your local branch have divergent histories. A plain git push is rejected. The right tool is:

git push --force-with-lease origin <branch>

The recovery move

If a rebase goes wrong after you’ve already exited it (so --abort is no longer available), the reflog has your back:

git reflog
# 5a7b3c8 HEAD@{0}: rebase (finish): returning to refs/heads/feature
# 9c1d2e4 HEAD@{1}: rebase (start): checkout origin/main
# 1a2b3c4 HEAD@{2}: commit: E      <- the pre-rebase tip
# ...

git reset --hard HEAD@{2}

Every operation that moves HEAD leaves a reflog entry. The pre-rebase tip is reachable from the reflog for the configured expiry (default 90 days), long enough to recover from any normal mistake.

When not to rebase

Anti-pattern
Rebasing a shared long-lived branch
Smell

git push --force-with-lease origin develop after rebasing develop onto something.

Why it happens

The author wanted to “tidy up” develop or replay it on top of main. They forgot that everyone else has develop checked out and based their work on it.

Fix instead

Reserve rebase for branches whose only consumer is you. For shared integration branches, use git merge and accept the merge commits. If you genuinely need to rewrite shared history, coordinate the rewrite explicitly with everyone affected — it’s not a tooling problem, it’s a communication problem.

Quick rules

  • Rebase your branch onto main, not main onto your branch.
  • Verify non-trivial rewrites with git range-diff @{u} @{1} @ — it shows whether the new commits really say the same thing as the old ones.
  • Rebase early and often on a long-lived feature branch. Small, frequent rebases produce small, frequent conflicts; deferred rebases produce one giant unmergeable knot.
  • Never rewrite a tagged commit. Tags pin a specific commit hash; rewriting the commit leaves the tag pointing at the orphaned original. Nothing warns you — the next release just ships the wrong code.

Related: 0026 Three ways to squash, and when each is right · Git for the squash variants that often follow a cleanup rebase.

Related