zz_

app/cli/lib/fileDiff.ts

zzmq.dev@gmail.com/zz / app / cli / lib / fileDiff.ts @ 6b78e56e Download
Ladder Summary
  • file The module defines a **FileEntry** interface and exports a single function, **diffFileSets**, which takes two arrays of `FileEntry` objects (representing “before” and “after” in‑memory file sets) and returns a unified, ANSI‑colored diff string for each changed or newly‑added file, omitting unchanged files. Internally it writes the two versions to temporary files and invokes `git diff --no-index --color=always`, stripping Git’s own header lines so the output can be embedded directly in chat transcripts. No other functions or classes are exported.
 // A real unified diff (not a side-by-side full-file redump) between two
 // in-memory file sets -- what `zz work --assist`'s chat transcript shows
 // for a revision, so a human reviewing a second/third draft sees what
 // actually changed instead of re-reading the whole file from scratch
 // every turn. Shells out to `git diff --no-index` against two temp
 // files rather than hand-rolling a diff algorithm: it's already
 // installed everywhere this runs, and `--color=always` gives a real
 // ANSI-colored unified diff for free.
 import { execFileSync } from "node:child_process";
 import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
 import { tmpdir } from "node:os";
 import { join } from "node:path";
 
 export interface FileEntry {
  path: string;
  content: string;
 }
 
 /** One block of diff text per changed/added file (joined with blank lines); omits files whose content is identical in both sets. Empty string if nothing changed. */
 export function diffFileSets(before: FileEntry[], after: FileEntry[]): string {
  const beforeMap = new Map(before.map((f) => [f.path, f.content]));
  const blocks: string[] = [];
  for (const f of after) {
  const prev = beforeMap.get(f.path);
  if (prev === f.content) continue;
  if (prev === undefined) {
  blocks.push(`--- ${f.path} (new) ---\n${f.content}`);
  continue;
  }
  blocks.push(`--- ${f.path} ---\n${unifiedDiff(prev, f.content, f.path)}`);
  }
  return blocks.join("\n\n");
 }
 
 // Drops git's own "diff --git <tempdir>/a <tempdir>/b" / "index .." /
 // "--- a" / "+++ b" header lines -- the caller already prints a clean
 // "--- <real path> ---" label above this, and the temp paths those
 // headers carry are meaningless noise in a chat transcript. Keeps
 // everything from the first "@@" hunk marker onward.
 function stripHeader(diffText: string): string {
  const lines = diffText.split("\n");
  const hunkStart = lines.findIndex((l) => l.replace(/\x1b\[[0-9;]*m/g, "").startsWith("@@"));
  return (hunkStart === -1 ? lines : lines.slice(hunkStart)).join("\n");
 }
 
 function unifiedDiff(before: string, after: string, label: string): string {
  const dir = mkdtempSync(join(tmpdir(), "zz-diff-"));
  try {
  const a = join(dir, "a");
  const b = join(dir, "b");
  writeFileSync(a, before);
  writeFileSync(b, after);
  try {
  // git diff --no-index exits 0 only when the files are identical
  // (never reached here -- the caller already checked that) --
  // exit 1 means "real diff produced," which execFileSync treats
  // as a thrown error whose own .stdout still has the diff text.
  return stripHeader(execFileSync("git", ["diff", "--no-index", "--color=always", a, b], { encoding: "utf8" }));
  } catch (err) {
  const stdout = (err as { stdout?: string }).stdout;
  return stdout ? stripHeader(stdout) : `(couldn't compute a diff for ${label} -- showing it unchanged)`;
  }
  } finally {
  rmSync(dir, { recursive: true, force: true });
  }
 }