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.