You are viewing a preview of this course. Sign in to start learning

Lesson 5: Rebase — Rewriting History

Master Git rebase to create clean, linear histories. Learn when to rebase vs merge, how to use interactive rebase, and the golden rule that keeps you safe.

Lesson 5: Rebase — Rewriting History ⏪

Introduction 🎬

You've been working on a feature branch for three days. Your commit history looks like this: "fix typo", "actually fix typo", "remove debug code", "WIP", "more WIP", "finally works". Now you need to merge this into main, but you're embarrassed by the messy history. What if you could rewrite history to make it look like you knew what you were doing all along?

Enter git rebase — one of Git's most powerful and most feared commands. While merge creates a new commit that ties two branches together, rebase replays your commits on top of another branch, creating a clean, linear history. It's like having a time machine for your code.

In this lesson, you'll learn:

  • 🔄 What rebase actually does under the hood
  • ✨ Interactive rebase for cleaning up commit history
  • ⚖️ When to use rebase vs merge
  • 🚫 The golden rule of rebasing (break this at your peril!)

Core Concepts 💡

What Is Rebase? 🎯

Rebase means "change the base of your branch." Instead of your feature branch starting from commit A, you move it to start from commit B. But Git doesn't actually move commits (remember, commits are immutable!). Instead, it replays your commits — it creates brand new commits with the same changes but different parent commits.

Think of it like copying a paragraph from one page to another. The words are the same, but it's technically a new paragraph in a new location.

BEFORE REBASE:

      C1---C2---C3  (feature)
     /
A---B---D---E  (main)

AFTER REBASE (feature onto main):

                C1'---C2'---C3'  (feature)
               /
A---B---D---E  (main)

Notice that C1, C2, C3 become C1', C2', C3' — they're new commits with new SHA hashes, even though they contain the same changes. The old commits (C1, C2, C3) still exist in Git's database, but nothing points to them anymore (they're orphaned).

How Rebase Works: The Replay Process 🎬

When you run git rebase main from your feature branch, Git:

  1. Finds the common ancestor between your branch and main (commit B in the diagram)
  2. Saves all your commits (C1, C2, C3) to a temporary area
  3. Resets your branch to point to the tip of main (commit E)
  4. Replays each saved commit one at a time on top of E
  5. Updates your branch pointer to the final replayed commit

During replay, Git:

  • Takes the diff from each of your commits
  • Applies that diff to the current state
  • Creates a new commit with the same message and author
  • Moves to the next commit and repeats

💡 Key Insight: Rebase doesn't actually change what your commits do, it changes where they start from.

Rebase vs Merge: The Fundamental Difference ⚖️

+----------------+-------------------------+-------------------------+
|                |         MERGE           |         REBASE          |
+----------------+-------------------------+-------------------------+
| History        | Non-linear (preserves   | Linear (rewrites        |
|                | branch structure)       | history)                |
+----------------+-------------------------+-------------------------+
| Creates        | One merge commit        | No new commits (copies  |
|                |                         | existing ones)          |
+----------------+-------------------------+-------------------------+
| Commit SHAs    | Preserved (no changes)  | Changed (new commits)   |
+----------------+-------------------------+-------------------------+
| Conflicts      | Resolved once, in       | May resolve multiple    |
|                | merge commit            | times (per commit)      |
+----------------+-------------------------+-------------------------+
| Traceability   | Can see when branches   | Branch history erased   |
|                | diverged/merged         |                         |
+----------------+-------------------------+-------------------------+
| Safety         | Safe for public         | DANGEROUS for public    |
|                | branches                | branches                |
+----------------+-------------------------+-------------------------+

When to use MERGE:

  • ✅ Integrating public/shared branches
  • ✅ You want to preserve exact history
  • ✅ Multiple people work on the same branch
  • ✅ For "official" merge points (feature → main)

When to use REBASE:

  • ✅ Updating your private feature branch
  • ✅ Cleaning up local commits before sharing
  • ✅ Creating a linear history for easier reading
  • ✅ Before creating a pull request

🧠 Mnemonic: "Merge for Main, Rebase for Revisions"

Interactive Rebase: Your History Editor ✨

Interactive rebase (git rebase -i) is like opening your commit history in a text editor. You can:

  • Reorder commits
  • Edit commit messages
  • Squash multiple commits into one
  • Delete commits entirely
  • Split one commit into multiple
  • Edit the changes in a commit

The command git rebase -i HEAD~3 opens an editor showing your last 3 commits:

pick a1b2c3d Add user authentication
pick d4e5f6g Fix login bug
pick h7i8j9k Update documentation

# Commands:
# p, pick = use commit
# r, reword = use commit, but edit message
# e, edit = use commit, but stop for amending
# s, squash = meld into previous commit
# f, fixup = like squash, but discard message
# d, drop = remove commit

You change pick to other commands, save, and Git executes them:

pick a1b2c3d Add user authentication
fixup d4e5f6g Fix login bug
reword h7i8j9k Update documentation

This will:

  1. Keep the first commit as-is
  2. Squash the bug fix into the first commit (combining their changes)
  3. Let you reword the documentation commit's message

💡 Pro Tip: Use fixup instead of squash when you just want to absorb a commit without editing the combined message.

The Golden Rule of Rebase 🚫⚠️

NEVER REBASE COMMITS THAT HAVE BEEN PUSHED TO A PUBLIC/SHARED BRANCH

This is the #1 rule. Breaking it causes chaos for your team. Here's why:

Your local history:         Teammate's clone:
A---B---C---D (main)        A---B---C---D (main)

You rebase and force-push:

Your new history:           Teammate's clone:
A---B---C'---D' (main)      A---B---C---D (main)
                                      ↑
                                  (now orphaned)

When your teammate tries to pull, Git sees:

  • Their main has C and D
  • Your main has C' and D' (different commits!)
  • Git thinks these are two divergent branches
  • Creates a messy merge combining duplicated work

🧠 Mnemonic: "Private branches → Rebase, Public branches → Merge" (PRM)

⚠️ Exception: Some teams use rebase workflows where everyone agrees to force-push and handle it. But this requires clear communication and discipline.

Detailed Examples 📝

Example 1: Basic Rebase to Update Feature Branch 🔄

Scenario: You're working on a feature branch. Meanwhile, your teammate merged updates to main. You want to incorporate those updates.

Current state:
      F1---F2  (feature)
     /
A---B---M1---M2  (main)

Using merge (creates merge commit):

git checkout feature
git merge main

Result:

      F1---F2---M3  (feature)
     /         /
A---B---M1---M2  (main)

Using rebase (clean linear history):

git checkout feature
git rebase main

Result:

                F1'---F2'  (feature)
               /
A---B---M1---M2  (main)

What happened:

  1. Git found common ancestor (B)
  2. Saved F1 and F2 temporarily
  3. Moved feature to point at M2
  4. Replayed F1 on top of M2 → created F1'
  5. Replayed F2 on top of F1' → created F2'

💡 When to use each: Use rebase if feature is private. Use merge if others are working on feature.

Example 2: Interactive Rebase to Clean Up History ✨

Scenario: You made 5 messy commits while developing a feature. Before creating a pull request, you want to clean them up.

Your history:

git log --oneline

a1b2c3d Add login form
b2c3d4e Fix button styling
c3d4e5f Add validation
d4e5f6g Fix typo in validation
e5f6g7h Actually fix typo

Start interactive rebase for last 5 commits:

git rebase -i HEAD~5

Editor opens:

pick a1b2c3d Add login form
pick b2c3d4e Fix button styling
pick c3d4e5f Add validation
pick d4e5f6g Fix typo in validation
pick e5f6g7h Actually fix typo

Edit to:

pick a1b2c3d Add login form
fixup b2c3d4e Fix button styling
pick c3d4e5f Add validation
fixup d4e5f6g Fix typo in validation
fixup e5f6g7h Actually fix typo

Save and close. Git replays:

Result:

git log --oneline

f1a2b3c Add validation
g2h3i4j Add login form

You've squashed 5 commits into 2 clean, logical commits! 🎉

💡 Pro Workflow: Many developers commit frequently during work (even broken code), then use interactive rebase to create clean, working commits before pushing.

Example 3: Rebasing with Conflicts 🔧

Scenario: You're rebasing, but one of your commits conflicts with changes in main.

git checkout feature
git rebase main

# Git starts replaying commits...
Auto-merging src/app.js
CONFLICT (content): Merge conflict in src/app.js
Could not apply c3d4e5f... Add new feature

Git pauses mid-rebase. You're now in rebase conflict resolution mode:

git status

interactive rebase in progress; onto a1b2c3d
Last command done (1 command done):
   pick c3d4e5f Add new feature
Next commands to do (2 remaining commands):
   pick d4e5f6g Another commit
   pick e5f6g7h Final commit
You are currently rebasing branch 'feature' on 'a1b2c3d'.

Unmerged paths:
  both modified:   src/app.js

Steps to resolve:

  1. Open src/app.js and fix conflicts (look for <<<<<<< markers)
  2. Stage the resolved file:
git add src/app.js
  1. Continue the rebase:
git rebase --continue

Git creates the new commit and moves to the next one. If that conflicts, repeat the process.

💡 Abort anytime: If rebase gets messy, run git rebase --abort to return to exactly where you started.

⚠️ Key Difference from Merge Conflicts: With merge, you resolve conflicts once. With rebase, you might resolve conflicts multiple times (once per commit being replayed).

Example 4: Rebase onto a Different Branch 🌳

Scenario: You created a feature2 branch off of feature1, but now you want feature2 to branch off main instead.

Current state:

      F1a---F1b  (feature1)
            |
            +---F2a---F2b  (feature2)
           /
A---B---C  (main)

You want:

Desired state:

      F1a---F1b  (feature1)
     /
A---B---C  (main)
        |
        +---F2a'---F2b'  (feature2)

Use git rebase --onto:

git checkout feature2
git rebase --onto main feature1

This says: "Take commits from feature2 that aren't in feature1, and replay them onto main."

💡 Use case: Useful when your work depends on a feature that later gets rejected or delayed.

Common Mistakes ⚠️

1. Rebasing Public Branches 🚫

THE cardinal sin of rebasing.

# NEVER DO THIS:
git checkout main
git pull
git rebase some-branch  # Rewrites shared history!
git push --force        # Forces others to deal with mess

Why it's bad: Your teammates' main branches now have orphaned commits. When they pull, Git creates a merge combining old and new versions of the same commits.

Fix: Only rebase branches you haven't pushed, or that only you work on.

2. Losing Work During Interactive Rebase 😱

Mistake: During interactive rebase, you accidentally drop a commit or mess up the order.

git rebase -i HEAD~5
# Oops, deleted a line accidentally
# Now that commit is gone!

Fix: Your commits aren't actually deleted — they're just orphaned. Use the reflog:

git reflog
# Find the commit SHA before rebase
git reset --hard abc123d  # Go back to before rebase

💡 Prevention: Before any risky rebase, create a backup branch:

git branch backup-feature
git rebase -i HEAD~5
# If things go wrong: git reset --hard backup-feature

3. Forgetting to Continue After Conflict ⏸️

Mistake: You resolve conflicts but forget to run git rebase --continue.

git rebase main
# Conflict occurs
# You fix it and stage with 'git add'
# But then you run 'git commit' instead!

Why it's bad: git commit during a rebase creates a weird merge commit and confuses the rebase process.

Fix: After resolving conflicts during rebase:

git add <fixed-files>
git rebase --continue  # NOT 'git commit'!

4. Force-Pushing Without --force-with-lease ⚠️

Mistake: After rebasing, you force-push without checking if others pushed:

git push --force  # Overwrites anything on remote!

Better: Use --force-with-lease:

git push --force-with-lease

This only forces if the remote branch hasn't changed since you last fetched. If someone else pushed, it fails safely.

💡 Alias it: git config --global alias.pushf 'push --force-with-lease'

5. Not Testing After Rebase 🧪

Mistake: Assuming rebased code still works.

During rebase, you might resolve conflicts incorrectly or introduce subtle bugs. Even if each commit worked originally, replaying them in a new context can break things.

Fix: After rebase, always run your tests:

git rebase main
# Run test suite
npm test
# Only push if tests pass
git push

🤔 Did you know? Git has a git rebase --exec option that runs a command after each commit during rebase:

git rebase -i HEAD~5 --exec "npm test"

If tests fail at any commit, rebase pauses so you can fix it!

Real-World Workflow Examples 🌍

Feature Branch Workflow with Rebase 🔄

  1. Start feature:
git checkout -b feature/user-auth
# Make several commits
  1. Main gets updated while you work:
# Daily: update your branch with latest main
git fetch origin
git rebase origin/main
# Resolve conflicts if any
  1. Clean up before submitting PR:
# Squash messy commits
git rebase -i origin/main
# Fix commit messages, squash "fix" commits
  1. Push (first time):
git push -u origin feature/user-auth
  1. If you need to rebase again after pushing:
git rebase origin/main
git push --force-with-lease  # Overwrites your remote branch

⚠️ Only force-push to your own feature branches, never to main or shared branches!

Team Agreement Example 🤝

Some teams use this workflow:

Rules:

  • Feature branches: rebase freely
  • Main branch: merge only, never rebase
  • Before merging to main: rebase feature onto main, then merge
# On feature branch
git rebase main          # Update with latest
git push --force-with-lease

# Create PR, get approval
# Merge with "Merge" button (not "Rebase and merge")

Result: Feature branches stay linear (easy to follow), main has merge commits (preserves integration points).

Key Takeaways 🎯

Rebase replays commits onto a new base, creating new commit SHAs

Linear vs non-linear: Rebase creates linear history, merge preserves branch structure

Interactive rebase lets you squash, reorder, and edit commits

Golden rule: Never rebase public/shared branches — only private feature branches

Rebase vs merge:

  • Rebase: Clean history, rewrites commits, dangerous if public
  • Merge: Preserves history, safe for shared branches, creates merge commits

Conflict resolution: Might happen multiple times during rebase (once per commit)

Force-push safely: Use --force-with-lease to avoid overwriting others' work

Create backups: Make a backup branch before risky rebases

Test after rebasing: Context changes can introduce bugs

Common workflow: Rebase feature branches daily to stay updated, squash before PR, merge into main

📚 Further Study

  1. Git Official Documentation on Rebase: https://git-scm.com/docs/git-rebase — Comprehensive reference with all options

  2. Atlassian Git Rebase Tutorial: https://www.atlassian.com/git/tutorials/rewriting-history/git-rebase — Excellent visual explanations and examples

  3. Interactive Rebase Deep Dive: https://git-scm.com/book/en/v2/Git-Tools-Rewriting-History — From the Pro Git book, covers advanced history rewriting


📋 Quick Reference Card

+----------------------------------+------------------------------------------+
|           COMMAND                |              WHAT IT DOES                |
+----------------------------------+------------------------------------------+
| git rebase main                  | Replay current branch commits onto main |
| git rebase -i HEAD~3             | Interactive rebase last 3 commits        |
| git rebase --continue            | Continue after resolving conflict        |
| git rebase --abort               | Cancel rebase, return to start           |
| git rebase --skip                | Skip current commit, move to next        |
| git rebase --onto A B            | Replay commits after B onto A            |
| git push --force-with-lease      | Safely force-push after rebase           |
+----------------------------------+------------------------------------------+

INTERACTIVE REBASE COMMANDS:
  pick   = use commit as-is
  reword = change commit message
  edit   = stop to modify commit content
  squash = combine with previous (keep both messages)
  fixup  = combine with previous (discard this message)
  drop   = remove commit entirely

GOLDEN RULE:
  ✅ Rebase: Private/local branches
  🚫 Never: Public/shared branches (like main)

WHEN TO USE WHAT:
  Rebase → Clean up your work before sharing
  Merge  → Integrate shared branches together

🧠 Remember: Rebase is a scalpel (precise, powerful, but dangerous if misused). Merge is a glue gun (safe, always works, but messier). Choose the right tool! 🔧