1import { sourceControlClients } from "@t3tools/client-runtime/source-control-clients";
2import type {
3 PullRequestAction,
4 PullRequestDetail,
5 PullRequestMergeMethod,
6 SourceControlProviderKind,
7} from "@t3tools/contracts";
8import type { AppSymbolName } from "../../components/AppSymbol";
9
10type ActionDetail = Pick<
11 PullRequestDetail,
12 "state" | "isDraft" | "mergeability" | "autoMergeEnabled" | "workflowApprovalsRequired"
13> & {
14 readonly capabilities: Pick<
15 PullRequestDetail["capabilities"],
16 "actions" | "mergeMethods" | "updateMethods" | "deleteBranch"
17 >;
18 readonly viewerPermissions: Pick<
19 PullRequestDetail["viewerPermissions"],
20 "actions" | "updateMethods"
21 >;
22 readonly mergeCapabilities: PullRequestDetail["mergeCapabilities"];
23};
24
25export const MERGE_METHODS = {
26 squash: { title: "Squash and merge", description: "Combine the commits into one commit." },
27 merge: {
28 title: "Create a merge commit",
29 description: "Keep the commits and add a merge commit.",
30 },
31 rebase: { title: "Rebase and merge", description: "Add the commits without a merge commit." },
32} as const satisfies Record<PullRequestMergeMethod, { title: string; description: string }>;
33
34export const PR_ACTION_LABELS = {
35 merge: "Merge pull request",
36 ready: "Mark ready for review",
37 draft: "Convert to draft",
38 close: "Close pull request",
39 reopen: "Reopen pull request",
40 "update-branch": "Update branch",
41 "enable-auto-merge": "Enable auto-merge",
42 "disable-auto-merge": "Disable auto-merge",
43 revert: "Create revert pull request",
44 "delete-branch": "Delete branch",
45 "approve-workflows": "Approve workflows",
46} as const satisfies Record<PullRequestAction, string>;
47
48export const PR_ACTION_ICONS = {
49 merge: "arrow.triangle.merge",
50 ready: "checkmark.circle",
51 draft: "pencil",
52 close: "xmark",
53 reopen: "arrow.clockwise",
54 "update-branch": "arrow.up.circle",
55 "enable-auto-merge": "clock",
56 "disable-auto-merge": "xmark",
57 revert: "arrow.uturn.backward",
58 "delete-branch": "trash",
59 "approve-workflows": "checkmark",
60} as const satisfies Record<PullRequestAction, AppSymbolName>;
61
62export const ALL_PR_ACTIONS: readonly PullRequestAction[] = [
63 "merge",
64 "ready",
65 "draft",
66 "update-branch",
67 "enable-auto-merge",
68 "disable-auto-merge",
69 "approve-workflows",
70 "close",
71 "reopen",
72 "revert",
73 "delete-branch",
74];
75
76export function allowedMergeMethods(pr: ActionDetail) {
77 return pr.capabilities.mergeMethods.filter((method) => pr.mergeCapabilities[method]);
78}
79
80export function allowedUpdateMethods(pr: ActionDetail) {
81 return (pr.capabilities.updateMethods ?? []).filter((method) =>
82 pr.viewerPermissions.updateMethods?.includes(method),
83 );
84}
85
86/** Provider support, account permissions, and the PR's state all narrow the offered actions. */
87export function canPerformPullRequestAction(pr: ActionDetail, action: PullRequestAction) {
88 if (!pr.capabilities.actions.includes(action) || !pr.viewerPermissions.actions.includes(action))
89 return false;
90 if (action === "reopen") return pr.state === "closed";
91 if (action === "revert") return pr.state === "merged";
92 if (action === "delete-branch")
93 return pr.state !== "open" && pr.capabilities.deleteBranch === true;
94 if (pr.state !== "open") return false;
95 switch (action) {
96 case "update-branch":
97 return allowedUpdateMethods(pr).length > 0;
98 case "approve-workflows":
99 return (pr.workflowApprovalsRequired ?? 0) > 0;
100 case "ready":
101 return pr.isDraft;
102 case "draft":
103 return !pr.isDraft;
104 case "merge":
105 return !pr.isDraft && pr.mergeability !== "conflicting" && allowedMergeMethods(pr).length > 0;
106 case "enable-auto-merge":
107 return !pr.isDraft && pr.autoMergeEnabled === false && allowedMergeMethods(pr).length > 0;
108 case "disable-auto-merge":
109 return pr.autoMergeEnabled === true;
110 default:
111 return true;
112 }
113}
114
115export const MERGE_CONFLICT_REASON = "Resolve the branch conflicts before merging.";
116
117export function mergeUnavailableReason(pr: ActionDetail, connectionAllowsWrites: boolean) {
118 if (pr.state === "merged") return "This pull request has been merged.";
119 if (pr.state === "closed") return "Reopen this pull request before merging.";
120 if (!connectionAllowsWrites) return "This environment connection is read-only.";
121 if (!pr.capabilities.actions.includes("merge"))
122 return "This provider does not support merging here.";
123 if (!pr.viewerPermissions.actions.includes("merge"))
124 return "Your account cannot merge this pull request.";
125 if (pr.isDraft) return "Mark this draft ready for review before merging.";
126 if (pr.mergeability === "conflicting") return MERGE_CONFLICT_REASON;
127 if (!allowedMergeMethods(pr).length) return "This repository has no available merge methods.";
128 return null;
129}
130
131export function mergeSummary(
132 pr: ActionDetail & Pick<PullRequestDetail, "autoMergeMethod">,
133 connectionAllowsWrites: boolean,
134) {
135 if (pr.state === "merged") return "Merged";
136 if (pr.state === "closed") return "Closed";
137 if (pr.isDraft) return "Draft";
138 if (pr.mergeability === "conflicting") return "Has conflicts";
139 if (!connectionAllowsWrites) return "Read-only connection";
140 const allowed = allowedMergeMethods(pr);
141 const method = pr.autoMergeMethod ?? (allowed.includes("squash") ? "squash" : allowed[0]);
142 const methodLabel = method ? `${method[0]?.toUpperCase()}${method.slice(1)}` : "Merge";
143 if (pr.autoMergeEnabled) return `${methodLabel} · auto-merge on`;
144 if (mergeUnavailableReason(pr, connectionAllowsWrites)) return "Unavailable";
145 return `${methodLabel}${pr.autoMergeEnabled === false ? " · auto-merge off" : ""}`;
146}
147
148export const openOnProviderLabel = (provider: SourceControlProviderKind) =>
149 `Open on ${sourceControlClients.get(provider).label}`;