harn-stdlib 0.10.177

Embedded Harn standard library source catalog
Documentation
/** Local repair replay. The runtime owns ordering; the connector owns publication. */
import { BumpPrState, BumpRepairs } from "std/bump/runtime"
import { command_run } from "std/command"

fn git(tools: HarnessTools, root: string, args: list<string>) {
  const result = command_run(tools, ["git", "-C", root] + args, {max_inline_bytes: 16777216})
  if result.stdout_truncated ?? false {
    throw "std/bump/repairs: git output exceeded the repair capture limit"
  }
  return result
}

fn checked(tools: HarnessTools, root: string, args: list<string>) -> string {
  const result = git(tools, root, args)
  if !result.success {
    throw "std/bump/repairs: git failed: " + trim(result.stderr ?? "")
  }
  return trim(result.stdout ?? "")
}

fn in_base(tools: HarnessTools, root: string, oid: string, base: string) -> bool {
  // Exit 1 means non-ancestry; an unreadable commit is a failure, not that verdict.
  const ancestry = git(tools, root, ["merge-base", "--is-ancestor", oid, base])
  if !ancestry.success && ancestry.exit_code != 1 {
    throw "std/bump/repairs: could not compare " + oid + " with base"
  }
  return ancestry.success
}

type RepairPartition = {
  replayable: list<string>,
  skipped: list<string>,
  refused: string?,
  detail: string,
}

/**
 * Split pending repairs into single-parent patches and skippable merges. A
 * merge whose incoming side is already in base, and which carries no content
 * the refresh or base lacks, only synced the branch with base.
 */
fn partition_merges(
  tools: HarnessTools,
  root: string,
  repairs: list<string>,
  base: string,
) -> RepairPartition {
  let skipped: list<string> = []
  let replayable: list<string> = []
  for oid in repairs {
    const parents = split(checked(tools, root, ["rev-list", "--parents", "-n", "1", oid]), " ")
    if len(parents) == 2 {
      replayable = replayable + [oid]
      continue
    }
    if len(parents) < 2 {
      return {
        replayable: [],
        skipped: [],
        refused: oid,
        detail: "repair must have exactly one parent: " + oid,
      }
    }
    const foreign = parents[2:].filter({ parent -> !in_base(tools, root, parent, base) }).to_list()
    if len(foreign) > 0 {
      return {
        replayable: [],
        skipped: [],
        refused: oid,
        detail: "repair merge " + oid + " brings in " + join(foreign, ", ")
          + ", which base does not contain; only a merge of the base branch is skipped",
      }
    }
    if len(parents) > 3 {
      return {
        replayable: [],
        skipped: [],
        refused: oid,
        detail: "repair merge " + oid + " has more than two parents and cannot be re-merged",
      }
    }
    const lost = lost_merge_paths(tools, root, oid, parents[1], parents[2], base)
    if len(lost) > 0 {
      return {
        replayable: [],
        skipped: [],
        refused: oid,
        detail: "repair merge " + oid
          + " carries content that neither the refresh nor base has in "
          + join(lost, ", ")
          + "; replay it as a single-parent commit",
      }
    }
    skipped = skipped + [oid]
  }
  return {replayable: replayable, skipped: skipped, refused: nil, detail: ""}
}

fn blob_at(tools: HarnessTools, root: string, rev: string, path: string) -> string? {
  const found = git(tools, root, ["rev-parse", "--verify", "--quiet", rev + ":" + path])
  return if found.success {
    trim(found.stdout ?? "")
  } else {
    nil
  }
}

fn worktree_blob(tools: HarnessTools, root: string, path: string) -> string? {
  const hashed = git(tools, root, ["hash-object", "--", path])
  return if hashed.success {
    trim(hashed.stdout ?? "")
  } else {
    nil
  }
}

/**
 * Paths whose content skipping a merge would lose. A path where the merge's
 * tree departs from git's own merge of its parents, or one that re-merging
 * conflicts on, loses nothing when the merge's blob equals what the refresh
 * just wrote there (the working tree before any repair is applied) or base's
 * blob. Absent on both sides counts as equal.
 */
fn lost_merge_paths(
  tools: HarnessTools,
  root: string,
  merge: string,
  first: string,
  second: string,
  base: string,
) -> list<string> {
  const mechanical = git(tools, root, ["merge-tree", "--write-tree", "--name-only", first, second])
  if mechanical.exit_code != 0 && mechanical.exit_code != 1 {
    throw "std/bump/repairs: could not re-merge the parents of " + merge + ": "
      + trim(mechanical.stderr ?? "")
  }
  // Output: the tree, then (on conflict) one conflicted path per line up to a blank line.
  const raw = split(mechanical.stdout ?? "", "\n")
  const tree = trim(raw[0] ?? "")
  let conflicted: list<string> = []
  if mechanical.exit_code == 1 {
    for line in raw[1:] {
      if line == "" {
        break
      }
      conflicted = conflicted + [line]
    }
  }
  const differing = lines(checked(tools, root, ["diff", "--name-only", tree, merge]))
  return (differing + conflicted).unique().filter(
    { path ->
      const kept = blob_at(tools, root, merge, path)
      kept != worktree_blob(tools, root, path) && kept != blob_at(tools, root, base, path)
    },
  )
    .to_list()
}

fn lines(value: string) -> list<string> {
  return split(value, "\n").filter({ line -> line != "" }).to_list()
}

/**
 * Replay non-bot commits and the repair IDs recorded by earlier publications.
 * Repair IDs survive flattening into a signed bump commit as Git trailers.
 * Every patch is applied with three-way conflict detection against refreshed
 * files. The checkout's HEAD stays at base for the signed worktree publisher.
 *
 * @effects: [fs, process, net]
 * @errors: [invalid commit IDs, unavailable commits, failed Git commands, oversized capture]
 */
pub fn replay_bump_repairs(
  tools: HarnessTools,
  root: string,
  is_generated_commit: fn(string) -> bool,
  existing: BumpPrState?,
  base: string,
  fetch: fn(string) -> {ok: bool, detail: string},
) -> BumpRepairs {
  if existing == nil {
    return {ok: true, commits: [], conflicts: [], detail: "no open bump PR"}
  }
  for oid in [base, existing.head_oid] {
    if regex_match("^[0-9a-f]{40}$", oid) == nil {
      throw "std/bump/repairs: base and PR head must be full commit IDs"
    }
  }
  const fetched = fetch(existing.head_oid)
  if !fetched.ok {
    throw "std/bump/repairs: could not fetch inspected PR head: " + fetched.detail
  }
  const history = lines(
    checked(tools, root, ["rev-list", "--reverse", base + ".." + existing.head_oid]),
  )
  let repairs: list<string> = []
  for oid in history {
    if is_generated_commit(oid) {
      const trailers = lines(
        checked(
          tools,
          root,
          ["show", "-s", "--format=%(trailers:key=Harn-Repair-Commit,valueonly)", oid],
        ),
      )
      for repair in trailers {
        if len(repair) != 40 || regex_match("^[0-9a-f]{40}$", repair) == nil {
          throw "std/bump/repairs: invalid repair commit trailer"
        }
        if !contains(repairs, repair) {
          repairs = repairs + [repair]
        }
      }
    } else if !contains(repairs, oid) {
      repairs = repairs + [oid]
    }
  }
  let pending: list<string> = []
  for oid in repairs {
    if !git(tools, root, ["cat-file", "-e", oid + "^{commit}"]).success {
      const fetched_repair = fetch(oid)
      if !fetched_repair.ok {
        throw "std/bump/repairs: could not fetch recorded repair " + oid
      }
    }
    if !in_base(tools, root, oid, base) {
      pending = pending + [oid]
    }
  }
  repairs = pending
  if len(repairs) == 0 {
    return {ok: true, commits: [], conflicts: [], detail: "no unmerged repairs"}
  }
  const partition = partition_merges(tools, root, repairs, base)
  if partition.refused != nil {
    return {
      ok: false,
      commits: repairs,
      conflicts: [partition.refused ?? ""],
      detail: partition.detail,
    }
  }
  const skipped = partition.skipped
  repairs = partition.replayable
  if len(repairs) == 0 {
    return {
      ok: true,
      commits: [],
      conflicts: [],
      detail: "no unmerged repairs",
      skipped_merges: skipped,
    }
  }
  checked(tools, root, ["add", "--all"])
  for oid in repairs {
    const parents = split(checked(tools, root, ["rev-list", "--parents", "-n", "1", oid]), " ")
    const patch = git(tools, root, ["diff", "--binary", parents[1], oid])
    if !patch.success {
      throw "std/bump/repairs: could not read repair patch " + oid
    }
    if (patch.stdout ?? "") == "" {
      continue
    }
    const applied = command_run(
      tools,
      {mode: "argv", argv: ["git", "-C", root, "apply", "--3way", "--index"], stdin: patch.stdout},
    )
    if !applied.success {
      const conflicts = lines(checked(tools, root, ["diff", "--name-only", "--diff-filter=U"]))
      return {
        ok: false,
        commits: repairs,
        conflicts: conflicts,
        detail: "repair " + oid + " failed: " + trim(applied.stderr ?? ""),
      }
    }
  }
  return {
    ok: true,
    commits: repairs,
    conflicts: [],
    detail: "replayed ${len(repairs)} repair commits",
    skipped_merges: skipped,
  }
}