Interactive Rebase Mastery
A comprehensive guide to Git interactive rebase for editing, squashing, reordering, splitting, and cleaning up commits before sharing code.
Interactive Rebase Mastery
Interactive rebase is Git's editing suite for commit history. You can squash five WIP commits into one clean commit, reword a misleading message, reorder commits for logical flow, split a commit that changed too many things, and drop commits you no longer need. All without losing any code.
I rebase before every merge to main. The feature branch is my draft — interactive rebase turns it into a polished final version. This guide covers every interactive rebase operation with real scenarios.
Prerequisites
- Git installed (v2.20+)
- Understanding of commits and branches
- Terminal access
- A feature branch with multiple commits to practice on
Starting an Interactive Rebase
# Rebase the last N commits
git rebase -i HEAD~5
# Rebase onto a branch point
git rebase -i main
# Rebase onto a specific commit
git rebase -i abc1234
Git opens your editor with a list of commits:
pick abc1234 WIP: start search feature
pick def5678 fix typo in search query
pick ghi9012 WIP: add search results page
pick jkl3456 fix tests
pick mno7890 add search API documentation
# Rebase abc1234..mno7890 onto xyz5678 (5 commands)
#
# Commands:
# p, pick = use commit
# r, reword = use commit, but edit the commit message
# e, edit = use commit, but stop for amending
# s, squash = use commit, but meld into previous commit
# f, fixup = like "squash", but discard this commit's log message
# x, exec = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop = remove commit
Change the pick keywords, save, and close the editor. Git replays the commits with your changes.
The Commands
Pick — Keep As-Is
pick abc1234 feat: add search feature
Keep the commit unchanged. The default for all commits.
Reword — Change the Message
reword abc1234 feat: add search feature
Git stops and opens your editor to change the commit message. The code stays the same.
# Before
pick abc1234 added search stuff
# Change to
reword abc1234 added search stuff
# Editor opens with the message — change it to:
# feat: add full-text search endpoint with pagination
Squash — Combine with Previous
pick abc1234 feat: add search endpoint
squash def5678 fix: handle empty search query
squash ghi9012 add search result pagination
All three commits become one. Git opens an editor with all three messages combined so you can write a single coherent message:
# This is a combination of 3 commits.
# This is the 1st commit message:
feat: add search endpoint
# This is the commit message #2:
fix: handle empty search query
# This is the commit message #3:
add search result pagination
# Edit this into a single message:
feat: add full-text search endpoint with pagination and empty query handling
Fixup — Squash Without Message
pick abc1234 feat: add search endpoint
fixup def5678 fix typo
fixup ghi9012 fix another typo
Like squash but discards the fixup commits' messages. The result keeps only the first commit's message. This is the most common cleanup operation.
Edit — Stop and Modify
edit abc1234 feat: add search and user profiles
Git stops after applying this commit. You can:
- Modify files and amend the commit
- Split the commit into multiple commits
- Add or remove changes
# Git stops at the commit
# Make your changes
git add modified-file.js
git commit --amend
# Or split into multiple commits
git reset HEAD~1
git add src/search.js
git commit -m "feat: add search endpoint"
git add src/profiles.js
git commit -m "feat: add user profiles page"
# Continue the rebase
git rebase --continue
Drop — Remove a Commit
drop abc1234 debug: add console.log everywhere
The commit is removed entirely. The changes from that commit are gone.
Exec — Run a Command
pick abc1234 feat: add search endpoint
exec npm test
pick def5678 feat: add pagination
exec npm test
Git runs the command after applying each commit. If the command fails (non-zero exit), the rebase stops so you can fix the issue. This verifies that each commit passes tests independently.
Break — Pause the Rebase
pick abc1234 feat: add search
break
pick def5678 feat: add pagination
Git stops at the break point. You can inspect the state, run tests, or make changes before continuing with git rebase --continue.
Common Scenarios
Cleaning Up Before Merge
Starting with messy history:
pick a1 WIP: start auth feature
pick a2 fix import
pick a3 more auth work
pick a4 fix tests
pick a5 add password hashing
pick a6 typo fix
pick a7 add auth middleware
pick a8 update test assertions
pick a9 add documentation
Clean it up:
pick a1 WIP: start auth feature
fixup a2 fix import
fixup a3 more auth work
fixup a4 fix tests
squash a5 add password hashing
fixup a6 typo fix
pick a7 add auth middleware
fixup a8 update test assertions
pick a9 add documentation
Result: Three clean commits instead of nine messy ones:
feat: add user authentication with password hashingfeat: add auth middleware for protected routesdocs: add authentication documentation
Reordering Commits
Move related commits together:
# Original order
pick a1 feat: add search endpoint
pick a2 fix: correct user validation
pick a3 feat: add search pagination
pick a4 test: add user validation tests
pick a5 test: add search tests
# Reordered — group by feature
pick a1 feat: add search endpoint
pick a3 feat: add search pagination
pick a5 test: add search tests
pick a2 fix: correct user validation
pick a4 test: add user validation tests
Warning: Reordering can cause conflicts if later commits depend on earlier ones. Git will stop and ask you to resolve conflicts if this happens.
Splitting a Commit
A commit did too many things:
edit abc1234 feat: add search and update user profiles and fix pagination
When Git stops:
# Undo the commit but keep the changes
git reset HEAD~1
# Commit each change separately
git add src/search.js src/searchService.js
git commit -m "feat: add search endpoint"
git add src/profiles.js
git commit -m "feat: update user profiles page"
git add src/pagination.js
git commit -m "fix: correct pagination offset calculation"
# Continue
git rebase --continue
Editing a Commit's Content
Fix a bug in an older commit:
edit abc1234 feat: add search endpoint
pick def5678 feat: add pagination
pick ghi9012 test: add search tests
When Git stops:
# Make the fix
vim src/search.js
# Amend the commit
git add src/search.js
git commit --amend --no-edit
# Continue — subsequent commits are replayed on top
git rebase --continue
Combining the First Two Commits
You cannot squash the first commit using normal interactive rebase. Use --root:
git rebase -i --root
pick a1 Initial commit
squash a2 Add project structure
pick a3 feat: add first feature
Autosquash with Fixup Commits
Create fixup commits that automatically squash during rebase:
# Make a change that fixes commit abc1234
git add src/search.js
git commit --fixup abc1234
# Git creates: "fixup! feat: add search endpoint"
# Later, rebase with autosquash
git rebase -i --autosquash main
Git automatically marks fixup commits with fixup and places them after their target:
pick abc1234 feat: add search endpoint
fixup xyz7890 fixup! feat: add search endpoint
pick def5678 feat: add pagination
Enable autosquash by default:
git config --global rebase.autoSquash true
Handling Rebase Conflicts
During Rebase
# Conflict occurs
# CONFLICT (content): Merge conflict in src/app.js
# Resolve the conflict
vim src/app.js # Fix conflict markers
# Mark as resolved
git add src/app.js
# Continue
git rebase --continue
Aborting
# Cancel the entire rebase — return to the original state
git rebase --abort
Skipping a Commit
# Skip the current commit (its changes are lost)
git rebase --skip
Safety and Recovery
The Reflog Safety Net
Every rebase is recoverable:
# View the reflog
git reflog
# abc1234 HEAD@{0}: rebase (finish): returning to refs/heads/feature
# def5678 HEAD@{1}: rebase (squash): feat: add search
# ghi9012 HEAD@{2}: rebase (start): checkout main
# jkl3456 HEAD@{3}: commit: WIP more search ← Before rebase
# Undo the entire rebase
git reset --hard HEAD@{3}
Creating a Backup Branch
# Before rebasing, create a backup
git branch backup/feature-search
# Rebase
git rebase -i main
# If something goes wrong
git reset --hard backup/feature-search
git branch -D backup/feature-search
Using Rerere
# Enable rerere to remember conflict resolutions
git config rerere.enabled true
# Now if the same conflict appears during rebase,
# Git resolves it automatically using the recorded resolution
Complete Working Example: Full Rebase Cleanup
#!/bin/bash
# Demonstrate interactive rebase cleanup workflow
# Create a demo repo with messy history
mkdir rebase-demo && cd rebase-demo
git init
# Initial commit
echo "# My App" > README.md
git add README.md
git commit -m "Initial commit"
# Create a feature branch with messy commits
git checkout -b feature/auth
echo "var jwt = require('jsonwebtoken');" > src/auth.js
mkdir -p src
git add -A
git commit -m "WIP start auth"
echo "// TODO: add validation" >> src/auth.js
git add -A
git commit -m "wip"
echo "var bcrypt = require('bcrypt');" >> src/auth.js
git add -A
git commit -m "add password hashing"
echo "typo fix" >> README.md
git add -A
git commit -m "fix typo"
echo "module.exports = { auth: true };" >> src/auth.js
git add -A
git commit -m "export auth module"
echo "var test = require('assert');" > tests/auth.test.js
mkdir -p tests
git add -A
git commit -m "add tests"
echo "// more tests" >> tests/auth.test.js
git add -A
git commit -m "fix test"
# Show messy log
echo "=== Before Rebase ==="
git log --oneline
# Interactive rebase (in a real workflow, you'd edit the todo list)
# For this demo, show what the cleaned result would look like
echo ""
echo "=== Rebase Plan ==="
echo "pick <hash> WIP start auth"
echo "fixup <hash> wip"
echo "fixup <hash> add password hashing"
echo "fixup <hash> export auth module"
echo "fixup <hash> fix typo"
echo "pick <hash> add tests"
echo "fixup <hash> fix test"
echo ""
echo "Result: 2 clean commits instead of 7 messy ones"
echo " 1. feat: add authentication with JWT and password hashing"
echo " 2. test: add authentication tests"
Common Issues and Troubleshooting
"Could not apply" error during rebase
A commit cannot be applied cleanly because it conflicts with the rewritten history:
Fix: Resolve the conflict markers in the affected files, git add them, and run git rebase --continue. If the conflict is too complex, use git rebase --abort to start over. Consider a different squash strategy.
Commits disappear after rebase
You dropped a commit accidentally or a squash consumed commits you wanted to keep:
Fix: Use git reflog to find the pre-rebase state and git reset --hard HEAD@{N} to restore it. Always check the rebase todo carefully before saving.
Rebase loop — same conflicts keep appearing
A reordered commit depends on code from a commit that now comes after it:
Fix: Reorder commits so dependencies come first. Or enable rerere (git config rerere.enabled true) to automatically resolve recurring conflicts.
"Cannot rebase: Your index contains uncommitted changes"
You have staged but uncommitted changes:
Fix: Commit or stash your changes before rebasing. git stash && git rebase -i main && git stash pop.
Interactive rebase on a branch pushed to remote
Rebasing changes commit hashes. If the branch was pushed, force-push is needed:
Fix: Use git push --force-with-lease after rebasing. This is safe for branches only you use. Never force-push shared branches (main, develop).
Best Practices
- Rebase before merging to main. Clean up WIP commits, typo fixes, and debug artifacts. The feature branch is your draft — main should get the polished version.
- Use fixup more than squash. Fixup silently merges into the previous commit. Squash opens an editor for message editing. For cleanup, fixup is faster.
- Use
--autosquashwith--fixupcommits. Creating fixup commits during development and autosquashing during rebase is the cleanest workflow. - Keep the reflog in mind. Every rebase is reversible through the reflog for at least 30 days. This should give you confidence to experiment.
- Create backup branches before complex rebases.
git branch backup/before-rebasetakes a second and provides instant rollback. - Use
exec npm testto verify each commit. Insertexeccommands between picks to ensure every commit in the cleaned history is independently valid. - Never rebase published commits on shared branches. Rebasing rewrites history. Other developers who based work on the original commits will have conflicts.
- Enable
rebase.autoSquashglobally. This makes--fixupcommits automatically position themselves during interactive rebase without manual reordering.