skip to content

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

level: seniorimportance: should knowfreq 45%

answer

  1. Ancestry, not content, decides
  2. Look at what the merge base is
  3. No parent link was ever created
  4. Old fork point means changes replay

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.

solid answer

~40 s

A squash merge records a single-parent commit on `main`, so nothing in the graph connects `main` to the feature branch's commits. When you merge that branch again, `git merge-base` still returns the old fork point, and Git computes the branch's changes relative to that base — changes whose effect is already present in `main`. Git then tries to apply them on top, producing conflicts or duplicated hunks. The same reason explains why `git branch --merged main` never lists a squash-merged branch. The fix is procedural, not a merge flag: once a branch has been squashed in, stop building on the old base. Delete the branch, or reset it to `main` and start fresh commits there. If commits were added after the squash, rebase only those onto `main` so the already-squashed work is not replayed.

code

console · 7 lines
console
$ git branch --merged main
* main
$ git log --oneline main..feature
8f2c1a9 tighten validation
1b7e4d0 add rate limiter
$ git merge feature
CONFLICT (content): Merge conflict in gateway/limits.go

go deeper

for a junior

Remember the rule of thumb: once a branch has been squash-merged, delete it or restart it from the target branch instead of continuing to commit on it.

for a middle

Explain that the merge base is computed from parent links, and a squash commit has only one parent, so the old fork point is still used on the next merge.

for a senior

Diagnose it from the symptoms — unmerged branch listing, replayed hunks — and pick the right recovery: retire the branch or rebase only the commits made after the squash.

for a principal

Own the policy consequence: a squash convention must ship with a branch-lifecycle rule, otherwise every long-lived branch pays this cost repeatedly.

## The symptom A feature branch is squash-merged into `main`: one new commit on `main` carries the whole change. Work continues on the branch, and a week later merging it again explodes with conflicts in files nobody has touched since — often the entire feature appearing as a conflict against itself. ## The mechanism Every merge starts by computing the **merge base**, the best common ancestor of the two tips. Git then diffs each side against that base and combines the two sets of changes. A squash merge produces a commit with exactly one parent — the previous tip of `main`. It has no second parent, so there is no edge in the commit graph from `main` to any commit on the feature branch. As far as ancestry is concerned, nothing was merged. The merge base between `main` and `feature` is still the commit where the branch was originally created. So on the second merge, Git computes: - **Their side**: every change the branch made since the original fork point — including everything already squashed into `main`. - **Our side**: everything `main` did since that fork point — including the squash commit, which contains an equivalent result but as a *different* set of hunks with different context. Both sides therefore claim to modify the same regions, and Git cannot tell that the two are semantically the same work. Where the results are byte-identical Git may silently accept them; where the squash-time conflict resolution, a review fixup, or a later edit changed anything, you get a conflict. Applied changes can also duplicate: the same block inserted twice. ## Why content equality does not help Git's merge base is a graph property, not a content property. Two commits whose trees are identical are still unrelated if there is no ancestry between them. This is the single fact to state in an interview: **the merge base advances only through parent links, and a squash creates none.** It is also why `git branch --merged main` does not list a squash-merged branch — that command tests whether the branch tip is reachable from `main`, and it is not. ## How to detect the situation - `git log --oneline main..feature` still lists commits you know already shipped. - `git branch --merged main` omits a branch whose work is demonstrably in `main`. - `git log --graph` on `main` shows a single commit carrying the whole feature and no join with the branch. ## How to handle it The correct responses are procedural: 1. **Delete the branch after squashing.** This is the default discipline; a squash-merged branch has served its purpose. Recreate from `main` when you need to work on the area again. 2. **Restart the branch from `main`.** If the branch must keep its name, point it at the current `main` (a reset to `main`) so its base includes the squash commit, then add new work on top. Anything already squashed is deliberately dropped. 3. **Move only the genuinely new commits.** If commits were added after the squash, rebase just those onto `main` — for example replaying the range that starts after the last squashed commit — so the already-integrated work is not replayed. ## What is not the fix - **`-X theirs` or `-X ours` on the re-merge.** These pick a side for conflicting hunks; they do not correct the merge base, so you are still merging stale changes and may silently drop or resurrect work. - **Force-pushing.** Nothing about the target branch's history is wrong; the mismatch is in what the branch is based on. - **Merging again with `--no-ff`.** Topology flags do not change which base is used. ## The design tradeoff behind it This is the standing cost of squash merging: you trade an exactly-once, ancestry-accurate record of integration for a tidy one-commit-per-feature log. A merge commit records "this branch is now contained in main" in the graph itself, so re-merging is cheap and correct, and `git branch --merged` is meaningful. A squash records only the content. Neither is wrong, but a team choosing squashes must pair the choice with the branch-lifecycle rule — squash, then retire the branch — or it will keep meeting this conflict storm and blame the merge algorithm for it.

  • How do you keep working on a branch that has already been squash-merged?
    Restart it from the target branch: point the branch at the current `main` so the squash commit is in its history, then commit new work on top. If commits were added after the squash, rebase only those onto `main` rather than the whole branch, so already-integrated work is not replayed.
  • Why does git branch --merged not list a squash-merged branch?
    Because it tests reachability, not content equality. It asks whether the branch tip is an ancestor of the branch you are checking against. A squash commit has one parent and no edge to the branch, so the branch tip stays unreachable even though its changes are present.
  • Would using -X ours on the second merge be a reasonable fix?
    No. Strategy options only decide which side wins in conflicting hunks; the merge base is still the stale fork point, so genuinely new work on the branch can be discarded and old work resurrected without any warning. Fix the branch's base instead of masking the symptom.

Photocopying a colleague's notes into your notebook does not make their notebook part of yours; next time you compare, every page still looks new to you.

saying these in an interview costs you the question

  • Thinks Git tracks squash merges as merged branches
  • Assumes the merge base advances when content matches
  • Suggests -X theirs as the general fix for the conflicts
  • Blames the merge algorithm rather than the missing parent link
  • Proposes force-pushing main to clear the conflicts

context