Rebase has a reputation for danger. That reputation comes from one specific misuse — rewriting shared history — and not from the operation itself, which is one of the most useful things Git can do.
What it does
Before:
A---B---C---F ← main
\
D---E ← feature
git switch feature; git rebase main
After:
A---B---C---F---D'---E' ← feature
↑
main
Git finds the commits unique to feature (D and E), sets the branch aside, moves to main's tip, then replays each one as a new commit. D′ and E′ contain the same changes but have different hashes — different parents, so different content, so different hash.
The result is a linear history that looks as though you started your work from the current main all along.
Rebase versus merge for keeping up to date
git merge main | git rebase main | |
|---|---|---|
| History shape | Merge commits appear on your branch | Linear |
| Hashes | Unchanged | All rewritten |
| Safe when shared | Yes | No |
| Conflicts | Once, all at the end | Potentially once per commit |
| Records what really happened | Yes | No — it presents a tidied story |
Never rebase commits that others have based work on. Your unpushed local branch: rebase freely. Your own pushed feature branch that nobody else uses: rebase and force-with-lease, it is fine and normal. A shared branch, or main: never. Rewriting hashes under someone else forces them to reconcile a diverged history, and they will usually get it wrong and duplicate commits.
Conflicts during a rebase
Because rebase replays commits one at a time, a conflict can occur at each one:
git rebase main # CONFLICT in src/auth.js # fix the file, then git add src/auth.js git rebase --continue # or skip this commit entirely git rebase --skip # or give up and restore the original branch git rebase --abort
During a rebase Git is replaying your commits onto the upstream, so it considers the upstream "ours" and your work "theirs" — the opposite of a merge. Rely on the actual content rather than the labels, and turn on merge.conflictstyle = zdiff3 so you can see the common ancestor too.
Interactive rebase
This is where rebase becomes a genuine craft tool:
git rebase -i main # everything since main git rebase -i HEAD~5 # the last 5 commits git rebase -i --root # the entire history
Git opens an editor with a script:
pick a3f9c2e Add login form pick b4e8d1f wip pick c5f9e2a fix typo pick d6a0f3b Add validation pick e7b1a4c oops forgot file # p, pick = keep as is # r, reword = keep the change, edit the message # e, edit = stop here so you can amend or split it # s, squash = merge into the previous commit, combine messages # f, fixup = merge into the previous commit, discard this message # d, drop = remove the commit entirely # Reorder lines to reorder commits.
Rewriting it as:
pick a3f9c2e Add login form fixup b4e8d1f wip fixup c5f9e2a fix typo pick d6a0f3b Add validation fixup e7b1a4c oops forgot file
…produces two clean commits from five messy ones.
The autosquash workflow
The tidiest way to handle review corrections. When fixing something that belongs to an earlier commit:
git commit --fixup=a3f9c2e # creates a commit titled "fixup! Add login form" # later, fold them all in automatically git rebase -i --autosquash main
Git positions each fixup under its target and pre-marks it. Set git config --global rebase.autosquash true and it happens on every interactive rebase.
Splitting a commit
git rebase -i HEAD~3 # mark the commit 'edit' git reset HEAD~ # uncommit it, keep the changes git add -p # stage the first logical piece git commit -m "First half" git add . git commit -m "Second half" git rebase --continue
Force-pushing after a rebase
Your local branch and the remote have now genuinely diverged, so a normal push is rejected. That rejection is Git protecting you, not an obstacle to route around:
git push --force-with-lease
--force says "make the remote look like me, whatever is there". If a colleague pushed while you were rebasing, their commits are gone — silently, with no error. --force-with-lease first checks the remote is still where you last saw it and refuses otherwise. There is no situation where plain --force is meaningfully better; make the safe one a habit, or alias it.