skip to content

Why does git pull sometimes create a merge commit you did not ask for?

level: middleimportance: must knowfreq 62%

answer

  1. Depends on whether the histories diverged
  2. Fast-forward creates no commit
  3. Pull's second half is a separate command
  4. Two config keys change the second half
  5. pull.rebase and pull.ff=only

basics

~20 s

Because pull's second half is a merge by default. If you have local commits and the remote branch has moved on, the histories have diverged, so instead of fast-forwarding Git joins them with a merge commit. Setting pull.rebase or pull.ff=only changes that.

solid answer

~50 s

`git pull` is `git fetch` followed by an integration step, and by default that step is `git merge`. When your branch has no commits of its own the merge is a fast-forward and no commit is created — which is why pull usually looks harmless. As soon as you have committed locally and someone else has pushed, the two histories have diverged and the merge produces a real merge commit, typically with the message "Merge branch 'main' of ...". Repeat that across a team and the shared branch fills with these bubbles. The fixes are configuration: `pull.rebase=true` replays your local commits on top of the fetched tip instead, and `pull.ff=only` makes pull refuse anything but a fast-forward so you decide explicitly when you have diverged. Modern Git already errors out on a diverging pull until one of these is set.

code

console · 6 lines
console
$ git pull
hint: You have divergent branches and need to specify how to reconcile them.
hint:   git config pull.rebase false  # merge
hint:   git config pull.rebase true   # rebase
hint:   git config pull.ff only       # fast-forward only
fatal: Need to specify how to reconcile divergent branches.

go deeper

for a junior

Know that pull merges by default, that a merge commit appears only when both you and the remote have new commits, and that pull.rebase and pull.ff=only exist to change this.

for a middle

Explain the fast-forward-versus-diverged distinction that decides whether a commit is created, and describe what each of pull.rebase and pull.ff=only does to the integration step.

for a senior

Bring the production judgment: why bubbles degrade a shared history, which setting you standardise on and why, and that modern Git already refuses a diverging pull with no configured mode.

for a principal

Own the policy tradeoff — linear history versus preserved integration points, what the team's history is used for (bisecting, auditing, release notes), and how the choice is communicated rather than imposed by each developer's local config.

## Where the commit comes from `git pull` does two things: it fetches, then it integrates. The integration is a separate command — `git merge` unless you configure otherwise. So the merge commit is not a quirk of pull; it is an ordinary merge that pull ran for you. Whether that merge creates a commit depends entirely on the shape of the history. If your branch tip is an ancestor of the fetched tip — you have committed nothing locally — the merge is a **fast-forward**: your branch pointer simply moves forward and no commit object is created. If you have made local commits while the remote also advanced, neither tip is an ancestor of the other. The histories have **diverged**, a fast-forward is impossible, and the merge creates a commit with two parents: your local tip and the fetched tip. ## Why it is a problem in practice The commit itself is harmless in isolation. The pattern is not. On a busy shared branch, every developer who commits locally and then pulls before pushing adds one of these commits, each with a generated message that says nothing about the work. The result is a history where a large share of the commits are noise, `git log --graph` becomes unreadable, and genuine merges — the ones that record an actual integration decision — are lost among the accidental ones. They are often called merge bubbles because of how they look in a graph view: a short side branch that immediately rejoins. A second, subtler cost: the accidental merge is created by whoever happened to pull, at whatever moment they pulled, so the history records the order of people's pulls rather than the order of the work. ## The three configurable behaviours - **`pull.rebase=true`** — pull rebases instead of merging. Your local commits are replayed on top of the fetched tip, producing a linear history and no merge commit. The cost is that your local commits get new hashes, and the rebase can stop on conflicts; a rebasing pull also refuses to run with a dirty working tree unless autostash is enabled. Values `merges` and `interactive` are also accepted, and `branch.<name>.rebase` sets it per branch. - **`pull.ff=only`** — pull is allowed to fast-forward and nothing else. If you have diverged it stops and tells you, leaving your branch untouched, and you choose the integration yourself. This is the most conservative setting and the one that never surprises you. - **`pull.rebase=false`** — the historical default, an explicit merge. Each has a command-line equivalent for one invocation: `--rebase`, `--ff-only`, `--no-rebase`. ## What modern Git already does Recent Git refuses to guess. If your branch and its upstream have diverged and none of these settings is configured, the pull aborts with a hint listing `git config pull.rebase false`, `git config pull.rebase true` and `git config pull.ff only`, and a final `fatal: Need to specify how to reconcile divergent branches.` Nothing is changed. Older versions silently merged, which is how most of these bubbles got into long-lived repositories in the first place. ## Choosing For a branch only you work on, `pull.rebase=true` gives the cleanest result: your unpushed work is replayed on top of everyone else's and the shared branch stays linear. For a branch that others have already based work on, rebasing your local copy is fine — you are only rewriting commits you have not shared — but be aware the rebase creates new commit objects. Many teams set `pull.ff=only` globally instead, on the grounds that being stopped and forced to think is better than any automatic choice. Note that rebasing is not free of consequence: it replays your commits, so it can conflict repeatedly and it changes what you had already committed locally. Whichever you pick, the underlying discipline is the same one the fetch-versus-pull distinction teaches: fetch is safe and informational, integration is a decision, and a decision should not be made by a default you never chose. ## Cleaning up after the fact Once an accidental merge commit is pushed to a shared branch, removing it means rewriting published history, which is a much bigger conversation than preventing it. The realistic answer in an interview is prevention: configure the pull behaviour, and prefer fetch-then-integrate when you know you have local work.

  • Why does pull usually not create a merge commit, even without any configuration?
    Because most pulls are fast-forwards. If you have committed nothing locally, your branch tip is an ancestor of the fetched tip, so Git just slides the pointer forward and updates the working tree — no commit object is needed. The merge commit only appears once both sides have commits the other lacks.
  • What does pull.ff=only do when your branch has diverged from the remote?
    It refuses. The fetch still happens, so your remote-tracking refs are current, but the integration step stops rather than merging or rebasing, and your branch, index and working tree are left untouched. You then choose explicitly — merge, rebase, or reset — which is precisely the point of the setting.
  • Is setting pull.rebase globally safe on branches other people share?
    It rewrites only your own unpushed commits, which is safe, but the replayed commits get new hashes and the rebase can stop on conflicts. The risk is not the config itself; it is a developer who then force-pushes the rewritten branch over shared work. Teams that worry about that often prefer pull.ff=only.

Pull with the default merge behaviour is like a document tool that silently resolves every simultaneous edit by inserting a note saying "two people edited here" — correct, but after a hundred of them the notes outnumber the text.

saying these in an interview costs you the question

  • Says git pull always creates a merge commit
  • Thinks the merge commit comes from the fetch step
  • Believes rebase and merge pulls differ only in message
  • Claims pull.ff=only silently merges when diverged
  • Cannot name a config key that changes pull's behaviour

context