Skip to content

← Writing

engineering

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 --rebase keeps 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, then git rebase --continue — never git commit. (Conflict resolution is its own post.)
  • --autosquash pairs with git commit --fixup=<hash>: mark fixups as you go, then git rebase -i --autosquash orders 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.