Every developer who's shipped a "WIP", "fix typo", "actually fix it", "oops" string of commits knows the pain of opening a pull request and seeing a commit history that tells no story at all. Interactive rebase is the tool that turns that mess into a clean, linear narrative your reviewers can actually follow.
Why Clean History Matters
A pull request's commit history isn't just a log — it's documentation. Future developers (including you, in six months) will use git blame and git log to understand why a change was made. A history full of noise makes that archaeology painful.
A clean commit history is a gift to your future self and your reviewers.
The Basic Command
git rebase -i HEAD~5
This opens your last 5 commits in an editor, each prefixed with pick. From here you can reorder lines, or swap pick for one of several rebase commands.
The Rebase Commands
| Command | Shorthand | Effect |
|---|---|---|
| pick | p | Keep the commit as-is |
| reword | r | Keep changes, edit the commit message |
| edit | e | Pause here to amend the commit |
| squash | s | Merge into the previous commit, combine messages |
| fixup | f | Merge into the previous commit, discard this message |
| drop | d | Remove the commit entirely |
How It Works Under the Hood
Rebase doesn't move commits — it replays them. Git walks your branch's commits one at a time, applying each as a patch on top of the new base, and builds brand-new commit objects (with new SHAs) along the way.
A Real Example
Say your branch has this history before opening a PR:
a1b2c3d WIP
e4f5g6h fix typo
h7i8j9k actually add the feature
k0l1m2n oops forgot a file
Run git rebase -i HEAD~4, then change the plan to:
pick h7i8j9k actually add the feature
fixup k0l1m2n oops forgot a file
fixup a1b2c3d WIP
fixup e4f5g6h fix typo
Save and exit — Git squashes all four into a single, well-described commit.
Editing a Commit Message Mid-Rebase
Mark a commit reword, and after saving the plan, Git will stop and open your editor just for that commit's message:
git rebase -i HEAD~3
# change 'pick' to 'reword' on the line you want to rename
# Git opens your editor — update the message, save, close
# rebase continues automatically
Splitting or Amending a Commit
Mark a commit edit to pause the rebase right after it's applied:
git rebase -i HEAD~3
# mark the target commit as 'edit'
# rebase pauses here — make your changes
git add <file>
git commit --amend
# then continue
git rebase --continue
Handling Conflicts
Since rebase replays commits one by one, conflicts can surface at any step:
# fix the conflicting files
git add <resolved-files>
git rebase --continue
# or bail out entirely and go back to where you started
git rebase --abort
Pushing Rebased History
Because rebase rewrites commit SHAs, a normal git push will be rejected. You need a force push — but use the safer variant:
git push --force-with-lease
--force-with-lease refuses to overwrite the remote branch if someone else has pushed to it since your last fetch, protecting teammates from losing work — unlike a plain --force.
When to Use Which
Use squash when a commit's changes belong with the previous one but the message itself still adds context worth keeping. Use fixup for pure "oops" commits where the message adds nothing. Use reword when the code is fine but the message is misleading or too terse. Use drop for commits that turned out to be dead ends, like an experiment you abandoned.
Common Pitfalls
Never rebase commits that other people have already pulled and built on top of — you'll rewrite history out from under them and create painful merge conflicts on their end. Rebase is for local or PR-branch cleanup before merge, not for branches already shared and built upon. Also double-check your rebase plan before saving; a misplaced drop silently deletes a commit's changes, though git reflog can usually recover it if you catch the mistake quickly.
Interactive rebase rewrites history — that's exactly why it's powerful, and exactly why it should stay confined to branches only you (or your PR) are working on.

