1import type { PullRequestRef, PullRequestStack, ThreadPullRequestLink } from "@t3tools/contracts";
2
3/** A stack merge includes the unmerged layers up to the selected PR, in host order. */
4export function pullRequestStackMerge(stack: PullRequestStack, number: number) {
5 const position = stack.layers.findIndex((layer) => layer.number === number);
6 const layers = stack.layers.slice(0, position + 1).filter((layer) => layer.state !== "merged");
7 const heads = layers.flatMap((layer) =>
8 layer.headSha ? [{ number: layer.number, headSha: layer.headSha }] : [],
9 );
10 const reason =
11 position < 0
12 ? "This pull request is no longer in the stack. Refresh it."
13 : stack.layers[position]?.state !== "open" || layers.length === 0
14 ? "There are no open layers to merge through this pull request."
15 : layers.some((layer) => layer.state !== "open")
16 ? "Reopen the closed layers before merging this stack."
17 : layers.some((layer) => layer.isDraft)
18 ? "Mark every layer being merged ready for review."
19 : heads.length !== layers.length
20 ? "Refresh the stack to load its latest revisions before merging."
21 : null;
22 return { position: position + 1, layers, heads, reason };
23}
24
25/** Saved membership supplies navigation while offline, never revisions for a merge. */
26export function savedPullRequestStack(
27 links: readonly ThreadPullRequestLink[],
28 reference: PullRequestRef,
29): PullRequestStack | null {
30 const matches = links.filter(
31 (link) =>
32 link.host.toLowerCase() === reference.host?.toLowerCase() &&
33 link.repository.toLowerCase() === reference.repository.toLowerCase(),
34 );
35 const candidates = matches.filter(
36 (link) =>
37 link.number === reference.number ||
38 link.stack?.layers.some((layer) => layer.number === reference.number),
39 );
40 const newest = [...candidates].sort(
41 (a, b) =>
42 Date.parse(b.snapshot?.syncedAt ?? b.linkedAt) -
43 Date.parse(a.snapshot?.syncedAt ?? a.linkedAt),
44 )[0]?.stack;
45 if (!newest?.layers.some((layer) => layer.number === reference.number)) return null;
46 return {
47 ...newest,
48 layers: newest.layers.map((layer) => {
49 const snapshot = [...matches]
50 .filter((link) => link.number === layer.number)
51 .sort(
52 (a, b) =>
53 Date.parse(b.snapshot?.syncedAt ?? b.linkedAt) -
54 Date.parse(a.snapshot?.syncedAt ?? a.linkedAt),
55 )[0]?.snapshot;
56 return {
57 number: layer.number,
58 headBranch: layer.headBranch,
59 state: layer.state,
60 ...(snapshot ? { title: snapshot.title, isDraft: snapshot.isDraft } : {}),
61 };
62 }),
63 };
64}
65
66export function singlePullRequestMergeAllowed(input: {
67 readonly supportsStackActions: boolean;
68 readonly hasStack: boolean;
69 readonly isFresh: boolean;
70}) {
71 return !input.supportsStackActions || (!input.hasStack && input.isFresh);
72}