Introduction
Git has two fundamentally different merge outcomes: fast-forward, where one branch's tip is simply moved, and three-way, where a new commit with two parents is created. The choice depends on whether the branches have diverged.
Fast-forward
If main's tip is an ancestor of feature, no real merge is needed. Git just moves main forward:
A---B---C feature
/
main
# after git merge feature
A---B---C main, feature
Command:
git switch main
git merge feature
To force a real merge commit even when fast-forward is possible:
git merge --no-ff feature
Three-way merge
When both branches have diverged, Git finds the merge base (the most recent common ancestor) and combines the changes from both sides:
A---B---C feature
/
A---D---E main
# after git merge feature
A---D---E---M main
\ /
B---C feature
Git's default merge strategy since 2.33 is ort; before that it was recursive. Both compute a three-way merge per file using the merge base.
Inspecting
git merge-base main feature
git log --oneline --graph --all
git show --stat HEAD # see the merge commit
Strategies and options
git merge -X ours feature # prefer our side on conflicts
git merge -X theirs feature # prefer their side
git merge -X ignore-space-change feature
git merge --squash feature # apply changes without merge commit
--squash creates a single commit that contains all changes from feature, with no parent link to it. Useful for tidy history but loses the per-commit detail.
Aborting and undoing
git merge --abort # mid-merge, return to pre-merge state
git reset --hard ORIG_HEAD # after a finished merge, undo it
Configuring fast-forward policy
git config --global merge.ff false # always create merge commit
git config --global merge.ff only # only fast-forward, refuse otherwise
git config --global pull.ff only # same for pulls
Merge drivers and attributes
Some files merge poorly with text-based three-way (machine-generated lockfiles, generated documentation). Configure a custom merge driver via .gitattributes and git config:
# .gitattributes
package-lock.json merge=ours
Gemfile.lock merge=union
git config merge.ours.driver true
git config merge.union.name "Line union merge"
git config merge.union.driver "git merge-file --union %A %O %B"
The built-in union strategy keeps both sides; ours always picks our version; custom drivers can run any program. Use sparingly; surprising merge behaviour confuses reviewers.
Common mistakes
Believing fast-forward is somehow special or risky; it is not, it is just a pointer move. Using --no-ff on every merge in a fast-moving project, polluting history with empty merge commits. Using -X ours when you meant --strategy=ours (which discards the other side entirely) or vice versa. Finally, manually editing the merge commit message after the fact and breaking the standardized "Merge branch 'X'" wording that tools rely on; if you must edit, do it before git commit.