N
Naveenr.dev
Chapter 03
10 min read2026-08-07
📖 Git SeriesChapter 03 · 11 chapters

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.

Git interactive rebase and history rewriting
Git interactive rebase and history rewriting

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

bash
git rebase -i HEAD~4

This opens an editor listing your last 4 commits, oldest first:

text
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:

CommandWhat it does
pickKeep the commit as-is
rewordKeep the changes, edit the message
squashMerge into the previous commit (combines messages)
fixupMerge into the previous commit (discards this message)
dropDelete this commit entirely
(reorder lines)Reorders commits when replayed

Squashing the WIP mess into one clean commit

text
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.
text
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:

bash
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.

bash
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:

bash
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

bash
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:

bash
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

MergeRebase
History shapePreserves what happened, including merge commitClean linear sequence
Commit hashesUnchangedChanged for every rebased commit
Safe on shared branchesYesOnly if nobody else has pulled
Best forCombining a finished feature into mainCleaning 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.