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.

published: reading time: 13 min read updated: March 31, 2026

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

CommandShortcutEffect
pickpUse commit as-is
rewordrUse commit, but edit the message
editeStop for amending the commit
squashsCombine with previous commit, keep both messages
fixupfCombine with previous commit, discard this message
dropdRemove the commit entirely
breakbPause rebase (you decide what to do)
execxRun a shell command after this commit
labellLabel current HEAD with a name
resettReset 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

ScenarioImpactMitigation
Rebasing pushed commitsTeam members have divergent historyNever rebase shared branches; use merge instead
Force push after rebaseOverwrites remote historyUse --force-with-lease instead of --force
Losing commits during rebaseWork appears to vanishUse git reflog to find and recover lost commits
Rebase conflict cascadeMultiple conflicts in sequenceConsider merge; or resolve carefully one at a time
Accidentally dropping commitsPermanent data lossReview 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

ApproachProsCons
RebaseClean linear history, easier bisectRewrites history, dangerous on shared branches
MergePreserves true history, safe for sharedCreates merge commits, harder to follow
SquashSingle clean commitLoses individual commit context
FixupClean history, no extra messagesHides the fact that fixes were needed
Edit commitsPerfect commit granularityTime-consuming, requires careful planning
Drop commitsRemoves mistakes permanentlyLoses 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 --signoff is 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 --force instead 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 -i to squash, split, reorder, and edit commits
  • Never rebase commits that have been pushed to shared branches
  • Use --force-with-lease instead of --force when updating remote
  • Recover lost commits with git reflog
  • Abort a bad rebase with git rebase --abort
  • Use --autosquash to automatically combine fixup commits
  • Run tests after rebase to verify nothing broke

Interview Q&A

What is the difference between 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.

What is the difference between 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.

Why should you never rebase commits that have been pushed to a shared branch?

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.

How do you split a single large commit into multiple smaller commits?

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.

What does 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 reflog to 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

DimensionRebaseMerge
History cleanlinessLinear, easy to readNon-linear, shows true integration points
Collaboration costHigh — requires coordination on shared branchesLow — safe for any branch
Recovery difficultyHard — rewritten SHAs, reflog neededEasy — revert the merge commit
Bisect friendlinessExcellent — single pathGood — but merge commits add noise
Audit trailRewritten — original context lostPreserved — records when/what was integrated
Conflict resolutionPer commit — may resolve same conflict multiple timesOnce — all conflicts resolved together
Team size impactDegrades with team sizeScales well to large teams
Best use caseLocal feature branches before PRShared 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 --signoff is used
  • Blame history is disrupted — git blame shows 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.

#git #version-control #rebase

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.

#git #version-control #svn

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.

#git #version-control #changelog