DEV Community

Cover image for What actually happens when you `git merge --no-ff`
Matías Denda
Matías Denda

Posted on

What actually happens when you `git merge --no-ff`

Most developers use git merge without ever thinking about what's happening internally. Then one day they see --no-ff in a team's workflow documentation, Google it, read three Stack Overflow answers, and walk away with a vague sense that "it creates a merge commit or something."

This post is the version I wish I'd read earlier. Two diagrams, one clear distinction, and why it actually matters for your team.

The setup

You're on main. Your coworker merged their feature. You branched off, added two commits, and now it's time to merge your branch back. What happens next depends on one flag.

# You're here
git checkout main
git merge feature
Enter fullscreen mode Exit fullscreen mode

Git has two ways to integrate your feature branch into main. The one it picks by default depends on whether the branches have diverged.

Case 1: Fast-forward (the default, when possible)

If main hasn't moved since you branched off, Git doesn't create a new commit. It just moves the main pointer forward to the tip of your feature branch.

Fast-forward merge: main pointer simply moves forward, no new commit

That's it. No merge commit. The feature branch and main now point to the same commit. If you look at git log on main, it reads like D and E were always there. The branch effectively disappears from history.

Case 2: --no-ff (always create a merge commit)

With --no-ff, Git creates an explicit merge commit even when a fast-forward was possible:

no-ff merge: a new merge commit M is created with two parents

M is a new commit whose parents are C (the previous tip of main) and E (the tip of feature). It has no code changes of its own — its diff is empty — but it records that these commits were integrated together at this point.

Why this distinction matters

The two histories above contain the same code. So does it matter? Yes, and here's where it bites real teams.

It matters for git bisect

git bisect helps you find which commit introduced a bug by doing a binary search through history. With fast-forward merges, the search descends into individual feature commits — you might land on a half-finished refactor where the bug is genuinely present but so is a broken test, making the bisect useless.

With --no-ff, you can run git bisect --first-parent and bisect merge commits only, treating each feature as an atomic unit. Found the regression? You know which feature to revert, not which arbitrary mid-feature commit to blame.

It matters for git revert

If you merged with --no-ff and need to roll back the feature, you revert the single merge commit:

git revert -m 1 <merge-commit-hash>
Enter fullscreen mode Exit fullscreen mode

That undoes all of D and E in one go. With fast-forward, you'd need to revert each commit individually — or figure out which commits belonged to the feature in the first place.

It matters for reading history

git log --graph --first-parent main with --no-ff merges shows you a clean list of features integrated into main, one per line. Without merge commits, the log is a flat stream of every individual commit ever made. For a large team, the difference is between "I can see what shipped last week" and "good luck."

What GitHub and GitLab do

When you click "Merge pull request" on GitHub or GitLab, they default to creating a merge commit (--no-ff behavior). The "Rebase and merge" and "Squash and merge" options exist too, but the default merge commit exists precisely because of the benefits above.

This is why teams that use the GitHub/GitLab UI religiously often have cleaner history than teams that merge locally on the command line — the UI forces a pattern that the command line leaves optional.

When fast-forward is fine

For throwaway branches, personal experiments, or single-commit fixes where the commit already tells the whole story, fast-forward is perfectly appropriate. The rule of thumb I use:

  • Single commit fix → fast-forward is fine
  • Feature branch with 2+ commits--no-ff preserves the grouping
  • Release branch merge → always --no-ff
  • Hotfix branch merge → always --no-ff (you want revertability)

Making it a team default

If you want the team to use --no-ff consistently, either set it at the repo level:

git config --global merge.ff false
Enter fullscreen mode Exit fullscreen mode

Or — better — require it via branch protection rules on your hosting platform. That way nobody's local config can bypass it.


This post is adapted from a chapter of Git in Depth: From Solo Developer to Engineering Teams, a 658-page book I just released on Git for working developers — from day-to-day tools to CI/CD, branching strategies, and Git at organizational scale. Launch price $29 with code EARLYBIRD (first 100 copies).

Next week: git worktree — the stash replacement nobody teaches you.

Top comments (0)