Skip to main content

Git Rebase Logic: Why Can't I Just Merge? πŸ€”

Β· 17 min read
Mahmut Salman
Software Developer

The Confusion πŸ˜•β€‹

Backend Dev: "I'm looking at my Git history and I see that frontend branch has 13 commits that aren't on main, and main has 1 commit that frontend doesn't have. My mentor says I need to rebase. But... why? Why can't I just merge?"

Frontend Mentor: "Excellent question! This is where understanding the LOGIC of rebase becomes crucial. Let me show you exactly why rebase is necessary in this situation."

The Situation πŸ“Šβ€‹

Frontend Mentor: "First, let's see what you discovered:"

# Check commits on frontend that aren't on main
git log main..frontend --oneline
# 13 commits shown (your frontend work)

# Check commits on main that aren't on frontend
git log frontend..main --oneline
# e463a2f chore: Update gitignore

Backend Dev: "So I have 13 commits on frontend, and main has 1 commit I don't have?"

Frontend Mentor: "Exactly! This is what we call divergent branches. Let me visualize this:"

Visualizing the Divergence πŸŒ³β€‹

Initial State (when you created frontend branch):
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
A---B---C (main, frontend - both at same point)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

After working on frontend branch:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
D---E---F---G---H---I---J---K---L---M---N---O---P---Q (frontend - 13 commits)
/
A---B---C (main - hasn't moved)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Meanwhile, someone updated main:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
D---E---F---G---H---I---J---K---L---M---N---O---P---Q (frontend)
/
A---B---C---[e463a2f] (main - moved forward!)
"Update gitignore"
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Backend Dev: "Oh! So while I was working on frontend, someone else pushed a commit to main?"

Frontend Mentor: "Exactly! Your branches have diverged. They share a common ancestor (C), but they've each moved in different directions."

The Key Question: Why Not Just Merge? πŸ€·β€‹

Backend Dev: "Okay, so why can't I just do git merge frontend on main?"

Frontend Mentor: "You CAN! But let's see what happens:"

Option 1: Direct Merge (Creates Merge Commit)​

git checkout main
git merge frontend

Result:

        D---E---F---G---H---I---J---K---L---M---N---O---P---Q  (frontend)
/ \
A---B---C---[e463a2f]---------------------------------------[MERGE] (main)
"Update gitignore" commit

Backend Dev: "It creates a merge commit? What's wrong with that?"

Frontend Mentor: "Nothing is technically wrong! But look at the history:"

git log --oneline --graph
# * [MERGE] Merge branch 'frontend' into main
# |\
# | * Q Frontend commit 13
# | * P Frontend commit 12
# | * O Frontend commit 11
# | ... (11 more commits)
# | * D Frontend commit 1
# * | e463a2f chore: Update gitignore
# |/
# * C Previous commit

Backend Dev: "The history shows a fork and then a merge back together?"

Frontend Mentor: "Yes! It preserves the fact that branches diverged. Some teams prefer this, but many teams prefer a linear history."

The Rebase Alternative: Linear History πŸ“β€‹

Frontend Mentor: "Now let's see what rebase does:"

Option 2: Rebase (Creates Linear History)​

git checkout frontend
git rebase main

What rebase does:

STEP 1: Find common ancestor
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
D---E---F---G---H---I---J---K---L---M---N---O---P---Q (frontend)
/
A---B---C ← Common ancestor
\
[e463a2f] (main)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

STEP 2: Temporarily remove frontend commits
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
A---B---C---[e463a2f] (main)

(D, E, F, ... Q temporarily saved)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

STEP 3: Replay frontend commits on top of main
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
A---B---C---[e463a2f]---D'---E'---F'---G'---H'---I'---J'---K'---L'---M'---N'---O'---P'---Q' (frontend)
(main)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Backend Dev: "Wait, what's with the prime symbols (D', E', F')?"

Frontend Mentor: "Great catch! The commits have the same CHANGES, but they're technically NEW commits with different hashes because they have a different parent!"

The Aha Moment πŸ’‘β€‹

Backend Dev: "So rebase replays my commits on top of the latest main?"

Frontend Mentor: "EXACTLY! Let me show you the difference side by side:"

Merge vs Rebase Comparison​

BEFORE (Divergent Branches):
D---E---F (frontend - 3 commits)
/
A---B---C---X (main - 1 new commit)


AFTER MERGE (Preserves Divergence):
D---E---F
/ \
A---B---C---X-----M (main with merge commit)


AFTER REBASE + MERGE (Linear History):
A---B---C---X---D'---E'---F' (main, frontend)

Backend Dev: "So with rebase, it looks like I made my commits AFTER the main commit, even though I actually made them before?"

Frontend Mentor: "YES! That's the key insight! Rebase rewrites history to create a linear timeline."

The Detailed Rebase Process πŸ”β€‹

Frontend Mentor: "Let me walk you through exactly what git rebase main does when you run it on frontend:"

Step-by-Step Rebase Logic​

1️⃣ IDENTIFY COMMON ANCESTOR
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Git finds where frontend and main diverged:
- Common ancestor: commit C
- Frontend commits after C: D, E, F, G... Q (13 commits)
- Main commits after C: e463a2f (1 commit)

2️⃣ SAVE YOUR COMMITS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Git creates patches for each frontend commit:
- Patch D: "what changes did D make?"
- Patch E: "what changes did E make?"
- ... (13 patches total)

3️⃣ MOVE TO NEW BASE
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Git moves frontend pointer to latest main:
frontend: C β†’ e463a2f (main's latest commit)

4️⃣ REPLAY COMMITS ONE BY ONE
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Git applies each patch in order:
- Apply patch D β†’ creates commit D' on top of e463a2f
- Apply patch E β†’ creates commit E' on top of D'
- Apply patch F β†’ creates commit F' on top of E'
- ... (13 commits replayed)

5️⃣ RESULT
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Frontend now has:
- All commits from main (including e463a2f)
- All your commits (D', E', F'... Q') on top
- Linear history!

Backend Dev: "So it's like Git is copying my commits and putting them on top of the latest main?"

Frontend Mentor: "Exactly! It's not actually copying - it's replaying the changes. Same changes, new commits."

Real-World Analogy πŸ—οΈβ€‹

Frontend Mentor: "Think of it like building a house:"

Merge Scenario (Parallel Construction)​

Timeline 1: You build rooms A, B, C on one foundation
Timeline 2: Team builds room X on the same foundation

Merge: Connect both timelines
Result: House shows it was built in parallel branches

Rebase Scenario (Sequential Construction)​

Original Timeline 1: You build A, B, C
Original Timeline 2: Team builds X

Rebase: Pretend you built A, B, C AFTER X
Result: House looks like it was built sequentially
(Even though you actually worked in parallel)

Backend Dev: "So rebase makes it look like work happened in sequence, even though it was parallel?"

Frontend Mentor: "PERFECT! That's exactly what rebase does!"

Why Your Mentor Said "We Should Rebase" πŸŽ―β€‹

Frontend Mentor: "Now, back to your original situation. Here's why rebase was recommended:"

Your Situation Analysis​

Current State:
- Main has: A---B---C---[e463a2f gitignore]
- Frontend has: A---B---C---[13 commits]

Problem: Frontend doesn't have the gitignore update!

Backend Dev: "Oh! So if I merge frontend into main without rebasing, I'll have the gitignore update, but it won't be at the BASE of my commits?"

Frontend Mentor: "Exactly! Look at the difference:"

Without Rebase (Direct Merge)​

git checkout main
git merge frontend

Result:

Your 13 commits don't have the gitignore update
↓
D---E---F---G---H---I---J---K---L---M---N---O---P---Q
/ \
A---B---C---[gitignore]---------------------------------------[MERGE]
↑
gitignore is only in main, not in your commits

With Rebase (Then Merge)​

git checkout frontend
git rebase main
git checkout main
git merge frontend

Result:

A---B---C---[gitignore]---D'---E'---F'---G'---H'---I'---J'---K'---L'---M'---N'---O'---P'---Q'
↑
ALL commits include gitignore update!

Backend Dev: "Ah! So by rebasing first, ALL my commits are rebuilt on top of the gitignore update, which means they all include that change?"

Frontend Mentor: "YES! Now you've got it! This is why rebase is valuable - it ensures your commits are built on the LATEST base."

When to Use Rebase vs Merge? πŸ€”β€‹

Backend Dev: "So when should I use rebase vs merge?"

Frontend Mentor: "Great question! Here's the guideline:"

Use Rebase When:​

  1. You want linear history

    A---B---C---D---E  (clean line)
  2. Updating your feature branch

    # While working on feature, main moves forward
    git checkout feature
    git rebase main # Get latest main changes
  3. Before merging to main

    # Make sure feature is up-to-date with main
    git checkout feature
    git rebase main
    git checkout main
    git merge feature # Fast-forward merge

Use Merge When:​

  1. You want to preserve history

            feature commits
    / \
    A---B---C---D----------M (shows parallel work)
  2. Integrating long-lived branches

    # Release branch merging back to main
    git checkout main
    git merge release-v2.0
  3. Public branches that others use

    # Don't rebase branches others are working on!
    git merge feature # Safe for shared branches

The Golden Rule βš οΈβ€‹

Frontend Mentor: "There's one critical rule about rebase:"

NEVER REBASE COMMITS THAT HAVE BEEN PUSHED TO A SHARED BRANCH

Backend Dev: "Why not?"

Frontend Mentor: "Because rebase creates NEW commits! If others have based work on your old commits, rewriting history will break their work!"

Safe Rebase​

# βœ… SAFE: Rebase local commits
git checkout feature # Only you have this
git rebase main # Safe to rebase

Dangerous Rebase​

# ❌ DANGEROUS: Rebase pushed commits
git checkout main # Shared with team
git rebase feature # DON'T DO THIS!
# Now everyone's main is broken!

Your Complete Workflow πŸ”„β€‹

Frontend Mentor: "Here's your complete rebase workflow for your situation:"

# STEP 1: Rebase frontend onto main
# (Incorporate main's gitignore commit)
git checkout frontend
git rebase main

# What happens:
# Before: C---[13 commits] (frontend)
# \
# [gitignore] (main)
#
# After: C---[gitignore]---[13 new commits] (frontend)

# STEP 2: Switch to main and merge
git checkout main
git merge frontend

# This is a "fast-forward" merge:
# main pointer just moves forward to frontend's position

# Final result:
# A---B---C---[gitignore]---[13 commits] (main, frontend)

Backend Dev: "So rebase prepares my branch, and merge applies it to main?"

Frontend Mentor: "EXACTLY! You've mastered the logic of rebase! πŸŽ‰"

The Safety Net: Using --ff-only πŸ›‘οΈβ€‹

Backend Dev: "Wait, my mentor told me to use git merge --ff-only frontend instead of just git merge frontend. What's the difference?"

Frontend Mentor: "Excellent catch! The --ff-only flag is a safety mechanism that ensures your rebase worked correctly!"

What is Fast-Forward Merge? πŸš€β€‹

Frontend Mentor: "First, let's understand what a fast-forward merge is:"

Before Fast-Forward:
A---B---C---D---E---F (frontend)
↑
(main)

After Fast-Forward:
A---B---C---D---E---F (main, frontend)
↑
(both branches point here)

Backend Dev: "So a fast-forward merge is when main just 'moves forward' to catch up with frontend?"

Frontend Mentor: "EXACTLY! No new commit is created - main just moves its pointer forward. This only works when main hasn't moved since the rebase."

The --ff-only Flag πŸ”’β€‹

Frontend Mentor: "Now, the --ff-only flag tells Git: 'Only merge if you can fast-forward. Otherwise, FAIL!'"

# Without --ff-only (allows merge commit)
git checkout main
git merge frontend
# βœ“ Creates merge commit if can't fast-forward

# With --ff-only (enforces fast-forward)
git checkout main
git merge --ff-only frontend
# βœ“ Only succeeds if fast-forward is possible
# βœ— Fails if merge commit would be needed

Backend Dev: "So it's like a safety check?"

Frontend Mentor: "EXACTLY! Let me show you why this is important:"

Why --ff-only is Important πŸŽ―β€‹

Scenario 1: Everything Perfect (Fast-Forward Works)

# After rebase
git checkout frontend
git rebase main
# Successfully rebased frontend on main

# Try to merge
git checkout main
git merge --ff-only frontend
# βœ“ Updating e463a2f..a1b2c3d
# βœ“ Fast-forward (no merge commit needed!)

# Result:
A---B---C---[gitignore]---[13 commits] (main, frontend)

Frontend Mentor: "This succeeds because frontend is directly ahead of main - a clean fast-forward!"

Scenario 2: Something Went Wrong (Fast-Forward Fails)

# After rebase
git checkout frontend
git rebase main
# Rebased successfully

# But wait! Someone pushed to main while you were rebasing
# (Another developer pushed a new commit)

git checkout main
git merge --ff-only frontend
# βœ— fatal: Not possible to fast-forward, aborting.

Backend Dev: "So if fast-forward fails, it means something changed?"

Frontend Mentor: "YES! It means either:

  1. Someone pushed to main after your rebase
  2. Something went wrong during rebase
  3. You didn't actually rebase"

Visualizing the Safety Check πŸ”β€‹

Without --ff-only (Allows Merge Commit):

git merge frontend  # No safety check

What could happen:

If someone pushed to main after rebase:

Your rebased commits
/ \
A---B---C---[gitignore]---[new]---[MERGE] (main)
↑
Someone else's commit

Backend Dev: "So we'd get a merge commit even after rebasing?"

Frontend Mentor: "EXACTLY! That defeats the purpose of rebasing for linear history!"

With --ff-only (Enforces Safety):

git merge --ff-only frontend  # Safety check!

What happens:

If someone pushed to main:
βœ— fatal: Not possible to fast-forward, aborting.

You then know to:
1. Fetch latest main: git pull origin main
2. Rebase again: git checkout frontend && git rebase main
3. Try merge again: git checkout main && git merge --ff-only frontend

The Complete Safe Workflow πŸ”„β€‹

Frontend Mentor: "Here's the recommended safe workflow:"

# STEP 1: Update main and rebase
git checkout frontend
git fetch origin main # Get latest remote main
git rebase main # Rebase on main

# What happens:
# Before: C---[gitignore]---[13 commits] (frontend)
# \
# [gitignore] (main)
#
# After: C---[gitignore]---[13 new commits] (frontend)

# STEP 2: Switch to main and fast-forward merge
git checkout main
git merge --ff-only frontend # Safety check enabled!

# If this succeeds:
# βœ“ Perfect! Linear history maintained
# A---B---C---[gitignore]---[13 commits] (main, frontend)

# If this fails:
# βœ— Someone pushed to main after your rebase
# β†’ Go back to STEP 1 and rebase again

Backend Dev: "So --ff-only is like a validation that our rebase worked correctly?"

Frontend Mentor: "PERFECT! It validates that:

  1. βœ“ Rebase completed successfully
  2. βœ“ No one pushed to main since rebase
  3. βœ“ Linear history will be maintained"

What Happens Without --ff-only? βš οΈβ€‹

Backend Dev: "What if I don't use --ff-only?"

Frontend Mentor: "Git will create a merge commit, which defeats the purpose of rebasing!"

# Without --ff-only
git checkout main
git merge frontend

If someone pushed to main after rebase:

        Your rebased commits
/ \
A---B---C---[gitignore]---[new]---[MERGE] (main)
↑ ↑
Someone else's Merge commit
commit (defeats rebase!)

Backend Dev: "So we went through all the trouble of rebasing, but ended up with a merge commit anyway?"

Frontend Mentor: "EXACTLY! That's why --ff-only is important - it prevents this from happening!"

Real-World Example πŸŒβ€‹

Frontend Mentor: "Let me show you a real scenario that happened:"

# 9:00 AM - You start rebasing
git checkout frontend
git rebase main
# Successfully rebased 13 commits

# 9:05 AM - While you were rebasing, teammate pushes to main
# (You don't know this yet!)

# 9:10 AM - You try to merge WITHOUT --ff-only
git checkout main
git merge frontend
# Creates merge commit M (not what you wanted!)

# Result:
Your 13 commits
/ \
A---B---C---[gitignore]---[teammate]---M (main)

Now with --ff-only:

# 9:00 AM - You start rebasing
git checkout frontend
git rebase main
# Successfully rebased 13 commits

# 9:05 AM - Teammate pushes to main

# 9:10 AM - You try to merge WITH --ff-only
git checkout main
git merge --ff-only frontend
# βœ— fatal: Not possible to fast-forward, aborting.

# You immediately know something changed!
git fetch origin main
git checkout frontend
git rebase main # Rebase on latest main
git checkout main
git merge --ff-only frontend # Now it works!

Backend Dev: "So --ff-only caught the problem and forced me to update my rebase!"

Frontend Mentor: "YES! That's exactly its purpose!"

Other Merge Flags πŸš©β€‹

Frontend Mentor: "While we're here, let me show you related flags:"

# --ff-only: Only fast-forward, fail otherwise
git merge --ff-only frontend
# βœ“ Fast-forward if possible
# βœ— Fail if merge commit needed

# --no-ff: Never fast-forward, always create merge commit
git merge --no-ff frontend
# βœ“ Always creates merge commit
# Use when you want to preserve branch history

# --ff (default): Fast-forward if possible, merge commit otherwise
git merge frontend
# βœ“ Fast-forward if possible
# βœ“ Create merge commit if needed

Backend Dev: "So --ff-only is the strict mode?"

Frontend Mentor: "Exactly! It's strict mode for ensuring linear history after rebase."

The Complete Command πŸ“β€‹

Frontend Mentor: "So your complete workflow is:"

# Full command with safety
git checkout main && git merge --ff-only frontend

# Breaking it down:
# git checkout main β†’ Switch to main branch
# && β†’ Only continue if checkout succeeds
# git merge --ff-only β†’ Merge with fast-forward enforcement
# frontend β†’ Merge frontend branch

Backend Dev: "The && ensures we're on main before merging?"

Frontend Mentor: "YES! Another safety feature. If checkout fails, merge won't run."

When Fast-Forward Fails: The Fix πŸ”§β€‹

Frontend Mentor: "If --ff-only fails, here's how to fix it:"

# 1. Fast-forward fails
git merge --ff-only frontend
# fatal: Not possible to fast-forward, aborting.

# 2. Update your local main
git pull origin main

# 3. Go back to frontend and rebase again
git checkout frontend
git rebase main

# 4. Try merge again
git checkout main
git merge --ff-only frontend
# βœ“ Should work now!

Backend Dev: "So the fix is always to rebase again on the latest main?"

Frontend Mentor: "EXACTLY! Rebase ensures your commits are on top of the absolute latest main."

The Aha Moment πŸ’‘β€‹

Backend Dev: "So --ff-only is like a quality gate that ensures:

  1. My rebase actually worked
  2. No one changed main since I rebased
  3. I'll get a clean linear history"

Frontend Mentor: "PERFECT! You've completely understood why --ff-only is recommended after rebase! It's your safety net for maintaining clean Git history! 🎯"

Backend Dev: "And if it fails, I just need to rebase again on the latest main and try again!"

Frontend Mentor: "YES! You've mastered the safe rebase workflow! πŸš€"

Common Rebase Scenarios πŸ“‹β€‹

Scenario 1: Main Hasn't Moved (Fast-Forward)​

Before:
D---E---F (frontend)
/
A---B---C (main - hasn't moved)

After rebase:
A---B---C---D---E---F (frontend)

(Commits don't change because base is the same!)

Scenario 2: Main Has Moved (Your Case!)​

Before:
D---E---F (frontend)
/
A---B---C---X (main - moved!)

After rebase:
A---B---C---X---D'---E'---F' (frontend)

(Commits rebuilt on new base X!)

Scenario 3: Merge Conflicts During Rebase​

git rebase main
# Auto-merging file.txt
# CONFLICT (content): Merge conflict in file.txt

# Fix the conflict
vim file.txt # Resolve conflict
git add file.txt
git rebase --continue

# Repeat for each conflicting commit

Backend Dev: "So rebase can have conflicts just like merge?"

Frontend Mentor: "Yes! The difference is you resolve conflicts for EACH commit during rebase, vs one conflict at the end during merge."

The Mental Model πŸ§ β€‹

Frontend Mentor: "Here's the mental model you should have:"

MERGE:
"Combine two branches at their current endpoints"
Result: Shows parallel history

REBASE:
"Replay my commits on top of another branch"
Result: Linear history (rewrites commit hashes)

Backend Dev: "So merge is about combining, rebase is about replaying?"

Frontend Mentor: "PERFECT! That's exactly it!"

Visualizing the Difference πŸ“Šβ€‹

STARTING POINT:
D---E---F (feature)
/
A---B---C---X---Y (main)


OPTION 1: MERGE (git checkout main && git merge feature)
D---E---F
/ \
A---B---C---X---Y---M (main)
↑
merge commit


OPTION 2: REBASE (git checkout feature && git rebase main)
A---B---C---X---Y---D'---E'---F' (feature)
↑
new base

Key Takeaways πŸŽ―β€‹

  1. Rebase Replays Commits

    • Takes your commits and applies them on a new base
    • Creates new commit hashes (same changes, different parent)
  2. Linear vs Branching History

    • Rebase: Linear history (looks sequential)
    • Merge: Branching history (shows parallel work)
  3. Rebase Updates Feature Branch

    • Incorporates latest changes from main
    • Makes your commits based on latest code
  4. Two-Step Process

    • Step 1: Rebase feature onto main (updates feature)
    • Step 2: Merge feature into main (updates main)
  5. Golden Rule

    • Never rebase public/shared commits
    • Only rebase local, unpushed commits

The Final Aha Moment πŸ’‘β€‹

Backend Dev: "So when my mentor saw that main has 1 commit that frontend doesn't have, they knew we need to rebase so that my 13 commits include that 1 commit at their base?"

Frontend Mentor: "EXACTLY! Without rebase, your commits would be missing the gitignore update. With rebase, all your commits are rebuilt on top of that update, so they include it!"

Backend Dev: "And that's why we do rebase BEFORE merging - to make sure our commits have the latest base!"

Frontend Mentor: "PERFECT! You've completely understood the logic of rebase! πŸš€"

Backend Dev: "This makes so much sense now. It's not just about clean history - it's about making sure my commits are built on the latest code!"

Frontend Mentor: "That's the key insight! Rebase is about re-basing your commits on a new foundation. Now you understand why it's called 'rebase'! 🎯"


Have you been confused by when to use rebase vs merge? Share your experience in the comments below! πŸ’­