You want to know whether two branches will clash before you merge them for real. git merge-tree runs the calculation and leaves your checkout completely untouched. The examples use the installed Git 2.43.0 and its modern --write-tree mode.
Allow about fifteen minutes. You need Git, a repository containing both commits or branch names, and permission to read its objects. These checks are ordinary user commands: they need no sudo, and this guide never creates a merge commit or updates a branch.
Start by confirming the version and the command shape. This step is read-only:
$ git --version
git version 2.43.0
$ git merge-tree -h
usage: git merge-tree [--write-tree] [<options>] <branch1> <branch2>
The first two arguments are the commits or branch names to merge, and --write-tree goes before them. That mode runs the merge calculation and writes the resulting tree object, but it does not write that tree to your checkout. The older --trivial-merge form is deprecated with a different, limited output format, so leave it out of new commands.
Checkpoint: Run git status --short before and after a test. The output should be identical. A dirty checkout is allowed, but keeping it unchanged makes the safety property easy to verify.
Run the two branches you want to compare, replacing the example names with references from your repository:
$ git merge-tree --write-tree main feature/example
c5f43f22c211eae997158c7544475e59656d9b2a
$ printf 'exit status: %s\n' "$?"
exit status: 0
A clean result is one line containing the object ID of the top-level tree the merge would produce. Your own ID will differ. Exit status 0 means the merge was clean: no commit was made, no branch moved, and neither the index nor the working tree changed.
The tree object is useful to tooling, but it is not a commit and not a branch tip. Do not read that first line as a new commit ID. To inspect its paths, use Git's tree-reading commands against the object instead:
$ TREE_ID=$(git merge-tree --write-tree main feature/example)
$ git ls-tree "$TREE_ID"
100644 blob <object> README.md
040000 tree <object> src
The sample object IDs here are placeholders. If you assign the output to a variable, check the status immediately: a conflicted merge prints more than one section, so it must never be mistaken for a plain tree ID.
Make the status test explicit. Status 1 means the merge ran but has conflicts:
$ set +e
$ git merge-tree --write-tree main feature/example
dfa9fe8a1f2332dfce0a59752d6d3c27d3292911
100644 <base-object> 1 note.txt
100644 <main-object> 2 note.txt
100644 <feature-object> 3 note.txt
Auto-merging note.txt
CONFLICT (content): Merge conflict in note.txt
$ status=$?
$ set -e
$ printf 'merge status: %s\n' "$status"
merge status: 1
The exact object IDs, whitespace and messages will vary. What matters is the status: 0 is clean, 1 is conflicted, and any other non-zero value means Git could not complete or start the merge. A conflicted result can still include a top-level tree representing the attempted merge, possibly containing conflict markers, but it is not permission to update a branch automatically.
In a shell script, capture the status without letting set -e abort before you can classify it:
if output=$(git merge-tree --write-tree "$BASE_BRANCH" "$TOPIC_BRANCH"); then
printf '%s\n' 'merge is clean'
else
status=$?
if [ "$status" -eq 1 ]; then
printf '%s\n' 'merge has conflicts' >&2
else
printf 'merge-tree failed with status %s\n' "$status" >&2
exit "$status"
fi
fi
This keeps the output in a variable for reporting. For large merges, send it to a temporary file instead of assuming it is only one short line.
For a human-friendly list of paths with higher-order conflict stages, add --name-only:
$ git merge-tree --write-tree --name-only main feature/example
dfa9fe8a1f2332dfce0a59752d6d3c27d3292911
note.txt
Auto-merging note.txt
CONFLICT (content): Merge conflict in note.txt
Useful for displaying affected file names, but the empty or non-empty path list is not the merge verdict. Some conflicts do not map neatly to an individual file, and several logical conflicts can affect the same path. Always check the exit status too.
Without --name-only, the conflict section carries mode, object ID, stage and path records for programs that need index-like data. Do not infer the full conflict type from stage numbers, and do not take those object IDs and re-merge them to recreate the result. The top-level tree preserves context, including useful conflict-marker annotations.
--stdin reads one merge per line: two branches, or a supplied merge base followed by -- and the two branches.
$ printf '%s\n' \
'main feature/example' \
'release -- main feature/example' \
| git merge-tree --write-tree --stdin
0\0<tree-id>\0
1\0<tree-id>\0
The \0 shown there means NUL bytes, not four printable characters. With standard input, Git prefixes each result with a numeric merge status and separates records with NUL bytes. Here the status value is 0 for conflicts and 1 for a clean merge, the reverse of the ordinary process exit status. A negative status means a requested merge could not run.
That reversal is an easy automation trap. Pick one input mode and test its documented output contract before wiring it into a larger pipeline. --merge-base cannot be combined with --stdin, and supplying a merge base directly is not equivalent to Git finding multiple merge bases itself.
By default, branches with no common history are rejected. If joining them is genuinely intended, opt in explicitly:
$ git merge-tree --write-tree --allow-unrelated-histories old-root new-root
<tree-id>
This option only permits the merge calculation. It does not make the histories trustworthy, resolve content disagreements, or create the eventual merge commit. Treat it as a review boundary: confirm the two references and their provenance before handing the resulting tree to another command.
--write-tree syntax.--stdin use different status conventions.git status --short confirms that the index and working tree remain unchanged.