Finding the merge base and why it matters Jump to heading

Every three-way merge compares three snapshots: ours, theirs, and the merge base — the most recent commit both branches share. Git does not diff ours against theirs. It diffs each against the base, and combines the two sets of changes. That makes the base the single most important input to a merge, and the one people look at least. A base further back than you expect makes Git think both sides changed things they did not, producing conflicts nobody can explain. A base that includes a reverted change makes Git believe one side deliberately removed code. This page shows how to find the base, how to read a merge through it, and how to recognise when it is the source of a strange result. It belongs to 3-way merge fundamentals.

When to use this approach Jump to heading

  • A merge produced conflicts in code you are sure only one side touched.
  • A merge succeeded cleanly but silently undid or re-applied a change.
  • You are about to merge a long-lived branch and want to predict the result.
  • You are scripting merges or CI previews and need the base explicitly — as in previewing merges with git merge-tree.

Step 1 — Find the base Jump to heading

git merge-base prints the best common ancestor of two commits. With --all it prints every candidate, which matters when there is more than one.

git merge-base main feature/search
git merge-base --all main feature/search      # more than one line = criss-cross history
git log -1 --format='%h %ad %s' --date=short "$(git merge-base main feature/search)"
What a three-way merge comparesThe feature branch forked from main at commit B. Since then main gained M1 and M2, and the feature gained F1 and F2. Git merges by diffing B against M2 and B against F2, then combining both sets of changes.base B is the reference point for both diffsmainABM1M2featureBF1F2baseBours = B→M2, theirs = B→F2; anything changed in neither diff is left alone What a three-way merge comparesThe feature branch forked from main at commit B. Since then main gained M1 and M2, and the feature gained F1 and F2. Git merges by diffing B against M2 and B against F2, then combining both sets of changes.base B is the reference point for both diffsmainABM1M2featureBF1F2baseBours = B→M2, theirs = B→F2; anything changed in neither diff is left alone

Step 2 — See the merge as Git sees it: two diffs from the base Jump to heading

Before merging, look at both diffs from the base. The three-dot notation does exactly that: main...feature means “changes on feature since the merge base”.

git diff main...feature/search --stat      # what the feature changed since the base
git diff feature/search...main --stat      # what main changed since the base
# Paths changed on both sides are the only possible conflict sites
git diff --name-only main...feature/search | sort > /tmp/theirs.txt
git diff --name-only feature/search...main | sort > /tmp/ours.txt
comm -12 /tmp/ours.txt /tmp/theirs.txt

The last command lists the only files that can conflict. If a file is not in that list and you still see a conflict, the base is not what you think.

# Verification: the predicted conflict set matches the real one
git merge --no-commit --no-ff feature/search; git diff --name-only --diff-filter=U; git merge --abort

Step 3 — Recognise a base that is older than you expected Jump to heading

A base far in the past means both sides have drifted a long way, and many files changed on both. This happens when a branch was created from an old commit, or when a previous merge between the branches was undone by a rebase, removing the newer shared commit.

A recent base against a stale oneWith a recent base, each side's diff contains only its own work, and conflicts appear only where both truly changed the same lines. With a stale base, each diff includes months of unrelated changes, so far more files appear changed on both sides and conflicts multiply.Recent baseStale basefiles changed on both sidesa fewdozensconflictsreal overlapsmany spuriouscausenormalold fork point, lost mergeif conflicts look unrelated to the work, check how old the base is A recent base against a stale oneWith a recent base, each side's diff contains only its own work, and conflicts appear only where both truly changed the same lines. With a stale base, each diff includes months of unrelated changes, so far more files appear changed on both sides and conflicts multiply.Recent baseStale basefiles changed on both sidesa fewdozensconflictsreal overlapsmany spuriouscausenormalold fork point, lost mergeif conflicts look unrelated to the work, check how old the base is
# How far back is the base, in commits and in days?
base=$(git merge-base main feature/search)
git rev-list --count "$base..main"
git log -1 --format='%cr' "$base"

The fix is usually to bring the branch up to date with a merge or rebase from main, which moves the base forward. For long-lived branches that is routine maintenance, described in keeping a long-lived branch in sync with main.

Step 4 — Understand reverts through the base Jump to heading

The base explains the most confusing merge behaviour in Git: merging a branch again after reverting its earlier merge. Suppose feature was merged into main, the merge was reverted, and later feature gets more commits and is merged again. The base is now the commit feature was at when first merged, so Git considers those original changes “already in both” — and the revert on main is a change only main made. The result keeps the revert, and the original changes stay missing.

# To bring the original changes back, revert the revert before merging again
git revert <sha-of-the-revert-commit>
git merge feature

The full procedure is in undoing a bad merge on a shared branch.

Step 5 — Merge against a different base when you must Jump to heading

Occasionally you know a better base than the one Git picks — for instance, when cherry-picked commits make Git’s chosen base misleading. git merge-tree and git merge-file accept an explicit base, so you can compute a merge result against it and compare.

# Three-way merge of a single file against an explicit base
git show "$CHOSEN_BASE:src/search.py" > /tmp/base.py
git show main:src/search.py            > /tmp/ours.py
git show feature/search:src/search.py  > /tmp/theirs.py
git merge-file -p --zdiff3 /tmp/ours.py /tmp/base.py /tmp/theirs.py > /tmp/result.py
echo "conflicts: $?"
When a merge looks wrong, check the base firstIf conflicts appear in files only one side touched, the base is older than expected. If a merge silently drops changes, a revert sits between the base and one tip. If the base looks synthetic, there were several bases. Each points to a different fix.What looked wrong about the merge?unexpected conflictsStale baseupdate the branch firstchanges silently missingRevert after baserevert the revertodd base in markersSeveral basessee criss-cross guidegit merge-base --all answers the first question in every case When a merge looks wrong, check the base firstIf conflicts appear in files only one side touched, the base is older than expected. If a merge silently drops changes, a revert sits between the base and one tip. If the base looks synthetic, there were several bases. Each points to a different fix.What looked wrong about the merge?unexpected conflictsStale baseupdate the branch firstchanges silently missingRevert after baserevert the revertodd base in markersSeveral basessee criss-cross guidegit merge-base --all answers the first question in every case

Validation checklist Jump to heading

Frequently Asked Questions Jump to heading

Is the merge base the same as where I created the branch? Jump to heading

Only until the branches are merged with each other. After you merge main into your branch, the base moves to that point of main. That is why merging main in regularly makes later merges smaller.

What does git diff A...B mean exactly? Jump to heading

It means the diff from the merge base of A and B to B. It shows what B changed since the branches diverged, ignoring anything A did, which is what reviewers and merge tools want.

Why does Git sometimes choose a base I did not expect? Jump to heading

Because “best common ancestor” is defined by the graph, not by dates or branch names. Merges between branches, cherry-picks and rebases all change the graph, and Git picks the ancestor nearest to both tips.