git rebase -i Without Fear
· Jerwin Arnado ·
You’ve been typing clean commit messages,
but a feature branch still ends up messy: a feat, then three fix typo commits, then a
wip, then the real work. Nobody needs that noise in main. Interactive rebase
(git rebase -i) lets you rewrite those commits into the history you wish you’d made —
before anyone reviews it.
It has a scary reputation. It shouldn’t. The danger is exactly one rule, and there’s a safety net for when you fumble.
The one rule, first
Never rebase commits you’ve already shared. Rebasing rewrites history — every commit
after the rebase point gets a new hash. If those commits exist on someone else’s machine
(because you pushed and they pulled), you’ve just forked reality, and the next git pull
on their side turns into a mess of duplicated commits.
Safe to rebase: your local, un-pushed branch, or a feature branch only you work on.
Not safe: main, or any branch a teammate has pulled. Tattoo it on your arm.
Starting an interactive rebase
Rebase the last N commits by counting back from HEAD:
git rebase -i HEAD~4
Or rebase everything on your branch since it diverged from main:
git rebase -i main
Git opens an editor listing the commits oldest-first (the opposite of git log),
each prefixed with the word pick:
pick a1b2c3d feat(auth): add passkey login
pick d4e5f6a fix typo
pick b7c8d9e fix another typo
pick e0f1a2b wip
You edit this list — the commands, not the messages — save, and Git replays the commits according to your instructions.
The commands you’ll actually use
| Command | Short | What it does |
|---|---|---|
pick |
p |
Keep the commit as-is |
reword |
r |
Keep the commit, edit its message |
squash |
s |
Merge into the previous commit, combine both messages |
fixup |
f |
Merge into the previous commit, discard this message |
drop |
d |
Delete the commit entirely |
edit |
e |
Pause at this commit to amend its contents |
Reorder lines to reorder commits. Delete a line and the commit is dropped (same as
drop). The most common cleanup — fold the typo-fixes into the real commit:
pick a1b2c3d feat(auth): add passkey login
fixup d4e5f6a fix typo
fixup b7c8d9e fix another typo
reword e0f1a2b wip
Save and close. Git replays: the two fixups vanish into the feat commit (their
messages discarded), and it stops to let you reword the wip into something real like
fix(auth): handle the cancelled-prompt case. Four messy commits become two clean ones.
squash vs fixup: both combine into the commit above, but squash opens an editor
to merge the two messages, while fixup throws the squashed commit’s message away. Use
fixup for “fix typo” noise, squash when both messages have content worth keeping.
Amending a commit’s contents with edit
reword changes a message; edit lets you change the actual files in an old commit.
Mark it edit, and the rebase pauses there:
# rebase stops at the edit commit; make your changes, then:
git add path/to/fixed/file
git commit --amend --no-edit
git rebase --continue
This is how you retroactively split a forgotten file into the commit where it belonged.
When it goes sideways: --abort and reflog
Two safety nets, in order of how much they save you.
Mid-rebase, want out? Nothing is final until the rebase finishes:
git rebase --abort
That returns the branch to exactly where it was before you started. No harm done.
Already finished, and it’s wrong? This is where people panic — and where the reflog
earns its keep. Git records every position HEAD has held, even the ones a rebase
“erased”:
git reflog
e0f1a2b HEAD@{0}: rebase (finish): returning to refs/heads/feature
9a8b7c6 HEAD@{1}: rebase (start): checkout main
3f2e1d0 HEAD@{2}: commit: wip ← the branch before the rebase
Find the pre-rebase entry and reset to it:
git reset --hard HEAD@{2}
Your messy-but-intact branch is back. A rebase is never truly destructive for ~90 days — the old commits sit in the reflog until they’re garbage-collected. Knowing this is the entire difference between “rebase is terrifying” and “rebase is just an undo away.”
Caveats and best practices
- Rebase before you push, not after. Clean the branch locally, then push it for review. This sidesteps the one rule entirely.
git pull --rebasekeeps your local commits on top of upstream changes instead of creating a merge bubble. Set it as the default:git config --global pull.rebase true.- Conflicts pause the rebase, one commit at a time. Resolve,
git add, thengit rebase --continue— nevergit commit. (Conflict resolution is its own post.) --autosquashpairs withgit commit --fixup=<hash>: mark fixups as you go, thengit rebase -i --autosquashorders them automatically. A huge time-saver once it’s habit.
Conclusion
git rebase -i main # rewrite the branch into the history you meant to make
# fix it up: pick / reword / squash / fixup / drop / edit
git rebase --abort # bail out mid-flight, no harm
git reset --hard HEAD@{N} # undo a finished rebase via the reflog
Interactive rebase is just an editor for your recent history. Keep it to un-pushed commits, remember the reflog has your back, and the fear evaporates.