TL;DR
- Interactive rebase (
git rebase -i) lets you rewrite commit history: squash, reorder, edit, drop. - Use it to create clean, logical commits before merging to main.
- Never rebase public/shared branches — only your own feature branches.
git rebase --ontofor moving commits between branches.
Step 1: Why Rebase?
Rebase exists because real development is messy: you make WIP commits, fix typos in previous commits, realize commits should be reordered, or accumulate "oops" commits that clutter history. Interactive rebase lets you rewrite history before sharing it — squashing 15 messy commits into 3 logical ones, editing commit messages, dropping debugging commits, and reordering changes. Clean history isn't vanity; it makes git log, git bisect, and git revert actually useful for your team months later.
The Problem
Your feature branch has messy commits:
feat: add user login
fix: typo in login
wip: debugging
fix: actually fix the bug
feat: add logout button
fix: forgot to import
The Solution
Interactive rebase cleans it into logical, reviewable commits:
feat: add user authentication (login + logout)
Step 2: Basic Interactive Rebase
Interactive rebase opens your editor with a list of commits and commands you can apply to each one: pick (keep as-is), squash (merge into previous), reword (change message), edit (pause to amend), drop (remove), and reorder (just move lines). It replays commits one by one applying your instructions, creating new commits with new SHAs. This is the most powerful history-editing tool in Git and the one every developer should master for producing clean, reviewable pull requests.
# Rebase last N commits
git rebase -i HEAD~5
# Or rebase since branching from main
git rebase -i main
This opens your editor with a "todo" list:
pick a1b2c3d feat: add user login
pick d4e5f6g fix: typo in login
pick h7i8j9k wip: debugging
pick l0m1n2o fix: actually fix the bug
pick p3q4r5s feat: add logout button
Commands Available
| Command | Short | What it does |
|---|---|---|
pick |
p |
Keep commit as-is |
reword |
r |
Keep commit, edit message |
edit |
e |
Pause to amend the commit |
squash |
s |
Merge into previous commit (keep both messages) |
fixup |
f |
Merge into previous commit (discard this message) |
drop |
d |
Delete commit entirely |
exec |
x |
Run a shell command |
Step 3: Common Operations
These are the operations you'll use in 90% of interactive rebases. Squashing combines multiple commits into one (turning "WIP", "fix tests", "actually fix tests" into a single clean commit). Rewording fixes typos or adds ticket numbers to messages. Splitting breaks a large commit into logical pieces. Each operation is a building block for crafting history that tells a story: why changes were made, in what logical order, with each commit representing one complete idea.
Squash Multiple Commits
# Editor shows:
pick a1b2c3d feat: add user login
fixup d4e5f6g fix: typo in login ← merge into above, discard message
fixup h7i8j9k wip: debugging ← merge into above, discard message
fixup l0m1n2o fix: actually fix the bug ← merge into above, discard message
pick p3q4r5s feat: add logout button
# Result: 2 clean commits instead of 5 messy ones
Reorder Commits
Simply change the line order in the editor:
# Move logout before login fix
pick p3q4r5s feat: add logout button
pick a1b2c3d feat: add user login
Edit a Commit
edit a1b2c3d feat: add user login
pick p3q4r5s feat: add logout button
Git pauses at that commit. Make changes, then:
git add .
git commit --amend
git rebase --continue
Split a Commit
# Mark commit as "edit"
# When Git pauses:
git reset HEAD~1 # Undo the commit but keep changes
git add login.ts
git commit -m "feat: add login logic"
git add logout.ts
git commit -m "feat: add logout logic"
git rebase --continue
Step 4: Rebase Onto (Move Commits)
--onto handles the scenario where you branched from the wrong place or need to transplant commits to a different base. Common use case: you started a feature from another feature branch, that branch gets merged and deleted, and now your commits are based on a dead branch. --onto main replants your commits on top of the current main, as if you'd branched from main originally. It's also how you extract a subset of commits from a longer branch.
--onto moves commits from one base to another:
# Scenario: You branched from 'develop' but should have branched from 'main'
#
# Before:
# main: A---B---C
# develop: D---E
# feature: F---G---H (your commits)
git rebase --onto main develop feature
# After:
# main: A---B---C---F'---G'---H'
# develop: D---E
Common Use Case: Update base
# Feature branch is based on old main
git checkout feature
git rebase main
# Now feature is based on latest main
# If conflicts:
# Fix conflicts in files
git add .
git rebase --continue
# Repeat until done
# Abort if things go wrong
git rebase --abort
Step 5: Merge vs Rebase
The merge-vs-rebase debate is one of Git's most common discussions. Merge preserves exact history (including the messy parts) and is always safe for shared branches. Rebase creates linear history that's easier to read but rewrites commits (dangerous for branches others have pulled). The pragmatic answer: rebase your local/feature branches before merging to main (clean history), but never rebase commits that have been pushed to a shared branch. This gives you the best of both worlds.
| Aspect | Merge | Rebase |
|---|---|---|
| History | Preserves all branches & merge commits | Linear, clean history |
| Safety | Safe for shared branches | ⚠️ Rewrites history — never on shared branches |
| Conflicts | Resolved once in merge commit | May need resolution per-commit |
| Use case | Integrating shared branches | Cleaning up before merge |
The Golden Rule
✅ Rebase YOUR feature branch onto main (before PR)
❌ Never rebase main or any shared branch
Recommended Workflow
# 1. Work on feature branch with messy commits
git checkout -b feature/user-auth
# ... commit, commit, commit ...
# 2. Before PR: rebase onto latest main
git fetch origin
git rebase origin/main
# 3. Clean up commits
git rebase -i origin/main
# 4. Force push YOUR branch (only if already pushed)
git push --force-with-lease
# --force-with-lease is safer than --force (fails if someone else pushed)
Step 6: Handling Conflicts During Rebase
Rebase conflicts happen because each commit is replayed individually onto the new base — if commit 3 of 7 conflicts, you resolve it and continue, potentially hitting more conflicts in later commits. This is more work than a single merge conflict but produces cleaner results. Understanding the workflow (--continue, --skip, --abort) and knowing that you can always --abort to return to pre-rebase state makes conflicts manageable rather than scary.
# Conflict occurs:
git rebase -i main
# CONFLICT in auth.ts
# Step 1: See what's conflicting
git status
# Both modified: src/auth.ts
# Step 2: Fix the conflict in your editor
# Look for <<<<<<< HEAD and >>>>>>> markers
# Step 3: Mark as resolved
git add src/auth.ts
# Step 4: Continue
git rebase --continue
# If you want to skip this commit:
git rebase --skip
# If you want to abort everything:
git rebase --abort
Step 7: Autosquash (Commit Intent)
Autosquash lets you declare intent at commit time: "this commit should be squashed into commit X during the next rebase." By naming commits fixup! <original message> or squash! <original message>, Git automatically reorders and marks them during rebase -i --autosquash. This is perfect for code review workflows: reviewer requests changes, you make fixup commits addressing each comment, then autosquash before merging. The result is clean history without manual rebase editing.
Mark commits to be automatically squashed during rebase:
# Create a fixup commit (auto-squashes into target)
git commit --fixup=abc1234
# Create a squash commit (combines messages)
git commit --squash=abc1234
# When you rebase with --autosquash, they auto-arrange:
git rebase -i --autosquash main
Git Config for Always Autosquash
git config --global rebase.autosquash true
Interview Questions
-
What's the difference between merge and rebase?
- Merge creates a merge commit preserving branch history. Rebase replays commits on top of another branch, creating linear history. Rebase rewrites commits (new hashes).
-
When should you NOT rebase?
- Never rebase commits that have been pushed to a shared branch. Other developers' work would be based on the old commits, causing conflicts.
-
What's
--force-with-lease?- A safer
git push --force. It fails if the remote branch has commits you haven't fetched — preventing you from accidentally overwriting someone else's work.
- A safer
-
How do you squash commits?
git rebase -i HEAD~N, changepicktosquashorfixupfor commits you want to combine. Or use--fixupcommits with--autosquash.