Three-way merge
import { threeWayMerge } from 'git-edge';
const { commitOid } = await threeWayMerge(repo, sourceRef, targetRef, opts?);Why not git.merge?
Section titled “Why not git.merge?”isomorphic-git’s own git.merge needs a worktree — a real (or hydrated-to-/tmp)
checkout to run its merge driver against. For a bare repo living entirely in
object storage with no durable disk, that means materializing the whole tree
locally before merging anything. threeWayMerge never does this: it reads
trees, blobs, and commits directly off the object graph and writes the result
the same way.
What it does, in order
Section titled “What it does, in order”-
Already up to date. If
targetRefalready containssourceRef(the same commit, or source is an ancestor of target), returns target’s oid — nothing is written and no ref moves. -
Fast-forward. If
sourceRefis a descendant oftargetRef,targetRefis simply moved tosourceRef’s oid. No merge commit. -
True three-way merge. Otherwise: finds the merge base via
git.findMergeBase(unrelated histories throwGitEdgeError) and merges the three root trees recursively. At each level, per entry name:- Both sides identical (oid and mode) → take it (a deletion on both sides stays deleted).
- Only one side differs from the base → take that side — a change, an addition, a mode change or a deletion.
- Both sides changed it differently:
- both are directories → recurse into them (subtrees only one side touched are taken by oid, never read);
- both are regular files → content merge (below); the mode one side changed wins, both changing it differently is a conflict;
- anything else — modify/delete, a file on one side and a directory on the other, symlinks, submodules — is a conflict.
-
Writes the merged trees and a merge commit (parents: target, then source) with
git.writeCommit, then movestargetRefto it. OnlytargetRefmoves — never the branchHEADnames. All new objects are stored as one pack (git-fs-s3’swithPackedWrites), written before the ref.
Content merge
Section titled “Content merge”For files both sides changed to different content, threeWayMerge runs a
line-level three-way merge via node-diff3
— the LCS-based diff3 algorithm GNU diffutils’ diff3 and git’s default
merge driver use. A file added independently on both sides merges against an
empty base. Any hunk both sides changed differently is a conflict.
Binary files (git’s heuristic: a NUL byte in any of the three versions) are never line-merged: both sides changing one differently is a conflict.
Conflicts
Section titled “Conflicts”If any path conflicts, threeWayMerge throws GitMergeConflictError once
every path has been examined — conflictingPaths: string[] (sorted) lists
them all. Nothing is written: no merged trees, no commit, no ref change.
import { GitMergeConflictError } from 'git-edge';
try { await threeWayMerge(repo, 'refs/heads/feature', 'refs/heads/main');} catch (err) { if (err instanceof GitMergeConflictError) { // err.conflictingPaths: string[] }}Concurrency
Section titled “Concurrency”A merge merges each directory’s entries concurrently, so a wide directory
would otherwise issue one object-store request per entry at once. threeWayMerge
bounds the object reads and writes in flight to 16 per call. Each permit wraps
one readTree, readBlob, writeTree, or writeBlob call and is released
before the recursive subtree merge, so a permit is never held while waiting on
a child merge — a wide tree cannot deadlock itself.
If you need a cheap pre-merge check before calling threeWayMerge — resolve
both refs and check ancestry, without walking any trees — use analyzeMerge
from git-fs-s3/ops. It
shares the same { fs, gitdir, cache? } shape and never detects content
conflicts; only a real threeWayMerge call does that.
Options
Section titled “Options”opts?: MergeOpts — all optional:
| Option | Default |
|---|---|
message |
`Merge ${sourceRef} into ${targetRef}` |
authorName |
"Git Edge" |
authorEmail |
"git-edge@local" |
Returns { commitOid: string } — the new merge commit’s oid, or the
fast-forwarded ref’s new oid when no merge commit was needed.