Interactive Rebase

Interactive rebase (git rebase -i) lets you rewrite a stretch of your commit history before anyone else sees it: reorder commits, combine several into one, edit commit messages, or drop commits entirely. Instead of Git silently replaying your commits onto a new base, interactive mode pauses and hands you a checklist so you decide exactly what happens to each commit. It’s the tool that turns a messy string of “wip”, “fix typo”, “actually fix it” commits into a clean, readable history before you open a pull request.

Overview: How Interactive Rebase Works

To understand interactive rebase you need to remember what a commit actually is. Every commit object stores a pointer to a tree (a snapshot of the whole project at that point), a pointer to its parent commit(s), an author, a committer, and a message. A branch like main is nothing more than a movable pointer that holds the SHA-1 (or SHA-256, on newer repos) of the latest commit on that line of work. HEAD normally points at a branch, which in turn points at a commit.

A normal git rebase takes the commits unique to your branch, temporarily removes them, moves your branch pointer to a new base commit, and then re-applies each of those commits one at a time as new commits on top of that base. Because each new commit has a different parent, its content hash changes too — rebasing always produces brand-new commit objects with new SHAs, even for commits whose diff didn’t change at all.

Interactive rebase does the exact same replay, but before it starts, Git opens your configured text editor with a “todo list”: one line per commit, oldest first, each prefixed with a command word (pick by default). You edit that list — changing commands, deleting lines, reordering lines — save and close the editor, and Git executes your instructions one line at a time. Internally, Git checks out the base commit into a detached HEAD state (you are not on any branch while the rebase is in progress), applies each todo-list step in order, and only moves your original branch pointer to the final result once every step succeeds. If a step fails — usually a merge conflict — the rebase pauses mid-way, leaving you in that detached state until you resolve the conflict and run git rebase --continue, or give up with git rebase --abort.

Syntax

git rebase -i <base>

<base> is the commit before the first one you want to touch — Git will show you every commit after it, in the todo list. Common forms:

  • HEAD~3 — the last 3 commits on the current branch.
  • main — every commit on your branch that isn’t on main yet (typical before opening a PR).
  • A commit SHA — rebase everything after that exact commit.
  • --root — rebase the entire history of the branch, including the very first commit.

Once the editor opens, each line is <command> <abbreviated-sha> <original message>. The commands you can use:

Command Shorthand Effect
pick p Keep the commit as-is
reword r Keep the commit’s changes, but stop to let you edit its message
edit e Keep the commit, but pause after applying it so you can amend it (change files, split it, etc.)
squash s Combine this commit into the previous one, and let you write a new combined message
fixup f Like squash, but silently discard this commit’s message and keep the previous one
drop d Remove the commit entirely (deleting the line does the same thing)
exec x Run a shell command at this point in the rebase (e.g. run tests)

Useful flags: --autosquash automatically reorders fixup!/squash! commits (see the example below); --abort cancels the rebase and restores your branch to how it was; --continue resumes after you’ve resolved a conflict or amended a commit during edit; --skip skips the commit that’s currently causing a conflict.

Examples

Example 1: Squashing three commits into one

git log --oneline -4
git rebase -i HEAD~3

Output:

a1b2c3d Fix typo in login error message
9f8e7d6 Add password validation
7c6b5a4 Add login form markup
5d4c3b2 Update README with setup instructions

The first command shows the last four commits. The second opens your editor with the last three (everything after 5d4c3b2) listed oldest-first:

pick 7c6b5a4 Add login form markup
pick 9f8e7d6 Add password validation
pick a1b2c3d Fix typo in login error message

You change the file to combine all three into one commit by marking the second and third as squash:

pick 7c6b5a4 Add login form markup
squash 9f8e7d6 Add password validation
squash a1b2c3d Fix typo in login error message

After saving, Git applies the first commit, then merges the next two into it, then opens a second editor screen containing all three original messages concatenated so you can write one clean message, such as feat: add login form with password validation. When it’s done, git log --oneline -1 shows a single new commit with a brand-new SHA — the three originals no longer exist on this branch.

Example 2: Reordering and rewording commits

git rebase -i HEAD~3

Suppose the todo list opens as:

pick 7c6b5a4 Add login form markup
pick 9f8e7d6 wip password stuff
pick a1b2c3d Fix typo in login error message

You want the validation commit’s message cleaned up, and you’d rather the typo fix come right after the markup commit. Edit the file to:

pick 7c6b5a4 Add login form markup
pick a1b2c3d Fix typo in login error message
reword 9f8e7d6 wip password stuff

Git replays the commits in this new order — markup, then the typo fix, then the validation commit — and because you marked the last one reword, it stops and opens your editor just for that commit’s message, letting you replace wip password stuff with something like feat: validate password strength on submit. Reordering like this works because each commit is just a patch being reapplied in sequence; Git doesn’t care what order you feed it, as long as the patches still apply cleanly.

Example 3: Using –autosquash with fixup commits

If you realize a bug in an already-committed change while working on something else, you can mark the fix for automatic squashing later instead of rebasing immediately:

git add src/auth/login.js
git commit --fixup 7c6b5a4

Output:

[feature/login-page 4e5f6a7] fixup! Add login form markup
 1 file changed, 3 insertions(+), 1 deletion(-)

Git creates a commit whose message is prefixed fixup! followed by the target commit’s original message. Later, run:

git rebase -i --autosquash 5d4c3b2

Git automatically reorders the todo list so the fixup! commit sits directly under 7c6b5a4 and marks it fixup for you — you don’t have to move any lines by hand. Saving the editor immediately merges the fix into the original commit and discards the throwaway message. This is the cleanest way to fix an earlier commit without breaking the flow of your current work.

How It Works Step by Step

When you run git rebase -i <base>, Git: (1) writes the todo list to a file (.git/rebase-merge/git-rebase-todo) and opens it in your editor; (2) once you save, checks out <base> into a detached HEAD; (3) processes each line top to bottom — for pick, it cherry-picks that commit’s diff onto the current HEAD, creating a new commit object with a new parent and therefore a new SHA; for squash/fixup, it applies the diff into the index without a separate commit, then folds it into the previous commit; for edit, it applies the commit and then pauses, leaving your changes staged and committed so far, waiting for you to amend; (4) if applying any step produces a conflict, Git stops immediately, leaves conflict markers in the affected files, and waits — you fix the files, git add them, and run git rebase --continue to resume from the next line; (5) once every line finishes, Git moves your original branch pointer to the tip of the newly built chain of commits and re-attaches HEAD to that branch. Nothing outside the branch tip changes — the old commits still exist in your repository’s object database and are reachable from the reflog until they’re eventually garbage-collected, which is why recovery is almost always possible if a rebase goes wrong.

Common Mistakes

Rebasing a branch other people have already pulled. Because every rebased commit gets a new SHA, anyone who already has the old commits now has history that has diverged from yours. Their next git pull creates a tangled mess or duplicate commits. The rule: only rebase commits that exist solely in your local, unpushed work, or commits on a branch you’re certain nobody else has based work on.

# Risky: rebasing a branch teammates already checked out
git switch main
git rebase -i HEAD~5
git push --force origin main

The fix, if you must rewrite a shared branch, is to coordinate with your team first, and to push with git push --force-with-lease instead of bare --force--force-with-lease refuses to push if the remote has commits you haven’t fetched yet, which catches the case where someone else pushed in the meantime.

Accidentally dropping a commit. Deleting a line from the todo list has the exact same effect as marking it drop — the commit’s changes vanish from the branch. If you notice missing work after a rebase finishes, the commit isn’t gone forever:

git reflog
git branch recovery-branch a1b2c3d

git reflog lists every position HEAD has pointed to recently, including the pre-rebase tip; once you find the lost commit’s SHA there, git branch lets you point a new branch at it to recover the work.

Continuing a rebase with unresolved conflict markers still in the file. After a conflict, it’s easy to run git add and git rebase --continue too quickly, committing a file that still contains <<<<<<< / ======= / >>>>>>> markers. Always re-open and re-read the file, or run git diff --check, before staging and continuing.

Best Practices

  • Never interactively rebase commits that have already been pushed to a shared branch others build on — keep it to local, unshared work.
  • Rebase to clean up a feature branch before opening a pull request, not to rewrite the story of what actually happened during development in a way that hides real decisions.
  • Use git commit --fixup <sha> plus git rebase -i --autosquash instead of manually reordering the todo list — it’s faster and less error-prone.
  • Prefer git push --force-with-lease over bare --force after any history rewrite, so you don’t silently overwrite a teammate’s work.
  • Write commit messages during reword/squash in Conventional Commits style, e.g. feat: add password validation or fix: correct login redirect loop.
  • If a rebase goes badly wrong, remember git rebase --abort restores your branch exactly as it was before you started — use it without hesitation.
  • Keep the reflog in mind as a safety net, but don’t rely on it long-term — unreferenced commits are eventually garbage-collected.

Practice Exercises

  • Create a branch with four small commits, including one commit message that’s just “wip”. Use git rebase -i to squash the “wip” commit into the commit before it and give the result a proper Conventional Commits message.
  • Make a commit, then realize you forgot to include a change to a second file. Use git commit --fixup targeting that commit, then run git rebase -i --autosquash against the commit before it and confirm the history ends up with the fix folded in and no leftover fixup! commit.
  • Start a git rebase -i HEAD~3, mark the middle commit edit, and when it pauses, amend that commit to add an extra small change before running git rebase --continue. Verify with git log --oneline that only the middle commit’s SHA changed relative to before you started editing it (later commits will also have new SHAs, since they were rebuilt on top).

Summary

  • Interactive rebase (git rebase -i <base>) opens a todo list of commits after <base>, letting you pick, reword, edit, squash, fixup, drop, or reorder them.
  • Every rewritten commit gets a brand-new SHA because its parent (and therefore its content hash) changed, even if its diff is identical to before.
  • Git replays commits by checking out the base into a detached HEAD, applying each todo-list step in order, and moving your branch pointer to the result only once every step succeeds.
  • Conflicts pause the rebase; resolve the files, stage them, and run git rebase --continue, or bail out entirely with git rebase --abort.
  • Never rebase commits that others have already pulled — it rewrites history and diverges everyone’s copy; use git push --force-with-lease if you must force-push after a rewrite.
  • Lost commits from a rebase (dropped lines, botched squashes) are almost always recoverable through git reflog until garbage collection runs.