History and Rewriting
Topic 17

Interactive Rebase

History

git rebase -i replays a range of commits one at a time through an editable to-do list. Between "this is the messy series of commits I actually made" and "this is the clean history I want to share," interactive rebase is the tool that lets you reorder, combine, drop, reword, or pause on each commit before anyone else sees it.

The power comes with one hard boundary: every commit you touch gets a new hash, so this is strictly a tool for local history. Run it on commits others have already pulled and you force every teammate to recover from a base that no longer exists for them. Used on your own unshared work, it is the difference between a reviewable branch and an apologetic one.

The to-do list, applied oldest commit first
pick
reword
squash / fixup
drop

The To-Do List

When you run git rebase -i <base>, Git opens an editor with one line per commit and a verb in front of each: pick, reword, edit, squash, fixup, drop, plus exec and break for scripting and pausing. The list runs top-to-bottom, oldest commit first — the reverse of what git log shows you. Internally this is just a rebase: Git resets to the base and replays each line in order, applying whatever verb you chose.

Because the order on screen is oldest-first, reordering means dragging the earlier commit up, not down. Getting this backwards is the single most common stumble for people who think in git log order.

Squash vs Fixup

Both squash and fixup fold a commit into the one above it. The difference is entirely about messages. squash opens an editor so you can combine both commit messages into one. fixup keeps the target commit's message and silently discards the squashed commit's message. Reach for fixup when the commit is an "oops, typo" with nothing worth keeping in its message, and squash when both messages carry information you want merged.

Reordering, Dropping, and Editing

Rearranging lines changes the apply order; deleting a line entirely drops that commit. Conflicts surface per replayed commit rather than all at once, which is why a long rebase can stop several times — each stop is one commit failing to apply cleanly. The edit verb stops the replay after applying a commit so you can amend its tree, change its author, or split it into several commits, then continue with git rebase --continue.

To split a fat commit, mark it edit, let the rebase stop on it, run git reset HEAD^ to un-commit it back into the working tree, then stage and commit it in pieces before continuing. Dropping a commit is equally direct, but watch for later commits that silently depended on the one you removed — the dependency surfaces as a conflict, not a warning.

The Autosquash Workflow

Rather than hand-sorting fixups, mark them as you go with git commit --fixup=<sha>, which writes a commit whose message is fixup! <original subject>. Later, git rebase -i --autosquash reads those markers, moves each fixup directly beneath its target, and pre-marks it as a fixup line. --autosquash matches a fixup! line to its target by the original commit's subject, so it pairs them up without you touching the to-do list at all.

Recovery When It Goes Wrong

If a rebase goes sideways, git rebase --abort returns the branch to exactly where it started. Even after a rebase completes, the original tip is preserved in the reflog as ORIG_HEAD, so git reset --hard ORIG_HEAD undoes a finished rebase you regret. That safety net is why local rebasing is low-risk: the pre-rebase state is always recoverable until the reflog expires.

Squash vs Fixup

squash — folds the commit into the one above and opens an editor so you can merge both commit messages into a single combined message. Choose it when both commits' messages carry information worth keeping.

fixup — folds the commit in the same way but keeps the target's message and silently discards the fixup commit's message. Choose it for "oops, typo" commits whose message adds nothing.

Common Mistakes
  • Rebasing commits already pushed and shared, then force-pushing — every teammate who pulled the old base is forced into a painful recovery to reconcile the rewritten commits.
  • Hitting a conflict and running git rebase --continue without staging the resolution, which commits the file with conflict markers still in it.
  • Forgetting the to-do list is oldest-first and reordering commits in the wrong direction, producing an apply order you did not intend.
  • Using --autosquash without setting rebase.autosquash=true or passing the flag, so fixup! commits land as ordinary commits instead of being folded.
  • Dropping a commit by deleting its line without noticing that a later commit depended on it, turning the removal into a mid-rebase conflict.
Best Practices
  • Mark cleanup commits with git commit --fixup=<sha> as you work, then collapse them in one pass with git rebase -i --autosquash.
  • Set git config --global rebase.autosquash true so autosquash is implicit on every interactive rebase.
  • Abort cleanly with git rebase --abort the moment a replay goes wrong, rather than trying to hand-unwind a half-finished rebase.
  • Split a fat commit by marking it edit, running git reset HEAD^, and re-staging the change in coherent pieces.
  • Verify every rewritten commit still builds with git rebase -i --exec "make test", which runs the command after each commit.
Comparable toolsMercurial hg histedit is a direct analog, plus hg rebase and the evolve extension for safe mutationSubversion no equivalent — history is immutable once committedPerforce no client-side interactive rewriteFossil deliberately forbids history rewriting by design

Knowledge Check

Why is rebasing local history fine but rebasing shared history destructive?

  • Rebase rewrites commits with new hashes; on shared commits, teammates who pulled the old base must recover, while local commits affect no one
  • Rebase deletes and rewrites the working tree files on disk, which only starts to cause problems once other people already have their own copy of that same tree
  • Shared commits are locked by the server once pushed, so Git cannot replay or rewrite them at all without admin rights
  • Local rebases skip conflict resolution entirely and fast-forward the branch, which is the reason they are considered safe

How does fixup differ from squash in an interactive rebase?

  • squash opens an editor to combine both messages; fixup keeps the target's message and discards the folded commit's message
  • fixup keeps the two commits separate in the to-do list while squash is the verb that actually merges them together
  • fixup moves the commit earlier in the to-do order while squash leaves the order alone and only rewrites the message
  • They behave identically and produce the same combined commit; the two names are simply interchangeable aliases for one verb

Why do conflicts appear per-commit during a rebase but only once during a merge?

  • Rebase replays each commit individually onto the new base, so any commit can conflict; a merge computes one combined result against the common ancestor
  • Merge always resolves every overlapping change automatically by preferring the incoming side, so conflicts never surface to you
  • Rebase conflicts are cosmetic markers that Git cleans up on its own, so you can run --continue without resolving anything
  • A merge replays each commit one by one onto the target branch, while a rebase instead batches all of them into a single combined three-way application

What does ORIG_HEAD let you do after a rebase completes?

  • Reset back to the pre-rebase tip with git reset --hard ORIG_HEAD, undoing the finished rebase
  • Replay the whole rebase again from scratch onto a different base branch in one step
  • Push the rewritten commits to the remote with a plain push, since the old tip is preserved
  • Recover uncommitted working-tree edits that were lost when the rebase checked out each commit

You got correct