Skip to content

g1t/apps/web/app/lib/commit-checks.ts

80 lines3,474 bytesCodeBlame
1/**
2 * Words for a commit's checks, as the badge beside a commit and its list
3 * say them (components/commit-checks.tsx).
4 */
5import type { CheckItem, CommitChecks } from "@g1t/contracts";
6
7/** The headline over a commit's checks. */
8export function checksHeadline(checks: Pick<CommitChecks, "state">): string {
9 if (checks.state === "failure") return "Some checks were not successful";
10 if (checks.state === "pending") return "Some checks haven't completed yet";
11 if (checks.state === "none") return "No checks";
12 return "All checks have passed";
13}
14
15/** "1 failing, 1 in progress and 2 successful checks", under the headline. */
16export function checksTally(checks: Pick<CommitChecks, "successful" | "failed" | "pending" | "skipped" | "total">): string {
17 const parts: string[] = [];
18 if (checks.failed) parts.push(`${checks.failed} failing`);
19 if (checks.pending) parts.push(`${checks.pending} in progress`);
20 if (checks.skipped) parts.push(`${checks.skipped} skipped`);
21 if (checks.successful) parts.push(`${checks.successful} successful`);
22 const list = parts.length > 1 ? `${parts.slice(0, -1).join(", ")} and ${parts[parts.length - 1]}` : (parts[0] ?? "No");
23 return `${list} ${checks.total === 1 ? "check" : "checks"}`;
24}
25
26/** `25s`, `1m 12s` or `1h 4m` between two times; empty without both. */
27export function took(start: string | null, end: string | null): string {
28 if (!start || !end) return "";
29 const seconds = Math.max(0, Math.round((new Date(end).getTime() - new Date(start).getTime()) / 1000));
30 if (Number.isNaN(seconds)) return "";
31 if (seconds < 60) return `${seconds}s`;
32 const minutes = Math.floor(seconds / 60);
33 if (minutes < 60) return `${minutes}m ${seconds % 60}s`;
34 return `${Math.floor(minutes / 60)}h ${minutes % 60}m`;
35}
36
37/**
38 * What one check says after its name: a status's description; for a check
39 * run, how it went and how long it took (`Successful in 25s`), then its
40 * title.
41 */
42export function checkDetail(item: CheckItem): string {
43 if (item.kind === "status") return item.description ?? stateWord(item);
44 const time = took(item.startedAt, item.completedAt);
45 let word = stateWord(item);
46 if (time && item.state === "success") word = `Successful in ${time}`;
47 else if (time && item.state === "failure") word = `Failing after ${time}`;
48 return item.description ? `${word} — ${item.description}` : word;
49}
50
51function stateWord(item: Pick<CheckItem, "state" | "startedAt">): string {
52 switch (item.state) {
53 case "success":
54 return "Successful";
55 case "failure":
56 return "Failing";
57 case "pending":
58 return item.startedAt ? "In progress" : "Queued";
59 case "cancelled":
60 return "Cancelled";
61 case "skipped":
62 return "Skipped";
63 default:
64 return "Neutral";
65 }
66}
67
68/**
69 * Where a check's Details link goes: its page on g1t when it has one (a
70 * check run's report, or a job's run), otherwise the reporter's page. A
71 * link to g1t itself is made a path; anything else opens apart.
72 */
73export function detailsLink(item: Pick<CheckItem, "url" | "detailsUrl">, site = "https://g1t.sh"): { href: string; external: boolean } | null {
74 const target = item.url ?? item.detailsUrl;
75 if (!target) return null;
76 if (target.startsWith("/")) return { href: target, external: false };
77 if (target.startsWith(`${site}/`)) return { href: target.slice(site.length), external: false };
78 if (/^https?:\/\//.test(target)) return { href: target, external: true };
79 return null;
80}