Skip to content

In-Memory Server-Side Merging with git-merge-tree

Porcelain commands such as git merge require an initialized working tree, write access to .git/index, and disk checkouts. In automated continuous integration pipelines, server-side webhooks, and merge queue bots, checking out files to test mergeability creates disk contention and slows pipeline execution.

Git 2.38 introduced the modern --write-tree mode for git merge-tree. This plumbing command executes a full three-way merge directly in memory against the object database. It creates new tree objects without checking out code, updating index files, or touching working trees.

DevOps engineers use in-memory merging to build high-throughput merge queues, validate pull request mergeability in milliseconds, and create synthetic test commits on bare repositories.


Command Anatomy

git merge-tree --write-tree [--merge-base=<commit>] <branch1> <branch2>

Exit codes

  • 0: Merge succeeded without conflicts.
  • 1: Merge encountered conflicts.
  • >1: Fatal error (such as an invalid object name or missing reference).

Output structure

When git merge-tree --write-tree runs, standard output returns:

  1. First line: The SHA-1 or SHA-256 hash of the resulting root tree object. If conflicts occur, Git writes a tree recording the conflicted stages.
  2. Subsequent lines: Informational messages, including auto-merges, rename detection notices, and conflict descriptions.
# Example clean merge output
$ git merge-tree --write-tree main feature/api
9f83d97e2f5b40b106e2365287f3b890885a539b

# Example conflicted merge output (exit code 1)
$ git merge-tree --write-tree main feature/legacy
d4a821e860950a6e5b4b1a8d0526017830b809ec
CONFLICT (content): Merge conflict in src/config.yaml
Auto-merging src/app.py

Core Flags and Options

Option Function DevOps application
--write-tree Executes modern 3-way merge and writes tree objects Required for merge queue automation
--merge-base=<commit> Sets explicit base commit instead of common ancestor Simulates rebase chains and squash merges
--name-only Outputs only the file paths containing conflicts Fast conflict filtering in CI pipelines
--messages Prints conflict and auto-merge notices to stdout Default mode; used for error diagnostics
--no-messages Suppresses all conflict and informational text Returns only the tree SHA for clean scripting
--allow-unrelated-histories Merges branches with no common ancestor commit Joining independent repositories
--trivial-merge Disallows three-way file content merges Rejects merges requiring content resolution

DevOps Engineering Patterns

1. Zero-checkout merge gate in CI

Pull request workflows often need to verify if a feature branch merges cleanly into origin/main before running expensive integration suites.

Traditional approaches require git checkout or git fetch with local branch merging. With git merge-tree, the validation completes in milliseconds on bare checkouts.

#!/usr/bin/env bash
set -euo pipefail

BASE_REF="origin/main"
PR_HEAD="origin/pr/104"

# Run in-memory merge test
if OUTPUT=$(git merge-tree --write-tree --name-only "$BASE_REF" "$PR_HEAD" 2>&1); then
  MERGE_TREE=$(echo "$OUTPUT" | head -n 1)
  echo "Merge test passed. Clean tree: $MERGE_TREE"
  exit 0
else
  STATUS=$?
  echo "Merge conflict detected (exit code $STATUS)."
  echo "Conflicting paths:"
  echo "$OUTPUT" | tail -n +2
  exit 1
fi

2. Synthesizing merge commits without checking out

CI pipelines often build and test the outcome of a merge without pushing the merge commit back to the target branch. Instead of checking out branches and running git merge --no-ff, synthesize the commit object using git merge-tree and git commit-tree.

#!/usr/bin/env bash
set -euo pipefail

BASE="origin/main"
HEAD="origin/feature/auth"

# Step 1: Compute merged tree in memory
MERGED_TREE=$(git merge-tree --write-tree --no-messages "$BASE" "$HEAD")

# Step 2: Resolve parent commit SHAs
PARENT1=$(git rev-parse "$BASE")
PARENT2=$(git rev-parse "$HEAD")

# Step 3: Write commit object directly to .git/objects
COMMIT_MSG="ci(test): synthetic merge of $HEAD into $BASE"
SYNTHETIC_COMMIT=$(git commit-tree "$MERGED_TREE" -p "$PARENT1" -p "$PARENT2" -m "$COMMIT_MSG")

echo "Created synthetic merge commit: $SYNTHETIC_COMMIT"

# Step 4: Checkout or build the synthetic commit directly in a runner worktree
# git checkout "$SYNTHETIC_COMMIT"

This sequence takes under 15 milliseconds because Git writes tree and commit objects directly to .git/objects without writing files to disk.

3. Monorepo speculative merge queues

Merge queues test pull requests in batches before landing on main. If developers submit PR #101, PR #102, and PR #103 simultaneously, the queue tests them sequentially:

  • Speculative Commit 1: main + PR 101
  • Speculative Commit 2: (main + PR 101) + PR 102
  • Speculative Commit 3: ((main + PR 101) + PR 102) + PR 103

If PR #102 conflicts with PR #101, the queue ejects PR #102 immediately without rebuilding the workspace.

#!/usr/bin/env bash
set -euo pipefail

CURRENT_BASE="origin/main"
QUEUE=("origin/pr/101" "origin/pr/102" "origin/pr/103")

for PR in "${QUEUE[@]}"; do
  echo "Evaluating $PR on top of $CURRENT_BASE..."

  if TREE=$(git merge-tree --write-tree --no-messages "$CURRENT_BASE" "$PR" 2>/dev/null); then
    P1=$(git rev-parse "$CURRENT_BASE")
    P2=$(git rev-parse "$PR")
    CURRENT_BASE=$(git commit-tree "$TREE" -p "$P1" -p "$P2" -m "merge-queue: speculative merge $PR")
    echo "  Success. Speculative tip advanced to: $CURRENT_BASE"
  else
    echo "  CONFLICT in $PR. Ejecting from current batch."
  fi
done

4. Simulating rebases with --merge-base

To test if a feature branch can rebase onto the latest upstream commit without running interactive rebasing commands, pass --merge-base pointing to the branch fork point.

#!/usr/bin/env bash
set -euo pipefail

UPSTREAM="origin/main"
FEATURE="feature/payment-v2"

FORK_POINT=$(git merge-base "$UPSTREAM" "$FEATURE")

# If we treat FORK_POINT as merge-base, we verify if FEATURE can merge onto UPSTREAM
if TREE=$(git merge-tree --write-tree --merge-base="$FORK_POINT" "$UPSTREAM" "$FEATURE" 2>/dev/null); then
  echo "Rebase simulation clean. Resulting tree: $TREE"
else
  echo "Rebase will require manual conflict resolution."
fi

In-Memory Merging vs Traditional git merge

Metric / Behaviour git merge (Porcelain) git merge-tree --write-tree (Plumbing)
Working tree required Yes No (works in bare repositories)
.git/index lock required Yes No
Filesystem disk I/O Reads and writes every changed file Writes only Git objects (.git/objects)
Execution speed 100ms - 5000ms+ (proportional to repo size) 5ms - 25ms (in-memory tree diffing)
Concurrency safety Unsafe across concurrent processes Safe; zero shared index mutations
Hook execution Fires pre-merge-commit, post-merge Zero hook triggers
Conflict handling Halts and writes conflict markers to disk Returns exit code 1 and conflict report to stdout

Server-Side and Hook Applications

Pre-receive merge gate

Run on a Git server (such as Forgejo, Gitea, or GitLab) inside .git/hooks/pre-receive to reject branch pushes that would create unresolvable conflicts against master:

#!/usr/bin/env bash
set -euo pipefail

while read -r OLD_REV NEW_REV REF_NAME; do
  if [[ "$REF_NAME" =~ ^refs/heads/feature/ ]]; then
    MAIN_REV=$(git rev-parse refs/heads/main)
    if ! git merge-tree --write-tree --name-only "$MAIN_REV" "$NEW_REV" >/dev/null 2>&1; then
      echo "REJECTED: Push creates merge conflicts with main branch." >&2
      exit 1
    fi
  fi
done

Best Practices

  1. Always capture exit codes explicitly. Git returns exit code 1 when conflicts occur, which triggers set -e traps if uncaught.
  2. Use --no-messages when you only need the root tree SHA for scripting.
  3. Use --name-only when reporting conflict summaries to pull request webhooks.
  4. Clean up dangling synthetic commits periodically with git prune if they are not referenced by branch or tag pointers.