@reviewflow/sdk 0.0.0-stage → 6.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +15 -0
- package/README.md +86 -2
- package/build/files-node.mjs +70 -0
- package/build/git-node.mjs +1792 -0
- package/build/index-browser.es.js +3328 -0
- package/build/index-node.mjs +3328 -0
- package/package.json +135 -3
- package/src/api/pullRequest.ts +617 -0
- package/src/config/insertCheckoutEntry.ts +54 -0
- package/src/diff/diffPaths.ts +78 -0
- package/src/diff/impacts.ts +47 -0
- package/src/diff/readingOrder.ts +428 -0
- package/src/diff/themes.ts +105 -0
- package/src/diff/types.ts +141 -0
- package/src/files/editableFile.ts +163 -0
- package/src/files.ts +13 -0
- package/src/git/groupDiffPaths.ts +134 -0
- package/src/git/manifests.ts +96 -0
- package/src/git/moduleBindings.ts +571 -0
- package/src/git/pullRequestDiff.ts +159 -0
- package/src/git/readBranchDiff.ts +359 -0
- package/src/git/readImpacts.ts +797 -0
- package/src/git/readingOrder.ts +190 -0
- package/src/git/run.ts +213 -0
- package/src/git.ts +38 -0
- package/src/github/applySuggestions.ts +399 -0
- package/src/github/editPullRequest.ts +790 -0
- package/src/github/fetchCompareDiff.ts +21 -0
- package/src/github/imageProxies.ts +43 -0
- package/src/github/lineDiff.ts +145 -0
- package/src/github/prQuery.ts +298 -0
- package/src/github/pullRequestDetail.ts +1347 -0
- package/src/github/pullRequestHead.ts +65 -0
- package/src/github/pullRequestRevisions.ts +124 -0
- package/src/github/transport.ts +171 -0
- package/src/index.ts +35 -0
- package/src/reviewflow/board.ts +333 -0
- package/src/reviewflow/changedParts.ts +80 -0
- package/src/reviewflow/checksFromReviewflow.ts +75 -0
- package/src/reviewflow/client.ts +589 -0
- package/src/reviewflow/follow.ts +345 -0
- package/src/reviewflow/status.ts +17 -0
- package/src/reviewflow/stream.ts +199 -0
- package/src/types/entities.ts +8 -0
- package/src/types/ids.ts +1 -0
- package/src/types/reviewflow.ts +32 -0
- package/src/utils/pullRequestEdit.ts +108 -0
- package/src/utils/pullRequestPart.ts +12 -0
- package/src/utils/pullRequestReaction.ts +19 -0
- package/src/utils/pullRequestSuggestion.ts +148 -0
|
@@ -0,0 +1,1347 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
PullRequestCandidatesResponse,
|
|
3
|
+
PullRequestCheck,
|
|
4
|
+
PullRequestChecksResponse,
|
|
5
|
+
PullRequestComment,
|
|
6
|
+
PullRequestCommit,
|
|
7
|
+
PullRequestCommitsResponse,
|
|
8
|
+
PullRequestDetail,
|
|
9
|
+
PullRequestDetailResponse,
|
|
10
|
+
PullRequestDiffSide,
|
|
11
|
+
PullRequestEvent,
|
|
12
|
+
PullRequestEventAction,
|
|
13
|
+
PullRequestFile,
|
|
14
|
+
PullRequestFileChange,
|
|
15
|
+
PullRequestFileViewed,
|
|
16
|
+
PullRequestFilesResponse,
|
|
17
|
+
PullRequestLabel,
|
|
18
|
+
PullRequestMergeMethod,
|
|
19
|
+
PullRequestMergeState,
|
|
20
|
+
PullRequestMergeable,
|
|
21
|
+
PullRequestPart,
|
|
22
|
+
PullRequestPartResponse,
|
|
23
|
+
PullRequestPerson,
|
|
24
|
+
PullRequestReactable,
|
|
25
|
+
PullRequestReactionContent,
|
|
26
|
+
PullRequestRead,
|
|
27
|
+
PullRequestRef,
|
|
28
|
+
PullRequestRequestedReviewer,
|
|
29
|
+
PullRequestReviewState,
|
|
30
|
+
PullRequestTimelineItem,
|
|
31
|
+
PullRequestTimelineResponse,
|
|
32
|
+
PullRequestViewerLastReview,
|
|
33
|
+
} from "../api/pullRequest.ts";
|
|
34
|
+
import type { PrReviewDecision } from "../types/entities.ts";
|
|
35
|
+
import { imageProxiesOf, mergeImageProxies } from "./imageProxies.ts";
|
|
36
|
+
import { checkOutcomes, splitOwnerRepo } from "./prQuery.ts";
|
|
37
|
+
import type { GitHubTransport } from "./transport.ts";
|
|
38
|
+
|
|
39
|
+
const timelineItemTypes = [
|
|
40
|
+
"ISSUE_COMMENT",
|
|
41
|
+
"PULL_REQUEST_REVIEW",
|
|
42
|
+
"PULL_REQUEST_COMMIT",
|
|
43
|
+
"ASSIGNED_EVENT",
|
|
44
|
+
"UNASSIGNED_EVENT",
|
|
45
|
+
"LABELED_EVENT",
|
|
46
|
+
"UNLABELED_EVENT",
|
|
47
|
+
"REVIEW_REQUESTED_EVENT",
|
|
48
|
+
"REVIEW_REQUEST_REMOVED_EVENT",
|
|
49
|
+
"CLOSED_EVENT",
|
|
50
|
+
"REOPENED_EVENT",
|
|
51
|
+
"MERGED_EVENT",
|
|
52
|
+
"READY_FOR_REVIEW_EVENT",
|
|
53
|
+
"CONVERT_TO_DRAFT_EVENT",
|
|
54
|
+
"HEAD_REF_FORCE_PUSHED_EVENT",
|
|
55
|
+
"BASE_REF_CHANGED_EVENT",
|
|
56
|
+
"RENAMED_TITLE_EVENT",
|
|
57
|
+
"REVIEW_DISMISSED_EVENT",
|
|
58
|
+
"AUTO_MERGE_ENABLED_EVENT",
|
|
59
|
+
"AUTO_MERGE_DISABLED_EVENT",
|
|
60
|
+
].join(", ");
|
|
61
|
+
|
|
62
|
+
const assigneeFields =
|
|
63
|
+
"__typename ... on User { login } ... on Bot { login } ... on Mannequin { login } ... on Organization { login }";
|
|
64
|
+
|
|
65
|
+
const reviewerFields =
|
|
66
|
+
"__typename ... on User { login } ... on Bot { login } ... on Mannequin { login } ... on Team { name }";
|
|
67
|
+
|
|
68
|
+
/** What a description, a comment or a review carries to be reacted to. */
|
|
69
|
+
const reactionFields =
|
|
70
|
+
"viewerCanReact reactionGroups { content viewerHasReacted reactors { totalCount } }";
|
|
71
|
+
|
|
72
|
+
const pullRequestQuery = ({
|
|
73
|
+
name,
|
|
74
|
+
viewer = false,
|
|
75
|
+
repositoryFields = "",
|
|
76
|
+
fields,
|
|
77
|
+
}: {
|
|
78
|
+
name: string;
|
|
79
|
+
viewer?: boolean;
|
|
80
|
+
repositoryFields?: string;
|
|
81
|
+
fields: string;
|
|
82
|
+
}): string => `query ${name}($owner: String!, $name: String!, $number: Int!) {${viewer ? "\n viewer { login }" : ""}
|
|
83
|
+
repository(owner: $owner, name: $name) {${repositoryFields}
|
|
84
|
+
pullRequest(number: $number) {${fields}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}`;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The pull request is read in parts, each its own query sent at the same time
|
|
91
|
+
* or when its tab opens: GitHub resolves a query's fields one after the other,
|
|
92
|
+
* so one document holding everything cost the sum of its connections — 1.4 to
|
|
93
|
+
* 1.8s, where the header alone answers in 0.6 to 0.8s. What the editors offer
|
|
94
|
+
* is `pullRequestCandidatesQuery`, apart for the same reason.
|
|
95
|
+
*
|
|
96
|
+
* Every connection stops at its first page. 100 files and 100 checks cover the
|
|
97
|
+
* pull requests a person reviews; past that the totals are still exact and the
|
|
98
|
+
* screen says how many it does not list. The timeline keeps the newest 100
|
|
99
|
+
* items (`last`), since the recent conversation is what a reviewer needs.
|
|
100
|
+
*/
|
|
101
|
+
export const pullRequestDetailQuery = pullRequestQuery({
|
|
102
|
+
name: "PullRequestDetail",
|
|
103
|
+
viewer: true,
|
|
104
|
+
repositoryFields: `
|
|
105
|
+
mergeCommitAllowed
|
|
106
|
+
squashMergeAllowed
|
|
107
|
+
rebaseMergeAllowed
|
|
108
|
+
viewerDefaultMergeMethod`,
|
|
109
|
+
fields: `
|
|
110
|
+
id
|
|
111
|
+
number
|
|
112
|
+
url
|
|
113
|
+
title
|
|
114
|
+
body
|
|
115
|
+
bodyHTML
|
|
116
|
+
state
|
|
117
|
+
isDraft
|
|
118
|
+
author { login avatarUrl }
|
|
119
|
+
headRefName
|
|
120
|
+
baseRefName
|
|
121
|
+
headRefOid
|
|
122
|
+
baseRefOid
|
|
123
|
+
createdAt
|
|
124
|
+
updatedAt
|
|
125
|
+
lastEditedAt
|
|
126
|
+
additions
|
|
127
|
+
deletions
|
|
128
|
+
changedFiles
|
|
129
|
+
mergeable
|
|
130
|
+
mergeStateStatus
|
|
131
|
+
viewerCanUpdate
|
|
132
|
+
viewerCanUpdateBranch
|
|
133
|
+
viewerCanApplySuggestion
|
|
134
|
+
mergeCommitHeadline: viewerMergeHeadlineText(mergeType: MERGE)
|
|
135
|
+
mergeCommitBody: viewerMergeBodyText(mergeType: MERGE)
|
|
136
|
+
squashCommitHeadline: viewerMergeHeadlineText(mergeType: SQUASH)
|
|
137
|
+
squashCommitBody: viewerMergeBodyText(mergeType: SQUASH)
|
|
138
|
+
${reactionFields}
|
|
139
|
+
reviewDecision
|
|
140
|
+
labels(first: 50) { nodes { name color description } }
|
|
141
|
+
assignees(first: 20) { nodes { login avatarUrl } }
|
|
142
|
+
reviewRequests(first: 50) {
|
|
143
|
+
nodes {
|
|
144
|
+
requestedReviewer {
|
|
145
|
+
__typename
|
|
146
|
+
... on User { login avatarUrl }
|
|
147
|
+
... on Bot { login avatarUrl }
|
|
148
|
+
... on Mannequin { login avatarUrl }
|
|
149
|
+
... on Team { slug name }
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
latestReviews(first: 50) { nodes { author { login } state submittedAt } }
|
|
154
|
+
commits { totalCount }`,
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
export const pullRequestChecksQuery = pullRequestQuery({
|
|
158
|
+
name: "PullRequestChecks",
|
|
159
|
+
fields: `
|
|
160
|
+
headCommits: commits(last: 1) {
|
|
161
|
+
nodes {
|
|
162
|
+
commit {
|
|
163
|
+
statusCheckRollup {
|
|
164
|
+
contexts(first: 100) {
|
|
165
|
+
totalCount
|
|
166
|
+
nodes {
|
|
167
|
+
__typename
|
|
168
|
+
... on CheckRun { name status conclusion title detailsUrl }
|
|
169
|
+
... on StatusContext { context state description targetUrl }
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}`,
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
export const pullRequestTimelineQuery = pullRequestQuery({
|
|
179
|
+
name: "PullRequestTimeline",
|
|
180
|
+
fields: `
|
|
181
|
+
timelineItems(last: 100, itemTypes: [${timelineItemTypes}]) {
|
|
182
|
+
filteredCount
|
|
183
|
+
pageInfo { hasPreviousPage }
|
|
184
|
+
nodes {
|
|
185
|
+
__typename
|
|
186
|
+
... on IssueComment {
|
|
187
|
+
id databaseId author { login avatarUrl } body bodyHTML createdAt lastEditedAt url viewerCanUpdate
|
|
188
|
+
${reactionFields}
|
|
189
|
+
}
|
|
190
|
+
... on PullRequestReview {
|
|
191
|
+
id databaseId author { login avatarUrl } state body bodyHTML submittedAt createdAt lastEditedAt url
|
|
192
|
+
viewerCanUpdate
|
|
193
|
+
${reactionFields}
|
|
194
|
+
commit { oid }
|
|
195
|
+
}
|
|
196
|
+
... on PullRequestCommit {
|
|
197
|
+
commit {
|
|
198
|
+
abbreviatedOid
|
|
199
|
+
messageHeadline
|
|
200
|
+
committedDate
|
|
201
|
+
author { name user { login avatarUrl } }
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
... on AssignedEvent { createdAt actor { login avatarUrl } assignee { ${assigneeFields} } }
|
|
205
|
+
... on UnassignedEvent { createdAt actor { login avatarUrl } assignee { ${assigneeFields} } }
|
|
206
|
+
... on LabeledEvent { createdAt actor { login avatarUrl } label { name color description } }
|
|
207
|
+
... on UnlabeledEvent { createdAt actor { login avatarUrl } label { name color description } }
|
|
208
|
+
... on ReviewRequestedEvent { createdAt actor { login avatarUrl } requestedReviewer { ${reviewerFields} } }
|
|
209
|
+
... on ReviewRequestRemovedEvent { createdAt actor { login avatarUrl } requestedReviewer { ${reviewerFields} } }
|
|
210
|
+
... on ClosedEvent { createdAt actor { login avatarUrl } }
|
|
211
|
+
... on ReopenedEvent { createdAt actor { login avatarUrl } }
|
|
212
|
+
... on MergedEvent { createdAt actor { login avatarUrl } }
|
|
213
|
+
... on ReadyForReviewEvent { createdAt actor { login avatarUrl } }
|
|
214
|
+
... on ConvertToDraftEvent { createdAt actor { login avatarUrl } }
|
|
215
|
+
... on AutoMergeEnabledEvent { createdAt actor { login avatarUrl } }
|
|
216
|
+
... on AutoMergeDisabledEvent { createdAt actor { login avatarUrl } }
|
|
217
|
+
... on HeadRefForcePushedEvent { createdAt actor { login avatarUrl } afterCommit { abbreviatedOid } }
|
|
218
|
+
... on BaseRefChangedEvent { createdAt actor { login avatarUrl } currentRefName previousRefName }
|
|
219
|
+
... on RenamedTitleEvent { createdAt actor { login avatarUrl } currentTitle previousTitle }
|
|
220
|
+
... on ReviewDismissedEvent { createdAt actor { login avatarUrl } review { author { login } } }
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
reviewThreads(first: 100) {
|
|
224
|
+
totalCount
|
|
225
|
+
nodes {
|
|
226
|
+
id
|
|
227
|
+
isResolved
|
|
228
|
+
isOutdated
|
|
229
|
+
path
|
|
230
|
+
line
|
|
231
|
+
startLine
|
|
232
|
+
diffSide
|
|
233
|
+
startDiffSide
|
|
234
|
+
originalLine
|
|
235
|
+
originalStartLine
|
|
236
|
+
viewerCanResolve
|
|
237
|
+
viewerCanUnresolve
|
|
238
|
+
viewerCanReply
|
|
239
|
+
comments(first: 20) {
|
|
240
|
+
totalCount
|
|
241
|
+
nodes {
|
|
242
|
+
id databaseId author { login avatarUrl } body bodyHTML createdAt lastEditedAt url diffHunk viewerCanUpdate
|
|
243
|
+
${reactionFields}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}`,
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
export const pullRequestCommitsQuery = pullRequestQuery({
|
|
251
|
+
name: "PullRequestCommits",
|
|
252
|
+
fields: `
|
|
253
|
+
commits(first: 100) {
|
|
254
|
+
totalCount
|
|
255
|
+
nodes {
|
|
256
|
+
commit {
|
|
257
|
+
oid
|
|
258
|
+
parents(first: 1) { totalCount nodes { oid } }
|
|
259
|
+
messageHeadline
|
|
260
|
+
messageBody
|
|
261
|
+
committedDate
|
|
262
|
+
author { name user { login avatarUrl } }
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}`,
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* The viewed marks change with no event on the pull request: read with the tab.
|
|
270
|
+
* The viewer's last review is found among `latestReviews` — one per author,
|
|
271
|
+
* pending ones left out — rather than the last page of every review, where a
|
|
272
|
+
* busy pull request pushed it out of reach.
|
|
273
|
+
*/
|
|
274
|
+
export const pullRequestFilesQuery = pullRequestQuery({
|
|
275
|
+
name: "PullRequestFiles",
|
|
276
|
+
fields: `
|
|
277
|
+
files(first: 100) {
|
|
278
|
+
nodes { path additions deletions changeType viewerViewedState }
|
|
279
|
+
}
|
|
280
|
+
latestReviews(first: 100) {
|
|
281
|
+
nodes { viewerDidAuthor submittedAt commit { oid } }
|
|
282
|
+
}`,
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
type GhActor = GhPerson;
|
|
286
|
+
|
|
287
|
+
interface GhLabel {
|
|
288
|
+
name: string;
|
|
289
|
+
color: string;
|
|
290
|
+
description: string | null;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
type GhRequestedReviewer =
|
|
294
|
+
| {
|
|
295
|
+
__typename: "Bot" | "Mannequin" | "User";
|
|
296
|
+
login: string;
|
|
297
|
+
avatarUrl: string | null;
|
|
298
|
+
}
|
|
299
|
+
| { __typename: "Team"; slug: string; name: string };
|
|
300
|
+
|
|
301
|
+
type GhCheckContext =
|
|
302
|
+
| {
|
|
303
|
+
__typename: "CheckRun";
|
|
304
|
+
name: string;
|
|
305
|
+
status: string;
|
|
306
|
+
conclusion: string | null;
|
|
307
|
+
title: string | null;
|
|
308
|
+
detailsUrl: string | null;
|
|
309
|
+
}
|
|
310
|
+
| {
|
|
311
|
+
__typename: "StatusContext";
|
|
312
|
+
context: string;
|
|
313
|
+
state: string;
|
|
314
|
+
description: string | null;
|
|
315
|
+
targetUrl: string | null;
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
interface GhPerson {
|
|
319
|
+
login: string;
|
|
320
|
+
avatarUrl?: string | null;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
interface GhReactionGroup {
|
|
324
|
+
content: string;
|
|
325
|
+
viewerHasReacted: boolean;
|
|
326
|
+
reactors: { totalCount: number };
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
interface GhReactable {
|
|
330
|
+
id: string;
|
|
331
|
+
viewerCanReact: boolean;
|
|
332
|
+
reactionGroups: GhReactionGroup[] | null;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
interface GhComment extends GhReactable {
|
|
336
|
+
databaseId: number;
|
|
337
|
+
author: GhPerson | null;
|
|
338
|
+
body: string;
|
|
339
|
+
/** absent from the queries that do not ask it */
|
|
340
|
+
bodyHTML?: string;
|
|
341
|
+
createdAt: string;
|
|
342
|
+
lastEditedAt: string | null;
|
|
343
|
+
url: string;
|
|
344
|
+
viewerCanUpdate: boolean;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
interface GhConnection<Node> {
|
|
348
|
+
totalCount: number;
|
|
349
|
+
nodes: (Node | null)[] | null;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
interface GhReview extends GhComment {
|
|
353
|
+
state: string;
|
|
354
|
+
submittedAt: string | null;
|
|
355
|
+
commit: { oid: string } | null;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
interface GhReviewThread {
|
|
359
|
+
id: string;
|
|
360
|
+
isResolved: boolean;
|
|
361
|
+
isOutdated: boolean;
|
|
362
|
+
path: string;
|
|
363
|
+
line: number | null;
|
|
364
|
+
startLine: number | null;
|
|
365
|
+
diffSide: string;
|
|
366
|
+
startDiffSide: string | null;
|
|
367
|
+
originalLine: number | null;
|
|
368
|
+
originalStartLine: number | null;
|
|
369
|
+
viewerCanResolve: boolean;
|
|
370
|
+
viewerCanUnresolve: boolean;
|
|
371
|
+
viewerCanReply: boolean;
|
|
372
|
+
comments: GhConnection<GhComment & { diffHunk: string }>;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
interface GhCommit {
|
|
376
|
+
oid: string;
|
|
377
|
+
parents: { totalCount: number; nodes: ({ oid: string } | null)[] | null };
|
|
378
|
+
messageHeadline: string;
|
|
379
|
+
messageBody: string;
|
|
380
|
+
committedDate: string;
|
|
381
|
+
author: { name: string | null; user: GhActor | null } | null;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
interface GhEventBase {
|
|
385
|
+
createdAt: string;
|
|
386
|
+
actor: GhActor | null;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
type GhNamed = { login: string } | { name: string } | null;
|
|
390
|
+
|
|
391
|
+
type GhTimelineNode =
|
|
392
|
+
| {
|
|
393
|
+
__typename: "PullRequestCommit";
|
|
394
|
+
commit: Pick<GhCommit, "author" | "committedDate" | "messageHeadline"> & {
|
|
395
|
+
abbreviatedOid: string;
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
| (GhComment & { __typename: "IssueComment" })
|
|
399
|
+
| (GhEventBase & {
|
|
400
|
+
__typename:
|
|
401
|
+
| "AutoMergeDisabledEvent"
|
|
402
|
+
| "AutoMergeEnabledEvent"
|
|
403
|
+
| "ClosedEvent"
|
|
404
|
+
| "ConvertToDraftEvent"
|
|
405
|
+
| "MergedEvent"
|
|
406
|
+
| "ReadyForReviewEvent"
|
|
407
|
+
| "ReopenedEvent";
|
|
408
|
+
})
|
|
409
|
+
| (GhEventBase & {
|
|
410
|
+
__typename: "AssignedEvent" | "UnassignedEvent";
|
|
411
|
+
assignee: GhNamed;
|
|
412
|
+
})
|
|
413
|
+
| (GhEventBase & {
|
|
414
|
+
__typename: "BaseRefChangedEvent";
|
|
415
|
+
currentRefName: string;
|
|
416
|
+
previousRefName: string;
|
|
417
|
+
})
|
|
418
|
+
| (GhEventBase & {
|
|
419
|
+
__typename: "HeadRefForcePushedEvent";
|
|
420
|
+
afterCommit: { abbreviatedOid: string } | null;
|
|
421
|
+
})
|
|
422
|
+
| (GhEventBase & {
|
|
423
|
+
__typename: "LabeledEvent" | "UnlabeledEvent";
|
|
424
|
+
label: GhLabel | null;
|
|
425
|
+
})
|
|
426
|
+
| (GhEventBase & {
|
|
427
|
+
__typename: "RenamedTitleEvent";
|
|
428
|
+
currentTitle: string;
|
|
429
|
+
previousTitle: string;
|
|
430
|
+
})
|
|
431
|
+
| (GhEventBase & {
|
|
432
|
+
__typename: "ReviewDismissedEvent";
|
|
433
|
+
review: { author: { login: string } | null } | null;
|
|
434
|
+
})
|
|
435
|
+
| (GhEventBase & {
|
|
436
|
+
__typename: "ReviewRequestedEvent" | "ReviewRequestRemovedEvent";
|
|
437
|
+
requestedReviewer: GhNamed;
|
|
438
|
+
})
|
|
439
|
+
| (GhReview & { __typename: "PullRequestReview" });
|
|
440
|
+
|
|
441
|
+
export interface GhPullRequestDetail extends GhReactable {
|
|
442
|
+
number: number;
|
|
443
|
+
url: string;
|
|
444
|
+
title: string;
|
|
445
|
+
body: string;
|
|
446
|
+
bodyHTML?: string;
|
|
447
|
+
state: string;
|
|
448
|
+
isDraft: boolean;
|
|
449
|
+
author: GhActor | null;
|
|
450
|
+
headRefName: string;
|
|
451
|
+
baseRefName: string;
|
|
452
|
+
headRefOid: string;
|
|
453
|
+
baseRefOid: string;
|
|
454
|
+
createdAt: string;
|
|
455
|
+
updatedAt: string;
|
|
456
|
+
lastEditedAt: string | null;
|
|
457
|
+
additions: number;
|
|
458
|
+
deletions: number;
|
|
459
|
+
changedFiles: number;
|
|
460
|
+
mergeable: string;
|
|
461
|
+
mergeStateStatus: string;
|
|
462
|
+
viewerCanUpdate: boolean;
|
|
463
|
+
viewerCanUpdateBranch: boolean;
|
|
464
|
+
viewerCanApplySuggestion: boolean;
|
|
465
|
+
mergeCommitHeadline: string;
|
|
466
|
+
mergeCommitBody: string;
|
|
467
|
+
squashCommitHeadline: string;
|
|
468
|
+
squashCommitBody: string;
|
|
469
|
+
reviewDecision: string | null;
|
|
470
|
+
labels: { nodes: (GhLabel | null)[] | null } | null;
|
|
471
|
+
assignees: { nodes: (GhActor | null)[] | null } | null;
|
|
472
|
+
reviewRequests: {
|
|
473
|
+
nodes: ({ requestedReviewer: GhRequestedReviewer | null } | null)[] | null;
|
|
474
|
+
} | null;
|
|
475
|
+
latestReviews: {
|
|
476
|
+
nodes:
|
|
477
|
+
| ({
|
|
478
|
+
author: { login: string } | null;
|
|
479
|
+
state: string;
|
|
480
|
+
submittedAt: string | null;
|
|
481
|
+
} | null)[]
|
|
482
|
+
| null;
|
|
483
|
+
} | null;
|
|
484
|
+
commits: { totalCount: number };
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
export interface GhPullRequestChecks {
|
|
488
|
+
headCommits: {
|
|
489
|
+
nodes:
|
|
490
|
+
| ({
|
|
491
|
+
commit: {
|
|
492
|
+
statusCheckRollup: {
|
|
493
|
+
contexts: {
|
|
494
|
+
totalCount: number;
|
|
495
|
+
nodes: (GhCheckContext | null)[] | null;
|
|
496
|
+
};
|
|
497
|
+
} | null;
|
|
498
|
+
};
|
|
499
|
+
} | null)[]
|
|
500
|
+
| null;
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
export interface GhPullRequestTimeline {
|
|
505
|
+
timelineItems: {
|
|
506
|
+
filteredCount: number;
|
|
507
|
+
pageInfo: { hasPreviousPage: boolean };
|
|
508
|
+
nodes: (GhTimelineNode | null)[] | null;
|
|
509
|
+
} | null;
|
|
510
|
+
reviewThreads: GhConnection<GhReviewThread> | null;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
export interface GhPullRequestCommits {
|
|
514
|
+
commits: GhConnection<{ commit: GhCommit }>;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
export interface GhPullRequestFiles {
|
|
518
|
+
files: {
|
|
519
|
+
nodes:
|
|
520
|
+
| ({
|
|
521
|
+
path: string;
|
|
522
|
+
additions: number;
|
|
523
|
+
deletions: number;
|
|
524
|
+
changeType: string;
|
|
525
|
+
viewerViewedState: string;
|
|
526
|
+
} | null)[]
|
|
527
|
+
| null;
|
|
528
|
+
} | null;
|
|
529
|
+
latestReviews: {
|
|
530
|
+
nodes:
|
|
531
|
+
| ({
|
|
532
|
+
viewerDidAuthor: boolean;
|
|
533
|
+
submittedAt: string | null;
|
|
534
|
+
commit: { oid: string } | null;
|
|
535
|
+
} | null)[]
|
|
536
|
+
| null;
|
|
537
|
+
} | null;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
interface GhMergeSettings {
|
|
541
|
+
mergeCommitAllowed?: boolean;
|
|
542
|
+
squashMergeAllowed?: boolean;
|
|
543
|
+
rebaseMergeAllowed?: boolean;
|
|
544
|
+
viewerDefaultMergeMethod?: string | null;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/** What a part's query answers: `viewer` and the merge settings for the header alone. */
|
|
548
|
+
export interface PullRequestPartData<GhPart> {
|
|
549
|
+
viewer?: { login: string } | null;
|
|
550
|
+
repository: (GhMergeSettings & { pullRequest: GhPart | null }) | null;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
export type PullRequestDetailData = PullRequestPartData<GhPullRequestDetail>;
|
|
554
|
+
|
|
555
|
+
const present = <Item>(items: (Item | null)[] | null | undefined): Item[] =>
|
|
556
|
+
(items ?? []).filter((item): item is Item => item !== null);
|
|
557
|
+
|
|
558
|
+
const toPerson = ({ login, avatarUrl }: GhActor): PullRequestPerson => ({
|
|
559
|
+
login,
|
|
560
|
+
avatarUrl: avatarUrl ?? null,
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
const toLabel = ({ name, color, description }: GhLabel): PullRequestLabel => ({
|
|
564
|
+
name,
|
|
565
|
+
color,
|
|
566
|
+
description: description === "" ? null : description,
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
const toState = (
|
|
570
|
+
state: string,
|
|
571
|
+
isDraft: boolean,
|
|
572
|
+
): PullRequestDetailResponse["pullRequest"]["state"] => {
|
|
573
|
+
if (state === "MERGED") return "merged";
|
|
574
|
+
if (state === "CLOSED") return "closed";
|
|
575
|
+
return isDraft ? "draft" : "open";
|
|
576
|
+
};
|
|
577
|
+
|
|
578
|
+
const reviewDecisions = new Map<string, PrReviewDecision>([
|
|
579
|
+
["APPROVED", "approved"],
|
|
580
|
+
["CHANGES_REQUESTED", "changesRequested"],
|
|
581
|
+
["REVIEW_REQUIRED", "reviewRequired"],
|
|
582
|
+
]);
|
|
583
|
+
|
|
584
|
+
const mergeables = new Map<string, PullRequestMergeable>([
|
|
585
|
+
["CONFLICTING", "conflicting"],
|
|
586
|
+
["MERGEABLE", "mergeable"],
|
|
587
|
+
]);
|
|
588
|
+
|
|
589
|
+
const reviewStates = new Map<string, PullRequestReviewState>([
|
|
590
|
+
["APPROVED", "approved"],
|
|
591
|
+
["CHANGES_REQUESTED", "changesRequested"],
|
|
592
|
+
["COMMENTED", "commented"],
|
|
593
|
+
["DISMISSED", "dismissed"],
|
|
594
|
+
["PENDING", "pending"],
|
|
595
|
+
]);
|
|
596
|
+
|
|
597
|
+
const fileChanges = new Map<string, PullRequestFileChange>([
|
|
598
|
+
["ADDED", "added"],
|
|
599
|
+
["CHANGED", "changed"],
|
|
600
|
+
["COPIED", "copied"],
|
|
601
|
+
["DELETED", "deleted"],
|
|
602
|
+
["MODIFIED", "modified"],
|
|
603
|
+
["RENAMED", "renamed"],
|
|
604
|
+
]);
|
|
605
|
+
|
|
606
|
+
const viewedStates = new Map<string, PullRequestFileViewed>([
|
|
607
|
+
["DISMISSED", "dismissed"],
|
|
608
|
+
["VIEWED", "viewed"],
|
|
609
|
+
]);
|
|
610
|
+
|
|
611
|
+
const toReviewer = (
|
|
612
|
+
reviewer: GhRequestedReviewer,
|
|
613
|
+
): PullRequestRequestedReviewer =>
|
|
614
|
+
reviewer.__typename === "Team"
|
|
615
|
+
? { kind: "team", slug: reviewer.slug, name: reviewer.name }
|
|
616
|
+
: {
|
|
617
|
+
kind: "user",
|
|
618
|
+
login: reviewer.login,
|
|
619
|
+
avatarUrl: reviewer.avatarUrl,
|
|
620
|
+
};
|
|
621
|
+
|
|
622
|
+
const nonEmpty = (text: string | null): string | null =>
|
|
623
|
+
text === null || text === "" ? null : text;
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
* A check run still running has no conclusion, so its status decides; a
|
|
627
|
+
* finished one reads like the board's rollup (`checkOutcomes`), where an
|
|
628
|
+
* unknown state counts as pending rather than as a pass.
|
|
629
|
+
*/
|
|
630
|
+
const toCheck = (context: GhCheckContext): PullRequestCheck => {
|
|
631
|
+
if (context.__typename === "StatusContext") {
|
|
632
|
+
return {
|
|
633
|
+
name: context.context,
|
|
634
|
+
outcome: checkOutcomes.get(context.state) ?? "pending",
|
|
635
|
+
running: false,
|
|
636
|
+
description: nonEmpty(context.description),
|
|
637
|
+
url: context.targetUrl,
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
return {
|
|
641
|
+
name: context.name,
|
|
642
|
+
outcome:
|
|
643
|
+
context.status === "COMPLETED" && context.conclusion !== null
|
|
644
|
+
? (checkOutcomes.get(context.conclusion) ?? "pending")
|
|
645
|
+
: "pending",
|
|
646
|
+
running: context.status === "IN_PROGRESS",
|
|
647
|
+
description: nonEmpty(context.title),
|
|
648
|
+
url: context.detailsUrl,
|
|
649
|
+
};
|
|
650
|
+
};
|
|
651
|
+
|
|
652
|
+
/** Failures first, then what is still running, each run in GitHub's order. */
|
|
653
|
+
const checkOrder: Record<PullRequestCheck["outcome"], number> = {
|
|
654
|
+
failed: 0,
|
|
655
|
+
pending: 1,
|
|
656
|
+
passed: 2,
|
|
657
|
+
};
|
|
658
|
+
|
|
659
|
+
const ghost: PullRequestPerson = { login: "ghost", avatarUrl: null };
|
|
660
|
+
|
|
661
|
+
const toAuthor = (actor: GhActor | null): PullRequestPerson =>
|
|
662
|
+
actor === null ? ghost : toPerson(actor);
|
|
663
|
+
|
|
664
|
+
const reactionContents = new Map<string, PullRequestReactionContent>([
|
|
665
|
+
["CONFUSED", "confused"],
|
|
666
|
+
["EYES", "eyes"],
|
|
667
|
+
["HEART", "heart"],
|
|
668
|
+
["HOORAY", "hooray"],
|
|
669
|
+
["LAUGH", "laugh"],
|
|
670
|
+
["ROCKET", "rocket"],
|
|
671
|
+
["THUMBS_DOWN", "thumbsDown"],
|
|
672
|
+
["THUMBS_UP", "thumbsUp"],
|
|
673
|
+
]);
|
|
674
|
+
|
|
675
|
+
/** GitHub answers every content, nobody's included: only those reacted with are kept, in its order. */
|
|
676
|
+
const toReactable = (subject: GhReactable): PullRequestReactable => ({
|
|
677
|
+
id: subject.id,
|
|
678
|
+
viewerCanReact: subject.viewerCanReact,
|
|
679
|
+
reactions: (subject.reactionGroups ?? []).flatMap((group) => {
|
|
680
|
+
const content = reactionContents.get(group.content);
|
|
681
|
+
if (content === undefined || group.reactors.totalCount === 0) return [];
|
|
682
|
+
return [
|
|
683
|
+
{
|
|
684
|
+
content,
|
|
685
|
+
count: group.reactors.totalCount,
|
|
686
|
+
viewerHasReacted: group.viewerHasReacted,
|
|
687
|
+
},
|
|
688
|
+
];
|
|
689
|
+
}),
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
const toComment = (comment: GhComment): PullRequestComment => ({
|
|
693
|
+
...toReactable(comment),
|
|
694
|
+
databaseId: comment.databaseId,
|
|
695
|
+
author: toAuthor(comment.author),
|
|
696
|
+
body: comment.body,
|
|
697
|
+
createdAt: comment.createdAt,
|
|
698
|
+
lastEditedAt: comment.lastEditedAt,
|
|
699
|
+
url: comment.url,
|
|
700
|
+
viewerCanUpdate: comment.viewerCanUpdate,
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
/**
|
|
704
|
+
* A review is shown when it said something: a verdict, or a body. One that
|
|
705
|
+
* only carried inline comments is an empty `COMMENTED` shell, and its comments
|
|
706
|
+
* are already shown as threads. `PENDING` is the viewer's own unsent draft.
|
|
707
|
+
*
|
|
708
|
+
* GitHub refuses to edit a review submitted without a body ("Could not edit a
|
|
709
|
+
* review with a missing body"), whatever `viewerCanUpdate` says.
|
|
710
|
+
*/
|
|
711
|
+
const toReviewItem = (review: GhReview): PullRequestTimelineItem | null => {
|
|
712
|
+
if (review.state === "PENDING") return null;
|
|
713
|
+
if (review.state === "COMMENTED" && review.body.trim() === "") return null;
|
|
714
|
+
return {
|
|
715
|
+
kind: "review",
|
|
716
|
+
...toReactable(review),
|
|
717
|
+
databaseId: review.databaseId,
|
|
718
|
+
author: toAuthor(review.author),
|
|
719
|
+
state: reviewStates.get(review.state) ?? "commented",
|
|
720
|
+
body: review.body,
|
|
721
|
+
createdAt: review.submittedAt ?? review.createdAt,
|
|
722
|
+
lastEditedAt: review.lastEditedAt,
|
|
723
|
+
url: review.url,
|
|
724
|
+
viewerCanUpdate: review.viewerCanUpdate && review.body !== "",
|
|
725
|
+
commitOid: review.commit?.oid ?? null,
|
|
726
|
+
};
|
|
727
|
+
};
|
|
728
|
+
|
|
729
|
+
const toSide = (side: string): PullRequestDiffSide =>
|
|
730
|
+
side === "LEFT" ? "LEFT" : "RIGHT";
|
|
731
|
+
|
|
732
|
+
const toThreadItem = (
|
|
733
|
+
thread: GhReviewThread,
|
|
734
|
+
): PullRequestTimelineItem | null => {
|
|
735
|
+
const comments = present(thread.comments.nodes);
|
|
736
|
+
const [first] = comments;
|
|
737
|
+
if (first === undefined) return null;
|
|
738
|
+
return {
|
|
739
|
+
kind: "thread",
|
|
740
|
+
id: thread.id,
|
|
741
|
+
path: thread.path,
|
|
742
|
+
line: thread.line,
|
|
743
|
+
startLine: thread.startLine,
|
|
744
|
+
side: toSide(thread.diffSide),
|
|
745
|
+
startSide:
|
|
746
|
+
thread.startDiffSide === null ? null : toSide(thread.startDiffSide),
|
|
747
|
+
originalLine: thread.originalLine,
|
|
748
|
+
originalStartLine: thread.originalStartLine,
|
|
749
|
+
resolved: thread.isResolved,
|
|
750
|
+
outdated: thread.isOutdated,
|
|
751
|
+
viewerCanResolve: thread.viewerCanResolve,
|
|
752
|
+
viewerCanUnresolve: thread.viewerCanUnresolve,
|
|
753
|
+
viewerCanReply: thread.viewerCanReply,
|
|
754
|
+
diffHunk: first.diffHunk,
|
|
755
|
+
comments: comments.map(toComment),
|
|
756
|
+
commentsTotal: thread.comments.totalCount,
|
|
757
|
+
createdAt: first.createdAt,
|
|
758
|
+
};
|
|
759
|
+
};
|
|
760
|
+
|
|
761
|
+
const nameOf = (named: GhNamed): string | null => {
|
|
762
|
+
if (named === null) return null;
|
|
763
|
+
return "login" in named ? named.login : named.name;
|
|
764
|
+
};
|
|
765
|
+
|
|
766
|
+
/** The GitHub user, else the git author's name with no avatar. */
|
|
767
|
+
const commitAuthor = (
|
|
768
|
+
commit: Pick<GhCommit, "author">,
|
|
769
|
+
): PullRequestPerson | null => {
|
|
770
|
+
const { author } = commit;
|
|
771
|
+
if (author === null) return null;
|
|
772
|
+
if (author.user !== null) return toPerson(author.user);
|
|
773
|
+
return author.name === null ? null : { login: author.name, avatarUrl: null };
|
|
774
|
+
};
|
|
775
|
+
|
|
776
|
+
const simpleActions = {
|
|
777
|
+
AutoMergeDisabledEvent: "autoMergeDisabled",
|
|
778
|
+
AutoMergeEnabledEvent: "autoMergeEnabled",
|
|
779
|
+
ClosedEvent: "closed",
|
|
780
|
+
ConvertToDraftEvent: "convertedToDraft",
|
|
781
|
+
MergedEvent: "merged",
|
|
782
|
+
ReadyForReviewEvent: "readyForReview",
|
|
783
|
+
ReopenedEvent: "reopened",
|
|
784
|
+
} as const satisfies Record<string, PullRequestEventAction>;
|
|
785
|
+
|
|
786
|
+
interface EventStamp {
|
|
787
|
+
actor: GhActor | null;
|
|
788
|
+
createdAt: string;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
const event = (
|
|
792
|
+
action: PullRequestEventAction,
|
|
793
|
+
{ actor, createdAt }: EventStamp,
|
|
794
|
+
extra: Partial<Pick<PullRequestEvent, "detail" | "label" | "subject">> = {},
|
|
795
|
+
): PullRequestEvent => ({
|
|
796
|
+
kind: "event",
|
|
797
|
+
action,
|
|
798
|
+
actor: actor === null ? null : toPerson(actor),
|
|
799
|
+
subject: null,
|
|
800
|
+
label: null,
|
|
801
|
+
detail: null,
|
|
802
|
+
createdAt,
|
|
803
|
+
...extra,
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
type GhEventNode = Exclude<
|
|
807
|
+
GhTimelineNode,
|
|
808
|
+
{ __typename: "IssueComment" | "PullRequestReview" }
|
|
809
|
+
>;
|
|
810
|
+
|
|
811
|
+
const toEvent = (node: GhEventNode): PullRequestEvent => {
|
|
812
|
+
switch (node.__typename) {
|
|
813
|
+
case "PullRequestCommit": {
|
|
814
|
+
const { commit } = node;
|
|
815
|
+
return event(
|
|
816
|
+
"committed",
|
|
817
|
+
{ actor: commitAuthor(commit), createdAt: commit.committedDate },
|
|
818
|
+
{ subject: commit.messageHeadline, detail: commit.abbreviatedOid },
|
|
819
|
+
);
|
|
820
|
+
}
|
|
821
|
+
case "AssignedEvent":
|
|
822
|
+
case "UnassignedEvent": {
|
|
823
|
+
return event(
|
|
824
|
+
node.__typename === "AssignedEvent" ? "assigned" : "unassigned",
|
|
825
|
+
node,
|
|
826
|
+
{ subject: nameOf(node.assignee) },
|
|
827
|
+
);
|
|
828
|
+
}
|
|
829
|
+
case "LabeledEvent":
|
|
830
|
+
case "UnlabeledEvent": {
|
|
831
|
+
return event(
|
|
832
|
+
node.__typename === "LabeledEvent" ? "labeled" : "unlabeled",
|
|
833
|
+
node,
|
|
834
|
+
{
|
|
835
|
+
subject: node.label?.name ?? null,
|
|
836
|
+
label: node.label === null ? null : toLabel(node.label),
|
|
837
|
+
},
|
|
838
|
+
);
|
|
839
|
+
}
|
|
840
|
+
case "ReviewRequestedEvent":
|
|
841
|
+
case "ReviewRequestRemovedEvent": {
|
|
842
|
+
return event(
|
|
843
|
+
node.__typename === "ReviewRequestedEvent"
|
|
844
|
+
? "reviewRequested"
|
|
845
|
+
: "reviewRequestRemoved",
|
|
846
|
+
node,
|
|
847
|
+
{ subject: nameOf(node.requestedReviewer) },
|
|
848
|
+
);
|
|
849
|
+
}
|
|
850
|
+
case "HeadRefForcePushedEvent": {
|
|
851
|
+
return event("headRefForcePushed", node, {
|
|
852
|
+
detail: node.afterCommit?.abbreviatedOid ?? null,
|
|
853
|
+
});
|
|
854
|
+
}
|
|
855
|
+
case "BaseRefChangedEvent": {
|
|
856
|
+
return event("baseRefChanged", node, {
|
|
857
|
+
subject: node.currentRefName,
|
|
858
|
+
detail: node.previousRefName,
|
|
859
|
+
});
|
|
860
|
+
}
|
|
861
|
+
case "RenamedTitleEvent": {
|
|
862
|
+
return event("renamed", node, {
|
|
863
|
+
subject: node.currentTitle,
|
|
864
|
+
detail: node.previousTitle,
|
|
865
|
+
});
|
|
866
|
+
}
|
|
867
|
+
case "ReviewDismissedEvent": {
|
|
868
|
+
return event("reviewDismissed", node, {
|
|
869
|
+
subject: node.review?.author?.login ?? null,
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
case "AutoMergeDisabledEvent":
|
|
873
|
+
case "AutoMergeEnabledEvent":
|
|
874
|
+
case "ClosedEvent":
|
|
875
|
+
case "ConvertToDraftEvent":
|
|
876
|
+
case "MergedEvent":
|
|
877
|
+
case "ReadyForReviewEvent":
|
|
878
|
+
case "ReopenedEvent": {
|
|
879
|
+
return event(simpleActions[node.__typename], node);
|
|
880
|
+
}
|
|
881
|
+
default: {
|
|
882
|
+
return node satisfies never;
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
};
|
|
886
|
+
|
|
887
|
+
const toTimelineNode = (
|
|
888
|
+
node: GhTimelineNode,
|
|
889
|
+
): PullRequestTimelineItem | null => {
|
|
890
|
+
if (node.__typename === "IssueComment") {
|
|
891
|
+
return { kind: "comment", ...toComment(node) };
|
|
892
|
+
}
|
|
893
|
+
if (node.__typename === "PullRequestReview") return toReviewItem(node);
|
|
894
|
+
return toEvent(node);
|
|
895
|
+
};
|
|
896
|
+
|
|
897
|
+
const isTruncated = <Node>(connection: GhConnection<Node> | null): boolean =>
|
|
898
|
+
connection !== null &&
|
|
899
|
+
connection.totalCount > present(connection.nodes).length;
|
|
900
|
+
|
|
901
|
+
const bodyHtmlsOf = (
|
|
902
|
+
pullRequest: GhPullRequestTimeline,
|
|
903
|
+
): (string | undefined)[] => [
|
|
904
|
+
...present(pullRequest.timelineItems?.nodes).map((node) =>
|
|
905
|
+
"bodyHTML" in node ? node.bodyHTML : undefined,
|
|
906
|
+
),
|
|
907
|
+
...present(pullRequest.reviewThreads?.nodes).flatMap((thread) =>
|
|
908
|
+
present(thread.comments.nodes).map((comment) => comment.bodyHTML),
|
|
909
|
+
),
|
|
910
|
+
];
|
|
911
|
+
|
|
912
|
+
const toTimeline = (
|
|
913
|
+
pullRequest: GhPullRequestTimeline,
|
|
914
|
+
): Pick<
|
|
915
|
+
PullRequestTimelineResponse,
|
|
916
|
+
"images" | "timeline" | "timelineTruncated"
|
|
917
|
+
> => {
|
|
918
|
+
const items: PullRequestTimelineItem[] = [
|
|
919
|
+
...present(pullRequest.timelineItems?.nodes).map(toTimelineNode),
|
|
920
|
+
...present(pullRequest.reviewThreads?.nodes).map(toThreadItem),
|
|
921
|
+
].filter((item) => item !== null);
|
|
922
|
+
|
|
923
|
+
return {
|
|
924
|
+
timeline: items.toSorted((a, b) => a.createdAt.localeCompare(b.createdAt)),
|
|
925
|
+
images: mergeImageProxies(bodyHtmlsOf(pullRequest)),
|
|
926
|
+
timelineTruncated:
|
|
927
|
+
pullRequest.timelineItems?.pageInfo.hasPreviousPage === true ||
|
|
928
|
+
isTruncated(pullRequest.reviewThreads),
|
|
929
|
+
};
|
|
930
|
+
};
|
|
931
|
+
|
|
932
|
+
type GhContexts = NonNullable<
|
|
933
|
+
NonNullable<
|
|
934
|
+
NonNullable<GhPullRequestChecks["headCommits"]["nodes"]>[number]
|
|
935
|
+
>["commit"]["statusCheckRollup"]
|
|
936
|
+
>["contexts"];
|
|
937
|
+
|
|
938
|
+
const toChecks = (contexts: GhContexts | null): PullRequestCheck[] =>
|
|
939
|
+
present(contexts?.nodes)
|
|
940
|
+
.map(toCheck)
|
|
941
|
+
.map((check, index) => ({ check, index }))
|
|
942
|
+
.toSorted(
|
|
943
|
+
(a, b) =>
|
|
944
|
+
checkOrder[a.check.outcome] - checkOrder[b.check.outcome] ||
|
|
945
|
+
a.index - b.index,
|
|
946
|
+
)
|
|
947
|
+
.map(({ check }) => check);
|
|
948
|
+
|
|
949
|
+
const toFiles = (pullRequest: GhPullRequestFiles): PullRequestFile[] =>
|
|
950
|
+
present(pullRequest.files?.nodes).map((file) => ({
|
|
951
|
+
path: file.path,
|
|
952
|
+
additions: file.additions,
|
|
953
|
+
deletions: file.deletions,
|
|
954
|
+
change: fileChanges.get(file.changeType) ?? "changed",
|
|
955
|
+
viewed: viewedStates.get(file.viewerViewedState) ?? "unviewed",
|
|
956
|
+
}));
|
|
957
|
+
|
|
958
|
+
const toRequestedReviewers = (
|
|
959
|
+
pullRequest: GhPullRequestDetail,
|
|
960
|
+
): PullRequestRequestedReviewer[] =>
|
|
961
|
+
present(pullRequest.reviewRequests?.nodes)
|
|
962
|
+
.map(({ requestedReviewer }) => requestedReviewer)
|
|
963
|
+
.filter((reviewer) => reviewer !== null)
|
|
964
|
+
.map(toReviewer);
|
|
965
|
+
|
|
966
|
+
const toReviewDecision = (decision: string | null): PrReviewDecision =>
|
|
967
|
+
decision === null ? null : (reviewDecisions.get(decision) ?? null);
|
|
968
|
+
|
|
969
|
+
const mergeStates = new Map<string, PullRequestMergeState>([
|
|
970
|
+
["BEHIND", "behind"],
|
|
971
|
+
["BLOCKED", "blocked"],
|
|
972
|
+
["CLEAN", "clean"],
|
|
973
|
+
["DIRTY", "dirty"],
|
|
974
|
+
["DRAFT", "draft"],
|
|
975
|
+
["HAS_HOOKS", "hasHooks"],
|
|
976
|
+
["UNSTABLE", "unstable"],
|
|
977
|
+
]);
|
|
978
|
+
|
|
979
|
+
const mergeMethodNames = new Map<string, PullRequestMergeMethod>([
|
|
980
|
+
["MERGE", "merge"],
|
|
981
|
+
["REBASE", "rebase"],
|
|
982
|
+
["SQUASH", "squash"],
|
|
983
|
+
]);
|
|
984
|
+
|
|
985
|
+
const toMergeMethods = (
|
|
986
|
+
repository: GhMergeSettings,
|
|
987
|
+
): PullRequestMergeMethod[] => {
|
|
988
|
+
const allowed: PullRequestMergeMethod[] = [];
|
|
989
|
+
if (repository.mergeCommitAllowed === true) allowed.push("merge");
|
|
990
|
+
if (repository.squashMergeAllowed === true) allowed.push("squash");
|
|
991
|
+
if (repository.rebaseMergeAllowed === true) allowed.push("rebase");
|
|
992
|
+
const preferred = mergeMethodNames.get(
|
|
993
|
+
repository.viewerDefaultMergeMethod ?? "",
|
|
994
|
+
);
|
|
995
|
+
return preferred !== undefined && allowed.includes(preferred)
|
|
996
|
+
? [preferred, ...allowed.filter((method) => method !== preferred)]
|
|
997
|
+
: allowed;
|
|
998
|
+
};
|
|
999
|
+
|
|
1000
|
+
const toCommit = ({ commit }: { commit: GhCommit }): PullRequestCommit => ({
|
|
1001
|
+
oid: commit.oid,
|
|
1002
|
+
parentOid: present(commit.parents.nodes)[0]?.oid ?? null,
|
|
1003
|
+
merge: commit.parents.totalCount > 1,
|
|
1004
|
+
headline: commit.messageHeadline,
|
|
1005
|
+
body: commit.messageBody.trim(),
|
|
1006
|
+
author: commitAuthor(commit),
|
|
1007
|
+
committedAt: commit.committedDate,
|
|
1008
|
+
});
|
|
1009
|
+
|
|
1010
|
+
const viewerLastReview = (
|
|
1011
|
+
pullRequest: GhPullRequestFiles,
|
|
1012
|
+
): PullRequestViewerLastReview | null => {
|
|
1013
|
+
const review = present(pullRequest.latestReviews?.nodes).find(
|
|
1014
|
+
({ viewerDidAuthor }) => viewerDidAuthor,
|
|
1015
|
+
);
|
|
1016
|
+
const oid = review?.commit?.oid;
|
|
1017
|
+
if (review === undefined || oid === undefined) return null;
|
|
1018
|
+
return { oid, submittedAt: review.submittedAt };
|
|
1019
|
+
};
|
|
1020
|
+
|
|
1021
|
+
const toDetail = (
|
|
1022
|
+
pullRequest: GhPullRequestDetail,
|
|
1023
|
+
repository: GhMergeSettings,
|
|
1024
|
+
ownerRepo: string,
|
|
1025
|
+
): PullRequestDetail => ({
|
|
1026
|
+
...toReactable(pullRequest),
|
|
1027
|
+
ownerRepo,
|
|
1028
|
+
number: pullRequest.number,
|
|
1029
|
+
url: pullRequest.url,
|
|
1030
|
+
title: pullRequest.title,
|
|
1031
|
+
body: pullRequest.body,
|
|
1032
|
+
state: toState(pullRequest.state, pullRequest.isDraft),
|
|
1033
|
+
author: pullRequest.author === null ? null : toPerson(pullRequest.author),
|
|
1034
|
+
headRef: pullRequest.headRefName,
|
|
1035
|
+
baseRef: pullRequest.baseRefName,
|
|
1036
|
+
headSha: pullRequest.headRefOid,
|
|
1037
|
+
baseSha: pullRequest.baseRefOid,
|
|
1038
|
+
createdAt: pullRequest.createdAt,
|
|
1039
|
+
updatedAt: pullRequest.updatedAt,
|
|
1040
|
+
lastEditedAt: pullRequest.lastEditedAt,
|
|
1041
|
+
additions: pullRequest.additions,
|
|
1042
|
+
deletions: pullRequest.deletions,
|
|
1043
|
+
changedFiles: pullRequest.changedFiles,
|
|
1044
|
+
mergeable: mergeables.get(pullRequest.mergeable) ?? "unknown",
|
|
1045
|
+
mergeState: mergeStates.get(pullRequest.mergeStateStatus) ?? "unknown",
|
|
1046
|
+
mergeMethods: toMergeMethods(repository),
|
|
1047
|
+
mergeCommitMessages: {
|
|
1048
|
+
merge: {
|
|
1049
|
+
title: pullRequest.mergeCommitHeadline,
|
|
1050
|
+
body: pullRequest.mergeCommitBody,
|
|
1051
|
+
},
|
|
1052
|
+
squash: {
|
|
1053
|
+
title: pullRequest.squashCommitHeadline,
|
|
1054
|
+
body: pullRequest.squashCommitBody,
|
|
1055
|
+
},
|
|
1056
|
+
},
|
|
1057
|
+
viewerCanUpdate: pullRequest.viewerCanUpdate,
|
|
1058
|
+
viewerCanUpdateBranch: pullRequest.viewerCanUpdateBranch,
|
|
1059
|
+
viewerCanApplySuggestion: pullRequest.viewerCanApplySuggestion,
|
|
1060
|
+
reviewDecision: toReviewDecision(pullRequest.reviewDecision),
|
|
1061
|
+
labels: present(pullRequest.labels?.nodes).map(toLabel),
|
|
1062
|
+
assignees: present(pullRequest.assignees?.nodes).map(toPerson),
|
|
1063
|
+
requestedReviewers: toRequestedReviewers(pullRequest),
|
|
1064
|
+
reviews: present(pullRequest.latestReviews?.nodes).map((review) => ({
|
|
1065
|
+
author: review.author?.login ?? "ghost",
|
|
1066
|
+
state: reviewStates.get(review.state) ?? "commented",
|
|
1067
|
+
submittedAt: review.submittedAt,
|
|
1068
|
+
})),
|
|
1069
|
+
commitsTotal: pullRequest.commits.totalCount,
|
|
1070
|
+
});
|
|
1071
|
+
|
|
1072
|
+
const toChecksPart = (
|
|
1073
|
+
pullRequest: GhPullRequestChecks,
|
|
1074
|
+
): Pick<PullRequestChecksResponse, "checks" | "checksTotal"> => {
|
|
1075
|
+
const contexts =
|
|
1076
|
+
pullRequest.headCommits.nodes?.[0]?.commit.statusCheckRollup?.contexts ??
|
|
1077
|
+
null;
|
|
1078
|
+
return { checks: toChecks(contexts), checksTotal: contexts?.totalCount ?? 0 };
|
|
1079
|
+
};
|
|
1080
|
+
|
|
1081
|
+
/**
|
|
1082
|
+
* What the label, reviewer and assignee editors offer. The pull request is
|
|
1083
|
+
* named only for GitHub's suggested reviewers and its author, whom GitHub
|
|
1084
|
+
* refuses as a reviewer.
|
|
1085
|
+
*/
|
|
1086
|
+
export const pullRequestCandidatesQuery = `query PullRequestCandidates($owner: String!, $name: String!, $number: Int!) {
|
|
1087
|
+
repositoryOwner(login: $owner) {
|
|
1088
|
+
... on Organization {
|
|
1089
|
+
teams(first: 100, orderBy: { field: NAME, direction: ASC }) {
|
|
1090
|
+
nodes { slug name }
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
repository(owner: $owner, name: $name) {
|
|
1095
|
+
labels(first: 100, orderBy: { field: NAME, direction: ASC }) {
|
|
1096
|
+
nodes { name color description }
|
|
1097
|
+
}
|
|
1098
|
+
assignableUsers(first: 100) { nodes { login avatarUrl } }
|
|
1099
|
+
pullRequest(number: $number) {
|
|
1100
|
+
author { login }
|
|
1101
|
+
suggestedReviewers { reviewer { login avatarUrl } }
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
}`;
|
|
1105
|
+
|
|
1106
|
+
interface GhCandidatesPullRequest {
|
|
1107
|
+
author: { login: string } | null;
|
|
1108
|
+
suggestedReviewers: ({ reviewer: GhActor | null } | null)[] | null;
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
export interface PullRequestCandidatesData {
|
|
1112
|
+
/** `teams` only on an organisation: a user has none */
|
|
1113
|
+
repositoryOwner?: {
|
|
1114
|
+
teams?: { nodes: ({ slug: string; name: string } | null)[] | null } | null;
|
|
1115
|
+
} | null;
|
|
1116
|
+
repository: {
|
|
1117
|
+
labels: { nodes: (GhLabel | null)[] | null } | null;
|
|
1118
|
+
assignableUsers: { nodes: (GhActor | null)[] | null } | null;
|
|
1119
|
+
pullRequest: GhCandidatesPullRequest | null;
|
|
1120
|
+
} | null;
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
/** Never the author, never anyone twice: GitHub refuses the author's review. */
|
|
1124
|
+
const reviewerCandidates = (
|
|
1125
|
+
pullRequest: GhCandidatesPullRequest,
|
|
1126
|
+
assignable: GhActor[],
|
|
1127
|
+
): { suggested: PullRequestPerson[]; others: PullRequestPerson[] } => {
|
|
1128
|
+
const author = pullRequest.author?.login;
|
|
1129
|
+
const seen = new Set<string>();
|
|
1130
|
+
const unseen = (actors: GhActor[]): PullRequestPerson[] =>
|
|
1131
|
+
actors.flatMap((actor) => {
|
|
1132
|
+
if (actor.login === author || seen.has(actor.login)) return [];
|
|
1133
|
+
seen.add(actor.login);
|
|
1134
|
+
return [toPerson(actor)];
|
|
1135
|
+
});
|
|
1136
|
+
const suggested = unseen(
|
|
1137
|
+
present(pullRequest.suggestedReviewers)
|
|
1138
|
+
.map(({ reviewer }) => reviewer)
|
|
1139
|
+
.filter((reviewer): reviewer is GhActor => reviewer !== null),
|
|
1140
|
+
);
|
|
1141
|
+
const others = unseen(
|
|
1142
|
+
assignable.toSorted((a, b) => a.login.localeCompare(b.login)),
|
|
1143
|
+
);
|
|
1144
|
+
return { suggested, others };
|
|
1145
|
+
};
|
|
1146
|
+
|
|
1147
|
+
interface PartParseParams<GhPart> {
|
|
1148
|
+
data: PullRequestPartData<GhPart> | null;
|
|
1149
|
+
errors: string[];
|
|
1150
|
+
ownerRepo: string;
|
|
1151
|
+
number: number;
|
|
1152
|
+
fetchedAt: string;
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
const gitHubRead = ({
|
|
1156
|
+
fetchedAt,
|
|
1157
|
+
errors,
|
|
1158
|
+
}: Pick<
|
|
1159
|
+
PartParseParams<unknown>,
|
|
1160
|
+
"errors" | "fetchedAt"
|
|
1161
|
+
>): PullRequestRead => ({
|
|
1162
|
+
fetchedAt,
|
|
1163
|
+
checkedAt: fetchedAt,
|
|
1164
|
+
source: "github",
|
|
1165
|
+
errors,
|
|
1166
|
+
});
|
|
1167
|
+
|
|
1168
|
+
/**
|
|
1169
|
+
* Raises when there is no pull request to show — a repository the token cannot
|
|
1170
|
+
* see, a number that does not exist — with GitHub's own reason; anything GitHub
|
|
1171
|
+
* refused beside a usable answer comes back in `errors`.
|
|
1172
|
+
*/
|
|
1173
|
+
const readPart = <GhPart>({
|
|
1174
|
+
data,
|
|
1175
|
+
errors,
|
|
1176
|
+
ownerRepo,
|
|
1177
|
+
number,
|
|
1178
|
+
}: PartParseParams<GhPart>): {
|
|
1179
|
+
pullRequest: GhPart;
|
|
1180
|
+
repository: GhMergeSettings;
|
|
1181
|
+
} => {
|
|
1182
|
+
const repository = data?.repository ?? null;
|
|
1183
|
+
const pullRequest = repository?.pullRequest ?? null;
|
|
1184
|
+
if (repository === null || pullRequest === null) {
|
|
1185
|
+
const reason = errors.length > 0 ? errors.join("; ") : "not found";
|
|
1186
|
+
throw new Error(
|
|
1187
|
+
`no pull request #${String(number)} in ${ownerRepo}: ${reason}`,
|
|
1188
|
+
);
|
|
1189
|
+
}
|
|
1190
|
+
return { pullRequest, repository };
|
|
1191
|
+
};
|
|
1192
|
+
|
|
1193
|
+
export const parsePullRequestDetail = (
|
|
1194
|
+
params: PartParseParams<GhPullRequestDetail>,
|
|
1195
|
+
): PullRequestDetailResponse => {
|
|
1196
|
+
const { pullRequest, repository } = readPart(params);
|
|
1197
|
+
return {
|
|
1198
|
+
pullRequest: toDetail(pullRequest, repository, params.ownerRepo),
|
|
1199
|
+
viewer: params.data?.viewer?.login ?? "",
|
|
1200
|
+
images: imageProxiesOf(pullRequest.bodyHTML),
|
|
1201
|
+
...gitHubRead(params),
|
|
1202
|
+
};
|
|
1203
|
+
};
|
|
1204
|
+
|
|
1205
|
+
export const parsePullRequestChecks = (
|
|
1206
|
+
params: PartParseParams<GhPullRequestChecks>,
|
|
1207
|
+
): PullRequestChecksResponse => ({
|
|
1208
|
+
...toChecksPart(readPart(params).pullRequest),
|
|
1209
|
+
...gitHubRead(params),
|
|
1210
|
+
});
|
|
1211
|
+
|
|
1212
|
+
export const parsePullRequestTimeline = (
|
|
1213
|
+
params: PartParseParams<GhPullRequestTimeline>,
|
|
1214
|
+
): PullRequestTimelineResponse => ({
|
|
1215
|
+
...toTimeline(readPart(params).pullRequest),
|
|
1216
|
+
...gitHubRead(params),
|
|
1217
|
+
});
|
|
1218
|
+
|
|
1219
|
+
export const parsePullRequestCommits = (
|
|
1220
|
+
params: PartParseParams<GhPullRequestCommits>,
|
|
1221
|
+
): PullRequestCommitsResponse => ({
|
|
1222
|
+
commits: present(readPart(params).pullRequest.commits.nodes).map(toCommit),
|
|
1223
|
+
...gitHubRead(params),
|
|
1224
|
+
});
|
|
1225
|
+
|
|
1226
|
+
export const parsePullRequestFiles = (
|
|
1227
|
+
params: PartParseParams<GhPullRequestFiles>,
|
|
1228
|
+
): PullRequestFilesResponse => {
|
|
1229
|
+
const { pullRequest } = readPart(params);
|
|
1230
|
+
return {
|
|
1231
|
+
files: toFiles(pullRequest),
|
|
1232
|
+
viewerLastReview: viewerLastReview(pullRequest),
|
|
1233
|
+
...gitHubRead(params),
|
|
1234
|
+
};
|
|
1235
|
+
};
|
|
1236
|
+
|
|
1237
|
+
const readGitHub = async <GhPart, Response>(
|
|
1238
|
+
transport: GitHubTransport,
|
|
1239
|
+
{ ownerRepo, number }: PullRequestRef,
|
|
1240
|
+
query: string,
|
|
1241
|
+
parse: (params: PartParseParams<GhPart>) => Response,
|
|
1242
|
+
): Promise<Response> => {
|
|
1243
|
+
const split = splitOwnerRepo(ownerRepo);
|
|
1244
|
+
if (split === null) throw new Error(`${ownerRepo} is not owner/repo`);
|
|
1245
|
+
const { data, errors } = await transport.graphql<PullRequestPartData<GhPart>>(
|
|
1246
|
+
query,
|
|
1247
|
+
{
|
|
1248
|
+
owner: split.owner,
|
|
1249
|
+
name: split.name,
|
|
1250
|
+
number,
|
|
1251
|
+
},
|
|
1252
|
+
);
|
|
1253
|
+
return parse({
|
|
1254
|
+
data,
|
|
1255
|
+
errors,
|
|
1256
|
+
ownerRepo,
|
|
1257
|
+
number,
|
|
1258
|
+
fetchedAt: new Date().toISOString(),
|
|
1259
|
+
});
|
|
1260
|
+
};
|
|
1261
|
+
|
|
1262
|
+
const partReaders: {
|
|
1263
|
+
[Part in PullRequestPart]: (
|
|
1264
|
+
transport: GitHubTransport,
|
|
1265
|
+
ref: PullRequestRef,
|
|
1266
|
+
) => Promise<PullRequestPartResponse<Part>>;
|
|
1267
|
+
} = {
|
|
1268
|
+
detail: (transport, ref) =>
|
|
1269
|
+
readGitHub(transport, ref, pullRequestDetailQuery, parsePullRequestDetail),
|
|
1270
|
+
checks: (transport, ref) =>
|
|
1271
|
+
readGitHub(transport, ref, pullRequestChecksQuery, parsePullRequestChecks),
|
|
1272
|
+
timeline: (transport, ref) =>
|
|
1273
|
+
readGitHub(
|
|
1274
|
+
transport,
|
|
1275
|
+
ref,
|
|
1276
|
+
pullRequestTimelineQuery,
|
|
1277
|
+
parsePullRequestTimeline,
|
|
1278
|
+
),
|
|
1279
|
+
commits: (transport, ref) =>
|
|
1280
|
+
readGitHub(
|
|
1281
|
+
transport,
|
|
1282
|
+
ref,
|
|
1283
|
+
pullRequestCommitsQuery,
|
|
1284
|
+
parsePullRequestCommits,
|
|
1285
|
+
),
|
|
1286
|
+
files: (transport, ref) =>
|
|
1287
|
+
readGitHub(transport, ref, pullRequestFilesQuery, parsePullRequestFiles),
|
|
1288
|
+
};
|
|
1289
|
+
|
|
1290
|
+
export const fetchPullRequestPart = <Part extends PullRequestPart>(
|
|
1291
|
+
transport: GitHubTransport,
|
|
1292
|
+
ref: PullRequestRef,
|
|
1293
|
+
part: Part,
|
|
1294
|
+
): Promise<PullRequestPartResponse<Part>> => partReaders[part](transport, ref);
|
|
1295
|
+
|
|
1296
|
+
/**
|
|
1297
|
+
* Raises only when GitHub answered nothing usable; a missing part (teams on a
|
|
1298
|
+
* user's repository, a pull request the token cannot see) offers none.
|
|
1299
|
+
*/
|
|
1300
|
+
export const parsePullRequestCandidates = ({
|
|
1301
|
+
data,
|
|
1302
|
+
errors,
|
|
1303
|
+
}: {
|
|
1304
|
+
data: PullRequestCandidatesData | null;
|
|
1305
|
+
errors: string[];
|
|
1306
|
+
}): PullRequestCandidatesResponse => {
|
|
1307
|
+
const repository = data?.repository ?? null;
|
|
1308
|
+
if (repository === null) {
|
|
1309
|
+
throw new Error(errors.length > 0 ? errors.join("; ") : "not found");
|
|
1310
|
+
}
|
|
1311
|
+
const assignable = present(repository.assignableUsers?.nodes);
|
|
1312
|
+
const reviewers = reviewerCandidates(
|
|
1313
|
+
repository.pullRequest ?? { author: null, suggestedReviewers: null },
|
|
1314
|
+
assignable,
|
|
1315
|
+
);
|
|
1316
|
+
return {
|
|
1317
|
+
repoLabels: present(repository.labels?.nodes).map(toLabel),
|
|
1318
|
+
suggestedReviewers: reviewers.suggested,
|
|
1319
|
+
reviewerCandidates: reviewers.others,
|
|
1320
|
+
teamCandidates: present(data?.repositoryOwner?.teams?.nodes).map(
|
|
1321
|
+
({ slug, name }) => ({ slug, name }),
|
|
1322
|
+
),
|
|
1323
|
+
assigneeCandidates: assignable
|
|
1324
|
+
.toSorted((a, b) => a.login.localeCompare(b.login))
|
|
1325
|
+
.map(toPerson),
|
|
1326
|
+
errors,
|
|
1327
|
+
};
|
|
1328
|
+
};
|
|
1329
|
+
|
|
1330
|
+
export const fetchPullRequestCandidates = async (
|
|
1331
|
+
transport: GitHubTransport,
|
|
1332
|
+
{
|
|
1333
|
+
ownerRepo,
|
|
1334
|
+
number,
|
|
1335
|
+
}: {
|
|
1336
|
+
ownerRepo: string;
|
|
1337
|
+
number: number;
|
|
1338
|
+
},
|
|
1339
|
+
): Promise<PullRequestCandidatesResponse> => {
|
|
1340
|
+
const split = splitOwnerRepo(ownerRepo);
|
|
1341
|
+
if (split === null) throw new Error(`${ownerRepo} is not owner/repo`);
|
|
1342
|
+
const { data, errors } = await transport.graphql<PullRequestCandidatesData>(
|
|
1343
|
+
pullRequestCandidatesQuery,
|
|
1344
|
+
{ owner: split.owner, name: split.name, number },
|
|
1345
|
+
);
|
|
1346
|
+
return parsePullRequestCandidates({ data, errors });
|
|
1347
|
+
};
|