harn-stdlib 0.10.171

Embedded Harn standard library source catalog
Documentation
/** Structured Git and forge commands for hosts that own execution and approval. */
pub type GitCommandOperation = "status" \
  | "diff" \
  | "log" \
  | "branch" \
  | "add" \
  | "commit" \
  | "checkout" \
  | "push" \
  | "worktree_add" \
  | "worktree_list" \
  | "worktree_remove" \
  | "open_pr" \
  | "pr_view" \
  | "pr_checks" \
  | "pr_merge" \
  | "issue_view" \
  | "issue_create"

pub type GitCommandRequest = {
  operation: GitCommandOperation,
  paths?: list<string>,
  name?: string,
  message?: string,
  staged?: bool,
  all?: bool,
  create?: bool,
  force?: bool,
  max_count?: int,
  remote?: string,
  branch?: string,
  set_upstream?: bool,
  path?: string,
  base_ref?: string,
  forge?: "github" | "gitlab",
  number?: int,
  title?: string,
  body?: string,
  draft?: bool,
  base?: string,
  method?: "squash" | "merge" | "rebase",
  auto?: bool,
  labels?: list<string>,
}

pub type GitCommandPlan = {operation: GitCommandOperation, argv: list<string>, mutating: bool}

pub type GitWorktreeEntry = {
  path: string,
  head: string,
  branch: string,
  detached: bool,
  bare: bool,
  locked: bool,
  prunable: bool,
}

fn __git_command_required(value: string?, label: string) -> string {
  const text = value ?? ""
  guard text.trim() != "" else {
    throw "std/git: ${label} is required"
  }
  guard !text.contains("\0") else {
    throw "std/git: ${label} cannot contain NUL"
  }
  return text
}

fn __git_command_ref(value: string?, label: string) -> string {
  const text = __git_command_required(value, label).trim()
  guard !text.starts_with("-") && !text.contains("\n") && !text.contains("\r") else {
    throw "std/git: ${label} must be a ref, not an option or multiple lines"
  }
  return text
}

fn __git_command_number(request: GitCommandRequest, required: bool = false) -> list<string> {
  const number = request.number
  if number == nil {
    if required {
      throw "std/git: number is required"
    }
    return []
  }
  guard number > 0 else {
    throw "std/git: number must be a positive integer"
  }
  return [to_string(number)]
}

fn __git_command_forge(request: GitCommandRequest) -> list<string> {
  const operation = request.operation
  const gitlab = request.forge == "gitlab"
  const executable = if gitlab {
    "glab"
  } else {
    "gh"
  }
  const pr = if gitlab {
    "mr"
  } else {
    "pr"
  }
  if operation == "pr_view" {
    const fields = if gitlab {
      ["-F", "json"]
    } else {
      ["--json", "number,state,title,headRefName,baseRefName,url,isDraft,mergeable,author"]
    }
    return [executable, pr, "view"] + __git_command_number(request) + fields
  }
  if operation == "pr_checks" {
    const number = __git_command_number(request)
    guard !gitlab || len(number) == 0 else {
      throw "std/git: GitLab checks address the current branch; omit number"
    }
    return if gitlab {
      ["glab", "ci", "status"]
    } else {
      ["gh", "pr", "checks"] + number
    }
  }
  if operation == "issue_view" {
    const fields = if gitlab {
      ["-F", "json"]
    } else {
      ["--json", "number,state,title,body,url,labels,author"]
    }
    return [executable, "issue", "view"] + __git_command_number(request, true) + fields
  }
  if operation == "pr_merge" {
    let argv = [executable, pr, "merge"] + __git_command_number(request)
    const method = request.method ?? "squash"
    if !gitlab || method != "merge" {
      argv = argv + ["--${method}"]
    }
    if request.auto ?? false {
      argv = argv
        + [
          if gitlab {
            "--auto-merge"
          } else {
            "--auto"
          },
        ]
    }
    return argv
  }
  const title = __git_command_required(request.title, "title")
  const body = request.body ?? ""
  const body_flag = if gitlab {
    "--description"
  } else {
    "--body"
  }
  if operation == "issue_create" {
    let argv = [executable, "issue", "create", "--title", title, body_flag, body]
    for label in request.labels ?? [] {
      argv = argv + ["--label", __git_command_required(label, "label")]
    }
    return argv
  }
  let argv = [executable, pr, "create", "--title", title, body_flag, body]
  if request.base != nil {
    argv = argv
      + [
        if gitlab {
          "--target-branch"
        } else {
          "--base"
        },
        __git_command_ref(request.base, "base"),
      ]
  }
  if request.draft ?? false {
    argv = argv + ["--draft"]
  }
  return argv
}

@complexity(allow)
/**
 * Plan one structured repository operation without granting execution authority.
 * Argument values remain argv entries, never executable shell text. Hosts retain
 * confinement, approval, cancellation, and transaction ownership.
 *
 * @effects: []
 * @errors: [ValueError]
 */
pub fn git_command_plan(request: GitCommandRequest) -> GitCommandPlan {
  const operation = request.operation
  let argv: list<string> = []
  let mutating = false
  if operation == "status" {
    argv = ["git", "status", "--short", "--branch"]
  } else if operation == "diff" {
    argv = ["git", "diff"]
      + if request.staged ?? false {
        ["--staged"]
      } else {
        []
      }
    const paths = request.paths ?? []
    if len(paths) > 0 {
      argv = argv + ["--"] + paths
    }
  } else if operation == "log" {
    const count = request.max_count ?? 20
    guard count >= 1 && count <= 100 else {
      throw "std/git: max_count must be between 1 and 100"
    }
    argv = ["git", "log", "--oneline", "--decorate", "-n", to_string(count)]
  } else if operation == "branch" && (request.name ?? "").trim() == "" {
    argv = ["git", "branch", "--list", "--all"]
  } else if operation == "branch" || operation == "checkout" {
    const name = __git_command_ref(request.name, "name")
    const create = operation == "branch" || (request.create ?? false)
    // checkout predates switch, and -- prevents a missing ref from restoring a path.
    argv = ["git", "checkout"]
      + if create {
        ["-b", name, "--"]
      } else {
        [name, "--"]
      }
    mutating = true
  } else if operation == "add" {
    const paths = request.paths ?? []
    guard len(paths) > 0 else {
      throw "std/git: add requires paths"
    }
    argv = ["git", "add", "--"] + paths
    mutating = true
  } else if operation == "commit" {
    const message = __git_command_required(request.message, "message")
    argv = ["git", "commit"] + if request.all ?? false {
      ["-a"]
    } else {
      []
    }
    argv = argv + ["-m", message]
    mutating = true
  } else if operation == "push" {
    const remote = __git_command_ref(request.remote ?? "origin", "remote")
    const branch = (request.branch ?? "").trim()
    guard branch == "" || (!branch.starts_with("+") && !branch.starts_with(":")) else {
      throw "std/git: push branch must not force-push or delete a remote ref"
    }
    argv = ["git", "push"]
      + if request.set_upstream ?? false {
        ["--set-upstream"]
      } else {
        []
      }
    argv = argv + [remote]
    if branch != "" {
      argv = argv + [__git_command_ref(branch, "branch")]
    }
    mutating = true
  } else if operation == "worktree_list" {
    argv = ["git", "worktree", "list", "--porcelain", "-z"]
  } else if operation == "worktree_add" {
    const path = __git_command_required(request.path, "path")
    argv = ["git", "worktree", "add"]
    if request.branch != nil {
      argv = argv + ["-b", __git_command_ref(request.branch, "branch")]
    }
    argv = argv + ["--", path]
    if request.base_ref != nil {
      argv = argv + [__git_command_ref(request.base_ref, "base_ref")]
    }
    mutating = true
  } else if operation == "worktree_remove" {
    argv = ["git", "worktree", "remove"]
      + if request.force ?? false {
        ["--force"]
      } else {
        []
      }
    argv = argv + ["--", __git_command_required(request.path, "path")]
    mutating = true
  } else if ["open_pr", "pr_view", "pr_checks", "pr_merge", "issue_view", "issue_create"].contains(
    operation,
  ) {
    argv = __git_command_forge(request)
    mutating = ["open_pr", "pr_merge", "issue_create"].contains(operation)
  } else {
    throw "std/git: unknown operation '${operation}'"
  }
  guard len(argv.filter({ value -> value.contains("\0") })) == 0 else {
    throw "std/git: arguments cannot contain NUL"
  }
  return {operation: operation, argv: argv, mutating: mutating}
}

/**
 * Decode NUL-delimited `worktree list --porcelain -z` without trimming paths.
 * Newline-delimited output is accepted for Git versions without -z.
 *
 * @effects: []
 * @errors: [ValueError]
 */
pub fn git_worktree_parse(text: string) -> list<GitWorktreeEntry> {
  const separator = if text.contains("\0") {
    "\0"
  } else {
    "\n"
  }
  let entries: list<GitWorktreeEntry> = []
  let current: GitWorktreeEntry? = nil
  for line in text.split(separator) {
    if line.starts_with("worktree ") {
      const path = line[9:]
      guard separator != "\n" || !path.starts_with("\"") else {
        throw "std/git: quoted worktree paths require NUL-delimited porcelain (-z)"
      }
      if current != nil {
        entries = entries + [current]
      }
      current = {
        path: path,
        head: "",
        branch: "",
        detached: false,
        bare: false,
        locked: false,
        prunable: false,
      }
    } else if current != nil {
      if line.starts_with("HEAD ") {
        current = current + {head: line[5:]}
      } else if line.starts_with("branch ") {
        const ref = line[7:]
        current = current
          + {
            branch: if ref.starts_with("refs/heads/") {
              ref[11:]
            } else {
              ref
            },
          }
      } else if line == "detached" {
        current = current + {detached: true}
      } else if line == "bare" {
        current = current + {bare: true}
      } else if line == "locked" || line.starts_with("locked ") {
        current = current + {locked: true}
      } else if line == "prunable" || line.starts_with("prunable ") {
        current = current + {prunable: true}
      }
    }
  }
  if current != nil {
    entries = entries + [current]
  }
  return entries
}