Stage 2 · Tools
Branching & Merging
Merging Strategies
Fast-forward, recursive, octopus — choosing the right merge strategy for your branch topology.
What Merging Does
Merging combines the histories of two or more branches into a single result. Git calculates which changes belong where, produces a new commit (called a merge commit), and points the current branch at that new commit. The source branch is left unchanged.
The merge strategy Git chooses depends on the topology of the branches. Understanding these strategies helps you avoid surprises and produce clean, readable history.
Fast-Forward Merge
A fast-forward merge happens when the target branch has no new commits since the source branch was created. Instead of creating a merge commit, Git simply moves the branch pointer forward to point at the same commit as the source branch.
# You are on main
git log --oneline
# f4e5d6c Initial commit
# Create feature branch and make commits
git switch -c feature/api
echo "API code" > api.ts
git add api.ts && git commit -m "Add API module"
# Switch back to main and merge
git switch main
git merge feature/api
# Updating f4e5d6c..a1b2c3d
# Fast-forward
# api.ts | 1 +
# 1 file changed, 1 insertion(+)
# Feature branch pointer moved forward — no merge commit created
git log --oneline
# a1b2c3d (HEAD -> main, feature/api) Add API module
# f4e5d6c Initial commitBecause main had no new commits since feature/api was created, Git fast-forwarded main to point to the same commit. No merge commit was created.
Even when a fast-forward is possible, use git merge --no-ff to force a merge commit. This preserves the visual context of where a feature branch started and ended in the history graph.
Recursive (3-Way) Merge
When both branches have new commits since they diverged, Git cannot fast-forward. It performs a recursive merge (also called a 3-way merge) by finding the common ancestor of both branches and combining the changes from all three points: the common ancestor, the source branch, and the target branch.
# Diverged history:
git log --oneline --graph --all
# * g7h8i9j (main) Fix error handling
# | * d4e5f6g (feature/api) Add rate limiting
# | * a1b2c3d Add API module
# |/
# * f4e5d6c Initial commit
# Merge feature into main
git switch main
git merge feature/api
# Merge made by the 'ort' strategy.
# api.ts | 3 ++-
# 1 file changed, 2 insertions(+), 1 deletion(-)
git log --oneline --graph
# * j1k2l3m (HEAD -> main) Merge branch 'feature/api'
# |\
# | * d4e5f6g (feature/api) Add rate limiting
# * | g7h8i9j Fix error handling
# |/
# * f4e5d6c Initial commitGit creates a merge commit with two parents: the tip of main and the tip of feature/api. The ort strategy (default since Git 2.33) handles the 3-way merge computation.
A merge commit always has two parent commits. The first parent is the branch you merged into (main), and the second is the branch you merged from (feature/api). This is how Git preserves the full history of both branches.
Merge Command Options
# Fast-forward only — fail if fast-forward is not possible
git merge --ff-only feature/api
# No fast-forward — always create a merge commit
git merge --no-ff feature/api -m "Merge feature/api into main"
# Squash merge — combine all commits into one
git merge --squash feature/api
git commit -m "Add complete API module"
# Merge without committing (useful for testing before finalizing)
git merge --no-commit feature/apiEach option gives you control over how Git handles the merge. --ff-only enforces linear history, --no-ff preserves branch context, and --squash simplifies the history into a single commit.
| Option | Creates Merge Commit | History Style | Best For |
|---|---|---|---|
| Default (auto) | Only if needed | Clean when possible | General use |
| --ff-only | Never | Strictly linear | Feature branches merged quickly |
| --no-ff | Always | Shows branch topology | Preserving feature context |
| --squash | Yes (after manual commit) | Single commit per merge | Cleaning up messy branches |
Octopus Merge
Octopus merge combines more than two branches in a single merge commit. It is primarily used in kernel development and large-scale integration scenarios. It cannot handle conflicts — if any two branches conflict, the merge fails.
# Merge multiple branches at once
git merge feature/api feature/auth feature/logging
# This creates a single merge commit with three parents
# If any branch conflicts, the merge aborts automaticallyOctopus merges are rare in everyday development. They are most useful when integrating multiple independent changes that do not touch the same files.
Octopus merge cannot resolve conflicts. If any two branches modify the same file, the merge aborts. Use it only when you are certain the branches are independent.
Ours Strategy
The ours strategy creates a merge commit but ignores all changes from the incoming branch. The resulting tree is identical to the current branch. This is useful when you want to mark a branch as merged without actually incorporating its changes.
# Merge but keep our version of everything
git merge -s ours feature/abandoned -m "Mark feature/abandoned as merged"
# The merge commit is created, but no files change
# Useful for closing a PR without merging its changesThe -s ours strategy is sometimes used in release workflows to merge a branch back into an older release branch, marking it as integrated without pulling in potentially incompatible changes.
Choosing a Strategy
| Scenario | Recommended Strategy | Why |
|---|---|---|
| Feature branch merged quickly | Fast-forward or --ff-only | Clean linear history |
| Feature branch with parallel work | Default recursive merge | Preserves both histories |
| Long-running release branch | --no-ff merge | Shows integration points clearly |
| Cleanup after review | --squash merge | One clean commit for the feature |
| Closing abandoned branch | -s ours | Marks it merged without the code |
| Integrating multiple independent branches | Octopus merge | Single commit, multiple parents |
Many teams prefer squash merging feature branches. This collapses all WIP commits into a single meaningful commit on the target branch, keeping history linear and clean. The tradeoff is losing the detailed commit history of the feature branch.
Mark this lesson complete to store local progress and unlock a cleaner resume path the next time you visit.