Concept · Merging & history
Fast-forward vs merge commits
If the target hasn’t moved, Git just slides the pointer; otherwise it records a commit with two parents.
Merging feature into main can happen in two ways, depending on whether main has moved since feature branched off.
Fast-forward
If main hasn't changed, feature is simply main plus some commits. No real merge is needed. Git moves the main pointer forward to feature's tip:
before: A ─ B ← main
└ C ─ D ← feature
after: A ─ B ─ C ─ D ← main, featureNo new commit is created, and the history stays a straight line. Afterwards there's no record that C and D were ever on a separate branch.
Merge commit (true merge)
If both branches have new commits, Git does a three-way merge and records a merge commit with two parents:
before: A ─ B ─ E ← main
└ C ─ D ← feature
after: A ─ B ─ E ───── M ← main
└ C ─ D ─┘M captures exactly when and what was integrated. git log --first-parent main shows one entry per merged branch, and git revert -m 1 M undoes the whole feature at once.
Choosing with flags
| Command | Behaviour |
|---|---|
git merge feature | Fast-forward if possible, otherwise a merge commit |
git merge --ff-only feature | Fast-forward or refuse; never creates a merge commit |
git merge --no-ff feature | Always a merge commit, even when a fast-forward was possible |
git pull --ff-only | Update your branch only if it hasn't diverged; a good default |
Set git config --global pull.ff only and git pull will never create a surprise merge commit on your local main. If it has diverged, Git stops and you decide whether to rebase or merge.
The team decision
--no-fffor feature branches keeps a visible "this PR landed here" commit, which is useful for reverting a feature or auditing.- Fast-forward only (after rebasing) or squash merges keep
mainlinear and easy to read and bisect, at the cost of losing the branch structure.
Hosting platforms' "merge", "squash and merge" and "rebase and merge" buttons map onto these choices. See merge vs rebase for the trade-off.