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¶
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:
- 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.
- 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¶
- Always capture exit codes explicitly. Git returns exit code 1 when conflicts occur, which triggers
set -etraps if uncaught. - Use
--no-messageswhen you only need the root tree SHA for scripting. - Use
--name-onlywhen reporting conflict summaries to pull request webhooks. - Clean up dangling synthetic commits periodically with
git pruneif they are not referenced by branch or tag pointers.