Merging is the art of reconciling divergent histories. When branches are cheap, divergence is natural. But eventually, work must converge. Merging is how Git brings parallel universes together. In this chapter, we’ll explore the mechanics of merging: fast-forward, recursive, and octopus. We’ll see when each applies, what they look like, and how to handle them with confidence.


The Concept of a Merge

A merge takes two histories and combines them into one. Git does this by finding a common ancestor, then applying changes from both sides. The result is a new commit that has more than one parent. This is how the DAG branches and reconnects.

Visualization:

gitGraph
  commit id: "A"
  commit id: "B"
  branch feature
  checkout feature
  commit id: "C"
  checkout main
  commit id: "D"
  merge feature

Here the merge commit has two parents: D and C. This preserves both lines of history.


Fast-Forward Merges

If no new commits were added on main since branching, Git can simply move the branch pointer forward. No merge commit needed.

git checkout main
git merge feature

Visualization:

gitGraph
  commit tag: "A"
  commit tag: "B"
  branch feature
  checkout feature
  commit tag: "C"
  checkout main
  merge feature

This is a “fast-forward.” It’s as if feature commits were made directly on main.


Recursive (Three-Way) Merges

If both branches diverged, Git must reconcile changes. It performs a three-way merge using the common ancestor.

git checkout main
git merge feature

Visualization:

gitGraph
  commit tag: "A"
  branch feature
  checkout feature
  commit tag: "C"
  checkout main
  commit tag: "D"
  merge feature

The merge commit has two parents. This preserves both timelines.


Octopus Merges

Sometimes you merge more than two branches at once. This is called an octopus merge. It’s common in integrating multiple topic branches at once, especially when conflicts are unlikely.

git checkout main
git merge feature1 feature2 feature3

Visualization:

gitGraph
  commit tag: "A"
  branch feature1
  checkout feature1
  commit tag: "F1"
  checkout main
  branch feature2
  checkout feature2
  commit tag: "F2"
  checkout main
  branch feature3
  checkout feature3
  commit tag: "F3"
  checkout main
  merge feature1
  merge feature2
  merge feature3

This produces a merge commit with multiple parents.


Handling Conflicts

Conflicts happen when both branches change the same lines. Git marks them in the file:

<<<<<<< HEAD
print("Hello from main")
=======
print("Hello from feature")
>>>>>>> feature

You resolve manually, then stage and commit:

git add conflicted_file.py
git commit

The merge completes.


Solo Workflow Example

You branch to try a new refactor. Meanwhile, you fix a bug on main. When you merge, Git creates a merge commit preserving both. You resolve conflicts as needed. This gives you a clean combined history.


Team Workflow Example

On a team, merges happen constantly. Developers create features, submit pull requests, and merge into main. The merge commit becomes a record of integration. Fast-forward merges may be disallowed to preserve branch structure.

Visualization of team merging:

gitGraph
  commit id: "Init"
  branch featureA
  checkout featureA
  commit id: "A1"
  checkout main
  commit id: "Fix1"
  merge featureA
  branch featureB
  checkout featureB
  commit id: "B1"
  checkout main
  merge featureB

Think Different Mindset

Merges aren’t messy—they’re musical. They reconcile parallel improvisations into harmony. Git doesn’t hide divergence; it celebrates it. A merge commit is a moment of collaboration, a record that different paths converged. Thinking this way, you stop fearing conflicts and start appreciating merges as the heartbeats of teamwork.


Merges come in flavors: fast-forward when history is linear, recursive when branches diverge, octopus when many streams converge. Each is a tool, not a trap. Master merging, and you master collaboration. In the next chapter, we’ll dive into rebasing and learn how to rewrite history into clean, elegant lines.