Skip to content

Merge vs rebase on the commit graph

Merge joins two histories and keeps every commit; rebase rewrites your commits as new ones on a new base, so keep it to unshared work.

Predict first

Before you read: predict

ExamHall candidate app. Dana cut fix/autosave from main and made two commits; the timer fix then landed on main. git log --graph --oneline --all --decorate prints:

* 724bc86 (HEAD -> main) timer: server deadline
| * dc651dd (fix/autosave) autosave: flush on hide
| * 9e6a142 autosave: debounce 800 ms
|/
* 9087394 app: candidate shell

She runs:

git switch fix/autosave
git rebase main
git log --oneline main..fix/autosave

What does the last command list?

Pick one

The 30-second version

A commit records a snapshot, a message and its parents, and its id hashes all of them. Merging diverged branches adds a two-parent commit and keeps every id; an undiverged branch just fast-forwards. A rebase replays each of your commits onto the new base as new commits, so rebase only commits nobody else has.

Merge joins two histories and keeps every commit; rebase rewrites your commits as new ones on a new base, so keep it to unshared work.

How it works

Step 1 of 13

A commit stores a snapshot of the files, the ids of its parent commits, the author, the committer and the message.
In the kitchen
  • a grid reference follows from the previous checkpoint plus the leg → an id covers the parent and the content (steps 1–2)

Analogy: A hillwalking club's route card

Each checkpoint is written as a leg (bearing and distance) from the one before it, and its grid reference is worked out from both.

On the route card In git
a grid reference follows from the previous checkpoint plus the leg an id covers the parent and the content (steps 1–2)
two groups meet at a rendezvous reached from both their last checkpoints; earlier references stay a merge commit: two parents, no id changes (step 4)
the main party never left the hut, so it just walks on along your checkpoints fast-forward (step 5)
re-plotting your legs from where the main party stands keeps each leg but gives every checkpoint a new reference; the old card survives rebase: copies with new ids; the reflog keeps the originals (steps 6–7)
a rendezvous reconciles the routes once; re-plotting fixes each leg crossing re-routed ground conflicts: once per merge, per commit in a rebase (steps 8–9)
a group that planned from your old checkpoint 3 now holds points the official card lacks never rebase shared history (step 13)

In real terms: a merge adds one commit joining two histories; a rebase makes new commits repeating your changes on a new base.

Breaks down when: a walker can copy an old grid reference by hand. Git cannot: an id is computed, so a new parent always means a new id. And unreachable old commits last only until garbage collection.

Contrasting case: a cherry-pick copies one chosen commit onto another branch, and your branch stays where it was.

Examples, run for real

4.1 ExamHall: the reviewed timer fix

EXH-2050: reviewers approved commit a1387a9 on fix/timer, and CI had tested it. The developer then rebased onto main and force-pushed, so the approved id left the branch and its CI status vanished. MR !2350: once a branch is pushed for review, bring main in with git merge; rebase only unpushed commits.

4.1 merge   new commit 4160fd1 parents: f0115cd a1387a9 (main was f0115cd, fix/timer is a1387a9)
4.1 merge   reviewed a1387a9 still in main's history: yes
4.1 rebase  before a1387a9 parent 87b207c | after 87ed8cc parent f0115cd | same change: yes
4.1 rebase  reviewed a1387a9 still on fix/timer: no | still stored: yes | fix/timer@{1} = a1387a9

The merge commit's parents are main's old tip and the reviewed commit, so the reviewed id stays in history (step 4). The rebase makes a new commit with the same change on a new parent (steps 2, 6). The reviewed commit still exists, and fix/timer@{1} names it, but the branch no longer contains it (step 7).

4.2 Proof domain, checkout: where a conflict surfaces

The spring-sale branch sets the basket discount to 10%; main meanwhile set it to 5% on the same line.

4.2 merge   exit 1 · in a merge: true · conflicted: checkout/cart.js · markers: <<<<<<< HEAD / ======= / >>>>>>> main
4.2 rebase  exit 1 · in a rebase: true · stopped while applying: sale: 10% basket discount · markers: <<<<<<< HEAD / ======= / >>>>>>> e0b2e19 (sale: 10% basket discount)

The merge stops once, with the branch as HEAD and main as the other side (step 8). The rebase stops while applying the branch's commit, so HEAD is main's side and the incoming side is your own commit (step 9).

4.3 Proof domain, checkout: rebasing a branch already on origin

Sam pushed feature/gift-cards, then rebased it onto a newer main and ran a plain git push.

4.3 push    exit 1 · ! [rejected] feature/gift-cards -> feature/gift-cards (non-fast-forward)
4.3 push    origin's tip is still 1e2202d (= the pushed 1e2202d: yes) · it is an ancestor of Sam's new tip: no

The rebased tip does not descend from the commit origin holds, so a plain push is refused (steps 6, 12). Origin keeps the old commit, as does every clone that fetched it.

Try it

Answer first, then check. Nothing is saved.

Question 1 of 5

Priya's coupon work is ready. git log --graph --oneline --all --decorate prints:

* 23b0ba4 (feature/coupon) coupon: apply discount
* e37ccbe coupon: parse code
* 74a2573 (HEAD -> main) checkout: tax line
* caf67e0 checkout: cart totals

She runs:

git switch main
git merge feature/coupon

What does the merge add to the repository, and where does main point afterwards?

Pick one

Question 2 of 5

Alex's promo branch sets a kettle's sale price in three commits; meanwhile main renamed the constant on that same line. git log --graph --oneline --all --decorate prints:

* 2ea92e1 (main) kettle: rename to SALE_PRICE_PENCE
| * 1669e47 (HEAD -> feature/kettle-promo) promo: kettle 3499
| * 6a68be5 promo: kettle 3999
| * efd8320 promo: kettle 4499
|/
* 4ff7af3 kettle: sale price constant

Whenever git stops on a conflict, Alex keeps main's new name with his own price on that line, then lets git continue. On one copy of the repository he runs git rebase main on his branch; on another copy, git merge main.

How many times does git stop for a conflict in each?

Pick one

Question 3 of 5

Sam pushed feature/refunds with two commits. Kim pulled it and committed refunds: admin button on top, not yet pushed. Then main gained payments: currency field on origin, and Sam fetched. Next:

sam$ git rebase origin/main
sam$ git push --force origin feature/refunds
kim$ git pull --no-rebase

Kim's git log --oneline now lists each of Sam's two refunds commits twice.

Which one change to these steps would have left Kim with Sam's update, her own commit, and each of Sam's commits once?

Pick one

Question 4 of 5

The admin console has two finished features, cut from main at the same commit. git log --graph --oneline --all --decorate prints:

* b998329 (feature/bulk-unlock) unlock: audit entry
* 32ea1a0 unlock: confirm dialog
* 75256fa unlock: select candidates
| * d5e3cd3 (feature/proctor-notes) notes: panel
| * 0eba212 notes: model
|/
* bb28b74 (HEAD -> main) admin: console shell

The team wants git log --first-parent --oneline main to show one line per feature, and every one of the 5 feature commits to be in main's history, so that git bisect can stop at each. Each feature is integrated from main, proctor notes first, with the same commands.

Which commands meet both goals?

Pick one

Question 5 of 5

Omar pushed feature/seat-map with two commits. Lena pulled it, committed seat-map: legend on top and pushed it. Then main gained a commit on origin. Omar runs:

omar$ git fetch origin
omar$ git rebase origin/main
omar$ git push --force origin feature/seat-map

Every command succeeds, and seat-map: legend is no longer on origin's feature/seat-map.

What removed it?

Pick one

Go deeper

Pro tips
  1. Lost commits after a rebase or reset? Read the branch's reflog, then branch from the entry before it.
    git reflog show feature/wishlist
    git branch rescue feature/wishlist@{1}
    When: a rebase, reset --hard or force-update moved a local branch off commits you still need (step 7). Verify: git branch rescue feature/wishlist@{1} -> a863669 (pre-rebase tip a863669: same); commits on rescue not on main: 2. Status: RAN.
  2. Turn on zdiff3 conflict markers, so each conflict also shows the base text both sides started from.
    git config --global merge.conflictStyle zdiff3
    When: resolving any merge or rebase conflict (steps 8–9). Verify: <<<<<<< HEAD / export const BASKET_DISCOUNT = 0.05; / ||||||| 045f512 / export const BASKET_DISCOUNT = 0; / ======= / export const BASKET_DISCOUNT = 0.1; / >>>>>>> feature/sale. Status: RAN.
Source

Git's documented behaviour is version-specific; the pack ran git 2.52.0. Valid as of 2026-10-01.

  • Pro Git, 3.6 Git Branching: Rebasing: https://git-scm.com/book/en/v2/Git-Branching-Rebasing (progit-rebasing.txt). Quoted: "Do not rebase commits that exist outside your repository and that people may have based work on." and "When you rebase stuff, you're abandoning existing commits and creating new ones that are similar but different."
  • Pro Git, 3.2 Basic Branching and Merging: https://git-scm.com/book/en/v2/Git-Branching-Basic-Branching-and-Merging (progit-basic-merging.txt). Quoted: "Git simplifies things by moving the pointer forward because there is no divergent work to merge together — this is called a "fast-forward."" and a merge commit "is special in that it has more than one parent." Supports steps 4–5.
  • Pro Git, 10.2 Git Internals: Git Objects: https://git-scm.com/book/en/v2/Git-Internals-Git-Objects (progit-git-objects.txt). Quoted: a commit object "specifies the top-level tree for the snapshot of the project at that point; the parent commits if any". Supports steps 1–2.
  • git-rebase documentation: https://git-scm.com/docs/git-rebase (git-rebase.txt). Quoted: "Reapply commits on top of another base tip" and "If there is a merge conflict during this process, git rebase will stop at the first problematic commit and leave conflict markers." Supports steps 6 and 9.
  • git-merge documentation: https://git-scm.com/docs/git-merge (git-merge.txt). Quoted: "Note that fast-forward updates do not create a merge commit", "With --no-ff, create a merge commit in all cases, even when the merge could instead be resolved as a fast-forward." and, for --squash, "This allows you to create a single commit on top of the current branch whose effect is the same as merging another branch". Supports steps 5 and 10.
  • git-push documentation: https://git-scm.com/docs/git-push (git-push.txt). Quoted, for --force: "Usually, git push will refuse to update a branch that is not an ancestor of the commit being pushed." and "It can cause the remote repository to lose commits; use it with care."
  • git-log documentation: https://git-scm.com/docs/git-log (git-log.txt). Quoted, for --first-parent: "When finding commits to include, follow only the first parent commit upon seeing a merge commit."
  • git-reflog documentation: https://git-scm.com/docs/git-reflog (git-reflog.txt). Quoted: "Reference logs, or "reflogs", record when the tips of branches and other references were updated in the local repository." Supports step 7 and pro tip 1.

Checked against the tools and docs as of 2026-10-01.