diffReport

tcmts.cli.diff.diffReport · #wikhwdwb · term · runs on the server

uses tcmts.cli.files.fileContent, which uses Buffer

A diff as lines of text: what tcm diff prints and the review page shows.

import type { Codebase } from "#codebase/tcmts/cli/codebase";
import { isEmpty, lineDiff, namesIn } from "#codebase/tcmts/cli/diff";
import type { NamespaceDiff, NameState, ReportLine } from "#codebase/tcmts/cli/diff";
import { fileContent, formatOf, isBytes } from "#codebase/tcmts/cli/files";
import { shortHash } from "#codebase/tcmts/core/hash";
import { render } from "#codebase/tcmts/loader";

export function diffReport(
  cb: Codebase,
  d: NamespaceDiff,
  before: NameState,
  after: NameState,
  title: string,
): ReportLine[] {
  const out: ReportLine[] = [[["bold", title]]];
  if (isEmpty(d)) return [...out, [["dim", "  no changes"]]];
  const src = cb.source();
  const [oldNames, newNames] = [namesIn(before), namesIn(after)];
  const body = (hash: string, nameOf: (h: string) => string | undefined, rec = src.get(hash)!) => {
    // a file is its text (links naming what they reach now), or its size if it's bytes
    if (rec.kind === "file") {
      const at = nameOf(hash) ?? "";
      const content = fileContent(cb, rec, at.slice(0, Math.max(0, at.lastIndexOf("."))));
      return isBytes(rec)
        ? [`(${formatOf(rec) || "bytes"}, ${content.length} bytes)`]
        : content.toString("utf8").trimEnd().split("\n");
    }
    return render([rec], src, cb.lay, {
      style: "scratch",
      omitImports: true,
      nameOf,
      exportDeclarations: true,
    })
      .text.trimEnd()
      .split("\n");
  };
  const name = (hash: string) => newNames(hash) ?? shortHash(hash);
  const section = (label: string, n: number) =>
    n &&
    out.push(
      [],
      [
        ["plain", "  "],
        ["dim", label],
      ],
    );

  const changedLines = (lines: ReturnType<typeof lineDiff>) =>
    lines.forEach(([op, line], k) => {
      // keep two lines of context around changes in long definitions
      const near = lines.slice(Math.max(0, k - 2), k + 3).some(([o]) => o !== " ");
      if (op === " " && !near && lines.length > 12) {
        if (lines[k - 1]?.[0] !== " " || k === 0) out.push([["dim", "        ⋮"]]);
        return;
      }
      out.push([
        op === "-"
          ? ["red", `      - ${line}`]
          : op === "+"
            ? ["green", `      + ${line}`]
            : ["dim", `        ${line}`],
      ]);
    });

  section("edited", d.edited.length);
  for (const c of d.edited) {
    out.push([
      ["plain", "    "],
      ["yellow", "~"],
      ["plain", " "],
      ["bold", c.path],
      ["plain", "  "],
      ["dim", `${shortHash(c.old)} → ${shortHash(c.new)}`],
    ]);
    changedLines(lineDiff(body(c.old, oldNames), body(c.new, newNames)));
  }
  section("comments, layout or doc changed (the code is the same)", d.reworded.length);
  for (const r of d.reworded) {
    out.push([
      ["plain", "    "],
      ["yellow", "~"],
      ["plain", " "],
      ["bold", r.path],
      ["plain", "  "],
      ["dim", shortHash(r.hash)],
    ]);
    const as = (text: string | undefined) => cb.store.defWithText(r.hash, text)!;
    changedLines(lineDiff(body(r.hash, oldNames, as(r.old)), body(r.hash, newNames, as(r.new))));
  }
  section("updated because a dependency changed (code unchanged)", d.propagated.length);
  for (const c of d.propagated)
    out.push([
      ["plain", "    "],
      ["yellow", "~"],
      ["plain", ` ${c.path}  `],
      ["dim", `${shortHash(c.old)} → ${shortHash(c.new)}  via ${c.because.map(name).join(", ")}`],
    ]);
  section("added", d.added.length);
  for (const a of d.added)
    out.push([
      ["plain", "    "],
      ["green", "+"],
      ["plain", " "],
      ["bold", a.path],
      ["plain", "  "],
      ["dim", `${shortHash(a.hash)} : ${src.get(a.hash)!.signature}`],
    ]);
  section("removed", d.removed.length);
  for (const r of d.removed)
    out.push([
      ["plain", "    "],
      ["red", "-"],
      ["plain", ` ${r.path}  `],
      ["dim", shortHash(r.hash)],
    ]);
  section("renamed", d.renamed.length);
  for (const r of d.renamed)
    out.push([
      ["plain", "    "],
      ["cyan", ">"],
      ["plain", ` ${r.from} → `],
      ["bold", r.to],
      ["plain", "  "],
      ["dim", shortHash(r.hash)],
    ]);
  section("aliased", d.aliased.length);
  for (const a of d.aliased)
    out.push([
      ["plain", "    "],
      ["cyan", "&"],
      ["plain", " "],
      ["bold", a.path],
      ["plain", "  "],
      ["dim", `same as ${a.existing.join(", ")}`],
    ]);
  section("conflicted", d.conflicted.length);
  for (const c of d.conflicted)
    out.push([
      ["plain", "    "],
      ["red", "!"],
      ["plain", " "],
      ["bold", c.path],
      ["plain", "  "],
      ["dim", c.hashes.map((h) => shortHash(h)).join(", ")],
    ]);
  return out;
}

uses

used by