Course outline · 0% complete

0/28 lessons0%

Course overview →

Merging: fast-forward vs true merge

lesson 5-3 · ~12 min · 15/28

Bringing branches together

Your new-sauce experiment worked and its commits deserve to be on main. That is a merge, and the direction matters. You stand on the branch that should receive the work, then pull the other branch in.

$ git switch main
$ git merge new-sauce

Getting that order backwards is the classic beginner mistake, and it does not fail loudly. Merging main into new-sauce succeeds, it just leaves the result on the wrong branch and main unchanged.

What Git does next depends on a single question: did main move while you were away on the branch? The two possible answers give the two kinds of merge, and the rest of this lesson is about telling them apart.

fast-forward: main never moved, so its label just slides aheadmainfeaturetrue merge: both branches moved, so Git makes a merge commitcommit on maincommit on featuremerge commit,two parents
Top: nothing new on main, so the merge is just sliding the main label forward (fast-forward). Bottom: both branches gained commits, so Git creates a merge commit whose two parents tie the histories together.

Reading both outcomes

Fast-forward. main gained no commits while you were away, so your branch's history is a simple continuation of it. Git slides the main label forward and creates no new commit at all.

$ git merge new-sauce
Updating a1b2c3d..c7d8e9f
Fast-forward
 sauce.txt | 3 +++

The word Fast-forward in the output is the tell. Nothing was combined, because there was nothing to combine.

True merge. Both branches gained commits, so the histories genuinely diverged. Git combines both sets of changes and seals them with a merge commit, the only kind of commit with two parents, one on each branch.

$ git merge new-sauce
Merge made by the 'ort' strategy.
 sauce.txt | 3 +++

That strategy name is just the algorithm Git used to work out the combination, and seeing it confirms a real merge happened rather than a slide.

Fast-forwardTrue merge
Conditionreceiver gained no commitsboth branches gained commits
New commitnoneone merge commit
History shapeone straight linetwo lines rejoining

Most of the time Git combines the branches without help, because changes to different files, or to different lines of one file, merge cleanly. When both branches edited the same lines, Git stops and asks you to decide. That is a merge conflict, and it gets all of unit 6.

After the merge: delete the label

Once new-sauce is merged, its commits live on main, so keeping the branch around buys nothing. Real repositories otherwise silt up with dozens of dead branches whose names no longer mean anything, which makes the routine merge, then delete.

$ git branch -d new-sauce
Deleted branch new-sauce (was c7d8e9f).

Deleting a branch deletes only the label file from lesson 5-1. The commits themselves are untouched, because they are reachable from main now and git log still lists them.

Safety is built into the flag. Lowercase -d refuses to delete a branch whose commits are not merged anywhere, since removing the label would leave that work with nothing pointing at it. The capital -D variant is the explicit override, the way of saying yes, discard that experiment on purpose.

Notice that Git even prints the hash it was pointing at, (was c7d8e9f), which is a small kindness. If the delete turns out to be a mistake, that hash plus git switch -c new-sauce c7d8e9f puts the label right back.

Git can fast-forward feature into main when main has received no new commits since feature branched off.

In that situation feature is a straight continuation of main, so no combining is needed. The main label simply slides forward along the existing chain until it reaches feature's commit.

The moment main picks up commits of its own, that stops being possible. The two histories have genuinely diverged, there is no single line to slide along, and Git must build a merge commit to tie them back together.

After merging new-sauce into main and running git branch -d new-sauce, nothing happened to the commits. They are part of main's history now, and only the small label file was removed.

A branch is only a movable label, as lesson 5-1 showed by writing one by hand. Once the merge is done, main reaches every one of those commits by walking parent pointers, so deleting the label removes 41 bytes and no history.

This is also the reason -d refuses to delete an unmerged branch. There, the label really is the only thing keeping those commits findable, so removing it would leave the work unreachable from any branch, recoverable only through the reflog from lesson 4-3.

A merge commit differs from every other commit in one way: it has two parents instead of one.

Those parents are the two branch tips that were joined, which is how the commit ties the histories together and lets git log reach back through both lines of work.

Everything else about it is an ordinary commit. It stores a full snapshot, an author, a date, and a message, and Git computes its hash the same way. The two-parent structure is also what makes the history graph readable later, since the merge commit is the visible point where a branch rejoined.

The full command is git merge new-sauce, run while standing on main.

It brings new-sauce's commits into main, either by fast-forwarding the label or by building a merge commit, depending on whether main moved in the meantime.

The direction rule is what to remember: switch to the receiver first, then merge the giver. Since you are already on the receiving branch, the branch you name in the command is always the one being pulled in.