1import type { ChangeRequestTerminology } from "@t3tools/client-runtime/source-control-clients";
2import type { PullRequestRef, PullRequestStack, ThreadPullRequestLink } from "@t3tools/contracts";
3
4export function pullRequestStackMerge(
5 stack: PullRequestStack,
6 number: number,
7 terminology: ChangeRequestTerminology,
8) {
9 const position = stack.layers.findIndex((layer) => layer.number === number);
10 const layers = stack.layers.slice(0, position + 1).filter((layer) => layer.state !== "merged");
11 const heads = layers.flatMap((layer) =>
12 layer.headSha ? [{ number: layer.number, headSha: layer.headSha }] : [],
13 );
14 const reason =
15 position < 0
16 ? `This ${terminology.singular} is no longer in the stack. Refresh it.`
17 : stack.layers[position]?.state !== "open" || layers.length === 0
18 ? `There are no open layers to merge through this ${terminology.singular}.`
19 : layers.some((layer) => layer.state !== "open")
20 ? "Reopen the closed layers before merging this stack."
21 : layers.some((layer) => layer.isDraft)
22 ? "Mark every layer being merged ready for review."
23 : heads.length !== layers.length
24 ? "Refresh the stack to load its latest revisions before merging."
25 : null;
26 return { position: position + 1, layers, heads, reason };
27}
28
29/** Saved membership supplies navigation while offline, never revisions for a merge. */
30export function savedPullRequestStack(
31 links: readonly ThreadPullRequestLink[],
32 reference: PullRequestRef,
33): PullRequestStack | null {
34 const matches = links.filter(
35 (link) =>
36 link.host.toLowerCase() === reference.host?.toLowerCase() &&
37 link.repository.toLowerCase() === reference.repository.toLowerCase(),
38 );
39 const candidates = matches.filter(
40 (link) =>
41 link.number === reference.number ||
42 link.stack?.layers.some((layer) => layer.number === reference.number),
43 );
44 const newest = [...candidates].sort(
45 (a, b) =>
46 Date.parse(b.snapshot?.syncedAt ?? b.linkedAt) -
47 Date.parse(a.snapshot?.syncedAt ?? a.linkedAt),
48 )[0]?.stack;
49 if (!newest?.layers.some((layer) => layer.number === reference.number)) return null;
50 return {
51 ...newest,
52 layers: newest.layers.map((layer) => {
53 const snapshot = [...matches]
54 .filter((link) => link.number === layer.number)
55 .sort(
56 (a, b) =>
57 Date.parse(b.snapshot?.syncedAt ?? b.linkedAt) -
58 Date.parse(a.snapshot?.syncedAt ?? a.linkedAt),
59 )[0]?.snapshot;
60 return {
61 number: layer.number,
62 headBranch: layer.headBranch,
63 state: layer.state,
64 ...(snapshot ? { title: snapshot.title, isDraft: snapshot.isDraft } : {}),
65 };
66 }),
67 };
68}
69
70/** The list summary describes the layer it was opened from, so another layer's route drops it. */
71export function pullRequestLayerParams<Params extends { readonly initialSummary?: unknown }>(
72 params: Params,
73 number: number,
74) {
75 const { initialSummary: _, ...layer } = params;
76 return { ...layer, number };
77}
78
79export function singlePullRequestMergeAllowed(input: {
80 readonly supportsStackActions: boolean;
81 readonly hasStack: boolean;
82 readonly isFresh: boolean;
83}) {
84 return !input.supportsStackActions || (!input.hasStack && input.isFresh);
85}