feat: add pr view and checks (task 0009)
All checks were successful
CI / test (pull_request) Successful in 36s
CI / test (push) Successful in 37s

Add `pr view <n>` and `pr checks <n>`, built on the truncation machinery
and review fetches from earlier slices.

`pr view` uses the three-call fetch pattern (PR + reviews in parallel, then
the head commit's combined status) so `checks`, `comment_count`, and
`review_count` are in the default output; `--comments`, `--reviews`, and
`--full` behave as on `issue view`, with `--reviews` exposing Gitea's
`official`/`stale` fields plus per-review inline comments.

`pr checks <n>` renders the checks summary line and the `{ name, conclusion }`
rows, or the scalar no-CI message when no statuses exist.

The state→conclusion mapping and summary live in a new `src/checks.ts`;
`commentRows` is extracted into `src/comment.ts` and shared with `issue view`.
This commit was merged in pull request #9.
This commit is contained in:
2026-07-12 20:22:17 -04:00
parent 6333058b72
commit 16d2f29f19
10 changed files with 991 additions and 35 deletions

102
src/checks.ts Normal file
View File

@@ -0,0 +1,102 @@
import type { CombinedStatus, CommitStatus } from "gitea-js";
import type { GiteaClient } from "./client.js";
import type { RepoContext } from "./context.js";
import { classifyHttpError } from "./errors.js";
/**
* A PR's CI checks, derived from its commit statuses. Shared by `pr view` (which
* renders only the {@link ChecksResult.summary} line) and `pr checks` (which
* renders the summary plus the per-check rows).
*/
/** gh-axi's four-value check classification (see the gitea-axi spec, "pr checks"). */
export type CheckConclusion = "pass" | "fail" | "skip" | "pending";
/** The summary shown when a PR has no commit statuses at all. */
export const NO_CHECKS_MESSAGE =
"0 passed, 0 failed — this PR has no CI checks configured";
export interface CheckRow {
name: string;
conclusion: CheckConclusion;
}
export interface ChecksResult {
/** The `N passed, N failed[, N skipped][, N pending], N total` line, or {@link NO_CHECKS_MESSAGE}. */
summary: string;
checks: CheckRow[];
}
/**
* Map a Gitea commit-status state to a check conclusion. `warning` counts as a
* failure, matching Gitea's own status-combine logic; `skipped` (only emitted by
* newer instances) is its own bucket. Anything else — `pending` and any state a
* future Gitea might add — is treated as still-in-progress rather than reported
* as a pass or a failure it is not.
*/
function classifyState(state: string | undefined): CheckConclusion {
switch (state) {
case "success":
return "pass";
case "failure":
case "error":
case "warning":
return "fail";
case "skipped":
return "skip";
default:
return "pending";
}
}
/**
* Reduce a PR's commit statuses to its checks summary and per-check rows. The
* `skipped` and `pending` counts are folded into the summary line only when
* non-zero, so an all-passing PR reads `N passed, 0 failed, N total`.
*/
export function summarizeChecks(statuses: CommitStatus[]): ChecksResult {
const checks: CheckRow[] = statuses.map((status) => ({
name: status.context ?? "",
conclusion: classifyState(status.status),
}));
return { summary: summaryLine(checks), checks };
}
function summaryLine(checks: CheckRow[]): string {
if (checks.length === 0) {
return NO_CHECKS_MESSAGE;
}
const count = (conclusion: CheckConclusion): number =>
checks.filter((check) => check.conclusion === conclusion).length;
const parts = [`${count("pass")} passed`, `${count("fail")} failed`];
const skipped = count("skip");
if (skipped > 0) {
parts.push(`${skipped} skipped`);
}
const pending = count("pending");
if (pending > 0) {
parts.push(`${pending} pending`);
}
parts.push(`${checks.length} total`);
return parts.join(", ");
}
/**
* Fetch a PR head SHA's combined commit status and reduce it to its checks
* summary and rows. One HTTP call — `pr view` issues it once the head SHA is
* known, after the PR and reviews fetches (ADR 0006's three-call pattern).
*/
export async function fetchChecks(
api: GiteaClient,
context: RepoContext,
sha: string,
): Promise<ChecksResult> {
let combined: CombinedStatus;
try {
const response = await api.repos.repoGetCombinedStatusByRef(context.owner, context.name, sha);
combined = response.data;
} catch (error) {
throw classifyHttpError(error);
}
return summarizeChecks(combined.statuses ?? []);
}