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:
- Finds the common ancestor between your branch and main (commit B in the diagram)
- Saves all your commits (C1, C2, C3) to a temporary area
- Resets your branch to point to the tip of main (commit E)
- Replays each saved commit one at a time on top of E
- 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:
- Keep the first commit as-is
- Squash the bug fix into the first commit (combining their changes)
- 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:
- Git found common ancestor (B)
- Saved F1 and F2 temporarily
- Moved feature to point at M2
- Replayed F1 on top of M2 → created F1'
- 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:
- Open
src/app.jsand fix conflicts (look for<<<<<<<markers) - Stage the resolved file:
git add src/app.js
- 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 🔄
- Start feature:
git checkout -b feature/user-auth
# Make several commits
- Main gets updated while you work:
# Daily: update your branch with latest main
git fetch origin
git rebase origin/main
# Resolve conflicts if any
- Clean up before submitting PR:
# Squash messy commits
git rebase -i origin/main
# Fix commit messages, squash "fix" commits
- Push (first time):
git push -u origin feature/user-auth
- 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
Git Official Documentation on Rebase: https://git-scm.com/docs/git-rebase — Comprehensive reference with all options
Atlassian Git Rebase Tutorial: https://www.atlassian.com/git/tutorials/rewriting-history/git-rebase — Excellent visual explanations and examples
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! 🔧