Git Rebase Explained — When to Use It and When to Avoid It
The Problem Rebase Solves
In Part 5, you learned how to merge branches. Merging works great, but it has a side effect: every time you merge, Git creates a merge commit — a special commit with two parents that ties the branches together. In a busy project with many branches, these merge commits pile up and create a tangled, hard-to-read history.
Let's use a real-world analogy to understand this.
Imagine you and your colleague Sarah are both writing chapters for a book. You're writing Chapter 3, she's writing Chapter 4. You both started from the same draft on Monday. By Friday, you've both finished. Now the editor needs to combine your work.
With merging, the editor staples both chapters into the manuscript and adds a sticky note saying "Combined Sarah's Chapter 4 with Chapter 3 on Friday." The chapters are there, the note is there, but the manuscript now has these extra sticky notes everywhere. After a year of this, the manuscript is full of sticky notes that make it hard to read the actual story.
With rebasing, the editor takes a different approach. Instead of stapling and adding a note, the editor rewrites your Chapter 3 as if you had written it after Sarah finished Chapter 4 — not alongside her. The final manuscript reads as a clean, straight timeline: Chapter 4 first, then Chapter 3, no sticky notes. The same content, but a cleaner narrative.
That's the core idea behind rebase: instead of combining two timelines with a merge commit, you replay your commits on top of another branch, creating a clean, linear history.
Setting Up a Practice Project
Let's build something real to work with. We'll simulate a common scenario: you're building a feature while the main branch keeps moving forward with other people's work.
mkdir git-rebase-practice
cd git-rebase-practice
git init
Create the initial project:
echo "<!DOCTYPE html>
<html>
<head><title>Recipe App</title></head>
<body>
<h1>My Recipe App</h1>
<ul id='recipes'></ul>
</body>
</html>" > index.html
echo "# Recipe App" > README.md
git add .
git commit -m "Initial commit: set up Recipe App"
Now create a feature branch to add a search function:
git switch -c feature/search
Add your first feature commit:
echo "function searchRecipes(query) {
console.log('Searching for: ' + query);
}" > search.js
git add search.js
git commit -m "Add search function skeleton"
Add a second feature commit:
echo "function searchRecipes(query) {
const recipes = document.querySelectorAll('#recipes li');
recipes.forEach(recipe => {
const text = recipe.textContent.toLowerCase();
recipe.style.display = text.includes(query.toLowerCase()) ? '' : 'none';
});
}
function clearSearch() {
const recipes = document.querySelectorAll('#recipes li');
recipes.forEach(recipe => recipe.style.display = '');
}" > search.js
git add search.js
git commit -m "Implement search filtering and add clearSearch function"
Now here's the key part — while you've been working on the search feature, a teammate pushed changes to main. Let's simulate that by switching to main and making commits:
git switch main
echo "body {
font-family: Georgia, serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
}" > style.css
git add style.css
git commit -m "Add base stylesheet"
echo "# Recipe App
A simple app to browse and search recipes.
## Getting Started
Open index.html in your browser." > README.md
git add README.md
git commit -m "Improve README with project description"
Let's see what our history looks like now:
git log --oneline --graph --all
Output:
* f5a6b7c (HEAD -> main) Improve README with project description
* e4d5c6b Add base stylesheet
| * d3c4b5a (feature/search) Implement search filtering and add clearSearch function
| * c2b3a4f Add search function skeleton
|/
* a1b2c3d Initial commit: set up Recipe App
Two branches have diverged from the same starting point. main has two new commits, feature/search has two new commits. This is the exact scenario where you choose between merge and rebase.
How Merge Would Handle This
If you merged right now (don't actually run this — we'll rebase instead), Git would create a merge commit tying the two branches together:
* m8n9o0p (main) Merge branch 'feature/search'
|\
| * d3c4b5a Implement search filtering and add clearSearch function
| * c2b3a4f Add search function skeleton
* | f5a6b7c Improve README with project description
* | e4d5c6b Add base stylesheet
|/
* a1b2c3d Initial commit: set up Recipe App
This works, but the history has a fork and a merge point. With one branch it's not bad. But imagine a project with 20 developers, each merging multiple branches per week. The graph becomes a tangled web of forks and merges that's difficult to follow.
How Rebase Handles This
Rebase takes a completely different approach. Instead of combining two timelines, it picks up your commits and replays them on top of the other branch, as if you had started your work from the latest version of main all along.
Let's do it. Switch to the feature branch:
git switch feature/search
Now rebase onto main:
git rebase main
Output:
Successfully rebased and updated refs/heads/feature/search.
Let's look at the history now:
git log --oneline --graph --all
Output:
* g7h8i9j (HEAD -> feature/search) Implement search filtering and add clearSearch function
* f6g7h8i Add search function skeleton
* f5a6b7c (main) Improve README with project description
* e4d5c6b Add base stylesheet
* a1b2c3d Initial commit: set up Recipe App
Look at that — a perfectly straight line. No forks, no merge commit, no tangled graph. Your two search commits now sit neatly on top of the latest main commits, as if you had started the feature after the stylesheet and README were added.
What Actually Happened Behind the Scenes
It's important to understand what Git did here, because the commits on your feature branch are not the same commits as before. Here's the step-by-step process Git followed:
- Git identified the common ancestor — the commit where feature/search and main diverged (the initial commit).
- Git temporarily saved your two feature commits and set them aside.
- Git moved the feature/search pointer to the tip of main (the README commit).
- Git replayed your first saved commit ("Add search function skeleton") on top of main, creating a new commit with a new ID but the same changes and message.
- Git replayed your second saved commit ("Implement search filtering") on top of that, again creating a new commit.
The original commits still exist in Git's internal storage (and can be found with git reflog), but your branch now points to the newly created copies. This is why rebase is called "rewriting history" — it doesn't edit the old commits; it creates new ones in a different location.
Now Merge with a Fast-Forward
After rebasing, merging the feature into main is a clean fast-forward — no merge commit needed:
git switch main
git merge feature/search
Output:
Updating f5a6b7c..g7h8i9j
Fast-forward
search.js | 11 +++++++++++
1 file changed, 11 insertions(+)
Check the final history:
git log --oneline
Output:
g7h8i9j (HEAD -> main, feature/search) Implement search filtering and add clearSearch function
f6g7h8i Add search function skeleton
f5a6b7c Improve README with project description
e4d5c6b Add base stylesheet
a1b2c3d Initial commit: set up Recipe App
A single, straight, beautiful timeline. Every commit in order. No merge commits. This is what teams mean when they talk about a "clean history."
Clean up the branch:
git branch -d feature/search
Rebase vs. Merge — A Side-by-Side Comparison
| Aspect | Merge | Rebase |
|---|---|---|
| History shape | Branching graph with merge commits | Clean, linear timeline |
| Commit IDs | Original commits preserved | New commits created (IDs change) |
| Merge commit | Yes — extra commit created | No — fast-forward possible |
| True history | Preserves exactly when branches diverged and converged | Rewrites history as if work was sequential |
| Safe for shared branches | Yes — always safe | No — dangerous on shared branches |
| Conflict resolution | Resolve once during merge | May need to resolve for each replayed commit |
| Complexity | Simple, hard to mess up | More powerful, but riskier if misused |
Neither is universally better. They're tools for different situations, and great developers know when to use each one.
Handling Conflicts During Rebase
Just like merging, rebasing can encounter conflicts — and the process for resolving them is slightly different. Because rebase replays your commits one at a time, you may need to resolve conflicts at each commit, not just once at the end.
Let's simulate a rebase conflict. Create a new scenario:
git switch -c feature/footer
Edit the HTML to add a footer:
echo "<!DOCTYPE html>
<html>
<head><title>Recipe App</title></head>
<body>
<h1>My Recipe App</h1>
<ul id='recipes'></ul>
<footer>Made with love by the Recipe Team</footer>
</body>
</html>" > index.html
git add index.html
git commit -m "Add footer to homepage"
Now switch to main and make a conflicting change to the same area of the file:
git switch main
echo "<!DOCTYPE html>
<html>
<head><title>Recipe App</title></head>
<body>
<h1>My Recipe App</h1>
<ul id='recipes'></ul>
<p class='copyright'>Copyright 2026 Recipe App</p>
</body>
</html>" > index.html
git add index.html
git commit -m "Add copyright notice to homepage"
Now try to rebase the footer branch onto main:
git switch feature/footer
git rebase main
Output:
CONFLICT (content): Merge conflict in index.html
error: could not apply c2b3a4f... Add footer to homepage
hint: Resolve all conflicts manually, mark them as resolved with
hint: "git add <pathspec>..." then run "git rebase --continue".
Git stopped the rebase because it hit a conflict while replaying your commit. Let's look at the file:
cat index.html
You'll see familiar conflict markers:
<!DOCTYPE html>
<html>
<head><title>Recipe App</title></head>
<body>
<h1>My Recipe App</h1>
<ul id='recipes'></ul>
<<<<<<< HEAD
<p class='copyright'>Copyright 2026 Recipe App</p>
=======
<footer>Made with love by the Recipe Team</footer>
>>>>>>> Add footer to homepage
</body>
</html>
Resolve it by keeping both — the copyright and the footer:
echo "<!DOCTYPE html>
<html>
<head><title>Recipe App</title></head>
<body>
<h1>My Recipe App</h1>
<ul id='recipes'></ul>
<footer>Made with love by the Recipe Team</footer>
<p class='copyright'>Copyright 2026 Recipe App</p>
</body>
</html>" > index.html
Stage the resolved file and continue the rebase:
git add index.html
git rebase --continue
Git opens your editor with the original commit message. Save and close it. The rebase completes.
The Three Rebase Escape Hatches
When you're in the middle of a conflicted rebase, you have three options:
git rebase --continue— After resolving the conflict, continue replaying the remaining commits.git rebase --skip— Skip the current commit entirely. Use this if the commit's changes are no longer needed after the conflict resolution.git rebase --abort— Cancel the entire rebase and go back to exactly where you were before. Your branch is restored to its pre-rebase state, as if nothing happened.
If things get confusing during a rebase conflict, --abort is always your safe exit. You can regroup, think about the conflicts, and try again when you're ready.
Interactive Rebase — Cleaning Up Your Commits
So far we've used rebase to move commits from one base to another. But rebase has a second, equally powerful use: interactive rebase, which lets you edit, reorder, combine, or delete commits before sharing them.
Here's a real-life scenario. You're working on a feature and your commit history looks like this:
git log --oneline
h8i9j0k Fix typo in search function
g7h8i9j WIP: trying something
f6g7h8i Add search filtering
e5f6g7h Add search function skeleton
You have a "WIP" commit that doesn't make sense on its own, and a typo fix that should really be part of the original commit. Before merging into main, you want to clean this up into a tidy, professional history. Interactive rebase is how.
Starting an Interactive Rebase
To edit the last 4 commits:
git rebase -i HEAD~4
Git opens your editor with a list of commits (oldest at the top):
pick e5f6g7h Add search function skeleton
pick f6g7h8i Add search filtering
pick g7h8i9j WIP: trying something
pick h8i9j0k Fix typo in search function
Each line starts with a command (pick) followed by the commit ID and message. You can change the command on each line to tell Git what to do with that commit.
The Interactive Rebase Commands
| Command | Short | What It Does |
|---|---|---|
pick |
p |
Keep the commit as-is |
reword |
r |
Keep the commit but change its message |
edit |
e |
Pause at this commit so you can amend it |
squash |
s |
Combine this commit with the one above it, keeping both messages |
fixup |
f |
Combine this commit with the one above it, discarding this commit's message |
drop |
d |
Delete this commit entirely |
Example: Cleaning Up the History
Let's clean up our messy history. Edit the interactive rebase list to:
pick e5f6g7h Add search function skeleton
fixup h8i9j0k Fix typo in search function
pick f6g7h8i Add search filtering
drop g7h8i9j WIP: trying something
Here's what we told Git to do:
- Line 1: Keep "Add search function skeleton" as-is.
- Line 2: We moved the typo fix up and changed it to fixup. This merges the typo fix into the search skeleton commit silently — the typo fix's message is discarded because it's not meaningful on its own.
- Line 3: Keep "Add search filtering" as-is.
- Line 4: drop the WIP commit entirely. It was an experiment that didn't lead anywhere.
Save and close the editor. Git replays the commits according to your instructions. Check the result:
git log --oneline
j0k1l2m Add search filtering
i9j0k1l Add search function skeleton
Two clean, meaningful commits instead of four messy ones. The typo fix was folded into the original commit. The WIP experiment is gone. This is the history your teammates want to review — clear, focused, and professional.
A Real-Life Analogy for Interactive Rebase
Think of interactive rebase as editing a draft before publishing. When you write an article, your first draft might have notes to yourself, half-finished paragraphs, and sections in the wrong order. You'd never publish that. You'd rewrite, reorder, and polish before hitting publish.
Interactive rebase is the same thing for your commit history. Your working commits are your rough draft. Before merging into main (publishing), you clean them up into a polished, readable narrative.
The Golden Rule of Rebase
This is the single most important rule in all of Git, and it's worth putting in its own section:
Never rebase commits that have been pushed to a shared repository.
Let's understand why with another real-life analogy.
Imagine you're writing a shared Google Doc with three colleagues. You all have a copy of the document synced to your computers. Now imagine you go back and rewrite paragraphs 3 through 7 — not by adding changes, but by deleting the old paragraphs and writing completely new ones in their place. When your colleagues try to sync, everything breaks. Their edits to those paragraphs no longer make sense because the paragraphs they edited don't exist anymore.
That's exactly what happens when you rebase shared commits. Rebase replaces commits with new ones. If your teammates have the old commits and you push the new ones, their Git history and yours no longer agree. The result is duplicated commits, confusing merge conflicts, and a lot of frustration.
Who This Affects
- Commits only on your local machine: Rebase freely. No one else has seen them, so rewriting history is safe.
- Commits on a branch only you use (even if pushed): Generally safe to rebase, but you'll need to force-push (git push --force-with-lease). More on this below.
- Commits on a shared branch (main, develop, or any branch others are working on): Never rebase. Use merge instead.
force-with-lease: The Safer Force Push
If you rebase a branch that you've already pushed (but that only you are working on), you'll need to force-push because the commit IDs have changed:
git push --force-with-lease
Regular --force overwrites the remote branch unconditionally, which is dangerous. --force-with-lease is safer — it checks that the remote branch hasn't been updated by someone else since your last pull. If it has, the push is rejected, preventing you from accidentally overwriting a teammate's work.
Always use --force-with-lease instead of --force. It should be your default.
When to Use Rebase
Here are the scenarios where rebase is the right tool:
1. Keeping Your Feature Branch Up to Date
This is the most common use case. You're working on a feature branch, and main has moved forward with other people's changes. Instead of merging main into your branch (which creates a merge commit), rebase your branch onto main:
git switch feature/my-feature
git rebase main
Your feature commits are replayed on top of the latest main, keeping the history linear. When you eventually merge into main, it's a clean fast-forward.
2. Cleaning Up Commits Before a Pull Request
Before submitting your work for code review, use interactive rebase to squash WIP commits, fix typo commits, reword unclear messages, and reorder commits into a logical sequence. Reviewers appreciate a clean history — it makes the code review faster and more effective.
3. Maintaining a Linear Project History
Some teams enforce a linear history on main as a policy. Every commit on main tells a clear story, and there are no merge commits cluttering the log. These teams use a "rebase and fast-forward merge" workflow exclusively.
4. Solo Projects
When you're the only person working on a project, there's no risk of rewriting shared history. Rebase everything. Your commit history will be a clean, chronological story of how the project evolved.
When to Avoid Rebase
Here are the scenarios where merge is the better choice:
1. On Shared Branches
As discussed above — never rebase main, develop, or any branch that other developers are basing their work on. The history rewrite will break everyone's local repositories.
2. When You Want to Preserve the True Timeline
Merge commits tell an honest story: "These two branches existed in parallel and were combined at this point." Some teams value this transparency, especially in regulated industries where audit trails matter. If the true history of how the project evolved is important, use merge.
3. When Conflicts Are Complex
During a rebase, you may need to resolve conflicts for each commit being replayed. If your branch has 15 commits and the underlying code has changed significantly, that could mean resolving 15 rounds of conflicts. A merge resolves all conflicts once. For complex, long-running branches with many conflicts, merge is often simpler.
4. When You're Not Comfortable With It Yet
There's no shame in using merge while you're still learning. Merge is safe, straightforward, and doesn't rewrite history. You can't break anything with merge. Learn rebase gradually, practice in throwaway repositories, and adopt it when you feel confident.
The Rebase Workflow — Step by Step
Here's the complete rebase workflow that many professional teams follow:
- Create a feature branch from main:
git switch main git pull git switch -c feature/new-feature - Work on the feature. Commit freely — messy commits, WIP, experiments. Don't worry about perfection yet.
# ... work work work ... git add . git commit -m "WIP: layout done" # ... more work ... git add . git commit -m "Add validation" # ... more work ... git add . git commit -m "Fix typo" - Stay up to date with main. Periodically rebase onto main to avoid diverging too far:
git switch main git pull git switch feature/new-feature git rebase main - Clean up before sharing. When the feature is done, use interactive rebase to polish the commit history:
git rebase -i HEAD~3Squash the WIP and typo commits, reword messages, reorder if needed.
- Push and open a pull request:
git push -u origin feature/new-feature(If you'd already pushed before rebasing, use git push --force-with-lease)
- Merge into main (via pull request or locally):
git switch main git merge feature/new-feature git branch -d feature/new-feature
This workflow gives you the best of both worlds: freedom to commit messily while working, and a clean, professional history when sharing with the team.
Common Rebase Mistakes
Mistake 1: Rebasing a Shared Branch
This has been said three times now, and it bears repeating. If other people are working off a branch, do not rebase it. The consequences range from confusion to hours of untangling duplicated commits. When in doubt, merge.
Mistake 2: Not Pulling Before Rebasing
Always pull the latest main before rebasing onto it. If you rebase onto an outdated main, you'll still have divergence when you try to merge.
Mistake 3: Panicking During a Conflict
Rebase conflicts can feel overwhelming, especially when they happen across multiple commits. Remember: git rebase --abort is always available. It resets everything to before the rebase started. Take a breath, abort, and try again when you're ready.
Mistake 4: Using --force Instead of --force-with-lease
If you must force-push after a rebase, always use --force-with-lease. Plain --force overwrites the remote branch blindly and can destroy a teammate's work if they pushed to the same branch.
Quick Reference
| Command | What It Does |
|---|---|
git rebase main |
Replay current branch's commits on top of main |
git rebase -i HEAD~n |
Interactive rebase: edit, squash, reorder, or drop the last n commits |
git rebase --continue |
Continue rebase after resolving a conflict |
git rebase --skip |
Skip the current commit during rebase |
git rebase --abort |
Cancel the rebase and restore the branch to its original state |
git push --force-with-lease |
Force-push safely after a rebase (checks for remote changes first) |
Interactive Rebase Commands
| Command | What It Does |
|---|---|
pick / p |
Keep the commit as-is |
reword / r |
Keep the commit, change its message |
edit / e |
Pause at this commit to amend it |
squash / s |
Combine with previous commit, keep both messages |
fixup / f |
Combine with previous commit, discard this message |
drop / d |
Delete this commit |