The idea: two ways to bring feature into main

You branched feature off main at commit B and made commits E and F. Meanwhile main got C and D. The histories have diverged. To combine them you can merge (join the two lines with a new commit) or rebase (copy your commits onto the new tip of main, so history looks as if you started later). Both end with the same files. They leave very different histories behind. The canvas runs both on the same starting repository: panel 1 merges, panel 2 rebases.

Top: the diverged history, A B C D on main and E F on feature branching from B. Middle: after git merge, a new commit M with two parents D and F, main points to M. Bottom: after git rebase and a fast-forward, one straight line A B C D E′ F′ with main and feature on F′, while the old E and F are faded with no branch pointing to them
Merge adds one commit and changes nothing that existed. Rebase makes new copies E′ and F′ and leaves the originals behind.

Branches are labels, commits point to parents

A commit stores a snapshot of the files (its tree), the hash of its parent commit(s), the author, the committer with a date, and a message. Its ID is the SHA-1 hash of all of that. The arrows on the canvas point from each commit to its parent, because that is the only link a commit stores. A branch is just a small file holding one commit hash: a label that moves forward when you commit. HEAD says which branch you are on (the box marked HEAD →). GitObjects and GitRefsHead in this series look at these two ideas closely.

git merge: a merge commit with two parents

git switch main
git merge feature        # Merge made by the 'ort' strategy.

Git finds the merge base, the newest commit both tips share (B). It compares B with D and with F and applies both sets of changes; this is the three-way merge (ThreeWayMerge in this series shows it line by line). The result is a new merge commit M whose parents are D (the first parent, the branch you were on) and F. main moves to M. Nothing else changes: C, D, E, F keep their hashes, so anyone who already has them is unaffected (Demo: merge).

git rebase: replay the commits, get new hashes

git switch feature
git rebase main          # Successfully rebased and updated refs/heads/feature.

Git lists the commits on feature that main does not have (main..feature: E, F). It moves to main's tip D and, oldest first, takes each commit's change (its diff against its old parent) and applies it again, making a new commit. The copy E′ has the same message and the same change as E, but its parent is D instead of B, and its tree now also contains C and D. Different contents, so a different hash: E′ is a brand-new commit (Demo: rebase). Then the feature label moves to F′.

The commit object of E next to the commit object of E′: same author and message, but the tree, the parent line and the committer date differ (highlighted), so the SHA-1 hashes differ
A commit's hash covers its tree, its parent and its committer date. Rebase changes all three, so every replayed commit gets a new ID.

The originals E and F are not deleted. They are orphaned: no branch reaches them, so git log no longer shows them, but git reflog does, and git reset --hard ORIG_HEAD undoes the rebase. git gc prunes unreachable commits once the reflog entries expire (30 days by default).

Fast-forward and --ff-only

When main is an ancestor of feature (main has nothing feature lacks), a merge needs no new commit: Git just moves the main label forward. That is a fast-forward, and the history stays one straight line. It happens after a rebase, and also when main never moved at all (Demo: main never moved). git merge --ff-only refuses to do anything else: on diverged branches it stops with fatal: Not possible to fast-forward, aborting. (Demo: fast-forward refused). git merge --no-ff does the opposite and makes a merge commit even when a fast-forward was possible, to keep a record of the branch.

Side by side

git mergegit rebase (+ fast-forward)
New commitsone merge commit Mone copy per feature commit (E′, F′)
Existing hashesunchangedfeature's commits get new hashes
History shapefork and join, as it happenedone straight line
Conflictsresolved once, in Mresolved per replayed commit
Safe on shared branchesyesno: others still hold the old commits
Undogit reset --hard HEAD~1 (before pushing)git reset --hard ORIG_HEAD or the reflog

The golden rule

Do not rebase commits that other people already have. After a rebase, your feature has E′ and F′; a teammate still has E and F. Pushing needs git push --force-with-lease, and when they pull, Git sees two different sets of commits with the same changes. Rebase freely on your own local branch, before you share it. If you keep working, merges pile up merge commits while rebase keeps a line (Demo: keep working).

Which one to use

  • Merge when the branch is shared, or when you want the history to show that work happened in parallel. Many teams merge pull requests with --no-ff for this.
  • Rebase your own branch onto the latest main before opening a pull request, for a clean linear history that is easy to read and to bisect. git pull --rebase does this for every pull.
  • Squash merge is a third option: all feature changes become one ordinary commit on main.

GitPackfiles in this series shows how all these commits, including the orphaned ones, are stored on disk.

What the page leaves out

Hashes on the canvas are short made-up hashes computed from the tree, parents, time and message, not real SHA-1 of real objects. The page leaves out merge conflicts and how each replayed commit can stop for one, interactive rebase (git rebase -i: reorder, squash, edit), --onto, rebasing merge commits (--rebase-merges), octopus merges, merge strategies other than ort, remote branches and pushing, and cherry-pick (rebase is a series of cherry-picks).