Interactive rebase — cleaning up commits before sharing them

Plain rebase replays commits unchanged onto a new base. Interactive rebase does the same replay, but lets you edit, reorder, combine, or drop the commits along the way — the actual tool behind "clean up this branch before opening a pull request."

Advanced

4 min read

The situation: a messy, real, in-progress commit history

$ git log --oneline
f4a1c22 fix typo
9e3b881 fix typo again
7c2d104 WIP
3a8e991 actually implement the login form
1b9f220 WIP, don't judge me

This is genuinely how commits often look while actively working — small fixes, typo corrections, "WIP" placeholders — none of which is a problem locally (from the daily-workflow and rebasing-vs-merging lessons, this only matters once it's shared). Before opening a pull request or merging this into main, though, this history is exactly the kind of noise the daily-workflow lesson's "commit small, commit often" advice was never meant to produce — five commits telling a confusing story, when the actual change is one coherent thing: implementing a login form.

git rebase -i: the same replay, but stopping at each commit

git rebase -i HEAD~5
pick 1b9f220 WIP, don't judge me
pick 3a8e991 actually implement the login form
pick 7c2d104 WIP
pick 9e3b881 fix typo again
pick f4a1c22 fix typo

# Commands:
# p, pick = use commit as-is
# r, reword = use commit, but edit the message
# s, squash = combine with the commit before it, keep both messages
# f, fixup = combine with the commit before it, discard this message
# d, drop = remove this commit entirely

git rebase -i opens an editable list of the commits being replayed, oldest first, each with a command (pick by default) that controls what happens to it. This is the actual mechanism behind "cleaning up history" — instead of just replaying commits unchanged (plain rebase, from its own lesson), interactive rebase lets each one be reworded, reordered, combined with its neighbor, or dropped entirely, before the replay actually happens.

squash and fixup: combining several commits into one

pick 1b9f220 WIP, don't judge me
squash 3a8e991 actually implement the login form
fixup 7c2d104 WIP
fixup 9e3b881 fix typo again
fixup f4a1c22 fix typo

Changing a commit's command from pick to squash or fixup merges it into the commit directly above it in the list — squash keeps both commit messages, combined, for editing into one final message; fixup discards the fixed-up commit's message entirely, keeping only the one it's being merged into. This is exactly how five commits — a rough start, the real implementation, and three small fix-up commits — become one clean commit: "Implement login form," with a single, meaningful message.

reword: fixing a commit message without touching its content

reword 3a8e991 actually implement the login form

Marking a commit reword (instead of pick) pauses the rebase at that commit specifically to let its message be rewritten — the actual code changes stay completely untouched, only the message changes. This is the fix for "I wrote a bad commit message and want to improve it before anyone else sees it," without needing squash/fixup at all if the content itself is already fine.

drop: removing a commit entirely

drop 1b9f220 WIP, don't judge me

Marking a commit drop (or simply deleting its line from the list) removes it from history entirely, as if it had never been made — genuinely useful for a commit that was itself a mistake (an experiment that didn't pan out, a debug print statement accidentally committed) rather than something to combine with a neighbor.

Reordering: just moving lines around

pick 3a8e991 actually implement the login form   <- moved to the top
pick 1b9f220 WIP, don't judge me

Since the list is just text, reordering commits is as simple as reordering the lines — the replay then happens in whatever order the list ends up in. This matters when commits would make more sense in a different order than they were originally made in, though it's worth noting that reordering commits that touch the same lines can introduce conflicts during the replay that didn't exist in the original order.

Why this is scoped to local, unshared commits — same rule as plain rebase

Interactive rebase creates entirely new commits, exactly like plain rebase —
the same "never rebase already-pushed, shared commits" rule from the
rebasing-vs-merging lesson applies here without exception

Interactive rebase is still rebase underneath — every commit it touches gets replayed as a new commit object with a new hash, the identical mechanism the rebasing-vs-merging lesson covered. The same hard rule applies without exception: this is for cleaning up commits that are still entirely local, before they're ever pushed and shared — running it on commits someone else has already pulled creates the exact same orphaned-work danger covered there.

Further reading

Check your understanding

A quick comprehension check — not tracked, not graded, just for you.

1. What does interactive rebase let you do that plain rebase doesn't?

2. What's the difference between marking a commit squash vs. fixup during an interactive rebase?

3. When is reword the right choice instead of squash or fixup?

4. Does interactive rebase change the rule against rebasing already-pushed, shared commits?