Files
mesh-catalog/modules/gitea/pulls.ts
T

103 lines
5.7 KiB
TypeScript

// A pull request's merge check (novox/hq to-be 45 §9): what the forge's announcer says when a pull
// request's head moves, and the status contexts the check is kept under.
//
// **Before merge, never after.** Each new head of an open pull request is announced as `pull.updated`, and the mesh
// checks it against every machine of its facts before it merges. The verdict is the checker's `mesh/check`, which this
// module writes from the checker's owed state (check.ts); the controller's `checked`, which once set the two older
// statuses, is no event any more (ADR 0571 clause 13).
//
// Pure functions here, so they are tested without a forge.
import type { GiteaPull, GiteaRepo } from "./client.js";
import { movedSince } from "./client.js";
/** The status context a merge check's gate is kept under: one per commit, the newest replacing the last. */
export const CHECK_CONTEXT = "mesh/merge-gate";
/** The status context of the repository's own merge-check.sh, the check's second layer (novox/hq ADR 0237). */
export const REPO_CHECK_CONTEXT = "mesh/repo-check";
/** The one status main requires (novox/hq ADR 0447 §1, issue 1301): the checker's verdict, written by this module from the
* checker's owed state (check.ts, ADR 0521). Its line names the head and the trunk commit it judged: "pass <head12> on <trunk12> …". */
export const CHECK_STATUS = "mesh/check";
/** The statuses a branch's rule requires, from the rule the forge applies to it: none when it is unprotected or checks
* no status; undefined when the rule could not be read. */
export function requiredOf(rule: { enable_status_check?: boolean; status_check_contexts?: string[] | null } | null | undefined): string[] | undefined {
if (rule === undefined) return undefined;
if (!rule || !rule.enable_status_check) return [];
return (rule.status_check_contexts ?? []).filter(Boolean);
}
/**
* **The controller's statuses are written only where a base still requires them** (issue 1301; ADR 0503 §3, §5): of
* mesh/merge-gate and mesh/repo-check, those the base requires — hq's mesh/repo-check alone once the forge requires
* mesh/check everywhere else, so no mesh/merge-gate stands on a head that nothing will set. A base whose rule could not
* be read keeps both, as before: a status nothing requires blocks nothing, and one that is required is never left out.
*/
export function maintained(required: string[] | undefined): string[] {
const both = [CHECK_CONTEXT, REPO_CHECK_CONTEXT];
return required === undefined ? both : both.filter((c) => required.includes(c));
}
/** Whether mesh/merge-gate is written on a head of this base (maintained). */
export function writesGate(required: string[] | undefined): boolean {
return maintained(required).includes(CHECK_CONTEXT);
}
/**
* The pending words a head's merge check is announced with (novox/hq issue 446): where the check is, and since when,
* in the time the operator reads. They stood as "the mesh is mapping this head onto its module graph" for the whole
* check — the few seconds the controller maps the head and then the queue wait and the build — and readers took the
* forge for lagging (issue 423). The controller maps and queues the check in seconds, so from the announcement it waits
* in the build queue; mesh-delivery says "building on <machine> since …" when a build machine takes it.
*/
export function pendingWords(announced: Date): string {
const two = (n: number) => String(n).padStart(2, "0");
return `waiting in the build queue since ${two(announced.getHours())}:${two(announced.getMinutes())}`;
}
/** The heads already announced, keyed by `headKey`, so a restart announces nothing twice. */
export type Announced = Record<string, string>;
/** The key a pull request's announced head is kept under: `owner/repo#number@base`. The base is in it because a pull
* request retargeted keeps its head, and a head judged into one base is not judged into another (issue 1422, de-spiegel #6). */
export function headKey(full: string, number: number, base?: string): string {
return `${full}#${number}@${base ?? ""}`;
}
/** Which open pull requests have a head not yet announced. A pull request merged or closed is not
* open and is never asked about. */
export function headsToAnnounce(full: string, pulls: GiteaPull[], announced: Announced): GiteaPull[] {
return pulls.filter((p) => p.state === "open" && !!p.head_sha && announced[headKey(full, p.number, p.base)] !== p.head_sha);
}
/** A pull request the forge found updated, by its repository: what `searchOpenPulls` answers. */
export interface PullSeen {
owner: string;
name: string;
updated_at?: string;
}
/** **The repositories a pass looks at** (novox/hq issue 347): every one that moved since the floor, every one
* holding a pull request updated since the floor — opening, reopening or retargeting a pull request moves its own
* update time and not its repository's — and every one on a sweep or the first pass. */
export function reposToLook(repos: GiteaRepo[], floor: string, pulls: PullSeen[], sweep: boolean): GiteaRepo[] {
if (sweep || !floor) return repos;
const moved = new Set(movedSince(repos, floor).map((r) => r.full_name));
const at = Date.parse(floor);
for (const p of pulls) {
if (!p.updated_at || !(Date.parse(p.updated_at) < at)) moved.add(`${p.owner}/${p.name}`);
}
return repos.filter((r) => moved.has(r.full_name));
}
export function clipped(d: string): string {
d = d.replace(/\s+/g, " ").trim();
return d.length > 140 ? d.slice(0, 139) + "…" : d;
}
/** What a group member's gate says while the group's composed check is the one that judges it: the member's own check can say
* nothing more of it, so asking that check again brings no verdict that names a trunk (novox/hq issue 1106). */
export const WAITS_FOR_COMPOSED = "waits for its group's composed check";