Git Rebase and Interactive Rebase: Rewriting History Safely
Master git rebase and interactive rebase — squashing, splitting, rewriting commits, and understanding when to rebase versus when to avoid it.
Introduction
Git rebase is one of the most powerful — and most dangerous — commands in your toolkit. It rewrites commit history by replaying your commits on top of a different base. Unlike merge, which preserves history as it happened, rebase creates a cleaner, linear narrative.
Interactive rebase takes this further, letting you edit, reorder, squash, split, and drop commits before they’re replayed. It’s the tool that transforms a messy series of “WIP” and “fix typo” commits into a polished, professional history.
The golden rule of rebase: never rebase commits that have been pushed to a shared branch. Rewriting shared history forces every collaborator to reconcile their copies, causing confusion and potential data loss. Use rebase freely on local branches; use merge for shared work.
When to Use / When Not to Use
When to Use Rebase
- Keeping feature branches current — rebase onto main instead of merging main into your branch
- Cleaning up commit history — squash WIP commits before opening a pull request
- Reordering commits — arrange commits in logical order for easier review
- Splitting large commits — break monolithic commits into focused, reviewable units
- Fixing commit messages — correct typos or add context to past commits
When Not to Use Rebase
- Shared/public branches — rewriting pushed history disrupts every collaborator
- After pushing to main — never rebase the main branch
- When history preservation matters — if you need an accurate audit trail, use merge
- During active collaboration on the same branch — coordinate with your team first
- Binary file changes — rebasing binary conflicts is painful and error-prone
Core Concepts
Rebase moves your commits to a new base. Instead of creating a merge commit, it replays each commit one by one on top of the target.
Before rebase:
main: A ── B ── C
\
feature: D ── E
After git rebase main:
main: A ── B ── C
\
feature: D' ── E'
Note that D’ and E’ are new commits with different SHAs. The original D and E still exist but are no longer referenced by the feature branch.
graph TD
Start["Feature branch: D-E"] --> Pull["git fetch origin"]
Pull --> Rebase["git rebase origin/main"]
Rebase --> Replay["Replay each commit\nonto new base"]
Replay --> Conflict{"Conflict?"}
Conflict -->|Yes| Resolve["Resolve, git add,\ngit rebase --continue"]
Conflict -->|No| Next{"More commits?"}
Resolve --> Next
Next -->|Yes| Replay
Next -->|No| Done["Linear history:\nA-B-C-D'-E'"]
Architecture or Flow Diagram
flowchart TD
A["Interactive Rebase:\ngit rebase -i HEAD~4"] --> B["Editor opens with todo list"]
B --> C{"Choose action per commit"}
C -->|pick| D["Keep commit as-is"]
C -->|reword| E["Keep changes, edit message"]
C -->|squash| F["Combine with previous commit"]
C -->|fixup| G["Combine, discard message"]
C -->|edit| H["Stop for manual edits"]
C -->|drop| I["Remove commit entirely"]
D --> J["Git replays commits\nin order"]
E --> J
F --> J
G --> J
H --> K["Make changes,\ngit commit --amend,\ngit rebase --continue"]
K --> J
I --> J
J --> L{"Conflicts?"}
L -->|Yes| M["Resolve, add, continue"]
L -->|No| N["Clean linear history"]
M --> J
Step-by-Step Guide / Deep Dive
Basic Rebase
# Rebase current branch onto main
git switch feature-x
git rebase main
# Rebase onto a specific commit
git rebase abc1234
# Rebase onto remote branch
git rebase origin/main
# Rebase with automatic stashing (saves uncommitted work)
git rebase --autostash main
Interactive Rebase
# Interactively edit the last 4 commits
git rebase -i HEAD~4
# Interactively edit all commits since branching from main
git rebase -i main
# Editor opens with a todo list like:
# pick abc1234 Add user model
# pick def5678 Fix validation bug
# pick ghi9012 WIP: add tests
# pick jkl3456 Fix typo
Interactive Rebase Commands
| Command | Shortcut | Effect |
|---|---|---|
pick | p | Use commit as-is |
reword | r | Use commit, but edit the message |
edit | e | Stop for amending the commit |
squash | s | Combine with previous commit, keep both messages |
fixup | f | Combine with previous commit, discard this message |
drop | d | Remove the commit entirely |
break | b | Pause rebase (you decide what to do) |
exec | x | Run a shell command after this commit |
label | l | Label current HEAD with a name |
reset | t | Reset HEAD to a label |
Squashing Commits
# Before (in interactive rebase editor):
pick abc1234 Implement login
pick def5678 Fix typo
pick ghi9012 Add tests
pick jkl3456 Fix test
# After (squash fixups into the main commit):
pick abc1234 Implement login
fixup def5678 Fix typo
fixup ghi9012 Add tests
fixup jkl3456 Fix test
# Result: single commit "Implement login" with all changes combined
Splitting a Commit
# In interactive rebase, mark the commit for editing:
edit abc1234 Large monolithic commit
# When rebase stops:
git reset HEAD~1 # Unstage the commit, keep changes
git add -p # Interactively stage first part
git commit -m "First logical change"
git add -p # Stage second part
git commit -m "Second logical change"
git rebase --continue # Continue the rebase
Rewriting Commit Messages
# Change the last commit message
git commit --amend -m "New, better message"
# Change older commit messages via interactive rebase
git rebase -i HEAD~5
# Change 'pick' to 'reword' for commits you want to edit
Production Failure Scenarios + Mitigations
| Scenario | Impact | Mitigation |
|---|---|---|
| Rebasing pushed commits | Team members have divergent history | Never rebase shared branches; use merge instead |
| Force push after rebase | Overwrites remote history | Use --force-with-lease instead of --force |
| Losing commits during rebase | Work appears to vanish | Use git reflog to find and recover lost commits |
| Rebase conflict cascade | Multiple conflicts in sequence | Consider merge; or resolve carefully one at a time |
| Accidentally dropping commits | Permanent data loss | Review the todo list carefully before saving |
Recovery After Bad Rebase
# If rebase is in progress
git rebase --abort
# If rebase completed but result is wrong
git reflog
# Find the pre-rebase HEAD position
git reset --hard ORIG_HEAD
# If you already force-pushed
# Team members should reset to the correct remote
git fetch origin
git reset --hard origin/main
Trade-offs
| Approach | Pros | Cons |
|---|---|---|
| Rebase | Clean linear history, easier bisect | Rewrites history, dangerous on shared branches |
| Merge | Preserves true history, safe for shared | Creates merge commits, harder to follow |
| Squash | Single clean commit | Loses individual commit context |
| Fixup | Clean history, no extra messages | Hides the fact that fixes were needed |
| Edit commits | Perfect commit granularity | Time-consuming, requires careful planning |
| Drop commits | Removes mistakes permanently | Loses work; use reflog to recover |
Implementation Snippets
# Complete workflow: clean up feature branch before PR
git fetch origin
git rebase origin/main # Get current with main
git rebase -i HEAD~8 # Clean up commit history
# squash fixups, reword messages, drop WIP commits
git push --force-with-lease # Update remote safely
# Auto-squash all fixup commits
git rebase -i --autosquash HEAD~10
# Rebase with exec to run tests after each commit
git rebase -i -x "npm test" HEAD~5
# Safe force push (won't overwrite others' work)
git push --force-with-lease origin feature-x
# Rebase only unpushed commits
git rebase -i @{upstream}
Observability Checklist
- Logs: Record rebase operations in CI logs for audit trails
- Metrics: Track rebase frequency vs merge frequency per team
- Alerts: Alert on force pushes to protected branches
- Traces: Link rebased commits to original PR numbers
- Dashboards: Display commit hygiene scores (squash rate, message quality)
Security/Compliance Notes
- Rewriting history can break audit trails — use merge in regulated environments
- Force pushes should be blocked on protected branches via platform settings
- Signed commits retain their signatures through rebase only if
git rebase --signoffis used - Document rebase policies in team contributing guidelines
- Never rebase branches containing security patches that have been audited at specific commits
Common Pitfalls / Anti-Patterns
- Rebasing after pushing — the cardinal sin of Git; creates divergence for everyone
- Squashing everything — losing all commit granularity makes bisecting impossible
- Forgetting to rebase before PR — reviewers see stale code and outdated conflicts
- Using
--forceinstead of--force-with-lease— can overwrite teammates’ pushes - Dropping commits accidentally — always review the interactive rebase todo list
- Rebasing merge commits — creates duplicated history; merge commits should stay merged
Quick Recap Checklist
- Rebase moves commits to a new base, creating new commit SHAs
- Use
git rebase -ito squash, split, reorder, and edit commits - Never rebase commits that have been pushed to shared branches
- Use
--force-with-leaseinstead of--forcewhen updating remote - Recover lost commits with
git reflog - Abort a bad rebase with
git rebase --abort - Use
--autosquashto automatically combine fixup commits - Run tests after rebase to verify nothing broke
Interview Q&A
git merge and git rebase?git merge creates a new merge commit that combines two branches, preserving the true history of when work happened. git rebase replays commits onto a new base, creating new commits with a linear history. Merge is non-destructive; rebase rewrites history.
squash and fixup in interactive rebase?Both combine a commit with the previous one. squash keeps both commit messages and opens an editor to combine them. fixup discards the squashed commit's message entirely, keeping only the previous commit's message. Use fixup for typo fixes and squash when you want to preserve context.
Rebase creates new commit SHAs for every rebased commit. If others have based work on the original commits, their history diverges from yours. They must manually reconcile the difference, which can cause lost work, duplicated commits, and team confusion.
Use git rebase -i and mark the commit as edit. When the rebase stops, run git reset HEAD~1 to unstage the commit while keeping changes in the working directory. Then use git add -p to selectively stage hunks for the first commit, commit it, repeat for subsequent commits, then git rebase --continue.
git push --force-with-lease do and why is it safer than --force?--force-with-lease only force-pushes if the remote branch is exactly where you expect it (matching your last fetch). If someone else pushed new commits since your fetch, it refuses to push, preventing you from accidentally overwriting their work. --force overwrites without checking.
Rebase Architecture: Commit History Linearization
Before Rebase
graph TD
A1["A"] --> B1["B"]
B1 --> C1["C"]
A1 --> D1["D"]
D1 --> E1["E"]
C1 -. "main" .-> C1
E1 -. "feature" .-> E1
classDef commit fill:#16213e,color:#00fff9
class A1,B1,C1,D1,E1 commit
After git rebase main
graph TD
A2["A"] --> B2["B"]
B2 --> C2["C"]
C2 --> D2["D' (new SHA)"]
D2 --> E2["E' (new SHA)"]
C2 -. "main" .-> C2
E2 -. "feature" .-> E2
classDef commit fill:#16213e,color:#00fff9
class A2,B2,C2,D2,E2 commit
Key insight: D’ and E’ are entirely new commits with different SHAs. The original D and E become unreachable from the feature branch (though recoverable via reflog). This is why rebasing shared branches is destructive — other developers’ copies of D and E no longer match.
Production Failure: Rebasing Shared Branches
Scenario: A developer rebases the develop branch after it has been pushed and pulled by 5 team members. The rebase rewrites all commit SHAs. Each team member now has a divergent history. When they push, Git rejects their commits as “non-fast-forward.” When they force-push to fix it, they overwrite each other’s work.
Impact: Team-wide history divergence, lost commits, hours of manual recovery, broken CI pipelines, and eroded trust in Git workflows.
Mitigation:
- Never rebase branches that others have pulled
- Protect shared branches (
main,develop) with force-push restrictions - Use merge (not rebase) for integrating shared branches
- Communicate rebase plans if absolutely necessary — coordinate with all team members
- Use
git reflogto recover lost commits after accidental rebases
# Block force pushes on protected branches (GitHub)
gh api repos/{owner}/{repo}/branches/main/protection \
--method PUT \
--field enforce_admins=true \
--field required_pull_request_reviews='{"required_approving_review_count":1}'
# Recover from accidental shared rebase
git reflog
git reset --hard ORIG_HEAD # restore pre-rebase state
git fetch origin
git reset --hard origin/develop # align with remote
Trade-offs: Rebase vs Merge
| Dimension | Rebase | Merge |
|---|---|---|
| History cleanliness | Linear, easy to read | Non-linear, shows true integration points |
| Collaboration cost | High — requires coordination on shared branches | Low — safe for any branch |
| Recovery difficulty | Hard — rewritten SHAs, reflog needed | Easy — revert the merge commit |
| Bisect friendliness | Excellent — single path | Good — but merge commits add noise |
| Audit trail | Rewritten — original context lost | Preserved — records when/what was integrated |
| Conflict resolution | Per commit — may resolve same conflict multiple times | Once — all conflicts resolved together |
| Team size impact | Degrades with team size | Scales well to large teams |
| Best use case | Local feature branches before PR | Shared branches, release integration |
Security/Compliance: Why Rebasing Public Branches Breaks Audit Trails
In regulated environments (finance, healthcare, government), commit history serves as an audit trail. Rebasing destroys this trail:
- Original commit timestamps are preserved, but committer dates change to the rebase time
- Commit SHAs change, breaking links to CI runs, code reviews, and deployment records
- Signed commits may lose their signatures during rebase unless
--signoffis used - Blame history is disrupted —
git blameshows the rebase author instead of the original author - Compliance requirements (SOC 2, HIPAA, SOX) often mandate immutable change records
# Preserve signoff during rebase (but not GPG signatures)
git rebase --signoff main
# Verify commit signatures after rebase
git log --show-signature
# Check if any commits lost their signatures
git log --format="%H %G?" | grep -v "^.* [GU]"
Best practice: Use merge (not rebase) for any branch that feeds into production deployments in regulated environments. Document the merge strategy in your compliance policy.
Resources
Category
Related Posts
Rebase vs Merge: When to Use Each in Git
Decision framework for choosing between git rebase and git merge. Understand trade-offs, team conventions, history implications, and production best practices.
Centralized vs Distributed VCS: Architecture, Trade-offs, and When to Use Each
Compare centralized (SVN, CVS) vs distributed (Git, Mercurial) version control systems — their architectures, trade-offs, and when to use each approach.
Automated Changelog Generation: From Commit History to Release Notes
Build automated changelog pipelines from git commit history using conventional commits, conventional-changelog, and semantic-release. Learn parsing, templating, and production patterns.