Skip to main content
CodeOath
← All posts

Git95 min total · 17 parts

Git Internals and Workflows: Branching, Merging, and Rebasing

Part 4 of 17 · ~4 min

Merge vs. Rebase: Same Two Branches, Different History

Diagram comparing git merge and git rebase producing different commit histories from the same two branches

Wednesday morning, and main has moved. While you were building shift swapping, Nadia shipped something the whole app needed: hoursUntil() now respects each site's own timezone instead of assuming UTC, which was quietly wrong for every clinic outside London.

                    e0b8d31  ←  main   ("Make hoursUntil respect each site's timezone")
                   /
a4f1c0e ──────────
                   \
                    7c1f4ae ─── 2b9d013 ─── 41e77c6  ←  feature/shift-swap

Two branches, one shared ancestor, both moved on. Your feature is built on an hoursUntil() that no longer exists in the form you wrote against, and you need Nadia's work in your branch before you can trust a single test you run.

Git gives you two ways to do that, and they produce genuinely different histories from the identical starting point.

git merge joins the two histories and records that it did. Run it from main, and what comes out is a merge commit carrying two parents at once — one thread leading back into main, the other back into your branch:

git switch main
git merge feature/shift-swap
                    e0b8d31 ─────────────────────── M  ←  main   (a merge commit, two parents)
                   /                               /
a4f1c0e ──────────                                /
                   \                             /
                    7c1f4ae ─── 2b9d013 ─── 41e77c6  ←  feature/shift-swap

The fork and the join are both permanently visible. Six months from now, git log --graph will still show that shift swapping was developed in parallel with the timezone fix and landed on Wednesday.

git rebase replays your commits somewhere else and pretends the fork never happened. Run from your branch, it takes each of your commits in order and reapplies it on top of main's current tip:

git switch feature/shift-swap
git rebase main
a4f1c0e ─── e0b8d31 ─── 7c1f4ae' ─── 2b9d013' ─── 41e77c6'  ←  feature/shift-swap

One straight line, as though you had started the feature this morning, after Nadia's fix, instead of on Monday before it.

Look carefully at those apostrophes, because they are the part that matters — and they are the same notation the diagram at the top of this chapter uses, where C' and D' are the rewritten copies of C and D. 7c1f4ae' is not 7c1f4ae. It contains the same change with the same message by the same author, but its parent is different — and a commit's hash is computed over its content and its parent. Different parent, different hash, different object. The originals still exist for now, unreferenced, exactly like that detached-HEAD commit. Your branch no longer points anywhere near them.

That is the single most consequential fact about rebasing, and every warning in the next two chapters follows from it.

MergeRebase
History shapeBranching, with merge commits marking where things joinedLinear — reads as if development happened sequentially
Original commit hashesPreservedReplaced (new hashes, same content)
What it recordsWhat actually happened, including the concurrencyA tidied story of what could have happened
Safe on commits others may have pulledYesNo — the next chapter is entirely about this
git log --graph readabilityShows true parallel work; can get noisy on a busy repoClean line; the parallel work is invisible
Number of conflicts you might resolveAt most once, in one merge commitOnce per replayed commit, potentially
Typical useLanding a finished feature branch on mainTidying your own unpushed commits; keeping a branch current without a merge commit each time

Neither is correct in the abstract. What most teams converge on — shiftplan included — is using both for the jobs they suit: rebase your own branch onto main while you are still working on it, so it stays current and its history stays readable; merge it into main when it is done, so the moment it landed is recorded. That gives you a clean feature branch and an honest trunk, which is usually what people mean when they argue about this.

The last row of that table deserves a beat. A merge asks you to reconcile two final states once. A rebase asks you to reconcile each of your commits in turn against the new base, so a branch with six commits can stop and ask you six separate times, sometimes about the same lines repeatedly. That is not a reason to avoid rebasing, but it is why a rebase of a long branch can feel like being asked the same question until you get it wrong.