LEARN · PHASE 02 · LESSON 14

Merge and Rebase

You can tell the difference between "combining two lines of work" and

Type
Learn
Prerequisites
Phase 02, Lessons 12 and 13
Time
~40 minutes

Learning Objectives

  • Distinguish a merge, a fast-forward, and a rebase by what they produce
  • Rebase a feature branch onto an updated main and read the result
  • Apply the rule that decides merge vs rebase for a given branch
  • Recover from a rebase you did not mean to run

The Problem

git pull gave you a graph you do not understand: a merge commit in the middle of your feature branch, a row of duplicate-looking hashes, and a message that says "Merge branch 'main' into feature". You cannot tell what happened, so you cannot undo any of it.

Worse: you pushed that branch. A colleague rebased the same branch, pushed it, and now you have two histories that share commits but not hashes. Merging both into main gives you every change twice.

Nobody chose any of this. git pull did it by default, and the default is not a decision you made.


The Concept

Both commands answer the same question — how do I combine two lines of work? They disagree about what the answer should look like afterwards.

Merge keeps both lines and records the join:

flowchart LR
    A1[A] --> A2[B] --> M[M: merge commit]
    B2 --> M
Mermaid source — renders as a diagram where mermaid.js is available

History shows that two lines existed and were combined. Nobody's commits are rewritten, so a merge is safe on shared history.

Rebase moves your commits to sit on top of the other line, as if you had written them there:

flowchart LR
    A1[A] --> A2[B]
    A2 --> R1[C'] --> R2[D']
Mermaid source — renders as a diagram where mermaid.js is available

Your commit C is replaced by a new commit C' with the same message and the same change, but a different parent — so a different hash. That is why rebasing a pushed branch hurts people: their C and your C' are unrelated objects, and merging both applies the change twice.

Fast-forward is the case where merge needs no merge commit: the other line has not moved away, so your label just slides forward. Git takes it automatically, and it is the reason a branch that has never been shared merges with no merge commit at all.

git merge main     # combine, keep both lines, add a merge commit if needed
git rebase main    # replay my commits on top of main

The rule

Rebase a branch you own. Merge a branch anyone else has.

Rebase is private history editing, and it is the right tool for a branch that exists only on your machine. Once a branch is pushed and someone else has it, their history is a fact about the world; rewriting your copy of it creates two incompatible versions of the same change. Merge instead.

For a repo you work on alone, and for a repo you are the only author of, the real reason to prefer rebase is readability: a linear history is easier to scan, git bisect works, and "which of these six commits is the bug" has a sensible answer.

Pull is two commands

git pull = git fetch + (git merge | git rebase). Say which one you mean:

git pull --rebase      # your commits land on top, no merge commit
git pull --no-rebase   # merge commit, if there is anything to merge

Whichever you pick, be the one who picked it.

Undoing a rebase

If a rebase goes wrong and you have not pushed:

git rebase --abort      # while in progress
git reflog              # after it finished — find the pre-rebase position
git reset --hard HEAD@{1}

Lesson 16 covers this properly. The one-line answer here: the reflog still has your old commits, because nothing was deleted.

The reframe: merge says "these two things both happened, and here is a

commit that says so"; rebase says "pretend this branch had been written after

that one all along" — which is a lie you may only tell about history nobody

else has seen.


Build It

1. Make two lines of history

cd ~/git-drill
git switch -c feature
printf 'feature\n' > feature.txt && git add feature.txt && git commit -m "feature work"
git switch main
printf 'main\n' > main.txt && git add main.txt && git commit -m "main work"
git log --oneline --graph --all

Two lines. No join yet.

✅ You pass when the graph shows two separate commits with no merge node.

2. Merge and see the join

git switch feature
git merge main
git log --oneline --graph

A merge commit appears with two parents. Both lines survive.

✅ You pass when git log --oneline on feature shows the merge commit and both original commits are still present in history.

3. Undo the merge without losing anything

git reset --hard HEAD~1
git log --oneline

✅ You pass when you are back on a single line with the pre-merge history intact, and you can say why reset --hard was safe here (nothing of yours was in the merge commit).

4. Rebase instead

git rebase main
git log --oneline --graph

Linear. "feature work" sits on top of "main work". No merge commit.

✅ You pass when the graph is a straight line and there is no merge node.

5. Watch the hashes change

Before the rebase, note the hash of feature work (git log --oneline on feature). After, note it again.

✅ You pass when the hash is different and you can say exactly which field changed — the parent — and why that makes the two commits different objects.

6. Try it both ways and compare

git switch -c experiment main
git merge feature      # merge commit, both lines visible
git switch -c experiment2 main
git rebase feature     # linear
git log --oneline --graph --all

✅ You pass when you have both pictures side by side and can say which one you would rather review.


Use It

Command / conceptWhen you reach for it
git merge <branch>Combining shared history, or keeping both lines visible
git rebase <branch>Updating a private branch on top of newer work
git pull --rebasePulling without a merge commit, deliberately
git pull --no-rebasePulling with a merge commit, deliberately
Fast-forwardWhen the other branch has no new commits of its own — no join needed
Merge commitTwo parents. Says "these lines were combined"
git rebase --abortA rebase in progress went sideways
The ruleRebase what you own, merge what others have

Exercises

  1. Take one real branch you have not pushed. Rebase it onto main. Compare git log --graph --oneline before and after. ✅ You pass when you have both graphs and can name what a reviewer would find easier in the rebased one, and what they would lose.
  1. Merge that same branch instead. Check whether git created a merge commit or fast-forwarded, and say which happened and why. ✅ You pass when you can say which of the two outcomes you got and name the condition that decided it.
  1. Rebase a branch that you pushed yesterday and someone else has pulled. Describe, in writing, what a teammate would experience if they then merged both copies of your branch into main. ✅ You pass when your description names duplicate changes appearing twice, and you can point at the rebase as the cause.

Key Terms

TermWhat people sayWhat it actually means
merge"joining branches"Combine two lines and record the join. Adds a merge commit with two parents when needed.
rebase"cleaning up history"Replay your commits on another base. New hashes, same changes. Safe only on history nobody else has.
fast-forward"a merge"A merge where one line has no new commits, so the label just slides. No merge commit.
merge commit"the merge"A commit with two parents. The explicit record that two lines joined.
git pull"getting the latest"fetch plus a merge or rebase you did not choose. Use --rebase / --no-rebase.
rebasing shared history"fixing the branch"Produces two incompatible versions of the same commits. Every later merge sees the change twice.
git rebase --abort"undo"Bails out of an in-progress rebase. Nothing was committed yet.

What this lesson skipped

Interactive rebase (git rebase -i). Squashing, reordering, rewording commits. It is the same mechanism plus an editor, and it earns its own practice when you are cleaning up your own unpushed work — Phase 16's territory.

Merge strategies beyond fast-forward and recursive. ours, octopus, and subtree merges exist. Nothing in this course needs them.

Bisect. git bisect finds a bad commit by binary search — far easier once history is linear, which is the argument this lesson made. It arrives with debugging material later.

Check yourself

4 questions. Answer before you reveal — recognising the right option is not the same as being able to produce it.

  1. You pushed a feature branch and a teammate pulled it. Now you rebase that branch onto main and force-push. What goes wrong for the teammate?

    1. Nothing — force-push replaces the branch and their next pull reconciles it
    2. Their existing commits and your rebased ones are different objects with the same changes, so merging both applies the work twice
    3. Their local branch is deleted when they pull
    4. The force-push is rejected because the branch has a merge commit
    Show answer

    B. Their existing commits and your rebased ones are different objects with the same changes, so merging both applies the work twice

    Rebase gives your commits new parents, so new hashes — while their copy keeps the old ones. Neither git nor the merge machinery knows they are the same change, so a merge of both branches applies it twice. Force-push replacing a branch (A) is exactly the failure: it discards what they had without merging it. Local branches are not deleted by a remote force-push (C), and the push is accepted, not rejected (D).

  2. git merge main on your branch reports 'Fast-forward'. What does that tell you?

    1. main had no commits your branch did not already contain, so your label slid forward with no merge commit
    2. main was fully merged into your branch with a merge commit
    3. Your branch was fast-forwarded to a remote copy of main
    4. The merge was trivial because the branches touched disjoint files
    Show answer

    A. main had no commits your branch did not already contain, so your label slid forward with no merge commit

    Fast-forward is specifically the no-join case: one line has no commits the other lacks, so there is nothing to record and no merge commit is created. A merge commit (B) is the opposite outcome. Remote copies are a different concept entirely (C) — Lesson 18. And touching disjoint files (D) is a real thing that helps, but it does not prevent a merge commit when both lines have new commits.

  3. You started pulling and the merge it triggered made a mess of your history. Which reason is the one the lesson says to act on?

    1. git pull always merges; pick the variant explicitly with --rebase or --no-rebase so the choice is yours
    2. git pull should be configured once in .gitconfig and never thought about again
    3. Only rebase is safe on a shared branch, merge should never be used
    4. The mess is unavoidable; the fix is a larger --force flag on push
    Show answer

    A. git pull always merges; pick the variant explicitly with --rebase or --no-rebase so the choice is yours

    pull is fetch plus a join, and the join is the part you should choose out loud. Setting it globally (B) moves the decision somewhere you will forget you made — the same class of bug as letting the default decide. The lesson's rule is directional, not absolute: rebase what you own, merge what others have (C). And force-pushing over it (D) makes the history worse, not the collaboration.

  4. A rebase finished and the result is wrong. You have not pushed. What gets the old commits back?

    1. git rebase --abort, which rewinds the already-finished rebase
    2. git revert, which adds a commit undoing the rebase
    3. The reflog still records where HEAD was before the rebase, so you can reset back to it
    4. Nothing — once a rebase completes, the original commits no longer exist
    Show answer

    C. The reflog still records where HEAD was before the rebase, so you can reset back to it

    A rebase moves labels and creates new commits; it deletes nothing, so every prior HEAD position is still in the reflog. --abort only applies while the rebase is still running (A). revert adds an undo commit on top, which leaves the rebased history in place — a different goal (B). And the originals are emphatically still there (C's neighbour D) — that is why rebasing an unpushed branch is safe enough to recommend.