Using git merge-tree: Check for Conflicts in 1 Second Without Switching Branches or Stashing

Git tutorial - IT technology blog
Git tutorial - IT technology blog

Quick Test: Catch Conflicts in 30 Seconds

Imagine you have 10 uncommitted modified files on your feature/login branch, not yet ready to commit. Suddenly, your team lead merges a major PR into main and asks you to rebase and check for conflicts.

The typical instinct is to run git stash, switch to main, pull latest changes, and attempt a dry merge. This approach requires tedious steps and risks losing uncommitted work if stash pop runs into issues. Starting with Git 2.36, you can perform this check entirely in memory without touching a single file on disk:

# Fetch the latest commits from remote to the local database
git fetch origin

# Check for conflicts between the current branch and origin/main
git merge-tree --write-tree HEAD origin/main

Git computes the merge in RAM and returns the result in about 30–50 milliseconds:

  • Case 1 – Clean merge: Git exits with code 0 and prints a single 40-character Tree OID hash:
4b825dc642cb6eb9a060e54bf8d69288fbee4904
  • Case 2 – Conflicts found: Git outputs the temporary tree hash, lists the conflicting files, and exits with code 1:
Auto-merging src/auth.js
CONFLICT (content): Merge conflict in src/auth.js

If your project has thousands of files and you only want to quickly list conflicting file names, add the --name-only flag:

git merge-tree --write-tree --name-only HEAD origin/main

Under the Hood: Why Is This Approach Better?

The Cost of the Traditional Trial-Merge Workflow

A standard git merge must update both the Staging Area (Index) and the Working Tree. If your directory contains uncommitted changes, Git halts immediately to prevent data loss. To bypass this, developers often go through this dance:

git stash
git merge --no-commit --no-ff origin/main
# Check for conflicts...
git merge --abort
git stash pop

This sequence introduces three major pain points:

  1. Modifies file timestamps (mtime): Build tools like Vite, Next.js, or Webpack detect timestamp changes and trigger a full rebuild from scratch. In large projects with 3,000–5,000 modules, you waste 15–30 seconds just waiting for recompilation.
  2. Triggers unintended Git hooks: Heavyweight pre-merge or post-checkout scripts run automatically in the background.
  3. Risk of data loss: If an abort fails to clean up properly, subsequent git stash pop operations can trigger messy conflict cascades, putting your uncommitted work at risk.

The Power of Plumbing Commands

In Git’s architecture:

  • Porcelain: High-level, user-friendly commands like checkout, commit, and merge that directly manipulate files on your filesystem.
  • Plumbing: Low-level commands designed for core operations and scripting, such as cat-file, commit-tree, and merge-tree.

The git merge-tree command operates at the plumbing layer. The 3-way merge algorithm calculates changes directly between commit tree objects located in .git/objects. Everything happens in RAM and Git’s object database, with zero writes to your working directory.

What Changed in the New Syntax (Git 2.36+)?

Prior to version 2.36 released in 2022, git merge-tree was cumbersome because it required developers to manually resolve the common ancestor (merge base):

# Legacy syntax (verbose, difficult to parse):
git merge-tree $(git merge-base branchA branchB) branchA branchB

# Modern syntax introduced in Git 2.36 (recommended):
git merge-tree --write-tree branchA branchB

The --write-tree flag makes this command a true logical equivalent of git merge, automatically determining the merge base and handling rename detection with 100% accuracy.

Practical Applications in Development Workflows

1. Pre-PR Validation Script

You can use this Bash script in your terminal to catch conflicts early before opening a PR on GitHub or GitLab:

#!/usr/bin/env bash
TARGET="origin/main"
CURRENT=$(git rev-parse --abbrev-ref HEAD)

echo "Scanning for conflicts between $CURRENT and $TARGET..."

OUTPUT=$(git merge-tree --write-tree HEAD "$TARGET" 2>&1)
STATUS=$?

if [ $STATUS -eq 0 ]; then
    echo "✅ Clean: No conflicts found, ready to merge!"
else
    echo "❌ Warning: Conflicts detected in the following files:"
    echo "$OUTPUT" | grep -E "(CONFLICT|Auto-merging)"
    exit 1
fi

2. Optimizing CI/CD Automation Bots

On CI/CD platforms like GitHub Actions or GitLab CI, verifying mergeability across dozens of concurrent PRs is resource-heavy if each job must check out a full working tree. Running git merge-tree against a bare repository allows the runner to determine PR mergeability in tens of milliseconds, automatically tagging PRs with needs-rebase without checking out any code.

Daily Tips and Tricks

Set Up a Shorthand Git Alias

Configure this once in your ~/.gitconfig:

git config --global alias.dry-merge 'merge-tree --write-tree'

Now you can simply run:

git dry-merge HEAD origin/main

Works Seamlessly Offline

As long as you have fetched earlier via git fetch, the command runs entirely offline. Conflict detection remains virtually instantaneous even on massive repositories like Chromium or the Linux kernel.

Exit Code Conventions for Scripting

  • 0: Clean merge with no conflicts.
  • 1: Content or file rename conflicts detected.
  • Other than 0 and 1: Syntax error or branch does not exist in the repository.
Share: