1import type { ThreadPullRequestLink } from "@t3tools/contracts";
2import type { ThreadPullRequestChain } from "@t3tools/shared/threadPullRequests";
3
4/** One line of a thread's pull-request list: a link plus how deep it sits in its stack. */
5export interface PullRequestListLine {
6 readonly link: ThreadPullRequestLink;
7 /** 0 for a pull request on the base branch; each layer above steps in by one. */
8 readonly depth: number;
9 /** Which chain the line belongs to, so callers can tell one stack's lines from another's. */
10 readonly chainKey: string;
11 /** Set on the bottom layer of a multi-layer stack, so that row can name the whole stack. */
12 readonly stack: { readonly kind: ThreadPullRequestChain["kind"]; readonly size: number } | null;
13}
14
15function activityAt(link: ThreadPullRequestLink): number {
16 const ms = Date.parse(link.snapshot?.updatedAt ?? link.linkedAt);
17 return Number.isNaN(ms) ? 0 : ms;
18}
19
20function chainKeyOf(chain: ThreadPullRequestChain): string {
21 const bottom = chain.layers[0]!;
22 return `${bottom.host}/${bottom.repository}#${bottom.number}`;
23}
24
25/**
26 * Flattens chains into indented lines, newest first. A stack sorts by its most recent layer and
27 * then reads bottom to top beneath that slot, so the layer you would review first is at the
28 * bottom of the indent and a fresh push anywhere in the stack floats the whole stack up.
29 */
30export function pullRequestListLines(
31 chains: ReadonlyArray<ThreadPullRequestChain>,
32): ReadonlyArray<PullRequestListLine> {
33 const ordered = [...chains].sort(
34 (left, right) =>
35 Math.max(...right.layers.map(activityAt)) - Math.max(...left.layers.map(activityAt)),
36 );
37 return ordered.flatMap((chain) => {
38 const chainKey = chainKeyOf(chain);
39 return chain.layers.map((link, depth) => ({
40 link,
41 depth,
42 chainKey,
43 stack:
44 depth === 0 && chain.layers.length > 1
45 ? { kind: chain.kind, size: chain.layers.length }
46 : null,
47 }));
48 });
49}