skip to content

questions

5

In Git, what is a fast-forward merge and when can Git perform one?

level: juniorimportance: must knowfreq 80%

answer

  1. No merge commit is created
  2. Something about ancestry of HEAD
  3. Merge base equals your current commit
  4. Git just moves the branch pointer

basics

~20 s

A fast-forward merge happens when the current branch's commit is already an ancestor of the commit being merged. Git records no merge commit; it simply moves the branch pointer forward and updates the index and working tree.

solid answer

~50 s

Git can fast-forward when the branch you are on has no commits of its own since the fork point — that is, `HEAD` is an ancestor of the tip you are merging, so the merge base **is** `HEAD`. There is nothing to combine, so Git skips the three-way merge entirely: it writes the target commit's SHA into the branch ref, updates the index and working tree to that tree, and adds a reflog entry like `merge feature: Fast-forward`. No new commit object is created, so history stays a straight line and the feature branch leaves no visible trace once deleted. If both branches have their own commits, a fast-forward is impossible and Git must create a merge commit with two parents. `git merge --no-ff` forces that merge commit even when a fast-forward would be possible.

go deeper

for a junior

Be ready to state the condition in one sentence: your branch has no commits of its own since the fork, so Git just moves the pointer forward and makes no merge commit.

for a middle

Explain the mechanics: the merge base equals HEAD, only the ref plus index and working tree change, no object is created, and the reflog records it as Fast-forward.

for a senior

Show why the choice matters in a shared repository — a fast-forwarded log hides which commits shipped together, which affects reverting and reading history later.

for a principal

Own the policy angle: whether a repository fast-forwards by default is a history-shape decision with consequences for review, revert granularity, and how the log is read months later.

## The one-sentence definition A **fast-forward** is a merge that requires no merging. When the commit your branch points at is already reachable from the commit you asked to merge, the other branch simply contains all of your history plus more. Git can therefore "fast-forward" your branch pointer along the chain to the newer commit instead of computing a combined result. ## The ancestry condition Every merge starts by finding the **merge base**: the best common ancestor of the two commits. If that merge base turns out to be your current commit (`HEAD`), then your side contributed nothing since the fork point. Formally, the fast-forward condition is `git merge-base --is-ancestor HEAD <other>` — HEAD is an ancestor of the other tip. The classic shape is: you create `feature` from `main`, commit on `feature`, nobody touches `main`, then you switch to `main` and merge. `main` still points at the fork point, so it is an ancestor of `feature`'s tip, and the merge fast-forwards. ## What Git actually does A fast-forward is almost a pointer assignment: - The ref file (or packed-refs entry) for the current branch is rewritten with the target commit's object id. - The index and working tree are updated to match that commit's tree. - A reflog entry is written, typically `merge feature: Fast-forward`, and `ORIG_HEAD` is set to where you were. - **No new object is created.** No commit, no tree, no blob. Because no three-way merge runs, a fast-forward cannot produce content conflicts. It can still refuse to run: if you have uncommitted local changes to files the update would overwrite, Git aborts with `error: Your local changes to the following files would be overwritten by merge`. That is a working-tree safety check, not a conflict. The typical output looks like: `Updating 3f1a2c4..9b7d0e1` followed by `Fast-forward` and a diffstat. ## When Git cannot fast-forward As soon as the branch you are on has at least one commit that the other branch does not have, the two histories have **diverged**. The merge base is now an older commit that is an ancestor of both, and Git must perform a real three-way merge: compare both sides against the base, combine the changes, and record a **merge commit** with two parents. That commit is the only place the combined result lives. ## Controlling the behaviour - `git merge --no-ff <branch>` records a merge commit even when a fast-forward was possible, keeping the feature branch visible as a bubble in the graph. - `git merge --ff-only <branch>` refuses to merge unless it can fast-forward, exiting with `fatal: Not possible to fast-forward, aborting.` and changing nothing. - `merge.ff` sets the default: `true` (fast-forward when possible — the default), `false` (behave like `--no-ff`), `only` (behave like `--ff-only`). ## How it reads in the log With `git log --graph --oneline`, a fast-forwarded merge is invisible: the commits from the feature branch appear inline on a single vertical line, indistinguishable from commits made directly on the branch. A `--no-ff` merge shows a fork and a join with a merge commit at the top. This is exactly why teams argue about the setting: fast-forwarding produces a clean linear log but erases the fact that a set of commits belonged together. ## Related but different A fast-forward is not a squash. `git merge --squash` also produces no merge commit, but it *does* create a new commit containing the combined changes, with a single parent, and the original commits are not part of the branch's history at all. A fast-forward keeps every original commit object exactly as it was. ## What interviewers are checking They want to hear the ancestry condition stated precisely — "my branch had no commits of its own" — rather than the vague "there were no conflicts". Absence of conflicts is a consequence, not the cause. The second thing they listen for is that a fast-forward changes only a pointer, which is what makes it cheap, unambiguous, and impossible to conflict.

  • How can you tell after the fact whether a merge fast-forwarded?
    The merge output prints `Fast-forward` and the reflog entry reads like `merge feature: Fast-forward`. In `git log --graph` there is no merge commit at that point — the branch history is a single line. `git show HEAD` shows an ordinary one-parent commit rather than a merge.
  • Can a fast-forward merge produce conflicts?
    No. No three-way merge runs, so there is nothing to conflict. It can still fail before doing anything if uncommitted local changes would be overwritten by the checkout; Git reports that the local changes would be overwritten and leaves the branch where it was.
  • Does a fast-forward lose any of the feature branch's commits?
    No. Every commit object stays exactly as it was and becomes part of the branch's history. What is lost is only the grouping: nothing in the graph marks where the feature branch started and ended, so after deleting the branch ref the commits look like they were made directly on the target branch.

It is like a bookmark in a book you have not written past: if the other reader is simply further along the same chapters, you slide the bookmark forward rather than stitching two versions together.

saying these in an interview costs you the question

  • Says a fast-forward creates a merge commit with one parent
  • Claims fast-forward means Git skipped applying the changes
  • Thinks a fast-forward is possible when both branches have new commits
  • Confuses fast-forward with squash merging
  • Explains fast-forward as merely 'there were no conflicts'

context

open as a page

In Git, how do --no-ff, --ff-only and merge.ff change what git merge produces?

level: middleimportance: must knowfreq 65%

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).

open as a page

In Git, what does git merge --squash do and what history does it leave behind?

level: middleimportance: must knowfreq 60%

basics

~20 s

git merge --squash stages the other branch's combined changes but makes no commit, does not move HEAD and records no MERGE_HEAD. The commit you make afterwards has a single parent, so the branch's individual commits and the merge link never enter history.

open as a page

After squash-merging a Git branch into main, why do later merges of that branch conflict?

level: seniorimportance: should knowfreq 45%

basics

~20 s

The squash commit has no parent link to the branch, so the merge base stays at the original fork point. Git therefore replays changes that main already contains in squashed form, and those re-applied hunks collide with the existing result.

open as a page

How would you decide between fast-forward, --no-ff merge commits, and squash merges for a shared Git branch?

level: principalimportance: should knowfreq 40%

basics

~20 s

Decide 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.

open as a page