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 those commands that rewards understanding but punishes carelessness. It takes your commits and replays them on top of a different base, creating a linear history instead of the branching mess that merge produces.
Interactive rebase (git rebase -i) goes further — you get to edit, reorder, squash, split, and drop commits before they’re replayed. It’s how you turn a messy series of “WIP” and “fix typo” commits into something worth showing to colleagues.
The catch: never rebase commits that have left your machine. Once others have pulled your work, rewriting that history means everyone has to reconcile divergent copies. Local branches, fine. Shared branches, don’t.
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
Reference: Basic Rebase
Reference: Common Interactive Rebase Operations
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 |
Common Interactive Rebase Operations
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
| 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-off Analysis
| 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 Considerations
- 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
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.
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 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 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.
Interview Questions
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.
git rebase --onto do and when would you use it?--onto rebases a range of commits onto a different branch, without requiring the source branch to be based on the target. The syntax is git rebase --onto <newbase> <upstream> <branch>. Use cases include: moving a feature branch that was branched from an old release to a new one, extracting commits from one branch to apply elsewhere, and correcting a mistaken branch base.
When a conflict occurs during rebase, Git marks the conflicting files. Resolve each conflict manually, then run git add <resolved-file> to stage the resolution. Do not run git commit. After staging all conflicts, continue the rebase with git rebase --continue. To abandon the entire rebase and return to the original state, use git rebase --abort.
git rebase -i and git rebase --autosquash?git rebase -i opens an editor for manual control over each commit (pick, squash, fixup, edit, drop, etc.). git rebase --autosquash automatically squashes commits prefixed with fixup! or squash! when combined with interactive rebase (git rebase -i --autosquash HEAD~n). --autosquash saves time by not requiring manual ordering of fixup commits in the todo list.
git merge over git rebase in a team workflow?Choose merge when: working on shared branches (main, develop), collaborating with multiple developers on the same branch, operating in regulated environments requiring audit trails, or when preserving the true history of integrations matters. Merge is safer and non-destructive but creates merge commits. Rebase is for local-only cleanup before sharing a feature branch.
git reflog help recover from a bad rebase?git reflog records every HEAD movement. After a bad rebase, run git reflog to find the commit hash before the rebase (shown as HEAD@{n} before the rebase operation). Then reset to that commit with git reset --hard HEAD@{n} or git reset --hard <hash>. Reflog entries are typically kept for 90 days by default.
git rebase --abort, --continue, and --skip?--abort cancels the rebase entirely and returns the branch to its pre-rebase state. --continue resumes the rebase after you have resolved conflicts and staged the changes. --skip abandons the current commit being applied and moves on to the next one — useful when a commit's changes are no longer needed or conflict resolution is impractical.
Technically yes (git rebase -p or manually), but it creates duplicated history and is not recommended. When you rebase a merge commit, Git replays the merged commits individually rather than preserving the merge as a single unit. This results in duplicate commits in the target branch. Merge commits should remain merges — rebasing them defeats the purpose of the merge and breaks history integrity.
git cherry-pick differ from git rebase for applying commits?Cherry-pick applies specific individual commits from one branch to another, creating new commits with new SHAs for each picked commit. Rebase replays a range of consecutive commits onto a new base, preserving their relative order and relationships. Cherry-pick is selective; rebase is wholesale. Both create new commits — neither should be used on shared history.
git rebase --signoff do, and when is it important?--signoff adds a Signed-off-by trailer to each rebased commit, similar to git commit --signoff. This is important in environments requiring Developer Certificate of Origin (DCO) compliance, such as Linux kernel development. Note that --signoff does not preserve GPG signatures — rebasing inherently changes commit SHAs, which breaks GPG verification.
git rebase?The upstream branch is the tracked reference branch (set via git branch --set-upstream-to or git config branch.<name>.remote). git rebase (without arguments) rebases the current branch against its upstream. The @{upstream} or @{u} shorthand refers to this branch. For example, git rebase -i @{upstream} rebases only unpushed commits.
Git automatically marks empty commits (no net changes) as droped during rebase. If two commits produce identical diffs, the second one becomes empty and is dropped. Commits that become invalid (e.g., due to conflict resolution making their changes redundant) are also silently dropped unless you explicitly prevent it. Use --keep-empty to preserve empty commits intentionally.
In CI/CD: rebasing creates new SHAs, so any pipeline triggered by commit SHA will run again for rebased commits. Force-pushing may trigger security alerts or require additional permissions. Pipelines relying on commit-based caching may need invalidation. Signed commits may break verification. Best practice: rebase locally before pushing, not after, to minimize pipeline noise and security concerns.
git rebase -x (exec) to validate each commit during rebase?git rebase -x "npm test" runs the specified command after applying each commit during rebase. If the command fails (non-zero exit), the rebase stops at that commit so you can fix the issue. This is useful for ensuring each commit in a series is independently valid — for example, running tests, linters, or build steps. Example: git rebase -i -x "npm test" HEAD~5.
A soft reset (git reset --soft) moves HEAD to the target commit but preserves changes in the index and working directory. A hard reset (git reset --hard) resets everything — HEAD, index, and working directory — to the target state. During rebase recovery, --hard is typically used with ORIG_HEAD or a reflog entry to completely restore the pre-rebase state.
git bisect work with rebased branches?git bisect performs a binary search through commit history to find a buggy commit. After rebase, the rebased commits have new SHAs but still contain the same changes, so bisect can still find bugs — it will identify the new commit hashes rather than the originals. Ensure you have tested both pre-rebase and post-rebase states when using bisect on a branch that was recently rebased.
Further Reading
- Git Rebase Documentation — Official Git documentation for the rebase command
- Atlassian: Merging vs. Rebasing — Comprehensive comparison of merge and rebase workflows
- Pro Git: Rewriting History — The interactive rebase chapter from the Pro Git book
- Think Like (a) Git: Rebase — Deep dive into how rebase works under the hood
- GitHub: About Git Rebase — GitHub’s guide to rebase best practices
Conclusion
Interactive rebase is the difference between a history that happened and a history that tells a story. Used responsibly on local and feature branches only, it’s one of Git’s most powerful cleanup tools. Squash, reorder, and edit commits before sharing — your collaborators will thank you.
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.