Branching and Merging
Topic 12

Merging

Branching

git merge combines the history of two branches, joining a feature branch's commits back into main. The shape of the result is decided entirely by topology: if nothing diverged you get a fast-forward, and if both sides moved you get a merge commit with two parents.

Knowing which of those two outcomes will happen before you run the command is most of the skill. The other half is choosing whether to let Git fast-forward or to force a merge commit for the sake of a readable history later.

Two merge outcomes, decided by topology
Fast-forward
Main hasn't moved since the branch left. Git just advances the pointer along the existing line — no merge commit is created.
Three-way
Both sides moved. Git builds a new merge commit with two parents, weaving the divergent histories back together.

Fast-Forward Merges

When the branch you are merging into has not moved since the feature branched off, there is no divergence to reconcile. Git simply advances the branch pointer forward to the feature's tip — a fast-forward. No merge commit is created, and the history stays linear. The downside is that the history then shows no sign a branch ever existed; the feature's commits sit inline with everything else.

Three-Way Merges

When both branches have new commits since they diverged, a fast-forward is impossible. Git finds the common ancestor, compares both tips against it, and writes a merge commit with two parents recording the combined result. This is the case where conflicts can arise, because both sides may have touched the same lines relative to that ancestor.

Controlling Merge Shape

--no-ff forces a merge commit even when a fast-forward was possible, so the feature's commits stay grouped under one node — this is GitHub's default "Create a merge commit" behavior. --ff-only refuses to merge unless it can fast-forward, which is a clean way to catch divergence early. --squash takes the feature's changes, stages them as one combined change, and lets you make a single commit — but it records no merge relationship at all.

Reading Merge History

git log --graph --oneline --decorate draws the branch topology as ASCII, showing exactly where branches forked and rejoined. Reading this before and after a merge is how you confirm Git produced the shape you intended rather than a surprise fast-forward or an extra merge node.

Merge vs the Alternatives

A merge commit is one extra node in the graph in exchange for an honest record that a group of commits formed a feature and rejoined at a point in time. When that record has no value — a one-commit fix, or a workflow that squashes everything — the node is just noise, and squash or rebase produces a flatter history. The choice is about what you want future readers of git log to be able to reconstruct.

Fast-Forward vs --no-ff Merge

Fast-forward — when the target has not moved, Git just advances the pointer, leaving a linear history with no record that a branch existed. Clean, but the feature's commits are indistinguishable from any others.

--no-ff merge — always writes a merge commit, preserving the fact that a set of commits belonged to one feature and joined at a known point. The cost is one extra node; the payoff is a history you can read as features rather than a flat stream.

Common Mistakes
  • Allowing fast-forward merges on main when you wanted feature boundaries visible, flattening history into a line with no way to see which commits formed a feature.
  • Using --squash and forgetting it records no merge relationship, so the source branch still reads as "unmerged" and may be merged again later.
  • Merging a branch into main without updating it first, producing avoidable conflicts a pre-merge git pull would have surfaced on your side.
  • Resolving a merge by blindly taking one side with -X ours or -X theirs and silently dropping the other side's legitimate changes.
  • Pushing a merge that broke the build because tests ran on each branch separately but never on the merged result.
Best Practices
  • Use --no-ff on shared branches when you want each feature to stay an identifiable unit in history.
  • Use git pull --ff-only to refuse surprise merge commits and catch divergence early.
  • Update the target branch with git pull before merging into it, so you resolve conflicts on your own side first.
  • Run the test suite on the merge result, not just on each branch, before pushing.
  • Verify the topology with git log --graph --oneline --decorate after merging to confirm you got the shape you intended.
Comparable toolsMercurial hg merge then a commit, also producing two-parent changesetsSubversion svn merge over revision ranges, historically painful conflict trackingPerforce p4 integrate / p4 mergeFossil auto-merges on fossil update / fossil merge

Knowledge Check

When can Git fast-forward a merge instead of creating a merge commit?

  • When the target branch has not moved since the feature branched off, so the pointer can simply advance
  • Whenever there are no conflicting lines between the two sides, regardless of how far the target has diverged in the meantime
  • Only when you explicitly pass the --no-ff flag on the command line to request that Git slide the pointer forward
  • Only on the very first merge ever made into a freshly created branch that has never received any other commits yet

What does --squash record about the source branch?

  • Nothing about the relationship — it stages the combined changes for one commit, so the source still reads as unmerged
  • A full merge commit with two parents linking both branches together, recording the source tip as a second parent so the history shows the join
  • A fast-forward that then deletes the source branch automatically once the combined changes have landed on the target tip
  • An annotated tag at the join point marking the source branch as fully integrated, so later merges know its commits already landed

Why choose --no-ff despite the extra commit?

  • It keeps the feature's commits grouped under one merge node, so history shows which commits formed a feature
  • It runs the project's full test suite automatically right before merging and blocks the merge commit if any of those checks fail
  • It resolves any overlapping conflicts on its own without prompting you, picking a side automatically so the merge never pauses
  • It completes faster than a fast-forward on large repositories by skipping the pointer update and writing the join directly

What is the risk of resolving a merge with -X theirs as a shortcut?

  • It silently drops your side's legitimate changes on every conflicting hunk rather than reconciling them
  • It aborts the whole merge and discards the commits on both branches, throwing away work on each side rather than just resolving hunks
  • It always forces a fast-forward instead of writing a merge commit, collapsing the conflicting sides into one straight line of history
  • It rewrites the commit hashes on the source branch as it merges, leaving collaborators holding now-orphaned versions of those commits

You got correct