Skip to content

Mastering Git Rebase and Interactive Rebase

A practical guide to git rebase and interactive rebase: rebase versus merge, squashing commits, rewriting history safely and clean team workflows.

5 min read
Git history visualization showing a messy branch being cleaned up with interactive rebase into clean linear commits

Git rebase rewrites commit history. That sentence either excites you or terrifies you. Both reactions are appropriate. Rebase is the most powerful history-editing tool in Git, and like any powerful tool, it requires understanding before use.

The core question teams argue about is: rebase or merge? Merge preserves the exact history of how branches diverged and converged. Rebase creates a clean linear history that is easier to read and bisect. Neither is universally better — the right choice depends on your team's workflow and what you value in commit history.

Rebase vs Merge: The Trade-off

shbash
# Starting state:
# main:    A --- B --- C
# feature:      \--- D --- E
 
# After merge:
# main:    A --- B --- C ------- M
# feature:      \--- D --- E ---/
# Creates a merge commit (M). Full history preserved.
 
# After rebase:
# main:    A --- B --- C
# feature:                --- D' --- E'
# Moves feature commits to tip of main. Linear history.
# D' and E' are NEW commits (different hashes than D and E)
tstypescript
// When to use each approach
const guidelines = {
  rebase: {
    use: [
      "Updating feature branch with latest main changes",
      "Cleaning up local commits before opening a PR",
      "Squashing WIP commits into logical units",
      "When you want a linear, readable git log",
    ],
    avoid: [
      "On commits that others have already pulled",
      "On the main/release branch directly",
      "When you need to preserve exact merge history",
    ],
  },
  merge: {
    use: [
      "Merging feature branches into main (via PR)",
      "When you need to know when branches diverged",
      "When preserving contributor attribution matters",
      "On shared branches with multiple contributors",
    ],
    avoid: [
      "For keeping feature branches up-to-date (creates noise)",
    ],
  },
};

Basic Rebase Workflow

The most common use of rebase: updating your feature branch with changes from main before opening a pull request.

shbash
# You're working on feature/auth and main has moved ahead
git checkout feature/auth
git fetch origin
 
# ❌ Merge creates unnecessary merge commits in your branch
git merge origin/main
# "Merge branch 'main' into feature/auth" — adds noise to history
 
# ✅ Rebase moves your commits to the tip of main
git rebase origin/main
# Your commits are replayed on top of the latest main
# No merge commit, clean linear history
 
# If conflicts occur during rebase:
# 1. Fix conflicts in the files
# 2. git add <resolved-files>
# 3. git rebase --continue
# Repeat for each conflicting commit
 
# If rebase goes wrong, abort and return to original state:
git rebase --abort
shbash
# After rebase, your branch has rewritten commits (new hashes)
# Force-push is required to update the remote branch
git push --force-with-lease origin feature/auth
 
# --force-with-lease is safer than --force:
# It fails if someone else pushed to the branch since your last fetch
# Prevents accidentally overwriting a teammate's work

Interactive Rebase: Rewriting History

Interactive rebase (git rebase -i) lets you edit, squash, reorder, and drop commits. This is how you turn messy work-in-progress commits into a clean, reviewable history.

shbash
# Rewrite the last 5 commits
git rebase -i HEAD~5
 
# This opens your editor with:
pick abc1234 Add user authentication endpoint
pick def5678 WIP: fix typo in auth
pick ghi9012 WIP: forgot to add test
pick jkl3456 Add authorization middleware
pick mno7890 Fix lint errors in auth module
 
# Commands:
# pick   = use commit as-is
# reword = use commit but edit the message
# edit   = pause at this commit for amending
# squash = merge into previous commit (keep both messages)
# fixup  = merge into previous commit (discard this message)
# drop   = remove this commit entirely
shbash
# Clean up the history:
pick abc1234 Add user authentication endpoint
fixup def5678 WIP: fix typo in auth
fixup ghi9012 WIP: forgot to add test
pick jkl3456 Add authorization middleware
fixup mno7890 Fix lint errors in auth module
 
# Result: 2 clean commits instead of 5 messy ones:
# abc1234' Add user authentication endpoint
# jkl3456' Add authorization middleware
# The WIP and lint-fix commits are absorbed into their parent commits

Practical Interactive Rebase Patterns

shbash
# Pattern 1: Squash all feature commits into one
git rebase -i main
# Mark all commits except the first as "squash"
# Write a comprehensive commit message for the whole feature
 
# Pattern 2: Split a commit that does too much
git rebase -i HEAD~3
# Mark the commit as "edit"
# When rebase pauses:
git reset HEAD~1           # Undo the commit but keep changes
git add src/auth.ts        # Stage first logical change
git commit -m "Add auth validation logic"
git add src/middleware.ts   # Stage second logical change
git commit -m "Add auth middleware"
git rebase --continue
# One commit becomes two focused commits
 
# Pattern 3: Reorder commits for better review
git rebase -i HEAD~4
# Move the "add tests" commit right after the "add feature" commit
# So the PR reads: feature → tests → refactor → docs
 
# Pattern 4: Fix a commit message deep in history
git rebase -i HEAD~10
# Mark the target commit as "reword"
# Edit the message when prompted
tstypescript
// ❌ Commit history that's hard to review
const badHistory = [
  "WIP",
  "fix",
  "more fixes",
  "actually fix the thing",
  "oops forgot file",
  "lint",
  "address PR feedback",
  "fix tests",
  "final fix",
];
 
// ✅ Commit history after interactive rebase
const cleanHistory = [
  "feat: add user authentication with JWT",
  "feat: add role-based authorization middleware",
  "test: add auth and authorization test suites",
  "docs: update API documentation for auth endpoints",
];
// Each commit is a logical unit that can be reviewed,
// reverted, or cherry-picked independently

Handling Conflicts During Rebase

Rebase replays commits one by one. If a commit conflicts with the target branch, Git pauses and asks you to resolve it — for each conflicting commit individually.

shbash
# During rebase, Git shows:
# CONFLICT (content): Merge conflict in src/auth.ts
# Fix conflicts and run "git rebase --continue"
 
# Step 1: See which files have conflicts
git status
# Both modified: src/auth.ts
 
# Step 2: Open the file and resolve conflicts
# <<<<<<< HEAD
# const token = jwt.sign(payload, SECRET, { expiresIn: '1h' });
# =======
# const token = jwt.sign(payload, config.secret, { expiresIn: '24h' });
# >>>>>>> Add authentication with configurable secret
 
# Step 3: Choose the right version (or combine both)
# const token = jwt.sign(payload, config.secret, { expiresIn: '1h' });
 
# Step 4: Stage resolved files and continue
git add src/auth.ts
git rebase --continue
# Git moves to the next commit in the rebase
shbash
# If you mess up a conflict resolution:
git rebase --abort  # Return to the exact state before rebase
 
# Pro tip: enable rerere (Reuse Recorded Resolution)
git config --global rerere.enabled true
# Git remembers how you resolved a conflict and auto-applies
# it if the same conflict appears again (common with long-lived branches)

The Golden Rule of Rebasing

Never rebase commits that have been pushed to a shared branch and pulled by others. Rebase rewrites commit hashes. If a teammate has commits based on the old hashes, their history diverges from yours.

shbash
# ❌ DANGEROUS: rebasing main after others have pulled
git checkout main
git rebase feature/experiment
git push --force
# Everyone who pulled main now has conflicting history
# They'll see "divergent branches" errors
 
# ❌ DANGEROUS: rebasing a shared feature branch without coordination
git checkout feature/shared-work
git rebase origin/main
git push --force
# Your teammate's local branch now conflicts with remote
 
# ✅ SAFE: rebase your own feature branch before PR
git checkout feature/my-work  # Only you work on this branch
git rebase origin/main
git push --force-with-lease origin feature/my-work
 
# ✅ SAFE: interactive rebase on unpushed commits
git rebase -i HEAD~3  # Only local commits, never pushed
# Free to squash, reorder, reword — nobody has these commits

Key Takeaways

  1. Rebase for updating feature branches, merge for integrating into main — rebase keeps your branch linear and clean; merge preserves the integration point in shared branches
  2. Interactive rebase turns messy WIP into reviewable commits — squash fixup commits, reword messages, and reorder commits before opening a PR
  3. Never rebase shared commits — only rebase commits that exist solely on your branch; rebasing shared history breaks everyone else's local state
  4. Use --force-with-lease instead of --force — it prevents overwriting someone else's push that happened between your fetch and push
  5. Enable rerere for repeated conflict resolution — if you rebase frequently and encounter the same conflicts, rerere records and auto-applies your resolutions
Wilfredo Rujel

Wilfredo Rujel

Full Stack Software Engineer

Share this postX