← Back to blog

Interactive Rebase Without Fear: A Practical Guide

tutorial git

The Most Avoided Tool in Git

Ask a room of developers which Git command makes them nervous, and git rebase -i will rank near the top. Interactive rebase has a reputation: it rewrites history, it opens a strange todo list in your editor, and it feels dangerously easy to break something. So most people simply avoid it -- and their pull requests arrive carrying commits like "wip", "fix typo", and "actually fix it".

That is a shame, because interactive rebase is the single best tool for turning a messy branch into a clean, reviewable history. In this guide we will demystify it with one concrete running example: a feature branch with 9 messy commits that we will reshape into 3 clean ones before opening a pull request. If you are still wondering whether rebase is even the right choice for your workflow, start with our comparison of rebase vs merge -- this article assumes you have decided to rebase and want to do it well.

Why a Clean History Matters

Cleaning up commits before a pull request is not about aesthetics. A tidy history pays off in three very practical ways:

  • Reviewability. A reviewer reading three logical commits -- "Add login form with validation", "Add remember me option", "Add error messages" -- can review each change in isolation. The same code split across nine half-finished commits forces them to review the whole diff at once, and review quality drops.
  • git bisect. When a bug appears weeks later, git bisect walks the history to find the commit that introduced it. That only works if every commit builds and makes sense on its own. A history full of "wip" commits that do not even compile makes bisect nearly useless.
  • Reverting. If a feature needs to be backed out, reverting one self-contained commit is trivial. Reverting a feature smeared across nine interleaved commits is an afternoon of archaeology.

Our Running Example: 9 Messy Commits

Here is the branch we will clean up. It is a perfectly normal working history -- this is how everyone actually codes:

$ git log --oneline main..feature/login
c9d0e1f oops forgot the css file
b8c9d0e add error messages
a7b8c9d wip 2
f6a7b8c add remember me checkbox
e5f6a7b actually fix it
d4e5f6a fix typo
c3d4e5f add form validation
b2c3d4e wip
a1b2c3d add login form skeleton

Nothing wrong with committing like this while you work. The problem is only if it ships like this. Our goal is to end up with three commits:

  1. Add login form with validation
  2. Add remember me option
  3. Add error messages with styling

Starting the Rebase: git rebase -i HEAD~9

To rewrite the last 9 commits, run:

git rebase -i HEAD~9

HEAD~9 means "the commit 9 steps before the current one" -- everything after that point is up for editing. Git opens your editor with the todo list:

pick a1b2c3d add login form skeleton
pick b2c3d4e wip
pick c3d4e5f add form validation
pick d4e5f6a fix typo
pick e5f6a7b actually fix it
pick f6a7b8c add remember me checkbox
pick a7b8c9d wip 2
pick b8c9d0e add error messages
pick c9d0e1f oops forgot the css file

Two things to understand before touching anything. First, this is just a text file: nothing happens until you save and close it, and if you delete every line or close without saving meaningful changes, the rebase is cancelled harmlessly. Second, the order is oldest first -- the opposite of git log. Git will replay the commits from top to bottom, applying the verb at the start of each line.

The Six Verbs

pick

Keep the commit exactly as it is. This is the default for every line.

reword

Keep the commit's changes, but stop to let you edit the commit message. Perfect for fixing "wip 2" without touching the code.

edit

Pause the rebase at this commit. You can amend it, split it into several commits, or test it, then continue with git rebase --continue. This is the most advanced verb and the one you will use least often.

squash

Meld this commit into the one above it, and open the editor so you can combine the two commit messages into one.

fixup

Like squash, but discard this commit's message entirely. The commit above keeps its message unchanged. This is what you want for "fix typo" commits -- their messages carry no information worth keeping.

drop

Delete the commit and its changes completely. Deleting the line from the todo list has the same effect, but writing drop makes your intent explicit.

Cleaning Up the Branch, Step by Step

Now let us apply this to our 9 commits. Two operations are needed: reordering and changing verbs.

First, the reorder. Looking at the diffs, "oops forgot the css file" is actually the stylesheet for the remember me checkbox, not for the error messages. In the todo list, reordering is just moving a line: cut the c9d0e1f line and paste it right after the remember me group. Git will replay the commits in the new order.

Then, the verbs. Everything up to "actually fix it" belongs to the login form, so it collapses into the first commit. We use squash on "add form validation" because we want to merge its message into the final one, and fixup on the noise commits whose messages deserve to disappear. Here is the edited todo list:

pick a1b2c3d add login form skeleton
fixup b2c3d4e wip
squash c3d4e5f add form validation
fixup d4e5f6a fix typo
fixup e5f6a7b actually fix it
pick f6a7b8c add remember me checkbox
fixup a7b8c9d wip 2
fixup c9d0e1f oops forgot the css file
reword b8c9d0e add error messages

Save and close. Git replays the commits and stops twice: once to let you write the combined message for the first group (type "Add login form with validation"), and once for the reword (type "Add error messages with styling"). For the remember me commit, a quick reword would have worked too -- here we left it as pick and its original message was already decent. The result:

$ git log --oneline main..feature/login
3f4e5d6 Add error messages with styling
2e3d4c5 Add remember me option
1d2c3b4 Add login form with validation

Nine commits became three, each one self-contained and reviewable. Notice that all the hashes changed -- these are brand new commits. Keep that detail in mind; it is the heart of the golden rule below.

The Pro Move: --autosquash

Once you are comfortable, you can prepare the cleanup while you work instead of sorting it out at the end. When you fix something in an earlier commit, record it like this:

git commit --fixup a1b2c3d

Git creates a commit whose message is fixup! add login form skeleton. Later, run:

git rebase -i --autosquash main

Git pre-fills the todo list with every fixup! commit already moved under its target and marked as fixup. You just save and close. To make this the default behaviour, set it once:

git config --global rebase.autosquash true

The Golden Rule: Never Rebase Shared Commits

Rebase does not modify commits -- it replaces them with new ones. If teammates have already pulled the old commits and based work on them, rewriting those commits creates two divergent histories, and the next merge becomes a mess of duplicates and conflicts.

The rule is simple: only rebase commits that have not been pushed, or that live on a branch only you work on. Cleaning up your own feature branch before opening a pull request is the textbook safe case. If you have already pushed the branch and you are still its only author, push the rewritten history with git push --force-with-lease, which refuses to overwrite work someone else pushed in the meantime. Never rewrite main or any branch your team pulls from.

When It Goes Wrong

Mid-rebase, two things can happen. If a replayed commit conflicts with an earlier change, Git pauses and asks you to resolve the conflict, then git add the files and run git rebase --continue. And if at any point you feel lost, there is a full eject button:

git rebase --abort

This returns the branch to the exact state it was in before you started, no questions asked. As long as you remember --abort exists, an in-progress rebase can never trap you.

What if the rebase finished and you realise the result is wrong? Even then, nothing is lost. The old commits still exist -- Git keeps them for weeks, and the reflog records every position your branch has ever pointed to, so you can move back to the pre-rebase state with a single reset. We cover that safety net in detail in recovering lost commits with git reflog. And if your mistake is smaller -- just the last commit needs changing -- you do not need a rebase at all: see how to undo a Git commit.

Interactive Rebase in GitSquid

The todo list is powerful, but editing verbs in a text file is exactly the part that makes people nervous. GitSquid replaces it with a visual interactive rebase: you reorder commits by drag & drop, and choose the action for each commit -- pick, squash, fixup, reword or drop -- from a dropdown picker. The whole plan is visible at a glance before anything runs, which removes most of the fear factor.

GitSquid also helps before you even start: the Conflict Predictor, included in the free tier, shows which files will conflict before you launch a rebase, so you know what you are signing up for instead of discovering conflicts halfway through. Rebase is available straight from the branch and commit context menus on the commit graph.

Quick Reference

Command / verb What it does
git rebase -i HEAD~n Interactively rewrite the last n commits
pick Keep the commit as is
reword Keep the changes, edit the message
edit Pause at this commit to amend or split it
squash Meld into the previous commit, combine messages
fixup Meld into the previous commit, discard this message
drop Delete the commit entirely
git commit --fixup <hash> Record a fix destined for an earlier commit
git rebase -i --autosquash Auto-place fixup! commits in the todo list
git rebase --continue Resume after resolving a conflict
git rebase --abort Cancel and restore the pre-rebase state

Interactive rebase rewards a little practice with a lot of power. Start on a throwaway branch, keep --abort in mind, respect the golden rule, and turning "wip, fix typo, actually fix it" into a clean history becomes routine. And if you would rather see the plan instead of editing a text file, Download GitSquid Free and try the visual rebase on your next feature branch.