How would you decide between fast-forward, --no-ff merge commits, and squash merges for a shared Git branch?
answer
- Name the properties, not a preference
- Revert granularity and log readability
- first-parent only means something sometimes
- Consistency beats the specific choice
basics
~20 sDecide by what you need history to support. Fast-forward and squash give a linear, easy-to-bisect log; --no-ff keeps each branch as a visible, revertible unit and makes git log --first-parent a changelog. Consistency across the repository matters more than the choice.
solid answer
~50 sI frame it as three properties: readability, revert granularity, and re-merge correctness. A `--no-ff` policy records every integration as a two-parent commit, so `git log --first-parent` reads as one line per change and `git revert -m 1` backs out a whole feature; the cost is a busier graph. Fast-forward-only keeps history a straight line where each commit is an independent bisect step, at the price of losing the grouping — and it forces contributors to rebase before integrating. Squashing gives one commit per branch, which is tidy but discards intermediate commits and, because it records no parent link, forces the branch to be retired afterwards. I would pick one, express it in config (`merge.ff`) and, where the repository is served by our own infrastructure, enforce it in a server-side `pre-receive` hook, then keep it uniform — a mixed history is the one nobody can read.
code
bash · 2 linesgit log --graph --oneline --first-parent main
git revert -m 1 9b7d0e1go deeper
Know that the three options produce different history shapes, not different code, and follow whatever convention the repository already uses.
Be able to state each option's mechanics and its immediate cost: lost grouping, an extra commit node, or lost intermediate commits.
Argue from operational needs — how a feature gets reverted in an incident, how a regression gets bisected — and show how the default is expressed in config.
Own the tradeoff and the enforcement: pick one shape, make the tooling enforce it where updates arrive, and name the conditions that would make you revisit it.
## Frame the question before answering it There is no universally correct topology, so the useful answer names the properties history is being optimised for and shows what each option costs. Four properties do most of the work: **readability of the log**, **revert granularity**, **bisect behaviour**, and **re-merge correctness**. ## Option 1 — fast-forward wherever possible Each branch's commits land inline; nothing marks where a branch started or ended. `git log --oneline` is a straight sequence, and every commit is an independent step for `git bisect`, which is the finest granularity you can get when hunting a regression. What you give up is grouping. Nothing in the graph says these six commits were one change, so reverting a feature means reverting each commit individually in reverse order. Enforcing this shape means contributors must rebase their branch onto the tip before integrating, which rewrites their commit ids and demands force-push discipline on the branch. `merge.ff = only` makes non-fast-forward merges fail loudly instead of quietly producing merge commits. ## Option 2 — always record a merge commit (--no-ff) Every integration becomes a two-parent commit. The benefits are concrete: - `git log --first-parent` walks only the mainline and shows exactly one entry per integrated branch — a changelog you did not have to write. - `git revert -m 1 <merge>` removes the whole feature in one commit, because the merge is the single point where it entered. - The branch's individual commits stay reachable, with authorship and rationale intact. The costs: a denser `--graph` view, merge commits that show no diff against their second parent (which confuses newcomers), and bisect that can descend into a branch's intermediate commits, some of which may not build. `merge.ff = false` makes it the default. ## Option 3 — squash each branch into one commit One commit per unit of work: the log is as readable as it gets, and review-churn commits never reach the shared branch. The costs are real and specific. The intermediate commits and their per-commit authorship are not part of the shared branch, so blame attributes everything to the squash. Bisect can only narrow a regression to the whole feature. And because the squash commit has no parent link to the branch, the merge base does not advance: `git branch --merged` never lists the branch, and merging it again replays already-integrated work. A squash policy is only coherent when paired with a rule that a branch is retired as soon as it is squashed in. ## How I would actually decide - **How large is a typical branch?** If branches are one or two commits, the three options barely differ and I would take the simplest — fast-forward. If branches routinely carry ten meaningful commits, squashing destroys information that took effort to create. - **How is the code reverted in an incident?** If the realistic recovery action is "back out that whole feature", `--no-ff` pays for itself the first time it is used. - **How disciplined is commit hygiene?** Squashing is a good shield for branches full of "wip" and "fix review" commits; it is a poor fit for authors who curate each commit deliberately. - **Does anything depend on the shape?** Release-note generation, changelog scripts and audit trails often read `--first-parent`, which only means something under a merge-commit policy. ## Making the decision stick A policy that lives in a wiki page is not a policy. Express it where Git can act on it: `merge.ff` in repository or user config for the default, `--ff-only` in the habits and scripts people run, and, on infrastructure you control, a server-side `pre-receive` hook that rejects pushes whose topology violates the rule. Note the local config is advisory — anyone can override it with a flag — so if the rule genuinely matters, the enforcement has to sit where the objects arrive. ## The strongest signal in an answer Say out loud that **consistency beats the specific choice**. A repository where half the features are squashed, some are fast-forwarded and some carry merge commits gives you the drawbacks of all three: `--first-parent` is incomplete, bisect granularity is unpredictable, and nobody can tell whether a branch was integrated. Also say what would make you revisit the decision — for example, if incident response repeatedly needs whole-feature reverts, or if the log has become unreadable — because a topology decision is reversible going forward even though it cannot be applied retroactively.
- What does git log --first-parent give you under a merge-commit policy?It walks only the first parent of each merge, so it lists one entry per integration and skips the individual commits inside each branch. That produces a coarse mainline changelog. Under a fast-forward or squash policy every commit is on the mainline already, so the option shows nothing extra.
- How does the choice affect git bisect?A linear history gives one independent step per commit, which is the finest granularity. Merge commits let bisect descend into a branch's intermediate commits, some of which may not even build. Squashing gives the coarsest result: a regression can only be pinned to a whole feature.
- Can you change a repository's topology policy retroactively?Not without rewriting published history, which invalidates every existing clone and reference to those commits. Treat the decision as forward-only: apply the new rule to future integrations and accept that the log will show a boundary where the convention changed.
- How would you enforce the policy rather than just document it?Set the default in config with `merge.ff`, but treat that as advisory since any flag overrides it. On infrastructure you control, a server-side `pre-receive` hook can inspect what is being pushed and reject topologies that violate the rule, because that is the one place every update must pass through.
saying these in an interview costs you the question
- Insists linear history is objectively correct with no tradeoffs
- Claims merge commits make git bisect unusable
- Says squashing is always cleanest, ignoring the re-merge cost
- Treats it as pure personal preference with no consequences
- Believes the topology choice changes which code ends up merged