Learning Objectives

By the end of this module, you will be able to:

  1. Explain what rebase does mechanically — replaying commits onto a new base
  2. Compare rebase vs merge and articulate when to use each
  3. Apply the golden rule: never rebase shared/public history
  4. Perform a simple rebase to bring a topic branch up to date with main
  5. Resolve conflicts during a rebase using the same tools you use for merge conflicts

1. What Is a Rebase?

A rebase changes the base of your branch. It takes the commits on your branch, detaches them, moves the branch pointer to a new starting point, and replays each commit one by one on top of that new base.

The formal definition — "change the base of your branch" — is technically correct but not very helpful. A more practical way to think about it:

A rebase makes it look like you started your work today, even if you actually branched off weeks ago.

The Mechanics, Step by Step

Suppose you're on topic and you run git rebase main:

Before rebase:

      D ← E          ← topic, HEAD
     /
A ← B ← C ← F ← G   ← main


Step 1: Git walks backwards from topic (E → D) and computes the
        diff (patch) for each commit. Stores them in temporary files.

Step 2: Git moves the topic pointer to where main is (commit G).
        Commits D and E become unreachable.

      D ← E          (orphaned — no pointer)
     /
A ← B ← C ← F ← G   ← main, topic, HEAD


Step 3: Git replays each patch, one by one, creating NEW commits.

A ← B ← C ← F ← G ← D' ← E'   ← topic, HEAD
                  ↑
                main

After rebase:
  - D' and E' have the SAME changes as D and E
  - But they are DIFFERENT commits (different SHA hashes)
  - The original D and E are orphaned (eventually garbage collected)
  - The history is now linear

Why Are the Commits "New"?

Remember from Module 3: a commit's SHA hash is computed from its content, its parent, the author, and the timestamp. When you rebase, the parent changes (D' now points to G instead of B), so the hash must be different. The changes (diffs) are the same, but the commits are entirely new objects in Git's database.


2. Rebase vs Merge

Both rebase and merge solve the same problem: integrating changes from one branch into another. They differ in how they record that integration.

Merge: Preserves History as It Happened

git switch main
git merge topic

      D ← E
     /      \
A ← B ← C ← M   ← main, HEAD
  • Creates a merge commit (M) with two parents
  • The branch topology is preserved — you can see that work happened in parallel
  • No commits are rewritten — safe for shared history

Rebase: Rewrites History to Look Linear

git switch topic
git rebase main

A ← B ← C ← D' ← E'   ← topic, HEAD
         ↑
       main
  • No merge commit — history is a straight line
  • Original commits are replaced with new ones (different SHAs)
  • Looks like the developer started after C, not after B

Comparison Table

AspectMergeRebase
History shapeBranching and merging visibleLinear, as if sequential
New commits createdOne merge commitN new commits (one per replayed commit)
Original commits preservedYesNo — replaced with new SHAs
Safe on shared branchesYesNo — rewrites history
Conflicts resolvedOnce (during merge)Once per replayed commit
git bisect friendlinessMerge commits complicate bisectClean linear history is ideal for bisect
git log readabilityCan be noisy with many branchesClean and easy to follow

When to Use Each

Use merge when:

  • Integrating a finished feature into main (the final merge)
  • You need to preserve the exact history for audit or compliance
  • Multiple people are working on the same branch

Use rebase when:

  • Keeping your topic branch up to date with main while you work
  • Cleaning up your commit history before creating a PR
  • You want git log --oneline to tell a clear story

The common pattern: Rebase while developing, merge when done.

# While working on your feature (regularly):
git fetch origin
git rebase origin/main
 
# When feature is complete (once):
git switch main
git merge --no-ff topic

3. The Golden Rule of Rebase

Never rebase commits that have been pushed to a shared branch that others are working on.

This is the single most important rule in Git. Here's why:

You and Alice both have:
A ← B ← C    ← main, origin/main

You rebase and force push:
A ← B ← C'   ← main (your version — C is gone)

Alice still has:
A ← B ← C    ← main (her version — C is still there)

Alice does git pull:
A ← B ← C ← M    ← main
     ↖      ↗
      C'──┘

Alice now has BOTH versions of C — chaos.

When you rebase, the old commits still exist in other people's clones. When they pull, Git tries to merge your rewritten history with their copy of the original history. The result is duplicate commits, confusion, and broken trust.

When Force Pushing Is Acceptable

There's one exception: your own topic branch that nobody else has checked out.

# You're working alone on feature-login
git rebase main
git push --force-with-lease    # Safe: checks that no one else pushed

In practice, this is the normal workflow. You rebase your personal branch, force push it, and nobody is affected. The danger only arises when you rebase main, develop, or any branch that other developers actively pull from.

The Coffee Algorithm

The transcript captures this perfectly:

  1. You push, your colleague pulls
  2. You rebase, you force push
  3. Your colleague pulls — cries
  4. You buy them coffee
  5. They do a hard reset to your rewritten branch
  6. You're friends again

In practice, step 5 is straightforward — but step 3 shouldn't happen if you follow the golden rule.


4. The Simple Rebase Workflow

This is the most common use of rebase in day-to-day work: keeping your topic branch up to date with main.

The Scenario

You branched off main a week ago. Since then, your teammates merged three PRs into main. Your branch is "behind." You want to catch up.

Your situation:
                 X ← Y          ← topic (your branch)
                /
A ← B ← C ← D ← E ← F ← G    ← main (updated by teammates)

Option A: Merge Main into Topic (What You Should Avoid)

git switch topic
git merge main

This works but creates an ugly graph:

                 X ← Y ← M     ← topic
                /        /
A ← B ← C ← D ← E ← F ← G    ← main

Every time you sync with main, another merge commit appears. After a week of regular syncing, your PR has more merge commits than actual work commits. Reviewers hate this.

Option B: Rebase Topic onto Main (The Clean Way)

git switch topic
git rebase main

Result:

A ← B ← C ← D ← E ← F ← G ← X' ← Y'   ← topic
                              ↑
                            main

Your two commits sit cleanly on top of the latest main. The PR diff shows only your changes. The graph is linear and easy to read.

Step-by-Step

# 1. Make sure main is up to date
git switch main
git pull
 
# 2. Switch back to your topic branch
git switch topic
 
# 3. Rebase onto main
git rebase main
 
# 4. If conflicts arise, resolve them (see Section 5)
 
# 5. Force push your updated branch (if previously pushed)
git push --force-with-lease

Rebasing Against a Remote Branch Directly

You can skip the switch main && pull step:

git fetch origin
git rebase origin/main

This rebases onto the remote-tracking branch directly without updating your local main. It's faster and avoids switching branches.


5. Resolving Rebase Conflicts

Rebase conflicts work exactly like merge conflicts — same markers, same tools. The difference is when they occur.

Merge: One Shot

During a merge, all conflicts from all diverging commits are combined into a single resolution step. You fix everything at once.

Rebase: One Commit at a Time

During a rebase, Git replays commits sequentially. Each commit might conflict. You fix conflicts for commit D', then continue, and commit E' might also conflict.

Replaying commit D' ... CONFLICT!
  → Resolve → git add → git rebase --continue
Replaying commit E' ... CONFLICT!
  → Resolve → git add → git rebase --continue
Done.

This is actually an advantage: each conflict is smaller and more focused, because you're looking at only one commit's changes at a time.

The Resolution Workflow

# 1. Start the rebase
git rebase main
 
# If a conflict occurs:
 
# 2. Check what's conflicted
git status
# rebase in progress; onto abc1234
# You are currently rebasing branch 'topic' on 'abc1234'.
#   fix conflicts and then run "git rebase --continue"
#
# Unmerged paths:
#   both modified:   app.py
 
# 3. Open your merge tool (or edit manually)
git mergetool
# Or open the file, look for <<<<<<< / ======= / >>>>>>> markers
 
# 4. After resolving, stage the file
git add app.py
 
# 5. Continue the rebase
git rebase --continue
 
# If more commits conflict, repeat steps 2-5

Your Three Escape Hatches

CommandEffect
git rebase --continueAccept the resolution and replay the next commit
git rebase --abortCancel the entire rebase and return to the original state
git rebase --skipSkip the current commit entirely (rarely needed)

--abort is your panic button. It restores your branch to exactly where it was before the rebase started. No harm done.

--skip is for edge cases — for example, if after resolving conflicts the commit would be empty (because the same change was already applied upstream), you can skip it.

A Note on Conflict Labels

During a merge, "ours" = the branch you're on, "theirs" = the branch you're merging in. During a rebase, the labels are swapped:

During rebase of topic onto main:
  "ours"   = main (the new base you're replaying onto)
  "theirs" = topic (the commit being replayed)

This catches everyone off guard the first time. Remember: during a rebase, Git has already reset to main (step 2 of the mechanics). So "ours" is main, and each replayed commit is "theirs."


6. git pull --rebase

When you run git pull on a branch that's behind its upstream, Git has to reconcile the divergence. By default, it merges — creating unnecessary merge commits in your local history.

# Default: git pull = fetch + merge
# If local and remote diverged, creates a merge commit
 
# Better: git pull --rebase = fetch + rebase
# Replays your local commits on top of the remote

Visualized

Before pull (diverged):
  Local:   A ← B ← X          ← main, HEAD
  Remote:  A ← B ← C ← D     ← origin/main

git pull (merge):
  A ← B ← X ← M    ← main
       ↖      ↗
        C ← D       ← origin/main

git pull --rebase:
  A ← B ← C ← D ← X'   ← main
                ↑
          origin/main

The rebase version is cleaner. Your local commit X is replayed on top of the remote changes, as if you had waited for C and D before writing X.

Setting the Default

# Make all future pulls use rebase
git config --global pull.rebase true
 
# Or the safest option: fail if fast-forward isn't possible
git config --global pull.ff only

With pull.rebase true, every git pull automatically becomes git pull --rebase. You can override on a case-by-case basis with git pull --no-rebase.


7. What If Rebase Didn't Exist?

To appreciate rebase, consider the alternative: using only merges to keep a topic branch up to date.

Without rebase (merge main into topic repeatedly):

           X ── M₁ ── Y ── M₂ ── Z    ← topic
          /    /          /
A ← B ← C ← D ←──── E ← F ← G       ← main

With rebase (rebase topic onto main before final merge):

A ← B ← C ← D ← E ← F ← G ── X' ── Y' ── Z'   ← topic
                              ↑
                            main

The merge-based approach creates a tangled web. Every sync adds a merge commit. The PR diff includes noise from the merge commits. git log --oneline --graph becomes unreadable.

The rebase-based approach keeps the topic branch as a clean, linear series of commits on top of the latest main. The PR shows only your actual changes. Reviewers can step through your commits in logical order.


Command Reference

CommandDescription
git rebase <branch>Rebase current branch onto <branch>
git rebase origin/mainRebase onto remote-tracking branch (no switch needed)
git rebase --continueContinue after resolving a conflict
git rebase --abortCancel the rebase entirely, restore original state
git rebase --skipSkip the current commit (use rarely)
git pull --rebaseFetch + rebase instead of fetch + merge
git push --force-with-leaseSafe force push after rewriting history
git config --global pull.rebase trueMake rebase the default pull strategy

Hands-On Lab: Rebase in Practice

Setup

mkdir rebase-lab && cd rebase-lab
git init
git switch -c main

Create the shared history:

echo "line 1: shared foundation" > app.txt
git add app.txt
git commit -m "Initial commit"
 
echo "line 2: base feature" >> app.txt
git add app.txt
git commit -m "Add base feature"

Part A: A Clean Rebase (No Conflicts)

Step 1 — Create a topic branch and add commits

git switch -c feature-alpha
echo "line 3: alpha work" >> app.txt
git add app.txt
git commit -m "Alpha: add first change"
 
echo "line 4: more alpha work" >> app.txt
git add app.txt
git commit -m "Alpha: add second change"

Step 2 — Simulate main advancing (someone else merges work)

git switch main
echo "# Configuration" > config.txt
git add config.txt
git commit -m "Add config file"
 
echo "debug=false" >> config.txt
git add config.txt
git commit -m "Add debug setting"

Step 3 — Observe the divergence

git log --oneline --graph --all

Checkpoint: You should see two branches diverging from "Add base feature." The main branch has 2 new commits, and feature-alpha has 2 new commits.

Step 4 — Rebase feature-alpha onto main

git switch feature-alpha
git rebase main

This should complete without conflicts (the branches modified different files).

git log --oneline --graph --all

Checkpoint: The graph should now be linear. feature-alpha sits on top of main's latest commit. All four commits appear in a straight line.

Step 5 — Verify the SHAs changed

Compare the commit hashes for your alpha commits before and after. They are different — the commits were recreated with new parents.

Part B: Rebase with Conflicts

Step 6 — Create a new branch that will conflict

git switch main
git switch -c feature-beta
 
# Modify line 1 (same file, same area as what main will change)
sed -i '' 's/line 1: shared foundation/line 1: beta foundation/' app.txt
git add app.txt
git commit -m "Beta: update foundation"

Step 7 — Make main modify the same line

git switch main
sed -i '' 's/line 1: shared foundation/line 1: MAIN foundation/' app.txt
git add app.txt
git commit -m "Main: update foundation"

Step 8 — Attempt the rebase

git switch feature-beta
git rebase main

Expected: Git reports a conflict in app.txt.

Step 9 — Resolve the conflict

git status

Open app.txt and look for the conflict markers:

<<<<<<< HEAD
line 1: MAIN foundation
=======
line 1: beta foundation
>>>>>>> Beta: update foundation

Remember: during rebase, HEAD (ours) is main (the new base), and the incoming change is your topic commit.

Choose a resolution (e.g., keep "beta foundation" or combine them):

# Edit app.txt to resolve, then:
git add app.txt
git rebase --continue

Checkpoint: Run git log --oneline --graph --all. The history should be linear, with your beta commit replayed on top of main's latest.

Part C: Compare with Merge

Step 10 — Create the same scenario but use merge instead

git switch main
git switch -c feature-gamma
 
sed -i '' 's/line 1:.*foundation/line 1: gamma foundation/' app.txt
git add app.txt
git commit -m "Gamma: update foundation"
 
git switch main
sed -i '' 's/line 1:.*foundation/line 1: MAIN-v2 foundation/' app.txt
git add app.txt
git commit -m "Main: another foundation update"

Step 11 — Merge main into feature-gamma (instead of rebasing)

git switch feature-gamma
git merge main

Resolve the conflict, then:

git add app.txt
git commit

Step 12 — Compare the graphs

git log --oneline --graph --all

Checkpoint: The gamma branch shows the classic diamond merge pattern (diverge → merge commit). The beta branch (from Part B) is a clean straight line. This is the visual difference between merge and rebase.

Part D: Abort Practice

Step 13 — Start a rebase and abort it

git switch main
git switch -c feature-abort
 
echo "abort test" >> app.txt
git add app.txt
git commit -m "Abort: test commit"
 
git switch main
echo "conflicting change" >> app.txt
git add app.txt
git commit -m "Main: conflicting change"
 
git switch feature-abort
git rebase main

When the conflict appears:

# Don't resolve it — just abort
git rebase --abort
git log --oneline

Checkpoint: Your branch is back to exactly where it was before the rebase. The abort restored everything.

Cleanup

cd ..
rm -rf rebase-lab

Challenge

Create a repository with a main branch and three topic branches (feature-1, feature-2, feature-3) that all diverge from the same commit on main. Advance main with two commits. Rebase all three features onto the updated main, one at a time, merging each into main with --no-ff before rebasing the next. End with a clean sequential graph as if the three developers waited for each other.


Common Pitfalls & Troubleshooting

PitfallSymptomFix
Rebasing a shared branchColleagues get duplicate commits after pullingCommunicate with team, have them git reset --hard origin/branch
Using --force instead of --force-with-leaseOverwrites colleague's push silentlyAlways use --force-with-lease
Confusing "ours" and "theirs" during rebaseAccidentally keep the wrong side of a conflictRemember: during rebase, "ours" = the base branch (main), "theirs" = your commit
Not fetching before rebasingRebasing onto a stale version of mainAlways git fetch first, or rebase onto origin/main directly
Rebasing a very long-lived branchDozens of conflicts, one per replayed commitRebase frequently (daily) to keep conflicts small
Rebase seems to "lose" commitsOld SHAs no longer appear in logExpected — rebase creates new commits. Use git reflog to find originals if needed
Forgetting --continue after resolvingSitting in a half-finished rebase stategit status tells you; run git rebase --continue

Pro Tips

  1. Rebase early, rebase often — The longer your branch diverges from main, the more conflicts you'll face. Rebasing daily keeps each resolution trivial.

  2. Rebase onto the remote directly to save time:

    git fetch origin
    git rebase origin/main

    No need to switch to main and pull first.

  3. Use git log to preview what will be replayed:

    git log --oneline main..topic

    This shows exactly the commits that rebase will replay.

  4. Recover from a bad rebase with reflog:

    git reflog
    # Find the SHA of topic before the rebase
    git reset --hard <sha>
  5. The team convention matters more than the tool. Some teams use merge-only workflows, some use rebase-only. Neither is wrong. What's wrong is mixing them inconsistently. Agree on a strategy and stick to it.

  6. Rebase before creating your PR, then merge the PR with --no-ff. This gives you the best of both worlds: clean commit history within the PR, plus a merge commit on main that marks where the feature was integrated.


Quiz / Self-Assessment

Q1: In your own words, describe what git rebase main does when you're on a topic branch.

Answer

Git identifies the commits unique to your topic branch (since it diverged from main), temporarily saves their diffs, moves the topic branch pointer to where main is, then replays each saved diff on top, creating new commits. The result is that your topic branch appears to have been started from the tip of main.

Q2: After a rebase, why do the commit SHAs change even though the code changes are the same?

Answer

A commit's SHA is computed from its content, parent pointer, author info, and timestamp. After a rebase, each replayed commit has a different parent (it now points to the new base instead of the old one). A different parent means a different input to the hash function, which produces a different SHA. The code diff is the same, but the commit object is different.

Q3: What is the golden rule of rebase?

Answer

Never rebase commits that have been pushed to a shared branch that others are working on. Rebase rewrites history (creates new commit SHAs), and if others have pulled the old commits, they'll end up with duplicates when they pull your rewritten version.

Q4: During a rebase conflict, what does "ours" refer to?

Answer

During a rebase, "ours" refers to the base branch (e.g., main) — the branch you're rebasing onto. This is the opposite of merge, where "ours" is the branch you're currently on. The swap happens because rebase first resets to the base branch, then replays your commits as "theirs."

Q5: You're working on feature-x and need to incorporate the latest changes from main. Write the commands for both the merge approach and the rebase approach.

Answer

Merge approach:

git switch feature-x
git merge main

Rebase approach:

git switch feature-x
git rebase main
# (then, if previously pushed:)
git push --force-with-lease

Q6: What does git rebase --abort do? When would you use it?

Answer

git rebase --abort cancels the in-progress rebase and restores your branch to exactly the state it was in before the rebase started. You use it when you encounter unexpected conflicts and want to start over, or when you realize you're rebasing the wrong branch.

Q7: Why is git pull --rebase generally preferred over git pull (with merge)?

Answer

git pull --rebase replays your local commits on top of the remote changes, creating a clean linear history. Plain git pull creates a merge commit every time local and remote have diverged, cluttering the log with unnecessary merge commits that have no semantic meaning (they just record a sync point).

Q8: You rebase your feature branch and now git push is rejected. Why? What should you do?

Answer

git push is rejected because rebase rewrote history — the local commits have different SHAs than the ones on the remote. Git sees this as a non-fast-forward push and refuses it to protect you from losing work. You should use git push --force-with-lease, which is safe because it verifies no one else pushed to your branch since your last fetch.

Q9: True or false: a rebase can produce more conflict resolution steps than a merge for the same set of changes.

Answer

True. A merge combines all divergent changes into a single resolution step. A rebase replays commits one at a time, and each commit can produce its own conflict. If you have 5 commits on your branch, you might have to resolve conflicts 5 separate times. However, each individual resolution is typically smaller and easier to reason about.

Q10: Explain the recommended workflow: "rebase while developing, merge when done."

Answer

While you're actively working on a feature branch, regularly rebase it onto main to stay up to date. This keeps your branch clean and minimizes conflict size. When your feature is complete and the PR is approved, merge it into main with --no-ff. The final merge commit serves as a marker in history showing when the feature was integrated, while the individual commits within the branch tell the story of how it was built.