Skip to content
Source
Sign inSign up
Open History to see the last commit

pullRequestActions.ts

143 lines5.1 KB

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