News
🌿 Git Tutorials Rewrite Git History Safely with Interactive Rebase

Rewrite Git History Safely with Interactive Rebase

Reorder, squash, edit, and drop commits on your feature branch without torching anyone else's work.

Interactive rebase is the tool for cleaning up your branch before you push it. It can also be the tool for destroying hours of your teammates' work if you use it on a branch someone else has pulled. This tutorial covers both sides.


The one safety rule

Never rewrite history on a branch other people have based work on. Private feature branches: rewrite freely. main, develop, or any shared integration branch: do not.

The reason: rebase does not edit commits, it creates new ones. If a teammate has already pulled the old commits, their history diverges from yours, and the next git pull turns into a merge-vs-force-push argument that no one wins.

With that out of the way —


Step 1 — Pick a base

Start an interactive rebase against the commit before the oldest one you want to touch. For a typical feature branch off main:

git fetch origin
git rebase -i origin/main

This loads every commit on your branch that is not yet on main into an editor.

Alternative: rebase against a relative ref — HEAD~5 means "the last 5 commits" — when you know the count:

git rebase -i HEAD~5

Step 2 — Read the todo list

Your editor opens with something like:

pick a1b2c3d add login form
pick e4f5g6h fix validation
pick i7j8k9l typo
pick m0n1o2p wire up API
pick q3r4s5t forgot to add file

Each line is one commit, oldest at the top (this is the opposite of git log). The left-most word is the action to take. Change the word to rewrite history.

Common actions:

  • pick — keep the commit as is
  • reword (r) — keep the commit, change the message
  • edit (e) — pause here, let me amend the commit
  • squash (s) — combine with the previous commit; keep both messages
  • fixup (f) — combine with the previous commit; discard this message
  • drop (d) — delete the commit entirely

Save and close the editor. Git executes the plan.


Step 3 — Squash small "oops" commits into the real one

This is the most common use. You have a commit that does the real work and three small follow-ups that fix typos or forgotten files. You want one clean commit.

Edit the todo list to:

pick a1b2c3d add login form
fixup e4f5g6h fix validation
fixup i7j8k9l typo
pick m0n1o2p wire up API
fixup q3r4s5t forgot to add file

Save. You are left with two commits: "add login form" (now including the fixes and the typo) and "wire up API" (now including the forgotten file). The three throwaway messages are gone.

fixup is almost always better than squash. The squashed message is never worth keeping and the combined message editor is an extra step.


Step 4 — Reorder commits

Sometimes you realize the order is wrong: a refactor should come before the feature commit that relies on it. Move the lines in the todo list:

pick m0n1o2p wire up API         ← was last, now first
pick a1b2c3d add login form      ← moved down

Save. Git replays them in the new order. If the reorder causes a conflict (likely, when commits touch the same file), git pauses and lets you resolve it exactly like a normal rebase:

# edit the conflicted files
git add .
git rebase --continue

If you decide mid-rebase that this was a bad idea:

git rebase --abort

That returns you to exactly where you started.


Step 5 — Edit a commit's contents, not just its message

Set the action to edit:

edit a1b2c3d add login form
pick e4f5g6h fix validation

Save. Git pauses after applying that commit. You can now:

# make changes
git add .
git commit --amend
git rebase --continue

Or split a big commit into two:

git reset HEAD^
git add path/to/first-piece.js
git commit -m "first piece"
git add path/to/second-piece.js
git commit -m "second piece"
git rebase --continue

The git reset HEAD^ undoes the commit but keeps the changes staged in the working tree, so you can recommit them in smaller pieces.


Step 6 — Push the rewritten branch

Because the commit hashes changed, a regular git push will be rejected. You need a force push:

git push --force-with-lease

--force-with-lease is the safer version of --force. It refuses to push if someone else has pushed to the branch in the meantime. That protects you from overwriting a teammate's commit that landed on your branch between your last pull and your rebase.

Never use plain git push --force on a branch anyone else might be looking at. The --with-lease flag is cheap insurance.


Step 7 — The undo button: git reflog

If you rebase and realize the result is worse than what you started with, your previous history is not lost. Git keeps a log of every HEAD position for about 90 days.

git reflog

Output looks like:

a1b2c3d HEAD@{0}: rebase -i (finish)
7d8e9f0 HEAD@{1}: rebase -i (start)
c4d5e6f HEAD@{2}: commit: wire up API
...

The state right before the rebase started is HEAD@{2} in this example (look for the rebase -i (start) line — the entry immediately before it is the pre-rebase state). Go back to it:

git reset --hard HEAD@{2}

Your branch is now exactly what it was before the rebase. There is a dedicated tutorial on reflog elsewhere in this section if you want to dig deeper — it is the single most underused git command.


Golden path for cleaning a feature branch before review

The workflow that becomes automatic after a few weeks:

git fetch origin
git rebase -i origin/main
# reorder, squash fixups, reword unclear messages
# save and close
git push --force-with-lease

A reviewer opens a pull request with five logical commits instead of twenty-three, each with a clear message, in an order that reads like a story. They will thank you — usually by merging faster.