Home DevOps Git Interactive Rebase: Rewrite History Without Losing Your Job
Intermediate 3 min · July 18, 2026
Git Rebasing: Interactive Rebase and Best Practices

Git Interactive Rebase: Rewrite History Without Losing Your Job

Master git interactive rebase with production patterns.

N
Naren Founder & Principal Engineer

20+ years shipping production infrastructure and CI/CD at scale. Everything here is grounded in real deployments.

Follow
Production
production tested
July 18, 2026
last updated
2,466
articles · all by Naren
Before you start⏱ 25 min
  • Basic git commands (commit, push, pull, log)
  • Understanding of branches and merge conflicts
  • Comfortable with the command line
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer

Use git rebase -i HEAD~N to rewrite the last N commits. In the editor, change 'pick' to 'squash', 'reword', 'edit', or 'drop'. Save and close. Git replays each commit according to your instructions.

✦ Definition~90s read
What is Git Rebasing?

Git interactive rebase (git rebase -i) lets you edit, squash, reorder, or drop commits before they become permanent. It's a surgical tool for cleaning up a messy local history before sharing it with your team.

Imagine you're building a model airplane.
Plain-English First

Imagine you're building a model airplane. You glued some parts in the wrong order, used too much glue, and left fingerprints. Interactive rebase is like being able to unglue those parts, clean them up, and reattach them in the right order — before you show the finished model to anyone. It's not time travel; it's careful disassembly and reassembly.

⚙ Browser compatibility
Latest versions — ✓ supported
ChromeFirefoxSafariEdge

Every developer has that moment: you look at your git log and see a disaster. 'Fix typo', 'WIP', 'actually fix it', 'merge conflict resolution attempt #4'. You'd never ship that mess, but somehow it's in your branch. Interactive rebase is your cleanup crew. It lets you rewrite local history into a coherent story before anyone else sees it. But here's the catch: rebase rewrites history. Do it wrong, and you'll force-push chaos onto your team. Do it right, and you'll be the dev who always has clean, reviewable PRs. This article will teach you the exact patterns I've used across dozens of production services — and the traps that have burned me at 3 AM.

Why Interactive Rebase Exists: The Problem of Ugly History

Your commit history is a story. When you're working alone, every 'oops' commit is a footnote only you need to read. But the moment you open a PR, that story becomes public. Reviewers don't want to wade through 'fix typo' and 'actually fix it' to understand your logic. They want a clean narrative: 'Add payment validation', 'Handle edge case for refunds', 'Update tests'. Interactive rebase is the editor that lets you rewrite that narrative before publishing. Without it, your PRs are harder to review, your git blame is noisier, and your team's history becomes a swamp.

ugly-history.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# io.thecodeforge — DevOps tutorial

# Simulate an ugly local history
git init ugly-history
cd ugly-history
echo "initial" > file.txt
git add file.txt && git commit -m "initial"
echo "feature start" >> file.txt
git commit -am "WIP: started feature"
echo "fix typo" >> file.txt
git commit -am "fix typo"
echo "actually working" >> file.txt
git commit -am "actually fix it"
echo "tests" >> file.txt
git commit -am "add tests"

# View the mess
git log --oneline
# Output:
# 4a3b2c1 add tests
# 9f8e7d6 actually fix it
# 1a2b3c4 fix typo
# 5e6f7a8 WIP: started feature
# 0d1e2f3 initial
Output
4a3b2c1 add tests
9f8e7d6 actually fix it
1a2b3c4 fix typo
5e6f7a8 WIP: started feature
0d1e2f3 initial
💡Senior Shortcut:
Before rebasing, always create a backup branch: git branch backup/my-feature. If the rebase goes sideways, you can git reset --hard backup/my-feature and try again.

The Anatomy of an Interactive Rebase: pick, squash, reword, edit, drop

When you run git rebase -i HEAD~N, git opens an editor with a list of the last N commits. Each line starts with a command: pick (use the commit as-is), squash (combine with the previous commit, merging messages), reword (change the commit message), edit (stop to amend the commit), or drop (delete the commit). The order of lines is the order commits will be replayed. Change the commands, save, and git replays them. This is the core mechanic. Everything else is pattern.

interactive-rebase-basics.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# io.thecodeforge — DevOps tutorial

# Continuing from the ugly-history example
cd ugly-history

# Rebase the last 4 commits (excluding 'initial')
git rebase -i HEAD~4

# In the editor, change the second 'pick' to 'squash' and third to 'squash':
# pick 5e6f7a8 WIP: started feature
# squash 1a2b3c4 fix typo
# squash 9f8e7d6 actually fix it
# pick 4a3b2c1 add tests

# Save and close. Git will prompt for a combined commit message.
# Enter: "Implement payment validation"

# Result: clean history
git log --oneline
# Output:
# b7c8d9e add tests
# a1b2c3d Implement payment validation
# 0d1e2f3 initial
Output
b7c8d9e add tests
a1b2c3d Implement payment validation
0d1e2f3 initial
⚠ Production Trap:
If you squash a commit that was already pushed to a remote shared branch, you're rewriting public history. Force-pushing will break every other developer who has pulled that branch. Only rebase commits that are purely local.

Squashing: Combining Multiple Commits into One

Squashing is the most common rebase operation. You have three commits: 'WIP', 'fix typo', 'actually fix it'. They should be one commit: 'Implement feature X'. Squash merges the changes and lets you write a single message. But there's a nuance: squash combines the commit into the one above it. If you want to squash the last three into the first, you need to reorder lines or use fixup (which discards the message). I use fixup when I don't care about the intermediate messages — just the code changes.

squash-vs-fixup.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# io.thecodeforge — DevOps tutorial

cd ugly-history

# Rebase last 4 commits
git rebase -i HEAD~4

# To squash all into the first, reorder and use fixup:
# pick 5e6f7a8 WIP: started feature
# fixup 1a2b3c4 fix typo
# fixup 9f8e7d6 actually fix it
# pick 4a3b2c1 add tests

# fixup is like squash but discards the commit message.
# Save and close. Git will only prompt for the first commit's message.

# Result:
git log --oneline
# Output:
# d4e5f6g add tests
# h1i2j3k WIP: started feature (now contains all changes)
# 0d1e2f3 initial
Output
d4e5f6g add tests
h1i2j3k WIP: started feature
0d1e2f3 initial
💡Senior Shortcut:
Use git commit --fixup=<commit> to mark a commit as a fixup for an earlier one. Then git rebase -i --autosquash automatically arranges them. Saves manual editing.

Rewording: Fixing Commit Messages Without Changing Code

Sometimes the code is perfect but the commit message is garbage. 'fix bug' tells you nothing six months later. Use reword to edit the message. Git will stop at that commit, open the editor, and let you type a new message. No code changes. This is safe even for pushed commits — as long as you haven't shared them. If you've already pushed, you're rewriting history, so coordinate.

reword-commit.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# io.thecodeforge — DevOps tutorial

git log --oneline -3
# Output:
# d4e5f6g add tests
# h1i2j3k WIP: started feature
# 0d1e2f3 initial

# Reword the middle commit
git rebase -i HEAD~2

# Change 'pick' to 'reword' on the line for h1i2j3k:
# reword h1i2j3k WIP: started feature
# pick d4e5f6g add tests

# Save and close. Git opens editor for new message.
# Enter: "Implement payment validation with edge cases"

# Result:
git log --oneline -3
# Output:
# d4e5f6g add tests
# i9j8k7l Implement payment validation with edge cases
# 0d1e2f3 initial
Output
d4e5f6g add tests
i9j8k7l Implement payment validation with edge cases
0d1e2f3 initial

Editing: Splitting a Commit into Multiple Commits

Sometimes a commit does too much: 'Add login and refactor database layer'. You want two clean commits. Use edit to stop at that commit, then git reset HEAD^ to unstage everything, then commit in pieces. This is advanced but invaluable for cleaning up a messy PR. I've used this to untangle commits that mixed business logic with config changes — a classic review nightmare.

edit-split-commit.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# io.thecodeforge — DevOps tutorial

# Assume we have a commit that added both login and db refactor
git log --oneline -1
# Output:
# a1b2c3d Add login and refactor database

# Start rebase to edit that commit
git rebase -i HEAD~1
# Change 'pick' to 'edit' on that line, save and close.

# Git stops at that commit. Now split it:
git reset HEAD^  # Unstage all files, keep changes working

# Stage and commit login part
git add login/
git commit -m "Add login functionality"

# Stage and commit db refactor
git add db/
git commit -m "Refactor database layer"

# Continue rebase
git rebase --continue

# Result: two commits instead of one
git log --oneline -3
# Output:
# b2c3d4e Refactor database layer
# c3d4e5f Add login functionality
# 0d1e2f3 initial
Output
b2c3d4e Refactor database layer
c3d4e5f Add login functionality
0d1e2f3 initial
⚠ The Classic Bug:

Dropping Commits: Removing Mistakes from History

You committed a debug log that prints secrets. Or you accidentally committed a large binary file. drop removes the commit entirely. But be careful: if later commits depend on changes in the dropped commit, you'll get merge conflicts. Git will stop and ask you to resolve. This is actually a safety net — it forces you to verify the drop is safe.

drop-commit.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# io.thecodeforge — DevOps tutorial

# Simulate a commit that added a debug log with secrets
echo "console.log('API_KEY: ' + process.env.API_KEY)" >> debug.js
git add debug.js && git commit -m "Add debug logging"

# Later commits that don't touch debug.js
echo "feature" >> app.js
git add app.js && git commit -m "Add feature"

# Now drop the debug commit
git rebase -i HEAD~2
# Change 'pick' to 'drop' on the debug commit line, save and close.

# Git replays the feature commit on top of the parent of the dropped commit.
# No conflicts because feature commit doesn't depend on debug.js.

git log --oneline
# Output:
# d4e5f6g Add feature
# 0d1e2f3 initial
Output
d4e5f6g Add feature
0d1e2f3 initial
🔥Interview Gold:
How do you remove a commit that introduced a security vulnerability? Answer: Use git rebase -i to drop it, then force-push to a new branch. But if the commit is already public, you must rotate any exposed secrets and consider the commit compromised.

Reordering Commits: Telling a Better Story

Sometimes commits are in the wrong order. Maybe you wrote tests before the implementation, but you want the implementation first in the log. In the rebase editor, simply move lines up or down. Git will replay commits in the new order. If there are dependencies, you'll get conflicts. This is a great way to force yourself to think about commit independence — each commit should compile and pass tests on its own.

reorder-commits.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# io.thecodeforge — DevOps tutorial

# Current order: tests first, then implementation
git log --oneline -3
# Output:
# a1b2c3d Add tests for payment
# b2c3d4e Implement payment logic
# 0d1e2f3 initial

# Reorder so implementation comes first
git rebase -i HEAD~2

# In editor, swap the lines:
# pick b2c3d4e Implement payment logic
# pick a1b2c3d Add tests for payment

# Save and close. Git replays in new order.
# No conflicts because tests don't depend on implementation (ideally).

git log --oneline -3
# Output:
# c3d4e5f Add tests for payment
# d4e5f6g Implement payment logic
# 0d1e2f3 initial
Output
c3d4e5f Add tests for payment
d4e5f6g Implement payment logic
0d1e2f3 initial
💡Senior Shortcut:
Before reordering, check if each commit compiles independently: git rebase -i --exec "make test" runs tests after each commit. If any fail, you need to fix dependencies.

Resolving Conflicts During Rebase: The Practical Guide

Conflicts during rebase are inevitable when you reorder, edit, or drop commits. Git stops at the conflicting commit and leaves conflict markers in the file. Fix them, git add, then git rebase --continue. The key difference from merge conflicts: each conflict is per-commit, so you resolve them one at a time. This is actually easier than resolving a giant merge conflict. Use git mergetool if you prefer a GUI. Never use git rebase --skip unless you're sure you want to drop that commit entirely.

resolve-rebase-conflict.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# io.thecodeforge — DevOps tutorial

# Simulate a conflict during reorder
git init conflict-demo
cd conflict-demo
echo "line1" > file.txt
git add file.txt && git commit -m "initial"
echo "line2" >> file.txt
git commit -am "add line2"
echo "line3" >> file.txt
git commit -am "add line3"

# Reorder commits (swap)
git rebase -i HEAD~2
# Swap lines, save. Git will try to apply 'add line3' before 'add line2'.
# Conflict: both add line2 and line3 to the same file.

# Git says: CONFLICT (content) in file.txt
# Open file.txt:
# <<<<<<< HEAD
# line1
# line3
# =======
# line1
# line2
# >>>>>>> add line2

# Fix: keep both lines in order:
# line1
# line2
# line3

nano file.txt  # edit to resolve
git add file.txt
git rebase --continue

# Git applies the next commit (now 'add line2') on top of the fixed state.
# No further conflicts.

git log --oneline
# Output:
# e5f6g7h add line2
# f6g7h8i add line3
# 0d1e2f3 initial
Output
e5f6g7h add line2
f6g7h8i add line3
0d1e2f3 initial
⚠ Never Do This:
Running git rebase --abort after resolving some conflicts discards all your resolution work. Only abort if you want to start over completely.

When Not to Rebase: The Golden Rule

Never rebase commits that have been pushed to a shared branch. Period. If you do, you'll force-push rewritten history, and everyone else's local branches will diverge. The result: merge commits, lost work, and angry teammates. The exception is your own feature branch that no one else has pulled. If you must rebase a shared branch (e.g., to clean up before merging), coordinate with the team, have everyone stash their work, and do it in a scheduled window. Better yet, use git merge --squash on the target branch instead.

🔥Production Trap:
I've seen a team lose a day of work because a dev rebased a shared branch and force-pushed. The fix: use git reflog on each developer's machine to find the old commit, then git reset --hard to that commit, then cherry-pick any new work. It's messy. Don't be that dev.

Interactive Rebase Workflow: A Production Pattern

Here's the workflow I use on every feature branch. Start with git checkout -b feature/payment-validation. Commit freely — WIP, fixups, whatever. Before pushing, run git rebase -i $(git merge-base HEAD master) to rebase all commits since branching from master. Squash fixups, reword messages, reorder for clarity. Then git push -u origin feature/payment-validation. Open PR. If master advances, rebase again: git pull --rebase origin master. Never merge master into your branch — that creates a merge commit that pollutes history. This keeps your PR linear and reviewable.

production-rebase-workflow.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# io.thecodeforge — DevOps tutorial

# Start feature branch
git checkout -b feature/payment-validation

# Work and commit freely
echo "validate card" > payment.js
git add payment.js && git commit -m "WIP: card validation"
echo "handle decline" >> payment.js
git commit -am "WIP: handle decline"
echo "refund logic" >> payment.js
git commit -am "WIP: refund"

# Before pushing, clean up history
git rebase -i $(git merge-base HEAD master)
# Squash all WIP into one commit, reword to "Add payment validation"

# Push
git push -u origin feature/payment-validation

# Later, master has new commits. Rebase instead of merge:
git pull --rebase origin master
# If conflicts, resolve them, then git rebase --continue

# Force-push (since you rebased, history changed)
git push --force-with-lease

# Open PR. Clean linear history.
💡Senior Shortcut:
Use git pull --rebase as default: git config --global pull.rebase true. This avoids accidental merge commits when pulling.

Recovering from a Botched Rebase: The reflog Safety Net

Even seniors mess up rebases. The reflog is your safety net. git reflog shows every action that moved HEAD — commits, rebases, resets. Find the commit before the rebase (look for 'rebase' entries or the last commit you recognize). Then git reset --hard <commit-hash> to go back. This works even if you've done multiple rebases. I've used this to recover branches that seemed completely destroyed. The reflog expires after 90 days by default, so act fast.

reflog-recovery.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# io.thecodeforge — DevOps tutorial

# After a bad rebase, check reflog
git reflog
# Output:
# a1b2c3d HEAD@{0}: rebase finished: returning to refs/heads/feature
# b2c3d4e HEAD@{1}: rebase: add tests
# c3d4e5f HEAD@{2}: rebase: implement payment
# d4e5f6g HEAD@{3}: commit: add tests
# e5f6g7h HEAD@{4}: commit: implement payment
# 0d1e2f3 HEAD@{5}: checkout: moving from master to feature

# The commit before rebase is e5f6g7h (HEAD@{4})
git reset --hard e5f6g7h
# Now you're back to the state before the rebase.

# Verify
git log --oneline -3
# Output:
# e5f6g7h implement payment
# d4e5f6g add tests
# 0d1e2f3 initial
Output
e5f6g7h implement payment
d4e5f6g add tests
0d1e2f3 initial
⚠ The Classic Bug:
If you've already force-pushed the bad rebase, your local reflog still has the old commits, but the remote doesn't. You can push the recovered branch with git push --force-with-lease, but warn your team first.
● Production incidentPOST-MORTEMseverity: high

The Force-Push That Killed a Friday Release

Symptom
CI pipeline failed with merge conflicts on a feature branch that had been clean for days.
Assumption
Someone merged master into the feature branch incorrectly.
Root cause
A junior dev ran git rebase master on a shared branch, then force-pushed. The rebase rewrote commits that another dev had based their work on. When the second dev pushed, git saw divergent histories and created a merge commit that broke the build.
Fix
We rolled back the branch to the pre-rebase state using git reflog on the junior's machine, then had both devs coordinate: one rebased, the other cherry-picked their commits onto the new base. Added a branch protection rule to disallow force-push on shared branches.
Key lesson
  • Never rebase a branch that more than one person has pushed to.
  • If you must, coordinate with the team and expect a messy reconciliation.
Production debug guideSystematic recovery paths for the failure modes engineers actually hit.3 entries
Symptom · 01
After rebase, branch has extra commits from other developers
Fix
1. Check git log --oneline to see if commits are duplicated. 2. Run git rebase --abort if still in progress. 3. Use git reflog to find the commit before rebase. 4. git reset --hard <pre-rebase-commit>. 5. Rebase again, but ensure you're rebasing onto the correct base (e.g., origin/master not master).
Symptom · 02
Force-push rejected because remote has new commits
Fix
1. Do NOT use --force. 2. Run git fetch origin. 3. Rebase your branch on the updated remote: git rebase origin/your-branch. 4. Resolve any conflicts. 5. Push again with --force-with-lease.
Symptom · 03
Rebase stopped with 'CONFLICT' but you want to skip the commit
Fix
1. Verify you want to drop the commit entirely. 2. Run git rebase --skip. 3. Be aware this removes the commit's changes permanently from this branch. 4. If unsure, abort with git rebase --abort.
★ Git Rebasing: Interactive Rebase and Best Practices Triage Cheat SheetFirst-response commands for when things go wrong — copy-paste ready.
Rebase in progress, want to undo everything
Immediate action
Check if you're in rebase state
Commands
git rebase --abort
git log --oneline -1
Fix now
Abort and return to original branch state.
Rebase finished but history is wrong+
Immediate action
Find the commit before rebase
Commands
git reflog
git reset --hard <hash>
Fix now
Reset to the commit before rebase.
Conflict markers in file, don't know how to resolve+
Immediate action
Open the file and look for <<<<<<<
Commands
git diff
git mergetool
Fix now
Edit file to remove markers and keep correct content, then git add and git rebase --continue.
Force-push rejected+
Immediate action
Fetch latest remote
Commands
git fetch origin
git rebase origin/your-branch
Fix now
Rebase on remote, then git push --force-with-lease.
OperationWhen to UseRisk Level
SquashCombine multiple related commits into oneLow (local only)
FixupCombine commits but discard messageLow (local only)
RewordFix a commit messageLow (local only)
EditSplit a commit or amend contentMedium (may cause conflicts)
DropRemove a commit entirelyHigh (may cause conflicts)
ReorderChange commit orderMedium (may cause conflicts)
⚙ Quick Reference
10 commands from this guide
FileCommand / CodePurpose
ugly-history.shgit init ugly-historyWhy Interactive Rebase Exists
interactive-rebase-basics.shcd ugly-historyThe Anatomy of an Interactive Rebase
squash-vs-fixup.shcd ugly-historySquashing
reword-commit.shgit log --oneline -3Rewording
edit-split-commit.shgit log --oneline -1Editing
drop-commit.shecho "console.log('API_KEY: ' + process.env.API_KEY)" >> debug.jsDropping Commits
reorder-commits.shgit log --oneline -3Reordering Commits
resolve-rebase-conflict.shgit init conflict-demoResolving Conflicts During Rebase
production-rebase-workflow.shgit checkout -b feature/payment-validationInteractive Rebase Workflow
reflog-recovery.shgit reflogRecovering from a Botched Rebase

Key takeaways

1
Interactive rebase is for local cleanup only. Never rebase commits that have been pushed to a shared branch.
2
Always create a backup branch before rebasing. git branch backup/my-feature is cheap insurance.
3
Use --force-with-lease instead of --force to avoid overwriting others' work.
4
The reflog is your safety net. If a rebase goes wrong, git reflog + git reset --hard can recover almost anything.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
How does interactive rebase handle merge conflicts differently than a me...
Q02SENIOR
When would you choose `git merge --squash` over interactive rebase for c...
Q03SENIOR
What happens if you run `git rebase --continue` after resolving conflict...
Q04JUNIOR
What is the difference between `git rebase -i HEAD~3` and `git rebase -i...
Q05SENIOR
A developer on your team force-pushed a rebased shared branch. How do yo...
Q06SENIOR
How would you design a CI check to enforce that PR branches have a clean...
Q01 of 06SENIOR

How does interactive rebase handle merge conflicts differently than a merge? Why is this useful?

ANSWER
During rebase, conflicts are resolved per commit, one at a time. This isolates each change, making it easier to understand and fix. In a merge, all conflicts appear at once, which can be overwhelming. Rebasing forces you to think about each commit's independence.
FAQ · 4 QUESTIONS

Frequently Asked Questions

01
How do I undo a git rebase that already finished?
02
What's the difference between squash and fixup in interactive rebase?
03
How do I abort a rebase that's in progress?
04
Can I rebase after pushing to a remote branch?
N
Naren Founder & Principal Engineer

20+ years shipping production infrastructure and CI/CD at scale. Everything here is grounded in real deployments.

Follow
Verified
production tested
July 18, 2026
last updated
2,466
articles · all by Naren
🔥

That's Git. Mark it forged?

3 min read · try the examples if you haven't

Previous
Introduction to Docker
48 / 51 · Git
Next
Git Stashing: Save and Restore Work-in-Progress