Interactive Rebase, Squashing, and Rewriting History Safely
A pull request with fourteen commits named "wip", "fix", and "actually fix" tells a reviewer nothing about what changed or why. Interactive rebase turns that into a small number of meaningful commits — as long as you know the one rule about which commits are safe to rewrite.

I've opened pull requests with commit histories like wip, wip 2, fix lint, actually fix lint, revert previous, ok now it works.
Every one of those commits is honest about how the work actually happened. Every one of them is useless to a reviewer six months from now trying to understand why a change was made.
Interactive rebase is the tool for turning that honest mess into a small number of commits that each tell one coherent story — before anyone else has to read it.
The one rule — understand this first
- Never rebase commits that other people have already pulled.
Rebase rewrites commits — same content, new hash — because a commit's hash is derived in part from its parent, and rebase changes the parent. If you rewrite commits someone else already has, their local branch and yours now disagree about what "the same" commit even is. Reconciling that divergence is a worse mess than whatever the messy history was trying to avoid.
In practice: Rebase freely on a branch only you're working on, before you push it. Once it's shared and others have pulled it, merge instead — don't rewrite.
Interactive rebase — the editor that changes history
git rebase -i HEAD~4
This opens an editor listing your last 4 commits, oldest first:
pick a1b2c3d Add login form skeleton
pick e4f5g6h wip
pick i7j8k9l fix validation
pick m0n1o2p actually fix validation
Each line has a command you can change:
| Command | What it does |
|---|---|
pick | Keep the commit as-is |
reword | Keep the changes, edit the message |
squash | Merge into the previous commit (combines messages) |
fixup | Merge into the previous commit (discards this message) |
drop | Delete this commit entirely |
| (reorder lines) | Reorders commits when replayed |
Squashing the WIP mess into one clean commit
pick a1b2c3d Add login form skeleton
fixup e4f5g6h wip
fixup i7j8k9l fix validation
fixup m0n1o2p actually fix validation
Save and close. Git replays these in order, folding the three fixup commits into the first one.
Result: a single commit — Add login form skeleton — containing all four commits' changes, with none of the intermediate noise.
Squashing is selective — not always "one big commit"
A common mistake: treating interactive rebase as "turn my whole branch into one commit, full stop."
Sometimes that's right. More often a branch has two or three genuinely separate concerns bundled across many small commits — a schema migration and the feature that depends on it, for instance.
The better outcome is two clean commits, not one that mixes migration and feature code. Reviewers benefit from seeing each concern independently. Future git revert operations do too.
- Squash the commits belonging to each concern onto their own respective first commit — not everything onto a single line.
pick a1b2c3d Add user schema migration
fixup b2c3d4e wip on migration
pick c3d4e5f Add user profile feature
fixup d4e5f6g fix profile validation
fixup e5f6g7h fix profile image upload
Result: two clean commits, each telling one story.
Amending the last commit
For the single most common case — you just committed, and immediately noticed a typo or forgot a file:
git add forgotten-file.js
git commit --amend
Replaces the most recent commit with a new one that includes the staged changes. Opens the editor to let you change the message too. Add --no-edit to keep the message unchanged and skip the editor.
This is a specialized, faster version of what you'd otherwise do with rebase -i HEAD~1.
- Same rule applies: only amend a commit that hasn't been pushed and pulled by anyone else.
Splitting a commit
Less common but genuinely useful: you committed two unrelated changes together and want them separated after the fact.
git rebase -i HEAD~3
Mark the commit you want to split with edit instead of pick. Git stops right after replaying that commit with its changes fully applied. From there:
git reset HEAD~1 # un-commit, keep changes in working directory
git add file-a.js
git commit -m "Add validation for signup form"
git add file-b.js
git commit -m "Fix typo in README"
git rebase --continue # replay the rest of the original commits
git reset HEAD~1 here un-does the commit but leaves the actual file changes sitting unstaged — exactly the state you need to re-stage and commit them separately.
When rebase goes wrong — how to get out
git rebase --abort
Aborts mid-rebase and puts your branch back exactly where it was before you started. The rebase equivalent of git merge --abort.
If a rebase finished and you realize afterward it went wrong:
git reflog
The reflog records every position HEAD has pointed to, including the branch tip from right before the rebase started. Nothing is unrecoverable just because a rebase already completed — the old commits are still in the object database.
Rebase vs. merge — what they're actually for
| Merge | Rebase | |
|---|---|---|
| History shape | Preserves what happened, including merge commit | Clean linear sequence |
| Commit hashes | Unchanged | Changed for every rebased commit |
| Safe on shared branches | Yes | Only if nobody else has pulled |
| Best for | Combining a finished feature into main | Cleaning up commits before opening a PR |
A practical team convention: Rebase your feature branch locally to clean it up before opening a PR, then merge (not rebase) that clean branch into main. You get a readable individual branch history and a merge commit marking where the feature actually landed.
Rewriting history on purpose is one thing — accidentally losing a commit, resetting a branch too far, or deleting a branch you needed is another. The next chapter covers Git's undo toolkit: the real difference between restore, reset --soft/--mixed/--hard, and revert, plus git reflog — the safety net that makes almost every "I think I just lost my work" situation recoverable.
Enjoyed this chapter?
Get an email when I publish the next chapter. No spam — just new technical deep-dives.
Comments
Share feedback or questions about this blog post.
No comments yet. Be the first to share your thoughts.