Previewing merges with git merge-tree Jump to heading

Most ways of finding out whether two branches merge cleanly involve doing the merge: check out one branch, merge the other, look, then abort. That needs a working tree, moves HEAD, and cannot run in parallel. Since Git 2.38, git merge-tree --write-tree performs a real merge β€” the same ort strategy, the same rename detection β€” entirely in the object database. It prints the resulting tree, lists conflicted paths, and exits with a status that says whether the merge was clean. Nothing is checked out and no ref moves, so it is safe on a bare repository, in CI, and across hundreds of branch pairs at once. This page shows how to use it for conflict previews and for building testable merge commits, within semantic conflicts and merge verification.

When to use this approach Jump to heading

  • You want to know whether a branch will merge cleanly without touching your working tree.
  • You want a dashboard of which open branches currently conflict with main.
  • You want CI to test the result of a merge before it happens, including on servers with bare repositories.
  • You are measuring conflict rates across history, as in measuring branch age against conflict risk.

Step 1 β€” Run a preview and read the output Jump to heading

Give merge-tree two commits. It finds their merge base, merges, writes the result tree and prints its ID. On conflict it also prints the conflicted paths with their stages, and informational messages.

git merge-tree --write-tree main feature/renewals
# 3f1c9a2e7d...                 (exit 0: clean)

git merge-tree --write-tree main feature/payments-v2
# 7a0b2c4d1e...                 (exit 1: conflicts)
# 100644 1d3f... 1	src/payments/client.py
# 100644 9e2a... 2	src/payments/client.py
# 100644 4b77... 3	src/payments/client.py
#
# Auto-merging src/payments/client.py
# CONFLICT (content): Merge conflict in src/payments/client.py

Even on conflict, the printed tree exists: it contains the files with conflict markers, exactly as a real merge would leave them in the working tree.

Previewing by checkout against merge-treePreviewing by checking out and merging needs a clean working tree, moves HEAD, and must be aborted afterwards, so it cannot run in parallel. merge-tree computes the same merge in the object database, leaves every ref and file alone, and can preview many pairs at once.checkout + merge --no-commitmerge-tree --write-treeneeds working treeyes, cleannomoves HEADyesnoparallel previewsnoyessame algorithmyesyes (ort)same answer, none of the side effects
# Quiet form for scripts: only paths, and the exit code
git merge-tree --write-tree --name-only --no-messages main feature/payments-v2 | tail -n +2

Step 2 β€” Inspect a conflicted preview Jump to heading

The tree from a conflicted preview contains marker-laden files, so you can look at exactly what someone would see when merging.

tree=$(git merge-tree --write-tree main feature/payments-v2 | head -1)
git show "$tree:src/payments/client.py" | sed -n '/<<<<<<<</,/>>>>>>>/p'

For a quick conflict count without reading files, count the distinct paths in the stage lines.

git merge-tree --write-tree --name-only main feature/payments-v2 | tail -n +2 | sed '/^$/,$d' | wc -l

Step 3 β€” Preview every open branch against main Jump to heading

Because previews have no side effects, a loop over all remote branches is safe and fast. Publish the result where branch owners see it.

#!/bin/sh
# conflict-report.sh β€” which branches currently conflict with main?
git fetch -q origin
for b in $(git for-each-ref --format='%(refname:short)' refs/remotes/origin/ | grep -vE 'origin/(main|HEAD)$'); do
  if paths=$(git merge-tree --write-tree --name-only --no-messages origin/main "$b" 2>/dev/null); then
    printf 'clean     %s\n' "$b"
  else
    n=$(printf '%s\n' "$paths" | tail -n +2 | sed '/^$/,$d' | wc -l)
    printf 'CONFLICT  %-40s %d file(s)\n' "$b" "$n"
  fi
done | sort
An example conflict reportA conflict report across open branches, run on one clone in a few seconds. Most branches merge cleanly with main. A few conflict, and the number of files tells their owners how much work an update will be.conflicted files vs main, per open branch (example)feature/renewalscleanfeature/search-tuningcleanfeature/payments-v23feature/export-retries1chore/deps-sept7the dependency branch is usually the worst β€” lockfiles conflict with everything

Run it on a schedule or on every push to main. Branch owners learn about conflicts when main changes, not when they next try to merge.

Step 4 β€” Build the merged result into a testable commit Jump to heading

A clean preview gives a tree. Wrap it in a commit with both branch tips as parents, and you have a commit identical to the merge that would be created β€” which CI can check out and test.

tree=$(git merge-tree --write-tree origin/main origin/feature/renewals) || { echo "conflicts"; exit 1; }
commit=$(git commit-tree "$tree" -p origin/main -p origin/feature/renewals -m "preview merge")
git push -q origin "$commit:refs/previews/feature-renewals"      # a ref CI can build
git checkout -q --detach "$commit" && make test
Testing a merge before it existsA job computes the merge tree of main and a branch, wraps it in a commit with both tips as parents, pushes it to a preview ref, and runs the test suite on it. The result tells the branch owner whether merging now would break main.jobobject storepreview reftest suitemerge-tree --write-treetree IDcommit-tree -p main -p branchpush preview commitmake test on previewthe preview commit has the same tree a real merge would β€” so the test result transfers

Pushing previews under a dedicated namespace such as refs/previews/ keeps them out of branch lists. Delete old previews periodically, or let a scheduled job overwrite them.

Step 5 β€” Use an explicit merge base when you need to Jump to heading

merge-tree computes the merge base itself. For special cases β€” simulating a merge from a different base, or scripting a cherry-pick-like merge β€” give it explicitly with --merge-base (Git 2.40+).

# Simulate applying feature's changes since <base> onto release/2.4
git merge-tree --write-tree --merge-base="$base" origin/release/2.4 origin/feature/fix

This is how some tools implement server-side rebases and cherry-picks without a working tree.

Validation checklist Jump to heading

Frequently Asked Questions Jump to heading

Is the old git merge-tree without --write-tree the same thing? Jump to heading

No. The old mode used a trivial merge algorithm and printed a hard-to-parse diff. It is deprecated; always pass --write-tree.

Does merge-tree respect .gitattributes merge drivers? Jump to heading

Yes. It uses the same merge machinery as git merge, including custom merge drivers configured in .gitattributes, so lockfile drivers and merge=union behave the same in previews.

Can merge-tree replace rerere in previews? Jump to heading

It does not consult the rerere cache, so previews show raw conflicts even where rerere would resolve them. That is usually what you want from a preview β€” an honest view of the conflict β€” but it means previews can report conflicts your team resolves automatically.