In Git, how do --no-ff, --ff-only and merge.ff change what git merge produces?
answer
- Three knobs on one decision
- Two flags plus one config key
- One forces a commit, one refuses
- merge.ff takes true, false, or only
basics
~20 s--no-ff always records a merge commit, even when a fast-forward was possible. --ff-only refuses to merge unless the merge is a fast-forward, changing nothing otherwise. The merge.ff config sets the default: true, false (like --no-ff), or only (like --ff-only).
solid answer
~40 sThese three control only the fast-forwardable case. `git merge --no-ff` forces a two-parent merge commit so the branch stays visible as a bubble in `git log --graph` and can be reverted as a unit with `git revert -m 1`. `git merge --ff-only` is the opposite guard: if the histories have diverged it aborts with `fatal: Not possible to fast-forward, aborting.` and leaves the branch untouched — useful when you want a strictly linear branch and would rather be told to rebase than get a surprise merge commit. `merge.ff` sets the repository or user default with values `true` (default), `false` (equivalent to `--no-ff`) and `only` (equivalent to `--ff-only`); an explicit command-line flag overrides the config. None of them change *which content* ends up merged — only the topology.
code
ini · 2 lines[merge]
ff = falsego deeper
Remember the two flags by their effect: --no-ff always leaves a merge commit, --ff-only refuses to merge when it cannot just move the pointer.
Explain that all three act on the same fast-forwardable case, that merge.ff takes true, false or only, and that a command-line flag overrides the config.
Be ready to justify a repository-wide default and say what it buys — revertability and first-parent readability versus a clean linear log — and to notice a history that mixes conventions.
Own the consistency argument: pick one topology per repository, make it the default in config, and be able to say what tooling depends on it before changing it.
## The decision they all touch When you run `git merge`, Git first asks whether the merge can fast-forward — whether your current commit is already an ancestor of the commit being merged. If it can, the default is to move the branch pointer and record nothing. `--no-ff`, `--ff-only` and `merge.ff` all sit on exactly that decision. If the histories have genuinely diverged, `--no-ff` is a no-op (a merge commit was required anyway) and `--ff-only` simply refuses. ## --no-ff `git merge --no-ff <branch>` tells Git to create a merge commit even though a fast-forward was available. The resulting commit has two parents: the first is where your branch was, the second is the tip of the merged branch. Its tree is identical to the merged branch's tree, because your side contributed nothing — so the merge commit adds no content, only structure. Why pay for that empty-looking commit? - **Grouping.** `git log --graph` shows the branch forking and rejoining, so a reader can see which commits shipped together. - **Revertability.** `git revert -m 1 <merge>` backs out the whole feature in one commit, because the merge commit is the single point where it entered the branch. - **First-parent history.** `git log --first-parent` then lists one entry per merged branch, giving a coarse, readable changelog of the branch. The cost is a busier graph and a commit that shows no diff against its second parent, which confuses readers who expect every commit to contain changes. ## --ff-only `git merge --ff-only <branch>` inverts the guarantee: perform the merge only if it is a fast-forward, otherwise do nothing at all and exit non-zero with `fatal: Not possible to fast-forward, aborting.` Nothing is staged, no conflict state is entered, and the branch ref is unchanged. This is a *guard*, not a rewrite. A common misconception is that `--ff-only` linearises history; it does not. It refuses, and it is then your decision to rebase your work, or to merge deliberately with an explicit merge commit. It is the flag to reach for when you want a branch to stay a straight line and you want the failure to be loud instead of silently accumulating merge commits. ## merge.ff `merge.ff` is the config key holding the default: - `true` — fast-forward when possible. This is Git's default. - `false` — never fast-forward; always create a merge commit. Same effect as passing `--no-ff` every time. - `only` — refuse anything that is not a fast-forward. Same effect as `--ff-only`. It lives anywhere config lives, so it can be set per repository (`git config merge.ff false`) or globally. A flag on the command line always beats the config, so `git merge --ff <branch>` still fast-forwards in a repository configured with `merge.ff = false`. A related key, `pull.ff`, controls the same decision for the merge performed by `git pull` and takes precedence over `merge.ff` in that case. Keep the two straight: configuring one does not silently configure the other's precedence. ## What they do not change None of these options changes the merge *result* in terms of file content. In the fast-forwardable case the resulting tree is the merged branch's tree either way; `--no-ff` only adds a commit node above it. They also do not touch the commits on the feature branch — nothing is rewritten, squashed, or reordered. If you want the incoming commits collapsed into one, that is `git merge --squash`, a different mechanism with a different topology. ## Choosing between them A team that values a linear, bisect-friendly log will set `merge.ff = only` (or rely on rebasing) so that merges either slide forward or fail loudly. A team that values seeing feature boundaries will set `merge.ff = false` so every integration is a visible, revertible bubble. The important part in an interview is not which one you prefer but that you can state what each produces and what that costs: consistency across the repository matters more than the choice itself, because a history that mixes both conventions is the one nobody can read.
- What happens if you pass --no-ff when the branches have already diverged?Nothing changes. A diverged merge must create a merge commit anyway, so `--no-ff` is a no-op there. The flag only has an effect in the case where Git could have fast-forwarded, which is why it is described as suppressing the fast-forward rather than as forcing a merge.
- Why is a --no-ff merge commit useful for reverting a feature?It is the single commit where the branch entered history, so `git revert -m 1 <merge>` produces one commit that removes the whole feature. With a fast-forward there is no such node, and you would have to revert each of the branch's commits individually, in reverse order.
- If merge.ff is set to false in a repository, can you still fast-forward once?Yes. Command-line options override config, so `git merge --ff <branch>` fast-forwards even where `merge.ff = false` is configured. Note that `git pull` consults `pull.ff` first, which overrides `merge.ff` for the merge that pull performs.
saying these in an interview costs you the question
- Thinks --no-ff changes which file contents end up merged
- Believes --ff-only rewrites history to make it linear
- Assumes merge.ff=only blocks all merges, not just non-fast-forwards
- Confuses merge.ff=false with automatically rebasing
- Cannot name any of the three values merge.ff accepts