Skip to content
Source
Sign inSign up

pullRequestEditing.ts

120 lines4.2 KB

1import type {
2 PullRequestComment,
3 PullRequestDetail,
4 PullRequestReviewThread,
5} from "@t3tools/contracts";
6​
7type EditingSubject = Pick<
8 PullRequestDetail,
9 "author" | "capabilities" | "viewer" | "viewerPermissions"
10>;
11​
12function sameLogin(one: string | null | undefined, other: string | null | undefined): boolean {
13 if (one == null || other == null) return false;
14 return one.trim().toLowerCase() === other.trim().toLowerCase();
16​
17export function canEditPullRequestChangeRequest(detail: EditingSubject): boolean {
18 if (detail.capabilities.edit?.changeRequest !== true) return false;
19 if (detail.viewerPermissions.editChangeRequest !== undefined) {
20 return detail.viewerPermissions.editChangeRequest;
21 }
22 return (
23 sameLogin(detail.viewer, detail.author?.login) ||
24 detail.viewerPermissions.actions.includes("merge")
25 );
27​
28export function canEditPullRequestMaintainerEdits(
29 detail: Pick<
30 PullRequestDetail,
31 "state" | "capabilities" | "viewerPermissions" | "maintainerCanModify"
32 >,
33): boolean {
34 return (
35 detail.state === "open" &&
36 detail.capabilities.edit?.maintainerEdits === true &&
37 detail.viewerPermissions.maintainerEdits === true &&
38 detail.maintainerCanModify !== undefined
39 );
41​
42export interface MaintainerEditsChoice {
43 readonly key: string;
44 readonly observedAt: number | undefined;
45 readonly value: boolean;
47​
48/**
49 * A saved choice ends at the first host reading delivered after the save. Readers refresh once it
50 * succeeds, which drops reads still in flight, so that reading started after the write.
51 */
52export function maintainerEditsChoiceInForce<
53 Choice extends { readonly key: string; readonly observedAt: number | undefined },
54>(
55 choice: Choice | null,
56 key: string,
57 observedAt: number | undefined,
58 saving: boolean,
59): Choice | null {
60 if (choice === null || choice.key !== key) return null;
61 if (saving) return choice.observedAt === observedAt ? choice : { ...choice, observedAt };
62 return observedAt !== undefined && choice.observedAt === observedAt ? choice : null;
64​
65/** A description written from this client, shown in place of the host's copy. */
66export interface DescriptionDraft {
67 readonly key: string;
68 readonly observedAt: number | undefined;
69 /** Null once the last save failed: until a host reading, nobody knows which saves landed. */
70 readonly body: string | null;
71 /** Saves still running. The host applies them in order, so the last one is `body`. */
72 readonly saves: number;
74​
75/** Same lifetime as a maintainer edits choice: until the first host reading after the saves. */
76export function descriptionDraftInForce(
77 draft: DescriptionDraft | null,
78 key: string,
79 observedAt: number | undefined,
80): DescriptionDraft | null {
81 return maintainerEditsChoiceInForce(draft, key, observedAt, (draft?.saves ?? 0) > 0);
83​
84export function startDescriptionSave(
85 draft: DescriptionDraft | null,
86 detail: { readonly url: string; readonly observedAt?: number | undefined },
87 body: string,
88): DescriptionDraft {
89 const saves = draft?.key === detail.url ? draft.saves : 0;
90 return { key: detail.url, observedAt: detail.observedAt, body, saves: saves + 1 };
92​
93/** Each save writes the whole body, so a failure only loses `body` when no later save is running. */
94export function settleDescriptionSave(
95 draft: DescriptionDraft | null,
96 key: string,
97 saved: boolean,
98): DescriptionDraft | null {
99 if (draft === null || draft.key !== key || draft.saves === 0) return draft;
100 const saves = draft.saves - 1;
101 return { ...draft, saves, body: saved || saves > 0 ? draft.body : null };
103​
104/** Hosts do not support rewriting review summaries through comment mutations. */
105export function canEditPullRequestComment(
106 detail: EditingSubject,
107 comment: Pick<PullRequestComment, "author" | "canEdit" | "kind">,
108): boolean {
109 if (detail.capabilities.edit?.comment !== true) return false;
110 if (comment.kind !== "issue-comment" && comment.kind !== "review-comment") return false;
111 if (comment.canEdit !== undefined) return comment.canEdit;
112 return sameLogin(detail.viewer, comment.author?.login);
114​
115export function canResolvePullRequestThread(
116 detail: Pick<PullRequestDetail, "viewerPermissions">,
117 thread: Pick<PullRequestReviewThread, "canResolve">,
118): boolean {
119 return detail.viewerPermissions.resolve && thread.canResolve !== false;