zz_

app/agent/threeWayMerge.ts

zzmq.dev@gmail.com/zz / app / agent / threeWayMerge.ts @ 85fc543a Download
Ladder Summary
  • file The module provides a line‑level three‑way merge utility that compares a set of proposed file contents against the current `HEAD` of a Git repository using Git’s own `merge-file` (with `--diff3`) to produce clean results or conflict markers. It exports the `MergeResult` interface (describing the clean flag, merged files and any conflicted files) and the `threeWayMerge` function, which determines which proposal files overlap with changes made since a given base commit, runs the merge in a temporary directory, and returns the merged contents together with any conflict information. The internal helper `changedPaths` (not exported) is used to detect files altered by other commits.
 // A real line-level 3-way merge (git's own `merge-file`, the same
 // machinery `git merge`/`git rebase` use), not symbol-name comparison.
 // Replaces asking a model to regenerate whole files from two divergent
 // blobs -- unreliable even in the trivial additive case (observed
 // directly: gpt-oss-120b dropped the same export identically on every
 // retry when asked to "reconcile" two full versions). Most conflicts
 // aren't real conflicts at all once you look at lines instead of
 // names: two proposals touching different files, or different lines
 // of the same file, merge cleanly with zero model involvement. Only a
 // genuine overlapping-line conflict needs any judgment at all, and
 // even then the model only ever sees the few marked lines that
 // actually conflict, not the whole file.
 import { execFileSync } from "node:child_process";
 import { mkdtempSync, writeFileSync, rmSync } from "node:fs";
 import { tmpdir } from "node:os";
 import { join } from "node:path";
 import { fileContentAtCommit } from "../engine/ingest.js";
 
 export interface MergeResult {
  clean: boolean;
  /** Final content for every proposal file that merged cleanly (including ones no other change touched at all). */
  files: { path: string; content: string }[];
  /** Files with a real line-level overlap -- content includes git's own <<<<<<</=======/>>>>>>> (and ||||||| base, via --diff3) markers. */
  conflicts: { path: string; markedContent: string }[];
 }
 
 function changedPaths(repoDir: string, fromSha: string, toSha: string): Set<string> {
  if (fromSha === toSha) return new Set();
  const out = execFileSync("git", ["-C", repoDir, "diff", "--name-only", fromSha, toSha], { encoding: "utf8" });
  return new Set(out.split("\n").filter(Boolean));
 }
 
 export function threeWayMerge(repoDir: string, baseSha: string, proposalFiles: { path: string; content: string }[]): MergeResult {
  const headSha = execFileSync("git", ["-C", repoDir, "rev-parse", "HEAD"], { encoding: "utf8" }).trim();
  if (baseSha === headSha) {
  // Nothing landed since this proposal was generated -- no merge to do.
  return { clean: true, files: proposalFiles, conflicts: [] };
  }
  const changedByOthers = changedPaths(repoDir, baseSha, headSha);
  const files: { path: string; content: string }[] = [];
  const conflicts: { path: string; markedContent: string }[] = [];
  const tmp = mkdtempSync(join(tmpdir(), "zz-merge-"));
  try {
  for (const f of proposalFiles) {
  if (!changedByOthers.has(f.path)) {
  // Nobody else touched this path -- the proposal's version applies cleanly on top of the current tree.
  files.push(f);
  continue;
  }
  const basePath = join(tmp, "base");
  const oursPath = join(tmp, "ours");
  const theirsPath = join(tmp, "theirs");
  writeFileSync(basePath, fileContentAtCommit(repoDir, baseSha, f.path));
  writeFileSync(oursPath, fileContentAtCommit(repoDir, headSha, f.path));
  writeFileSync(theirsPath, f.content);
  try {
  // -p: write the merge result to stdout instead of mutating
  // `ours` in place. --diff3 adds the base content into the
  // conflict markers too, so a model (or a human) resolving one
  // can see what changed on each side, not just the two results.
  const merged = execFileSync("git", ["merge-file", "-p", "--diff3", oursPath, basePath, theirsPath], { encoding: "utf8" });
  files.push({ path: f.path, content: merged });
  } catch (err) {
  const e = err as { status?: number; stdout?: string };
  if (e.status === 1 && typeof e.stdout === "string") {
  conflicts.push({ path: f.path, markedContent: e.stdout });
  } else {
  throw err;
  }
  }
  }
  } finally {
  rmSync(tmp, { recursive: true, force: true });
  }
  return { clean: conflicts.length === 0, files, conflicts };
 }