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.
On this page
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
main: A---B---C
\
feature: D---E 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)
-
Inspect the conflict
git statusshows 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. -
Resolve and stage
Edit the files, then
git add <file>for each one. Do not rungit commit— rebase does that for you. -
Continue
git rebase --continueapplies the resolved commit and moves on to the next. -
Escape hatches
git rebase --skipdrops the current commit.git rebase --abortreturns 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:
| Verb | Effect |
|---|---|
pick | Keep the commit as-is (the default). |
reword | Apply, then open the editor on the commit message. |
edit | Apply, then pause so you can git commit --amend or split the commit. |
squash | Combine into the previous commit, opening the editor to merge messages. |
fixup | Combine into the previous commit, discarding this message. |
drop | Skip 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>.
main: A---B---C---H
\
old-base: D---E---F 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
git push --force-with-lease origin develop after rebasing develop onto something.
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.
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, notmainonto 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
- Git· mutualFinding the commit that broke it with git bisectBinary search across commit history to find the first bad commit. Works manually or fully automated with a script — turning a needle-in-a-haystack search into log2(n) steps.
- Git· relatedGit submodules without surprise detached HEADsSubmodules embed one repo inside another by pinning a commit, not a branch. Knowing that single fact prevents most of the pain — detached HEADs, missed pushes, and silent divergence after a pull.
- Git· relatedGit worktrees for parallel branch workHow and when to use git worktree to keep multiple branches checked out at once, sharing one object store, without the disk cost or coordination headaches of multiple clones.
- Git· relatedStacked diffs: commits as review unitsTreat each commit (or tiny branch) as the unit of review, not a large feature branch. Smaller reviews, less waiting, better commit hygiene — and a non-trivial rebase tax to pay.