@reviewflow/sdk 0.0.0-stage → 6.21.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.
Files changed (50) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +86 -2
  3. package/build/files-node.mjs +70 -0
  4. package/build/git-node.mjs +1792 -0
  5. package/build/index-browser.es.js +3338 -0
  6. package/build/index-node.mjs +3338 -0
  7. package/package.json +136 -4
  8. package/src/api/pullRequest.ts +617 -0
  9. package/src/config/insertCheckoutEntry.ts +54 -0
  10. package/src/diff/diffPaths.ts +78 -0
  11. package/src/diff/impacts.ts +47 -0
  12. package/src/diff/readingOrder.ts +428 -0
  13. package/src/diff/themes.ts +105 -0
  14. package/src/diff/types.ts +141 -0
  15. package/src/files/editableFile.ts +163 -0
  16. package/src/files.ts +13 -0
  17. package/src/git/groupDiffPaths.ts +134 -0
  18. package/src/git/manifests.ts +96 -0
  19. package/src/git/moduleBindings.ts +571 -0
  20. package/src/git/pullRequestDiff.ts +159 -0
  21. package/src/git/readBranchDiff.ts +359 -0
  22. package/src/git/readImpacts.ts +797 -0
  23. package/src/git/readingOrder.ts +190 -0
  24. package/src/git/run.ts +213 -0
  25. package/src/git.ts +38 -0
  26. package/src/github/applySuggestions.ts +399 -0
  27. package/src/github/editPullRequest.ts +790 -0
  28. package/src/github/fetchCompareDiff.ts +21 -0
  29. package/src/github/imageProxies.ts +43 -0
  30. package/src/github/lineDiff.ts +145 -0
  31. package/src/github/prQuery.ts +298 -0
  32. package/src/github/pullRequestDetail.ts +1347 -0
  33. package/src/github/pullRequestHead.ts +65 -0
  34. package/src/github/pullRequestRevisions.ts +124 -0
  35. package/src/github/transport.ts +171 -0
  36. package/src/index.ts +35 -0
  37. package/src/reviewflow/board.ts +333 -0
  38. package/src/reviewflow/changedParts.ts +80 -0
  39. package/src/reviewflow/checksFromReviewflow.ts +75 -0
  40. package/src/reviewflow/client.ts +589 -0
  41. package/src/reviewflow/follow.ts +345 -0
  42. package/src/reviewflow/status.ts +17 -0
  43. package/src/reviewflow/stream.ts +199 -0
  44. package/src/types/entities.ts +8 -0
  45. package/src/types/ids.ts +1 -0
  46. package/src/types/reviewflow.ts +32 -0
  47. package/src/utils/pullRequestEdit.ts +108 -0
  48. package/src/utils/pullRequestPart.ts +12 -0
  49. package/src/utils/pullRequestReaction.ts +19 -0
  50. package/src/utils/pullRequestSuggestion.ts +148 -0
@@ -0,0 +1,3338 @@
1
+ const cEscapes = {
2
+ a: 7,
3
+ b: 8,
4
+ f: 12,
5
+ n: 10,
6
+ r: 13,
7
+ t: 9,
8
+ v: 11,
9
+ '"': 34,
10
+ "\\": 92
11
+ };
12
+ const unquoteGitPath = (quoted) => {
13
+ const bytes = [];
14
+ const encoder = new TextEncoder();
15
+ for (let index = 1; index < quoted.length - 1; index++) {
16
+ const char = quoted[index];
17
+ if (char !== "\\") {
18
+ bytes.push(...encoder.encode(char));
19
+ continue;
20
+ }
21
+ const next = quoted[index + 1] ?? "";
22
+ const octal = /^[0-7]{3}/.exec(quoted.slice(index + 1));
23
+ if (octal) {
24
+ bytes.push(Number.parseInt(octal[0], 8));
25
+ index += 3;
26
+ } else {
27
+ bytes.push(cEscapes[next] ?? next.codePointAt(0) ?? 0);
28
+ index += 1;
29
+ }
30
+ }
31
+ return new TextDecoder().decode(new Uint8Array(bytes));
32
+ };
33
+ const unquoted = (name) => name.length > 1 && name.startsWith('"') && name.endsWith('"') ? unquoteGitPath(name) : name;
34
+ const headerName = (raw, prefix) => {
35
+ const name = unquoted(raw.endsWith(" ") ? raw.slice(0, -1) : raw);
36
+ return name.startsWith(prefix) ? name.slice(prefix.length) : null;
37
+ };
38
+ const pathOfBlock = (block) => {
39
+ const added = /^\+\+\+ (.*)$/m.exec(block)?.[1];
40
+ const addedName = added === void 0 ? null : headerName(added, "b/");
41
+ if (addedName !== null) return addedName;
42
+ const removed = /^--- (.*)$/m.exec(block)?.[1];
43
+ const removedName = removed === void 0 ? null : headerName(removed, "a/");
44
+ if (removedName !== null) return removedName;
45
+ const renamed = /^rename to (.*)$/m.exec(block)?.[1];
46
+ if (renamed !== void 0) return unquoted(renamed);
47
+ const header = block.split("\n", 1)[0] ?? "";
48
+ const quotedSeparator = header.lastIndexOf(' "b/');
49
+ if (quotedSeparator !== -1) {
50
+ return headerName(header.slice(quotedSeparator + 1), "b/");
51
+ }
52
+ const separator = header.lastIndexOf(" b/");
53
+ return separator === -1 ? null : header.slice(separator + 3);
54
+ };
55
+ const diffPaths = (patch) => patch.split(/^diff --git /m).slice(1).map(pathOfBlock).filter((path) => path !== null);
56
+
57
+ const typescriptExtensions = [".ts", ".tsx", ".mts", ".cts"];
58
+ const extensionOf = (path) => {
59
+ const slash = path.lastIndexOf("/");
60
+ const dot = path.lastIndexOf(".");
61
+ return dot > slash ? path.slice(dot) : "";
62
+ };
63
+ const isTypescriptPath = (path) => typescriptExtensions.includes(extensionOf(path));
64
+ const attachableSuffix = /\.(?:test|spec|stories)(?=\.[cm]?tsx?$)/;
65
+ const testDirectories = /* @__PURE__ */ new Set([
66
+ "__fixtures__",
67
+ "__mocks__",
68
+ "__tests__",
69
+ "fixtures",
70
+ "test",
71
+ "tests"
72
+ ]);
73
+ const isAttachablePath = (path) => isTypescriptPath(path) && (attachableSuffix.test(path) || path.split("/").slice(0, -1).some((directory) => testDirectories.has(directory)));
74
+ const directoryOf = (path) => {
75
+ const slash = path.lastIndexOf("/");
76
+ return slash === -1 ? "" : path.slice(0, slash);
77
+ };
78
+ const joinPath = (directory, name) => directory === "" ? name : `${directory}/${name}`;
79
+ const coveredDirectoriesOf = (path) => {
80
+ const directory = directoryOf(path);
81
+ return directory.split("/").at(-1) === "__tests__" ? [directory, directoryOf(directory)] : [directory];
82
+ };
83
+ const attachableKinds = /* @__PURE__ */ new Set(["spec", "stories", "test"]);
84
+ const subjectsOf = (path) => {
85
+ const parts = path.slice(directoryOf(path).length).replace(/^\//, "").replace(/\.[cm]?tsx?$/, "").split(".");
86
+ const kind = parts.findIndex((part) => attachableKinds.has(part));
87
+ const stem = kind === -1 ? parts : parts.slice(0, kind);
88
+ const names = stem.map(
89
+ (_, dropped) => stem.slice(0, stem.length - dropped).join(".")
90
+ );
91
+ return coveredDirectoriesOf(path).flatMap(
92
+ (directory) => names.flatMap(
93
+ (name) => typescriptExtensions.map(
94
+ (extension) => joinPath(directory, name + extension)
95
+ )
96
+ )
97
+ );
98
+ };
99
+
100
+ const diffThemes = [
101
+ "github",
102
+ "github-high-contrast",
103
+ "pierre",
104
+ "vscode",
105
+ "one",
106
+ "catppuccin",
107
+ "rose-pine",
108
+ "solarized",
109
+ "gruvbox",
110
+ "everforest",
111
+ "kanagawa",
112
+ "vitesse",
113
+ "ayu",
114
+ "material",
115
+ "night-owl",
116
+ "min"
117
+ ];
118
+ const isDiffTheme = (value) => diffThemes.includes(value);
119
+ const families = {
120
+ github: {
121
+ label: "GitHub",
122
+ themes: { light: "github-light-default", dark: "github-dark-default" }
123
+ },
124
+ "github-high-contrast": {
125
+ label: "GitHub high contrast",
126
+ themes: {
127
+ light: "github-light-high-contrast",
128
+ dark: "github-dark-high-contrast"
129
+ }
130
+ },
131
+ pierre: {
132
+ label: "Pierre",
133
+ themes: { light: "pierre-light", dark: "pierre-dark" }
134
+ },
135
+ vscode: {
136
+ label: "VS Code",
137
+ themes: { light: "light-plus", dark: "dark-plus" }
138
+ },
139
+ one: { label: "One", themes: { light: "one-light", dark: "one-dark-pro" } },
140
+ catppuccin: {
141
+ label: "Catppuccin",
142
+ themes: { light: "catppuccin-latte", dark: "catppuccin-mocha" }
143
+ },
144
+ "rose-pine": {
145
+ label: "Ros\xE9 Pine",
146
+ themes: { light: "rose-pine-dawn", dark: "rose-pine" }
147
+ },
148
+ solarized: {
149
+ label: "Solarized",
150
+ themes: { light: "solarized-light", dark: "solarized-dark" }
151
+ },
152
+ gruvbox: {
153
+ label: "Gruvbox",
154
+ themes: { light: "gruvbox-light-medium", dark: "gruvbox-dark-medium" }
155
+ },
156
+ everforest: {
157
+ label: "Everforest",
158
+ themes: { light: "everforest-light", dark: "everforest-dark" }
159
+ },
160
+ kanagawa: {
161
+ label: "Kanagawa",
162
+ themes: { light: "kanagawa-lotus", dark: "kanagawa-wave" }
163
+ },
164
+ vitesse: {
165
+ label: "Vitesse",
166
+ themes: { light: "vitesse-light", dark: "vitesse-dark" }
167
+ },
168
+ ayu: { label: "Ayu", themes: { light: "ayu-light", dark: "ayu-dark" } },
169
+ material: {
170
+ label: "Material",
171
+ themes: { light: "material-theme-lighter", dark: "material-theme" }
172
+ },
173
+ "night-owl": {
174
+ label: "Night Owl",
175
+ themes: { light: "night-owl-light", dark: "night-owl" }
176
+ },
177
+ min: { label: "Min", themes: { light: "min-light", dark: "min-dark" } }
178
+ };
179
+ const diffThemeNames = (theme) => families[theme].themes;
180
+ const diffThemeOptions = diffThemes.map((theme) => ({
181
+ value: theme,
182
+ label: families[theme].label
183
+ }));
184
+
185
+ const isDiffWhitespace = (value) => value === "hide" || value === "show";
186
+
187
+ const fenceOpen = /^( {0,3})(`{3,}(?!`)|~{3,}(?!~))(.*)$/;
188
+ const withoutIndent = (line, indent) => {
189
+ let index = 0;
190
+ while (index < indent && line[index] === " ") index += 1;
191
+ return line.slice(index);
192
+ };
193
+ const parseSuggestions = (body) => {
194
+ const lines = body.replaceAll("\r\n", "\n").split("\n");
195
+ const suggestions = [];
196
+ let index = 0;
197
+ while (index < lines.length) {
198
+ const open = fenceOpen.exec(lines[index] ?? "");
199
+ index += 1;
200
+ if (open === null) continue;
201
+ const [, indent = "", fence = "", info = ""] = open;
202
+ if (fence.startsWith("`") && info.includes("`")) continue;
203
+ const close = new RegExp(
204
+ `^ {0,3}${fence[0] ?? ""}{${String(fence.length)},}[ \\t]*$`
205
+ );
206
+ const content = [];
207
+ while (index < lines.length && !close.test(lines[index] ?? "")) {
208
+ content.push(withoutIndent(lines[index] ?? "", indent.length));
209
+ index += 1;
210
+ }
211
+ index += 1;
212
+ if (info.trim().split(/\s/)[0] === "suggestion") suggestions.push(content);
213
+ }
214
+ return suggestions;
215
+ };
216
+ const rangeLength = (start, end) => {
217
+ if (end === null) return null;
218
+ return end - (start ?? end) + 1;
219
+ };
220
+ const suggestionOriginalLines = (thread) => {
221
+ const count = thread.line === null ? rangeLength(thread.originalStartLine, thread.originalLine) : rangeLength(thread.startLine, thread.line);
222
+ if (count === null || count < 1) return null;
223
+ const dropped = thread.side === "RIGHT" ? "-" : "+";
224
+ const sideLines = thread.diffHunk.replaceAll("\r\n", "\n").split("\n").filter(
225
+ (line) => !line.startsWith("@@") && !line.startsWith("\\") && !line.startsWith(dropped)
226
+ );
227
+ if (sideLines.length < count) return null;
228
+ return sideLines.slice(-count).map((line) => line.slice(1));
229
+ };
230
+ const suggestionCommitRefusal = ({
231
+ state,
232
+ viewerCanApplySuggestion,
233
+ suggestionCount,
234
+ outdated,
235
+ line,
236
+ side,
237
+ startSide
238
+ }) => {
239
+ if (state === "closed" || state === "merged") {
240
+ return `The pull request is ${state}`;
241
+ }
242
+ if (!viewerCanApplySuggestion) {
243
+ return "GitHub does not let you commit to this pull request's branch";
244
+ }
245
+ if (suggestionCount === 0) return "The comment holds no suggestion";
246
+ if (suggestionCount > 1) {
247
+ return "The comment holds more than one suggestion: GitHub commits one per comment";
248
+ }
249
+ if (outdated || line === null) {
250
+ return "Outdated: the lines it replaces changed since";
251
+ }
252
+ if (side === "LEFT" || startSide === "LEFT") {
253
+ return "It is on removed lines, which the branch no longer has";
254
+ }
255
+ return null;
256
+ };
257
+ const suggestionRangesOverlap = (a, b) => a.path === b.path && (a.startLine ?? a.line) <= b.line && (b.startLine ?? b.line) <= a.line;
258
+
259
+ const prFields = `fragment prFields on PullRequest {
260
+ number
261
+ state
262
+ isDraft
263
+ reviewDecision
264
+ mergedAt
265
+ mergeable
266
+ headRefOid
267
+ baseRefName
268
+ commits(last: 1) {
269
+ nodes {
270
+ commit {
271
+ statusCheckRollup {
272
+ contexts(first: 1) {
273
+ checkRunCountsByState { count state }
274
+ statusContextCountsByState { count state }
275
+ }
276
+ }
277
+ }
278
+ }
279
+ }
280
+ }`;
281
+ const splitOwnerRepo = (ownerRepo) => {
282
+ const segments = ownerRepo.split("/");
283
+ const [owner, name] = segments;
284
+ if (segments.length !== 2 || !owner || !name) return null;
285
+ return { owner, name };
286
+ };
287
+ const buildPrQuery = (targets) => {
288
+ const byRepo = /* @__PURE__ */ new Map();
289
+ const targetsByAlias = /* @__PURE__ */ new Map();
290
+ targets.forEach((target, index) => {
291
+ if (target.branch === "" || splitOwnerRepo(target.ownerRepo) === null) {
292
+ return;
293
+ }
294
+ const alias = `t${String(index)}`;
295
+ targetsByAlias.set(alias, target);
296
+ const existing = byRepo.get(target.ownerRepo);
297
+ const entry = { alias, branch: target.branch };
298
+ if (existing) existing.push(entry);
299
+ else byRepo.set(target.ownerRepo, [entry]);
300
+ });
301
+ if (targetsByAlias.size === 0) return null;
302
+ const repoBlocks = [...byRepo].map(([ownerRepo, branches], repoIndex) => {
303
+ const { owner, name } = splitOwnerRepo(ownerRepo);
304
+ const branchFields = branches.map(
305
+ ({ alias, branch }) => (
306
+ // Newest first: a branch that had a closed PR and then a new one is
307
+ // described by the new one, and `first: 1` is what keeps the cost flat.
308
+ ` ${alias}: pullRequests(headRefName: ${JSON.stringify(branch)}, first: 1, orderBy: { field: CREATED_AT, direction: DESC }) { nodes { ...prFields } }`
309
+ )
310
+ );
311
+ return [
312
+ ` r${String(repoIndex)}: repository(owner: ${JSON.stringify(owner)}, name: ${JSON.stringify(name)}) {`,
313
+ ...branchFields,
314
+ " }"
315
+ ].join("\n");
316
+ });
317
+ return {
318
+ query: `query {
319
+ ${repoBlocks.join("\n")}
320
+ }
321
+
322
+ ${prFields}`,
323
+ targetsByAlias
324
+ };
325
+ };
326
+ const checkOutcomes = /* @__PURE__ */ new Map([
327
+ ["ACTION_REQUIRED", "failed"],
328
+ ["ERROR", "failed"],
329
+ ["FAILURE", "failed"],
330
+ ["STARTUP_FAILURE", "failed"],
331
+ ["TIMED_OUT", "failed"],
332
+ ["COMPLETED", "passed"],
333
+ ["NEUTRAL", "passed"],
334
+ ["SKIPPED", "passed"],
335
+ ["SUCCESS", "passed"],
336
+ ["CANCELLED", "pending"],
337
+ ["EXPECTED", "pending"],
338
+ ["IN_PROGRESS", "pending"],
339
+ ["PENDING", "pending"],
340
+ ["QUEUED", "pending"],
341
+ ["STALE", "pending"],
342
+ ["WAITING", "pending"]
343
+ ]);
344
+ const countChecks = (pullRequest) => {
345
+ const totals = { failed: 0, passed: 0, pending: 0 };
346
+ const contexts = pullRequest.commits.nodes[0]?.commit.statusCheckRollup?.contexts;
347
+ for (const { count, state } of [
348
+ ...contexts?.checkRunCountsByState ?? [],
349
+ ...contexts?.statusContextCountsByState ?? []
350
+ ]) {
351
+ const outcome = checkOutcomes.get(state);
352
+ if (outcome !== void 0) totals[outcome] += count;
353
+ }
354
+ return {
355
+ checksPassed: totals.passed,
356
+ checksFailed: totals.failed,
357
+ checksPending: totals.pending
358
+ };
359
+ };
360
+ const prStates$1 = /* @__PURE__ */ new Map([
361
+ ["CLOSED", "closed"],
362
+ ["MERGED", "merged"]
363
+ ]);
364
+ const toPrState$1 = (state, isDraft) => {
365
+ if (state === "OPEN") return isDraft ? "draft" : "open";
366
+ return prStates$1.get(state) ?? null;
367
+ };
368
+ const reviewDecisions$1 = /* @__PURE__ */ new Map([
369
+ ["APPROVED", "approved"],
370
+ ["CHANGES_REQUESTED", "changesRequested"],
371
+ ["REVIEW_REQUIRED", "reviewRequired"]
372
+ ]);
373
+ const mergeables$2 = /* @__PURE__ */ new Map([
374
+ ["CONFLICTING", "conflicting"],
375
+ ["MERGEABLE", "mergeable"]
376
+ ]);
377
+ const toFetchedPr = (target, pullRequest) => {
378
+ const state = toPrState$1(pullRequest.state, pullRequest.isDraft);
379
+ if (state === null) return null;
380
+ return {
381
+ target,
382
+ number: pullRequest.number,
383
+ state,
384
+ reviewDecision: pullRequest.reviewDecision === null ? null : reviewDecisions$1.get(pullRequest.reviewDecision) ?? null,
385
+ mergeable: mergeables$2.get(pullRequest.mergeable) ?? "unknown",
386
+ mergedAt: pullRequest.mergedAt,
387
+ headSha: pullRequest.headRefOid,
388
+ baseRef: pullRequest.baseRefName,
389
+ ...countChecks(pullRequest)
390
+ };
391
+ };
392
+ const parsePrQueryResponse = (data, targetsByAlias) => {
393
+ if (data === null) return [];
394
+ const found = [];
395
+ for (const repository of Object.values(data)) {
396
+ if (!repository) continue;
397
+ for (const [alias, connection] of Object.entries(repository)) {
398
+ const target = targetsByAlias.get(alias);
399
+ const pullRequest = connection?.nodes?.[0];
400
+ if (!target || !pullRequest) continue;
401
+ const fetched = toFetchedPr(target, pullRequest);
402
+ if (fetched !== null) found.push(fetched);
403
+ }
404
+ }
405
+ return found;
406
+ };
407
+
408
+ const suggestionsQuery = `query SuggestionsToApply($owner: String!, $name: String!, $number: Int!, $threadIds: [ID!]!, $commentIds: [ID!]!) {
409
+ viewer { login }
410
+ repository(owner: $owner, name: $name) {
411
+ pullRequest(number: $number) {
412
+ id state headRefOid headRefName viewerCanApplySuggestion
413
+ headRepository { nameWithOwner }
414
+ }
415
+ }
416
+ threads: nodes(ids: $threadIds) {
417
+ ... on PullRequestReviewThread {
418
+ id path line startLine diffSide startDiffSide isOutdated isResolved
419
+ pullRequest { id }
420
+ comments(first: 1) { nodes { id } }
421
+ }
422
+ }
423
+ comments: nodes(ids: $commentIds) {
424
+ ... on PullRequestReviewComment {
425
+ id body state
426
+ replyTo { id }
427
+ author { login ... on User { databaseId name } }
428
+ }
429
+ }
430
+ }`;
431
+ const toPrState = (state) => {
432
+ if (state === "MERGED") return "merged";
433
+ if (state === "CLOSED") return "closed";
434
+ return "open";
435
+ };
436
+ const toSide$1 = (side) => {
437
+ if (side === null) return null;
438
+ return side === "LEFT" ? "LEFT" : "RIGHT";
439
+ };
440
+ const planSuggestions = (answer, edit) => {
441
+ const pullRequest = answer.repository?.pullRequest;
442
+ if (pullRequest === null || pullRequest === void 0) {
443
+ throw new Error("the pull request is not found");
444
+ }
445
+ if (pullRequest.headRefOid !== edit.expectedHeadSha) {
446
+ throw new Error(
447
+ `the pull request moved on to ${pullRequest.headRefOid.slice(0, 7)} since ${edit.expectedHeadSha.slice(0, 7)}: read it again before committing`
448
+ );
449
+ }
450
+ if (pullRequest.headRepository === null) {
451
+ throw new Error("the head branch's repository no longer exists");
452
+ }
453
+ const suggestions = [];
454
+ edit.suggestions.forEach((ref, index) => {
455
+ const thread = answer.threads[index];
456
+ const comment = answer.comments[index];
457
+ if (thread?.id !== ref.threadId) {
458
+ throw new Error(`no review thread ${ref.threadId}`);
459
+ }
460
+ if (thread.pullRequest.id !== pullRequest.id) {
461
+ throw new Error(`thread ${ref.threadId} is not of this pull request`);
462
+ }
463
+ if (comment?.id !== ref.commentId) {
464
+ throw new Error(`no review comment ${ref.commentId}`);
465
+ }
466
+ const root = comment.replyTo?.id ?? comment.id;
467
+ if (root !== thread.comments.nodes?.[0]?.id) {
468
+ throw new Error(`comment ${ref.commentId} is not in that thread`);
469
+ }
470
+ if (comment.state === "PENDING") {
471
+ throw new Error("a suggestion in a pending review cannot be committed");
472
+ }
473
+ const parsed = parseSuggestions(comment.body);
474
+ const refusal = suggestionCommitRefusal({
475
+ state: toPrState(pullRequest.state),
476
+ viewerCanApplySuggestion: pullRequest.viewerCanApplySuggestion,
477
+ suggestionCount: parsed.length,
478
+ outdated: thread.isOutdated,
479
+ line: thread.line,
480
+ side: toSide$1(thread.diffSide) ?? "RIGHT",
481
+ startSide: toSide$1(thread.startDiffSide)
482
+ });
483
+ const [lines] = parsed;
484
+ if (refusal !== null || thread.line === null || lines === void 0) {
485
+ throw new Error(`${thread.path}: ${refusal ?? "nothing to commit"}`);
486
+ }
487
+ const planned = {
488
+ threadId: thread.id,
489
+ resolved: thread.isResolved,
490
+ path: thread.path,
491
+ startLine: thread.startLine,
492
+ line: thread.line,
493
+ lines,
494
+ author: comment.author
495
+ };
496
+ if (suggestions.some((other) => suggestionRangesOverlap(other, planned))) {
497
+ throw new Error(
498
+ `${planned.path}: two suggestions of the batch replace line ${String(planned.line)}`
499
+ );
500
+ }
501
+ suggestions.push(planned);
502
+ });
503
+ return {
504
+ repositoryNameWithOwner: pullRequest.headRepository.nameWithOwner,
505
+ branchName: pullRequest.headRefName,
506
+ viewerLogin: answer.viewer.login,
507
+ suggestions
508
+ };
509
+ };
510
+ const applySuggestionsToText = (text, suggestions) => {
511
+ const eol = text.includes("\r\n") ? "\r" : "";
512
+ const lines = text.split("\n");
513
+ const endsWithNewline = lines.at(-1) === "";
514
+ const lineCount = endsWithNewline ? lines.length - 1 : lines.length;
515
+ const bottomFirst = suggestions.toSorted((a, b) => b.line - a.line);
516
+ for (const { startLine, line, lines: replacement } of bottomFirst) {
517
+ if (line > lineCount) {
518
+ throw new Error(
519
+ `line ${String(line)} is past the end of the file, ${String(lineCount)} lines long`
520
+ );
521
+ }
522
+ const first = startLine ?? line;
523
+ const isLastLine = line === lineCount && !endsWithNewline;
524
+ const put = replacement.map(
525
+ (content, index) => isLastLine && index === replacement.length - 1 ? content : content + eol
526
+ );
527
+ lines.splice(first - 1, line - first + 1, ...put);
528
+ }
529
+ return lines.join("\n");
530
+ };
531
+ const coAuthorTrailers = (authors, viewerLogin) => {
532
+ const seen = /* @__PURE__ */ new Set([viewerLogin]);
533
+ const trailers = [];
534
+ for (const author of authors) {
535
+ if (author?.databaseId === void 0 || seen.has(author.login)) continue;
536
+ seen.add(author.login);
537
+ const name = author.name === null || author.name === void 0 || author.name === "" ? author.login : author.name;
538
+ trailers.push(
539
+ `Co-authored-by: ${name} <${String(author.databaseId)}+${author.login}@users.noreply.github.com>`
540
+ );
541
+ }
542
+ return trailers;
543
+ };
544
+ const fileQuery = (count) => {
545
+ const indexes = Array.from({ length: count }, (_, index) => index);
546
+ const variables = indexes.map((index) => `$e${String(index)}: String!`).join(", ");
547
+ const fields = indexes.map(
548
+ (index) => `f${String(index)}: object(expression: $e${String(index)}) { ... on Blob { text isBinary isTruncated } }`
549
+ ).join("\n ");
550
+ return `query SuggestedFiles($owner: String!, $name: String!, ${variables}) {
551
+ repository(owner: $owner, name: $name) {
552
+ ${fields}
553
+ }
554
+ }`;
555
+ };
556
+ const readFiles = async (transport, nameWithOwner, sha, paths) => {
557
+ const split = splitOwnerRepo(nameWithOwner);
558
+ if (split === null) throw new Error(`${nameWithOwner} is not owner/repo`);
559
+ const variables = Object.fromEntries(
560
+ paths.map((path, index) => [`e${String(index)}`, `${sha}:${path}`])
561
+ );
562
+ const { data, errors } = await transport.graphql(fileQuery(paths.length), { ...split, ...variables });
563
+ if (errors.length > 0) {
564
+ throw new Error(`GitHub refused reading the files: ${errors.join("; ")}`);
565
+ }
566
+ const files = /* @__PURE__ */ new Map();
567
+ paths.forEach((path, index) => {
568
+ const blob = data?.repository?.[`f${String(index)}`];
569
+ const text = blob?.text;
570
+ if (text === null || text === void 0 || blob?.isBinary === true || blob?.isTruncated === true) {
571
+ throw new Error(`${path} cannot be read as text at ${sha.slice(0, 7)}`);
572
+ }
573
+ files.set(path, text);
574
+ });
575
+ return files;
576
+ };
577
+ const createCommitMutation = `mutation CommitSuggestions($input: CreateCommitOnBranchInput!) {
578
+ createCommitOnBranch(input: $input) { commit { oid } }
579
+ }`;
580
+ const toBase64 = (text) => {
581
+ let binary = "";
582
+ for (const byte of new TextEncoder().encode(text)) {
583
+ binary += String.fromCodePoint(byte);
584
+ }
585
+ return btoa(binary);
586
+ };
587
+ const messageBody = (body, trailers) => [body.trim(), trailers.join("\n")].filter((part) => part !== "").join("\n\n");
588
+ const commitSuggestions = async (transport, {
589
+ ownerRepo,
590
+ number,
591
+ edit
592
+ }) => {
593
+ const split = splitOwnerRepo(ownerRepo);
594
+ if (split === null) throw new Error(`${ownerRepo} is not owner/repo`);
595
+ const lookup = await transport.graphql(suggestionsQuery, {
596
+ ...split,
597
+ number,
598
+ threadIds: edit.suggestions.map(({ threadId }) => threadId),
599
+ commentIds: edit.suggestions.map(({ commentId }) => commentId)
600
+ });
601
+ if (lookup.data === null) {
602
+ throw new Error(
603
+ `GitHub refused reading the suggestions: ${lookup.errors.join("; ") || "no answer"}`
604
+ );
605
+ }
606
+ const plan = planSuggestions(lookup.data, edit);
607
+ const paths = [...new Set(plan.suggestions.map(({ path }) => path))];
608
+ const files = await readFiles(
609
+ transport,
610
+ plan.repositoryNameWithOwner,
611
+ edit.expectedHeadSha,
612
+ paths
613
+ );
614
+ const additions = paths.map((path) => {
615
+ const text = applySuggestionsToText(
616
+ files.get(path) ?? "",
617
+ plan.suggestions.filter((suggestion) => suggestion.path === path)
618
+ );
619
+ return { path, contents: toBase64(text) };
620
+ });
621
+ const { errors } = await transport.graphql(createCommitMutation, {
622
+ input: {
623
+ branch: {
624
+ repositoryNameWithOwner: plan.repositoryNameWithOwner,
625
+ branchName: plan.branchName
626
+ },
627
+ expectedHeadOid: edit.expectedHeadSha,
628
+ message: {
629
+ headline: edit.headline,
630
+ body: messageBody(
631
+ edit.body,
632
+ coAuthorTrailers(
633
+ plan.suggestions.map(({ author }) => author),
634
+ plan.viewerLogin
635
+ )
636
+ )
637
+ },
638
+ fileChanges: { additions }
639
+ }
640
+ });
641
+ if (errors.length > 0) {
642
+ throw new Error(`GitHub refused the commit: ${errors.join("; ")}`);
643
+ }
644
+ return plan.suggestions.filter(({ resolved }) => !resolved).map(({ threadId }) => threadId);
645
+ };
646
+
647
+ const pullRequestReactionContents = [
648
+ "thumbsUp",
649
+ "thumbsDown",
650
+ "laugh",
651
+ "hooray",
652
+ "confused",
653
+ "heart",
654
+ "rocket",
655
+ "eyes"
656
+ ];
657
+ const isPullRequestReactionContent = (value) => pullRequestReactionContents.includes(value);
658
+
659
+ const isStringArray = (value) => Array.isArray(value) && value.every((item) => typeof item === "string");
660
+ const isNonEmptyString = (value) => typeof value === "string" && value.trim() !== "";
661
+ const isPositiveInteger = (value) => typeof value === "number" && Number.isInteger(value) && value > 0;
662
+ const isSha = (value) => typeof value === "string" && /^[\da-f]{40}$/i.test(value);
663
+ const isSide = (value) => value === "LEFT" || value === "RIGHT";
664
+ const mergeMethods = ["merge", "rebase", "squash"];
665
+ const reviewEvents = [
666
+ "approve",
667
+ "comment",
668
+ "requestChanges"
669
+ ];
670
+ const editCommentTargets = ["issueComment", "review", "reviewComment"];
671
+ const parseMergeCommitMessage = (value) => {
672
+ if (typeof value !== "object" || value === null) {
673
+ return "a commit message must be an object";
674
+ }
675
+ const message = value;
676
+ if (!isNonEmptyString(message.title)) return "a commit needs a title";
677
+ if (typeof message.body !== "string") return "a commit body must be a string";
678
+ return { title: message.title, body: message.body };
679
+ };
680
+ const parseDraftComment = (value) => {
681
+ if (typeof value !== "object" || value === null) {
682
+ return "a comment must be an object";
683
+ }
684
+ const comment = value;
685
+ if (!isNonEmptyString(comment.path)) return "a comment needs a path";
686
+ if (!isNonEmptyString(comment.body)) return "a comment cannot be empty";
687
+ if (!isSide(comment.side)) return "a comment side must be LEFT or RIGHT";
688
+ if (!isPositiveInteger(comment.line)) {
689
+ return "a comment line must be a positive integer";
690
+ }
691
+ const startLine = comment.startLine ?? null;
692
+ const startSide = comment.startSide ?? null;
693
+ if (startLine === null) {
694
+ if (startSide !== null) return "startSide needs a startLine";
695
+ } else {
696
+ if (!isPositiveInteger(startLine)) {
697
+ return "startLine must be a positive integer";
698
+ }
699
+ if (startLine > comment.line) return "startLine cannot follow line";
700
+ if (!isSide(startSide)) return "a range needs startSide, LEFT or RIGHT";
701
+ }
702
+ return {
703
+ path: comment.path,
704
+ side: comment.side,
705
+ line: comment.line,
706
+ startLine,
707
+ startSide,
708
+ body: comment.body
709
+ };
710
+ };
711
+ const parseReview = (edit) => {
712
+ if (!reviewEvents.includes(edit.event)) {
713
+ return "a review event must be approve, comment or requestChanges";
714
+ }
715
+ const event = edit.event;
716
+ if (typeof edit.body !== "string") return "a review body must be a string";
717
+ if (!isSha(edit.commitId)) return "a review needs the head commit sha";
718
+ if (!Array.isArray(edit.comments)) return "a review takes a comments array";
719
+ const comments = [];
720
+ for (const raw of edit.comments) {
721
+ const parsed = parseDraftComment(raw);
722
+ if (typeof parsed === "string") return parsed;
723
+ comments.push(parsed);
724
+ }
725
+ if (event === "requestChanges" && edit.body.trim() === "") {
726
+ return "requesting changes needs a body";
727
+ }
728
+ if (event === "comment" && edit.body.trim() === "" && comments.length === 0) {
729
+ return "a comment review needs a body or a comment";
730
+ }
731
+ return {
732
+ kind: "review",
733
+ event,
734
+ body: edit.body,
735
+ commitId: edit.commitId,
736
+ comments
737
+ };
738
+ };
739
+ const parseSuggestionRef = (value) => {
740
+ if (typeof value !== "object" || value === null) {
741
+ return "a suggestion must be an object";
742
+ }
743
+ const ref = value;
744
+ if (!isNonEmptyString(ref.threadId)) return "a suggestion needs its threadId";
745
+ if (!isNonEmptyString(ref.commentId)) {
746
+ return "a suggestion needs its commentId";
747
+ }
748
+ return { threadId: ref.threadId, commentId: ref.commentId };
749
+ };
750
+ const parseApplySuggestions = (edit) => {
751
+ if (!isSha(edit.expectedHeadSha)) {
752
+ return "committing suggestions needs the head sha the screen read";
753
+ }
754
+ if (!Array.isArray(edit.suggestions) || edit.suggestions.length === 0) {
755
+ return "suggestions must be a non-empty array";
756
+ }
757
+ const suggestions = [];
758
+ for (const raw of edit.suggestions) {
759
+ const parsed = parseSuggestionRef(raw);
760
+ if (typeof parsed === "string") return parsed;
761
+ suggestions.push(parsed);
762
+ }
763
+ if (!isNonEmptyString(edit.headline)) return "a commit needs a headline";
764
+ if (typeof edit.body !== "string") return "a commit body must be a string";
765
+ return {
766
+ kind: "applySuggestions",
767
+ expectedHeadSha: edit.expectedHeadSha,
768
+ suggestions,
769
+ headline: edit.headline.trim(),
770
+ body: edit.body
771
+ };
772
+ };
773
+ const parsePullRequestEdit = (value) => {
774
+ if (typeof value !== "object" || value === null) return "edit is required";
775
+ const edit = value;
776
+ switch (edit.kind) {
777
+ case "title": {
778
+ if (typeof edit.title !== "string" || edit.title.trim() === "") {
779
+ return "a title cannot be empty";
780
+ }
781
+ return { kind: "title", title: edit.title.trim() };
782
+ }
783
+ case "labels": {
784
+ if (!isStringArray(edit.add) || !isStringArray(edit.remove)) {
785
+ return "labels take add and remove, arrays of names";
786
+ }
787
+ return { kind: "labels", add: edit.add, remove: edit.remove };
788
+ }
789
+ case "reviewers": {
790
+ if (!isStringArray(edit.add) || !isStringArray(edit.remove) || !isStringArray(edit.addTeams) || !isStringArray(edit.removeTeams)) {
791
+ return "reviewers take add, remove, addTeams and removeTeams, arrays of logins and team slugs";
792
+ }
793
+ return {
794
+ kind: "reviewers",
795
+ add: edit.add,
796
+ remove: edit.remove,
797
+ addTeams: edit.addTeams,
798
+ removeTeams: edit.removeTeams
799
+ };
800
+ }
801
+ case "draft": {
802
+ if (typeof edit.draft !== "boolean") return "draft must be a boolean";
803
+ return { kind: "draft", draft: edit.draft };
804
+ }
805
+ case "assignees": {
806
+ if (!isStringArray(edit.add) || !isStringArray(edit.remove)) {
807
+ return "assignees take add and remove, arrays of logins";
808
+ }
809
+ return { kind: "assignees", add: edit.add, remove: edit.remove };
810
+ }
811
+ case "body": {
812
+ if (typeof edit.body !== "string") return "body must be a string";
813
+ return { kind: "body", body: edit.body };
814
+ }
815
+ case "state": {
816
+ if (edit.state !== "closed" && edit.state !== "open") {
817
+ return "state must be closed or open";
818
+ }
819
+ return { kind: "state", state: edit.state };
820
+ }
821
+ case "merge": {
822
+ if (!mergeMethods.includes(edit.method)) {
823
+ return "a merge method must be merge, rebase or squash";
824
+ }
825
+ if (!isSha(edit.expectedHeadSha)) {
826
+ return "a merge needs the head sha the screen read";
827
+ }
828
+ const merge = {
829
+ kind: "merge",
830
+ method: edit.method,
831
+ expectedHeadSha: edit.expectedHeadSha
832
+ };
833
+ if (edit.commitMessage === void 0) return merge;
834
+ if (merge.method === "rebase") return "a rebase writes no commit message";
835
+ const commitMessage = parseMergeCommitMessage(edit.commitMessage);
836
+ if (typeof commitMessage === "string") return commitMessage;
837
+ return { ...merge, commitMessage };
838
+ }
839
+ case "updateBranch": {
840
+ if (!isSha(edit.expectedHeadSha)) {
841
+ return "updating the branch needs the head sha the screen read";
842
+ }
843
+ return { kind: "updateBranch", expectedHeadSha: edit.expectedHeadSha };
844
+ }
845
+ case "comment": {
846
+ if (!isNonEmptyString(edit.body)) return "a comment cannot be empty";
847
+ return { kind: "comment", body: edit.body };
848
+ }
849
+ case "editComment": {
850
+ if (!editCommentTargets.includes(
851
+ edit.target
852
+ )) {
853
+ return "editComment target must be issueComment, review or reviewComment";
854
+ }
855
+ if (!isPositiveInteger(edit.databaseId)) {
856
+ return "editComment needs the comment's databaseId";
857
+ }
858
+ if (!isNonEmptyString(edit.body)) return "a comment cannot be empty";
859
+ return {
860
+ kind: "editComment",
861
+ target: edit.target,
862
+ databaseId: edit.databaseId,
863
+ body: edit.body
864
+ };
865
+ }
866
+ case "reply": {
867
+ if (!isPositiveInteger(edit.inReplyTo)) {
868
+ return "a reply needs the databaseId of a comment of the thread";
869
+ }
870
+ if (!isNonEmptyString(edit.body)) return "a reply cannot be empty";
871
+ return { kind: "reply", inReplyTo: edit.inReplyTo, body: edit.body };
872
+ }
873
+ case "reaction": {
874
+ if (!isNonEmptyString(edit.subjectId)) {
875
+ return "a reaction needs the id of what it reacts to";
876
+ }
877
+ if (!isPullRequestReactionContent(edit.content)) {
878
+ return "unknown reaction content";
879
+ }
880
+ if (typeof edit.reacted !== "boolean") return "reacted must be a boolean";
881
+ return {
882
+ kind: "reaction",
883
+ subjectId: edit.subjectId,
884
+ content: edit.content,
885
+ reacted: edit.reacted
886
+ };
887
+ }
888
+ case "resolveThread": {
889
+ if (!isNonEmptyString(edit.threadId)) return "a thread id is required";
890
+ if (typeof edit.resolved !== "boolean") {
891
+ return "resolved must be a boolean";
892
+ }
893
+ return {
894
+ kind: "resolveThread",
895
+ threadId: edit.threadId,
896
+ resolved: edit.resolved
897
+ };
898
+ }
899
+ case "viewed": {
900
+ if (!isNonEmptyString(edit.path)) return "a file path is required";
901
+ if (typeof edit.viewed !== "boolean") return "viewed must be a boolean";
902
+ return { kind: "viewed", path: edit.path, viewed: edit.viewed };
903
+ }
904
+ case "review": {
905
+ return parseReview(edit);
906
+ }
907
+ case "applySuggestions": {
908
+ return parseApplySuggestions(edit);
909
+ }
910
+ case "inlineComment": {
911
+ if (!isSha(edit.commitId)) return "a comment needs the head commit sha";
912
+ const comment = parseDraftComment(edit.comment);
913
+ if (typeof comment === "string") return comment;
914
+ return { kind: "inlineComment", commitId: edit.commitId, comment };
915
+ }
916
+ default: {
917
+ return "unknown edit kind";
918
+ }
919
+ }
920
+ };
921
+ const pullRequestIdQuery = `query PullRequestId($owner: String!, $name: String!, $number: Int!) {
922
+ repository(owner: $owner, name: $name) { pullRequest(number: $number) { id } }
923
+ }`;
924
+ const readyForReviewMutation = `mutation ReadyForReview($id: ID!) {
925
+ markPullRequestReadyForReview(input: { pullRequestId: $id }) { pullRequest { isDraft } }
926
+ }`;
927
+ const convertToDraftMutation = `mutation ConvertToDraft($id: ID!) {
928
+ convertPullRequestToDraft(input: { pullRequestId: $id }) { pullRequest { isDraft } }
929
+ }`;
930
+ const resolveThreadMutation = `mutation ResolveThread($threadId: ID!) {
931
+ resolveReviewThread(input: { threadId: $threadId }) { thread { isResolved } }
932
+ }`;
933
+ const unresolveThreadMutation = `mutation UnresolveThread($threadId: ID!) {
934
+ unresolveReviewThread(input: { threadId: $threadId }) { thread { isResolved } }
935
+ }`;
936
+ const addReactionMutation = `mutation AddReaction($subjectId: ID!, $content: ReactionContent!) {
937
+ addReaction(input: { subjectId: $subjectId, content: $content }) { reaction { content } }
938
+ }`;
939
+ const removeReactionMutation = `mutation RemoveReaction($subjectId: ID!, $content: ReactionContent!) {
940
+ removeReaction(input: { subjectId: $subjectId, content: $content }) { reaction { content } }
941
+ }`;
942
+ const reactionContentNames = {
943
+ confused: "CONFUSED",
944
+ eyes: "EYES",
945
+ heart: "HEART",
946
+ hooray: "HOORAY",
947
+ laugh: "LAUGH",
948
+ rocket: "ROCKET",
949
+ thumbsDown: "THUMBS_DOWN",
950
+ thumbsUp: "THUMBS_UP"
951
+ };
952
+ const markViewedMutation = `mutation MarkViewed($id: ID!, $path: String!) {
953
+ markFileAsViewed(input: { pullRequestId: $id, path: $path }) { clientMutationId }
954
+ }`;
955
+ const unmarkViewedMutation = `mutation UnmarkViewed($id: ID!, $path: String!) {
956
+ unmarkFileAsViewed(input: { pullRequestId: $id, path: $path }) { clientMutationId }
957
+ }`;
958
+ const lookupPullRequestId = async (transport, { owner, name, number }) => {
959
+ const lookup = await transport.graphql(pullRequestIdQuery, { owner, name, number });
960
+ const id = lookup.data?.repository?.pullRequest?.id;
961
+ if (id === void 0) {
962
+ throw new Error(
963
+ `no pull request #${String(number)} in ${owner}/${name}: ${lookup.errors.join("; ") || "not found"}`
964
+ );
965
+ }
966
+ return id;
967
+ };
968
+ const pullRequestHeadQuery$1 = `query PullRequestHead($owner: String!, $name: String!, $number: Int!) {
969
+ repository(owner: $owner, name: $name) { pullRequest(number: $number) { headRefOid } }
970
+ }`;
971
+ const assertVerdictOnHead = async (transport, {
972
+ owner,
973
+ name,
974
+ number,
975
+ commitId
976
+ }) => {
977
+ const lookup = await transport.graphql(pullRequestHeadQuery$1, { owner, name, number });
978
+ const head = lookup.data?.repository?.pullRequest?.headRefOid;
979
+ if (head === void 0) {
980
+ throw new Error(
981
+ `no pull request #${String(number)} in ${owner}/${name}: ${lookup.errors.join("; ") || "not found"}`
982
+ );
983
+ }
984
+ if (head !== commitId) {
985
+ throw new Error(
986
+ `the pull request moved on to ${head.slice(0, 7)} since ${commitId.slice(0, 7)}, the commit reviewed: only a comment review can be sent on it`
987
+ );
988
+ }
989
+ };
990
+ const runMutation = async (transport, query, variables, what) => {
991
+ const { errors } = await transport.graphql(query, variables);
992
+ if (errors.length > 0) {
993
+ throw new Error(`GitHub refused ${what}: ${errors.join("; ")}`);
994
+ }
995
+ };
996
+ const setDraft = async (transport, { owner, name, number, draft }) => {
997
+ const id = await lookupPullRequestId(transport, { owner, name, number });
998
+ await runMutation(
999
+ transport,
1000
+ draft ? convertToDraftMutation : readyForReviewMutation,
1001
+ { id },
1002
+ "the draft change"
1003
+ );
1004
+ };
1005
+ const setViewed = async (transport, {
1006
+ owner,
1007
+ name,
1008
+ number,
1009
+ path,
1010
+ viewed
1011
+ }) => {
1012
+ const id = await lookupPullRequestId(transport, { owner, name, number });
1013
+ await runMutation(
1014
+ transport,
1015
+ viewed ? markViewedMutation : unmarkViewedMutation,
1016
+ { id, path },
1017
+ "the viewed mark"
1018
+ );
1019
+ };
1020
+ const restComment = (comment) => ({
1021
+ path: comment.path,
1022
+ body: comment.body,
1023
+ line: comment.line,
1024
+ side: comment.side,
1025
+ ...comment.startLine === null || comment.startSide === null ? {} : {
1026
+ // eslint-disable-next-line camelcase -- GitHub's field name
1027
+ start_line: comment.startLine,
1028
+ // eslint-disable-next-line camelcase -- GitHub's field name
1029
+ start_side: comment.startSide
1030
+ }
1031
+ });
1032
+ const reviewEventNames = {
1033
+ approve: "APPROVE",
1034
+ comment: "COMMENT",
1035
+ requestChanges: "REQUEST_CHANGES"
1036
+ };
1037
+ const resolveCommittedThreads = async (transport, threadIds, logger) => {
1038
+ const failures = [];
1039
+ for (const threadId of threadIds) {
1040
+ try {
1041
+ await runMutation(
1042
+ transport,
1043
+ resolveThreadMutation,
1044
+ { threadId },
1045
+ "resolving the thread"
1046
+ );
1047
+ } catch (error) {
1048
+ logger?.warn("thread of a committed suggestion left unresolved", {
1049
+ threadId,
1050
+ error
1051
+ });
1052
+ failures.push(error instanceof Error ? error.message : String(error));
1053
+ }
1054
+ }
1055
+ if (failures.length === 0) return null;
1056
+ return `The suggestions were committed, but ${String(failures.length)} thread${failures.length === 1 ? "" : "s"} stayed unresolved: ${failures.join("; ")}`;
1057
+ };
1058
+ const editPullRequest = async (transport, { ownerRepo, number, edit }, options = {}) => {
1059
+ const split = splitOwnerRepo(ownerRepo);
1060
+ if (split === null) throw new Error(`${ownerRepo} is not owner/repo`);
1061
+ const repoPath = `repos/${encodeURIComponent(split.owner)}/${encodeURIComponent(split.name)}`;
1062
+ const pullPath = `${repoPath}/pulls/${String(number)}`;
1063
+ const issuePath = `${repoPath}/issues/${String(number)}`;
1064
+ switch (edit.kind) {
1065
+ case "title": {
1066
+ await transport.rest({
1067
+ method: "PATCH",
1068
+ path: pullPath,
1069
+ body: { title: edit.title }
1070
+ });
1071
+ return null;
1072
+ }
1073
+ case "labels": {
1074
+ if (edit.add.length > 0) {
1075
+ await transport.rest({
1076
+ method: "POST",
1077
+ path: `${issuePath}/labels`,
1078
+ body: { labels: edit.add }
1079
+ });
1080
+ }
1081
+ for (const label of edit.remove) {
1082
+ await transport.rest({
1083
+ method: "DELETE",
1084
+ path: `${issuePath}/labels/${encodeURIComponent(label)}`
1085
+ });
1086
+ }
1087
+ return null;
1088
+ }
1089
+ case "reviewers": {
1090
+ if (edit.add.length > 0 || edit.addTeams.length > 0) {
1091
+ await transport.rest({
1092
+ method: "POST",
1093
+ path: `${pullPath}/requested_reviewers`,
1094
+ // eslint-disable-next-line camelcase -- GitHub's field name
1095
+ body: { reviewers: edit.add, team_reviewers: edit.addTeams }
1096
+ });
1097
+ }
1098
+ if (edit.remove.length > 0 || edit.removeTeams.length > 0) {
1099
+ await transport.rest({
1100
+ method: "DELETE",
1101
+ path: `${pullPath}/requested_reviewers`,
1102
+ // eslint-disable-next-line camelcase -- GitHub's field name
1103
+ body: { reviewers: edit.remove, team_reviewers: edit.removeTeams }
1104
+ });
1105
+ }
1106
+ return null;
1107
+ }
1108
+ case "draft": {
1109
+ await setDraft(transport, { ...split, number, draft: edit.draft });
1110
+ return null;
1111
+ }
1112
+ case "body": {
1113
+ await transport.rest({
1114
+ method: "PATCH",
1115
+ path: pullPath,
1116
+ body: { body: edit.body }
1117
+ });
1118
+ return null;
1119
+ }
1120
+ case "assignees": {
1121
+ if (edit.add.length > 0) {
1122
+ await transport.rest({
1123
+ method: "POST",
1124
+ path: `${issuePath}/assignees`,
1125
+ body: { assignees: edit.add }
1126
+ });
1127
+ }
1128
+ if (edit.remove.length > 0) {
1129
+ await transport.rest({
1130
+ method: "DELETE",
1131
+ path: `${issuePath}/assignees`,
1132
+ body: { assignees: edit.remove }
1133
+ });
1134
+ }
1135
+ return null;
1136
+ }
1137
+ case "state": {
1138
+ await transport.rest({
1139
+ method: "PATCH",
1140
+ path: pullPath,
1141
+ body: { state: edit.state }
1142
+ });
1143
+ return null;
1144
+ }
1145
+ case "merge": {
1146
+ await transport.rest({
1147
+ method: "PUT",
1148
+ path: `${pullPath}/merge`,
1149
+ body: {
1150
+ // eslint-disable-next-line camelcase -- GitHub's field name
1151
+ merge_method: edit.method,
1152
+ sha: edit.expectedHeadSha,
1153
+ ...edit.commitMessage === void 0 ? {} : {
1154
+ // eslint-disable-next-line camelcase -- GitHub's field name
1155
+ commit_title: edit.commitMessage.title,
1156
+ // eslint-disable-next-line camelcase -- GitHub's field name
1157
+ commit_message: edit.commitMessage.body
1158
+ }
1159
+ }
1160
+ });
1161
+ return null;
1162
+ }
1163
+ case "updateBranch": {
1164
+ await transport.rest({
1165
+ method: "PUT",
1166
+ path: `${pullPath}/update-branch`,
1167
+ // eslint-disable-next-line camelcase -- GitHub's field name
1168
+ body: { expected_head_sha: edit.expectedHeadSha }
1169
+ });
1170
+ return null;
1171
+ }
1172
+ case "comment": {
1173
+ await transport.rest({
1174
+ method: "POST",
1175
+ path: `${issuePath}/comments`,
1176
+ body: { body: edit.body }
1177
+ });
1178
+ return null;
1179
+ }
1180
+ case "editComment": {
1181
+ const id = String(edit.databaseId);
1182
+ if (edit.target === "review") {
1183
+ await transport.rest({
1184
+ method: "PUT",
1185
+ path: `${pullPath}/reviews/${id}`,
1186
+ body: { body: edit.body }
1187
+ });
1188
+ return null;
1189
+ }
1190
+ await transport.rest({
1191
+ method: "PATCH",
1192
+ path: edit.target === "issueComment" ? `${repoPath}/issues/comments/${id}` : `${repoPath}/pulls/comments/${id}`,
1193
+ body: { body: edit.body }
1194
+ });
1195
+ return null;
1196
+ }
1197
+ case "reply": {
1198
+ await transport.rest({
1199
+ method: "POST",
1200
+ path: `${pullPath}/comments/${String(edit.inReplyTo)}/replies`,
1201
+ body: { body: edit.body }
1202
+ });
1203
+ return null;
1204
+ }
1205
+ case "reaction": {
1206
+ await runMutation(
1207
+ transport,
1208
+ edit.reacted ? addReactionMutation : removeReactionMutation,
1209
+ {
1210
+ subjectId: edit.subjectId,
1211
+ content: reactionContentNames[edit.content]
1212
+ },
1213
+ "the reaction"
1214
+ );
1215
+ return null;
1216
+ }
1217
+ case "resolveThread": {
1218
+ await runMutation(
1219
+ transport,
1220
+ edit.resolved ? resolveThreadMutation : unresolveThreadMutation,
1221
+ { threadId: edit.threadId },
1222
+ "the thread change"
1223
+ );
1224
+ return null;
1225
+ }
1226
+ case "viewed": {
1227
+ await setViewed(transport, {
1228
+ ...split,
1229
+ number,
1230
+ path: edit.path,
1231
+ viewed: edit.viewed
1232
+ });
1233
+ return null;
1234
+ }
1235
+ case "review": {
1236
+ if (edit.event !== "comment") {
1237
+ await assertVerdictOnHead(transport, {
1238
+ ...split,
1239
+ number,
1240
+ commitId: edit.commitId
1241
+ });
1242
+ }
1243
+ await transport.rest({
1244
+ method: "POST",
1245
+ path: `${pullPath}/reviews`,
1246
+ body: {
1247
+ // eslint-disable-next-line camelcase -- GitHub's field name
1248
+ commit_id: edit.commitId,
1249
+ ...edit.body === "" ? {} : { body: edit.body },
1250
+ event: reviewEventNames[edit.event],
1251
+ comments: edit.comments.map(restComment)
1252
+ }
1253
+ });
1254
+ return null;
1255
+ }
1256
+ case "applySuggestions": {
1257
+ return resolveCommittedThreads(
1258
+ transport,
1259
+ await commitSuggestions(transport, { ownerRepo, number, edit }),
1260
+ options.logger
1261
+ );
1262
+ }
1263
+ case "inlineComment": {
1264
+ await transport.rest({
1265
+ method: "POST",
1266
+ path: `${pullPath}/comments`,
1267
+ // eslint-disable-next-line camelcase -- GitHub's field name
1268
+ body: { commit_id: edit.commitId, ...restComment(edit.comment) }
1269
+ });
1270
+ return null;
1271
+ }
1272
+ default: {
1273
+ throw new Error("unknown edit");
1274
+ }
1275
+ }
1276
+ };
1277
+ const applyPullRequestEdit = async (transport, request, options = {}) => {
1278
+ const followUpError = await editPullRequest(transport, request, options);
1279
+ const { edit } = request;
1280
+ if (edit.kind !== "viewed") return followUpError;
1281
+ try {
1282
+ options.onFileViewed?.(edit.path, edit.viewed);
1283
+ } catch (error) {
1284
+ options.logger?.error("viewed mark not applied to the last read", {
1285
+ error
1286
+ });
1287
+ }
1288
+ return followUpError;
1289
+ };
1290
+
1291
+ const fetchCompareDiff = (transport, { ownerRepo, base, head }) => {
1292
+ const [owner, name, ...rest] = ownerRepo.split("/");
1293
+ if (owner === void 0 || name === void 0 || rest.length > 0) {
1294
+ throw new Error(`${ownerRepo} is not owner/repo`);
1295
+ }
1296
+ return transport.text({
1297
+ path: `repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/compare/${base}...${head}`,
1298
+ accept: "application/vnd.github.diff"
1299
+ });
1300
+ };
1301
+
1302
+ const maxDiffCells = 16e4;
1303
+ const toLines = (value) => {
1304
+ if (value === "") return [];
1305
+ const lines = value.split("\n");
1306
+ if (lines.at(-1) === "") lines.pop();
1307
+ return lines;
1308
+ };
1309
+ const context = (text) => ({ kind: "context", text });
1310
+ const removed = (text) => ({ kind: "removed", text });
1311
+ const added = (text) => ({ kind: "added", text });
1312
+ const wholeSides = (before, after) => [
1313
+ ...before.map(removed),
1314
+ ...after.map(added)
1315
+ ];
1316
+ const shortestEdit = (before, after) => {
1317
+ const height = before.length;
1318
+ const width = after.length + 1;
1319
+ const common = new Uint32Array((height + 1) * width);
1320
+ for (let row2 = height - 1; row2 >= 0; row2--) {
1321
+ const beforeLine = before[row2];
1322
+ for (let column2 = after.length - 1; column2 >= 0; column2--) {
1323
+ common[row2 * width + column2] = beforeLine === after[column2] ? (common[(row2 + 1) * width + column2 + 1] ?? 0) + 1 : Math.max(
1324
+ common[(row2 + 1) * width + column2] ?? 0,
1325
+ common[row2 * width + column2 + 1] ?? 0
1326
+ );
1327
+ }
1328
+ }
1329
+ const lines = [];
1330
+ let row = 0;
1331
+ let column = 0;
1332
+ while (row < height && column < after.length) {
1333
+ if (before[row] === after[column]) {
1334
+ lines.push(context(before[row] ?? ""));
1335
+ row++;
1336
+ column++;
1337
+ continue;
1338
+ }
1339
+ if ((common[(row + 1) * width + column] ?? 0) >= (common[row * width + column + 1] ?? 0)) {
1340
+ lines.push(removed(before[row] ?? ""));
1341
+ row++;
1342
+ continue;
1343
+ }
1344
+ lines.push(added(after[column] ?? ""));
1345
+ column++;
1346
+ }
1347
+ return [
1348
+ ...lines,
1349
+ ...before.slice(row).map(removed),
1350
+ ...after.slice(column).map(added)
1351
+ ];
1352
+ };
1353
+ const diffLines = ({
1354
+ before,
1355
+ after
1356
+ }) => {
1357
+ const beforeLines = toLines(before);
1358
+ const afterLines = toLines(after);
1359
+ let head = 0;
1360
+ while (head < beforeLines.length && head < afterLines.length && beforeLines[head] === afterLines[head]) {
1361
+ head++;
1362
+ }
1363
+ let tail = 0;
1364
+ while (tail < beforeLines.length - head && tail < afterLines.length - head && beforeLines[beforeLines.length - 1 - tail] === afterLines[afterLines.length - 1 - tail]) {
1365
+ tail++;
1366
+ }
1367
+ const beforeMiddle = beforeLines.slice(head, beforeLines.length - tail);
1368
+ const afterMiddle = afterLines.slice(head, afterLines.length - tail);
1369
+ return [
1370
+ ...beforeLines.slice(0, head).map(context),
1371
+ ...beforeMiddle.length * afterMiddle.length > maxDiffCells ? wholeSides(beforeMiddle, afterMiddle) : shortestEdit(beforeMiddle, afterMiddle),
1372
+ ...beforeLines.slice(beforeLines.length - tail).map(context)
1373
+ ];
1374
+ };
1375
+
1376
+ const imgTag = /<img\b[^>]*>/gi;
1377
+ const decodeAttribute = (value) => value.replaceAll("&quot;", '"').replaceAll("&#39;", "'").replaceAll("&lt;", "<").replaceAll("&gt;", ">").replaceAll("&amp;", "&");
1378
+ const attribute = (tag, name) => {
1379
+ const match = new RegExp(`\\s${name}="([^"]*)"`, "i").exec(tag);
1380
+ return match?.[1] === void 0 ? null : decodeAttribute(match[1]);
1381
+ };
1382
+ const imageProxiesOf = (html) => {
1383
+ const proxies = {};
1384
+ for (const [tag] of (html ?? "").matchAll(imgTag)) {
1385
+ const src = attribute(tag, "src");
1386
+ const canonical = attribute(tag, "data-canonical-src");
1387
+ if (src !== null && canonical !== null && src !== canonical) {
1388
+ proxies[canonical] = src;
1389
+ }
1390
+ }
1391
+ return proxies;
1392
+ };
1393
+ const mergeImageProxies = (htmls) => Object.assign({}, ...htmls.map(imageProxiesOf));
1394
+
1395
+ const timelineItemTypes = [
1396
+ "ISSUE_COMMENT",
1397
+ "PULL_REQUEST_REVIEW",
1398
+ "PULL_REQUEST_COMMIT",
1399
+ "ASSIGNED_EVENT",
1400
+ "UNASSIGNED_EVENT",
1401
+ "LABELED_EVENT",
1402
+ "UNLABELED_EVENT",
1403
+ "REVIEW_REQUESTED_EVENT",
1404
+ "REVIEW_REQUEST_REMOVED_EVENT",
1405
+ "CLOSED_EVENT",
1406
+ "REOPENED_EVENT",
1407
+ "MERGED_EVENT",
1408
+ "READY_FOR_REVIEW_EVENT",
1409
+ "CONVERT_TO_DRAFT_EVENT",
1410
+ "HEAD_REF_FORCE_PUSHED_EVENT",
1411
+ "BASE_REF_CHANGED_EVENT",
1412
+ "RENAMED_TITLE_EVENT",
1413
+ "REVIEW_DISMISSED_EVENT",
1414
+ "AUTO_MERGE_ENABLED_EVENT",
1415
+ "AUTO_MERGE_DISABLED_EVENT"
1416
+ ].join(", ");
1417
+ const assigneeFields = "__typename ... on User { login } ... on Bot { login } ... on Mannequin { login } ... on Organization { login }";
1418
+ const reviewerFields = "__typename ... on User { login } ... on Bot { login } ... on Mannequin { login } ... on Team { name }";
1419
+ const reactionFields = "viewerCanReact reactionGroups { content viewerHasReacted reactors { totalCount } }";
1420
+ const pullRequestQuery = ({
1421
+ name,
1422
+ viewer = false,
1423
+ repositoryFields = "",
1424
+ fields
1425
+ }) => `query ${name}($owner: String!, $name: String!, $number: Int!) {${viewer ? "\n viewer { login }" : ""}
1426
+ repository(owner: $owner, name: $name) {${repositoryFields}
1427
+ pullRequest(number: $number) {${fields}
1428
+ }
1429
+ }
1430
+ }`;
1431
+ const pullRequestDetailQuery = pullRequestQuery({
1432
+ name: "PullRequestDetail",
1433
+ viewer: true,
1434
+ repositoryFields: `
1435
+ mergeCommitAllowed
1436
+ squashMergeAllowed
1437
+ rebaseMergeAllowed
1438
+ viewerDefaultMergeMethod`,
1439
+ fields: `
1440
+ id
1441
+ number
1442
+ url
1443
+ title
1444
+ body
1445
+ bodyHTML
1446
+ state
1447
+ isDraft
1448
+ author { login avatarUrl }
1449
+ headRefName
1450
+ baseRefName
1451
+ headRefOid
1452
+ baseRefOid
1453
+ createdAt
1454
+ updatedAt
1455
+ lastEditedAt
1456
+ additions
1457
+ deletions
1458
+ changedFiles
1459
+ mergeable
1460
+ mergeStateStatus
1461
+ viewerCanUpdate
1462
+ viewerCanUpdateBranch
1463
+ viewerCanApplySuggestion
1464
+ mergeCommitHeadline: viewerMergeHeadlineText(mergeType: MERGE)
1465
+ mergeCommitBody: viewerMergeBodyText(mergeType: MERGE)
1466
+ squashCommitHeadline: viewerMergeHeadlineText(mergeType: SQUASH)
1467
+ squashCommitBody: viewerMergeBodyText(mergeType: SQUASH)
1468
+ ${reactionFields}
1469
+ reviewDecision
1470
+ labels(first: 50) { nodes { name color description } }
1471
+ assignees(first: 20) { nodes { login avatarUrl } }
1472
+ reviewRequests(first: 50) {
1473
+ nodes {
1474
+ requestedReviewer {
1475
+ __typename
1476
+ ... on User { login avatarUrl }
1477
+ ... on Bot { login avatarUrl }
1478
+ ... on Mannequin { login avatarUrl }
1479
+ ... on Team { slug name }
1480
+ }
1481
+ }
1482
+ }
1483
+ latestReviews(first: 50) { nodes { author { login } state submittedAt } }
1484
+ commits { totalCount }`
1485
+ });
1486
+ const pullRequestChecksQuery = pullRequestQuery({
1487
+ name: "PullRequestChecks",
1488
+ fields: `
1489
+ headCommits: commits(last: 1) {
1490
+ nodes {
1491
+ commit {
1492
+ statusCheckRollup {
1493
+ contexts(first: 100) {
1494
+ totalCount
1495
+ nodes {
1496
+ __typename
1497
+ ... on CheckRun { name status conclusion title detailsUrl }
1498
+ ... on StatusContext { context state description targetUrl }
1499
+ }
1500
+ }
1501
+ }
1502
+ }
1503
+ }
1504
+ }`
1505
+ });
1506
+ const pullRequestTimelineQuery = pullRequestQuery({
1507
+ name: "PullRequestTimeline",
1508
+ fields: `
1509
+ timelineItems(last: 100, itemTypes: [${timelineItemTypes}]) {
1510
+ filteredCount
1511
+ pageInfo { hasPreviousPage }
1512
+ nodes {
1513
+ __typename
1514
+ ... on IssueComment {
1515
+ id databaseId author { login avatarUrl } body bodyHTML createdAt lastEditedAt url viewerCanUpdate
1516
+ ${reactionFields}
1517
+ }
1518
+ ... on PullRequestReview {
1519
+ id databaseId author { login avatarUrl } state body bodyHTML submittedAt createdAt lastEditedAt url
1520
+ viewerCanUpdate
1521
+ ${reactionFields}
1522
+ commit { oid }
1523
+ }
1524
+ ... on PullRequestCommit {
1525
+ commit {
1526
+ abbreviatedOid
1527
+ messageHeadline
1528
+ committedDate
1529
+ author { name user { login avatarUrl } }
1530
+ }
1531
+ }
1532
+ ... on AssignedEvent { createdAt actor { login avatarUrl } assignee { ${assigneeFields} } }
1533
+ ... on UnassignedEvent { createdAt actor { login avatarUrl } assignee { ${assigneeFields} } }
1534
+ ... on LabeledEvent { createdAt actor { login avatarUrl } label { name color description } }
1535
+ ... on UnlabeledEvent { createdAt actor { login avatarUrl } label { name color description } }
1536
+ ... on ReviewRequestedEvent { createdAt actor { login avatarUrl } requestedReviewer { ${reviewerFields} } }
1537
+ ... on ReviewRequestRemovedEvent { createdAt actor { login avatarUrl } requestedReviewer { ${reviewerFields} } }
1538
+ ... on ClosedEvent { createdAt actor { login avatarUrl } }
1539
+ ... on ReopenedEvent { createdAt actor { login avatarUrl } }
1540
+ ... on MergedEvent { createdAt actor { login avatarUrl } }
1541
+ ... on ReadyForReviewEvent { createdAt actor { login avatarUrl } }
1542
+ ... on ConvertToDraftEvent { createdAt actor { login avatarUrl } }
1543
+ ... on AutoMergeEnabledEvent { createdAt actor { login avatarUrl } }
1544
+ ... on AutoMergeDisabledEvent { createdAt actor { login avatarUrl } }
1545
+ ... on HeadRefForcePushedEvent { createdAt actor { login avatarUrl } afterCommit { abbreviatedOid } }
1546
+ ... on BaseRefChangedEvent { createdAt actor { login avatarUrl } currentRefName previousRefName }
1547
+ ... on RenamedTitleEvent { createdAt actor { login avatarUrl } currentTitle previousTitle }
1548
+ ... on ReviewDismissedEvent { createdAt actor { login avatarUrl } review { author { login } } }
1549
+ }
1550
+ }
1551
+ reviewThreads(first: 100) {
1552
+ totalCount
1553
+ nodes {
1554
+ id
1555
+ isResolved
1556
+ isOutdated
1557
+ path
1558
+ line
1559
+ startLine
1560
+ diffSide
1561
+ startDiffSide
1562
+ originalLine
1563
+ originalStartLine
1564
+ viewerCanResolve
1565
+ viewerCanUnresolve
1566
+ viewerCanReply
1567
+ comments(first: 20) {
1568
+ totalCount
1569
+ nodes {
1570
+ id databaseId author { login avatarUrl } body bodyHTML createdAt lastEditedAt url diffHunk viewerCanUpdate
1571
+ ${reactionFields}
1572
+ }
1573
+ }
1574
+ }
1575
+ }`
1576
+ });
1577
+ const pullRequestCommitsQuery = pullRequestQuery({
1578
+ name: "PullRequestCommits",
1579
+ fields: `
1580
+ commits(first: 100) {
1581
+ totalCount
1582
+ nodes {
1583
+ commit {
1584
+ oid
1585
+ parents(first: 1) { totalCount nodes { oid } }
1586
+ messageHeadline
1587
+ messageBody
1588
+ committedDate
1589
+ author { name user { login avatarUrl } }
1590
+ }
1591
+ }
1592
+ }`
1593
+ });
1594
+ const pullRequestFilesQuery = pullRequestQuery({
1595
+ name: "PullRequestFiles",
1596
+ fields: `
1597
+ files(first: 100) {
1598
+ nodes { path additions deletions changeType viewerViewedState }
1599
+ }
1600
+ latestReviews(first: 100) {
1601
+ nodes { viewerDidAuthor submittedAt commit { oid } }
1602
+ }`
1603
+ });
1604
+ const present = (items) => (items ?? []).filter((item) => item !== null);
1605
+ const toPerson = ({ login, avatarUrl }) => ({
1606
+ login,
1607
+ avatarUrl: avatarUrl ?? null
1608
+ });
1609
+ const toLabel = ({ name, color, description }) => ({
1610
+ name,
1611
+ color,
1612
+ description: description === "" ? null : description
1613
+ });
1614
+ const toState = (state, isDraft) => {
1615
+ if (state === "MERGED") return "merged";
1616
+ if (state === "CLOSED") return "closed";
1617
+ return isDraft ? "draft" : "open";
1618
+ };
1619
+ const reviewDecisions = /* @__PURE__ */ new Map([
1620
+ ["APPROVED", "approved"],
1621
+ ["CHANGES_REQUESTED", "changesRequested"],
1622
+ ["REVIEW_REQUIRED", "reviewRequired"]
1623
+ ]);
1624
+ const mergeables$1 = /* @__PURE__ */ new Map([
1625
+ ["CONFLICTING", "conflicting"],
1626
+ ["MERGEABLE", "mergeable"]
1627
+ ]);
1628
+ const reviewStates$1 = /* @__PURE__ */ new Map([
1629
+ ["APPROVED", "approved"],
1630
+ ["CHANGES_REQUESTED", "changesRequested"],
1631
+ ["COMMENTED", "commented"],
1632
+ ["DISMISSED", "dismissed"],
1633
+ ["PENDING", "pending"]
1634
+ ]);
1635
+ const fileChanges = /* @__PURE__ */ new Map([
1636
+ ["ADDED", "added"],
1637
+ ["CHANGED", "changed"],
1638
+ ["COPIED", "copied"],
1639
+ ["DELETED", "deleted"],
1640
+ ["MODIFIED", "modified"],
1641
+ ["RENAMED", "renamed"]
1642
+ ]);
1643
+ const viewedStates = /* @__PURE__ */ new Map([
1644
+ ["DISMISSED", "dismissed"],
1645
+ ["VIEWED", "viewed"]
1646
+ ]);
1647
+ const toReviewer = (reviewer) => reviewer.__typename === "Team" ? { kind: "team", slug: reviewer.slug, name: reviewer.name } : {
1648
+ kind: "user",
1649
+ login: reviewer.login,
1650
+ avatarUrl: reviewer.avatarUrl
1651
+ };
1652
+ const nonEmpty = (text) => text === null || text === "" ? null : text;
1653
+ const toCheck = (context) => {
1654
+ if (context.__typename === "StatusContext") {
1655
+ return {
1656
+ name: context.context,
1657
+ outcome: checkOutcomes.get(context.state) ?? "pending",
1658
+ running: false,
1659
+ description: nonEmpty(context.description),
1660
+ url: context.targetUrl
1661
+ };
1662
+ }
1663
+ return {
1664
+ name: context.name,
1665
+ outcome: context.status === "COMPLETED" && context.conclusion !== null ? checkOutcomes.get(context.conclusion) ?? "pending" : "pending",
1666
+ running: context.status === "IN_PROGRESS",
1667
+ description: nonEmpty(context.title),
1668
+ url: context.detailsUrl
1669
+ };
1670
+ };
1671
+ const checkOrder = {
1672
+ failed: 0,
1673
+ pending: 1,
1674
+ passed: 2
1675
+ };
1676
+ const ghost = { login: "ghost", avatarUrl: null };
1677
+ const toAuthor = (actor) => actor === null ? ghost : toPerson(actor);
1678
+ const reactionContents = /* @__PURE__ */ new Map([
1679
+ ["CONFUSED", "confused"],
1680
+ ["EYES", "eyes"],
1681
+ ["HEART", "heart"],
1682
+ ["HOORAY", "hooray"],
1683
+ ["LAUGH", "laugh"],
1684
+ ["ROCKET", "rocket"],
1685
+ ["THUMBS_DOWN", "thumbsDown"],
1686
+ ["THUMBS_UP", "thumbsUp"]
1687
+ ]);
1688
+ const toReactable = (subject) => ({
1689
+ id: subject.id,
1690
+ viewerCanReact: subject.viewerCanReact,
1691
+ reactions: (subject.reactionGroups ?? []).flatMap((group) => {
1692
+ const content = reactionContents.get(group.content);
1693
+ if (content === void 0 || group.reactors.totalCount === 0) return [];
1694
+ return [
1695
+ {
1696
+ content,
1697
+ count: group.reactors.totalCount,
1698
+ viewerHasReacted: group.viewerHasReacted
1699
+ }
1700
+ ];
1701
+ })
1702
+ });
1703
+ const toComment = (comment) => ({
1704
+ ...toReactable(comment),
1705
+ databaseId: comment.databaseId,
1706
+ author: toAuthor(comment.author),
1707
+ body: comment.body,
1708
+ createdAt: comment.createdAt,
1709
+ lastEditedAt: comment.lastEditedAt,
1710
+ url: comment.url,
1711
+ viewerCanUpdate: comment.viewerCanUpdate
1712
+ });
1713
+ const toReviewItem = (review) => {
1714
+ if (review.state === "PENDING") return null;
1715
+ if (review.state === "COMMENTED" && review.body.trim() === "") return null;
1716
+ return {
1717
+ kind: "review",
1718
+ ...toReactable(review),
1719
+ databaseId: review.databaseId,
1720
+ author: toAuthor(review.author),
1721
+ state: reviewStates$1.get(review.state) ?? "commented",
1722
+ body: review.body,
1723
+ createdAt: review.submittedAt ?? review.createdAt,
1724
+ lastEditedAt: review.lastEditedAt,
1725
+ url: review.url,
1726
+ viewerCanUpdate: review.viewerCanUpdate && review.body !== "",
1727
+ commitOid: review.commit?.oid ?? null
1728
+ };
1729
+ };
1730
+ const toSide = (side) => side === "LEFT" ? "LEFT" : "RIGHT";
1731
+ const toThreadItem = (thread) => {
1732
+ const comments = present(thread.comments.nodes);
1733
+ const [first] = comments;
1734
+ if (first === void 0) return null;
1735
+ return {
1736
+ kind: "thread",
1737
+ id: thread.id,
1738
+ path: thread.path,
1739
+ line: thread.line,
1740
+ startLine: thread.startLine,
1741
+ side: toSide(thread.diffSide),
1742
+ startSide: thread.startDiffSide === null ? null : toSide(thread.startDiffSide),
1743
+ originalLine: thread.originalLine,
1744
+ originalStartLine: thread.originalStartLine,
1745
+ resolved: thread.isResolved,
1746
+ outdated: thread.isOutdated,
1747
+ viewerCanResolve: thread.viewerCanResolve,
1748
+ viewerCanUnresolve: thread.viewerCanUnresolve,
1749
+ viewerCanReply: thread.viewerCanReply,
1750
+ diffHunk: first.diffHunk,
1751
+ comments: comments.map(toComment),
1752
+ commentsTotal: thread.comments.totalCount,
1753
+ createdAt: first.createdAt
1754
+ };
1755
+ };
1756
+ const nameOf = (named) => {
1757
+ if (named === null) return null;
1758
+ return "login" in named ? named.login : named.name;
1759
+ };
1760
+ const commitAuthor = (commit) => {
1761
+ const { author } = commit;
1762
+ if (author === null) return null;
1763
+ if (author.user !== null) return toPerson(author.user);
1764
+ return author.name === null ? null : { login: author.name, avatarUrl: null };
1765
+ };
1766
+ const simpleActions = {
1767
+ AutoMergeDisabledEvent: "autoMergeDisabled",
1768
+ AutoMergeEnabledEvent: "autoMergeEnabled",
1769
+ ClosedEvent: "closed",
1770
+ ConvertToDraftEvent: "convertedToDraft",
1771
+ MergedEvent: "merged",
1772
+ ReadyForReviewEvent: "readyForReview",
1773
+ ReopenedEvent: "reopened"
1774
+ };
1775
+ const event = (action, { actor, createdAt }, extra = {}) => ({
1776
+ kind: "event",
1777
+ action,
1778
+ actor: actor === null ? null : toPerson(actor),
1779
+ subject: null,
1780
+ label: null,
1781
+ detail: null,
1782
+ createdAt,
1783
+ ...extra
1784
+ });
1785
+ const toEvent = (node) => {
1786
+ switch (node.__typename) {
1787
+ case "PullRequestCommit": {
1788
+ const { commit } = node;
1789
+ return event(
1790
+ "committed",
1791
+ { actor: commitAuthor(commit), createdAt: commit.committedDate },
1792
+ { subject: commit.messageHeadline, detail: commit.abbreviatedOid }
1793
+ );
1794
+ }
1795
+ case "AssignedEvent":
1796
+ case "UnassignedEvent": {
1797
+ return event(
1798
+ node.__typename === "AssignedEvent" ? "assigned" : "unassigned",
1799
+ node,
1800
+ { subject: nameOf(node.assignee) }
1801
+ );
1802
+ }
1803
+ case "LabeledEvent":
1804
+ case "UnlabeledEvent": {
1805
+ return event(
1806
+ node.__typename === "LabeledEvent" ? "labeled" : "unlabeled",
1807
+ node,
1808
+ {
1809
+ subject: node.label?.name ?? null,
1810
+ label: node.label === null ? null : toLabel(node.label)
1811
+ }
1812
+ );
1813
+ }
1814
+ case "ReviewRequestedEvent":
1815
+ case "ReviewRequestRemovedEvent": {
1816
+ return event(
1817
+ node.__typename === "ReviewRequestedEvent" ? "reviewRequested" : "reviewRequestRemoved",
1818
+ node,
1819
+ { subject: nameOf(node.requestedReviewer) }
1820
+ );
1821
+ }
1822
+ case "HeadRefForcePushedEvent": {
1823
+ return event("headRefForcePushed", node, {
1824
+ detail: node.afterCommit?.abbreviatedOid ?? null
1825
+ });
1826
+ }
1827
+ case "BaseRefChangedEvent": {
1828
+ return event("baseRefChanged", node, {
1829
+ subject: node.currentRefName,
1830
+ detail: node.previousRefName
1831
+ });
1832
+ }
1833
+ case "RenamedTitleEvent": {
1834
+ return event("renamed", node, {
1835
+ subject: node.currentTitle,
1836
+ detail: node.previousTitle
1837
+ });
1838
+ }
1839
+ case "ReviewDismissedEvent": {
1840
+ return event("reviewDismissed", node, {
1841
+ subject: node.review?.author?.login ?? null
1842
+ });
1843
+ }
1844
+ case "AutoMergeDisabledEvent":
1845
+ case "AutoMergeEnabledEvent":
1846
+ case "ClosedEvent":
1847
+ case "ConvertToDraftEvent":
1848
+ case "MergedEvent":
1849
+ case "ReadyForReviewEvent":
1850
+ case "ReopenedEvent": {
1851
+ return event(simpleActions[node.__typename], node);
1852
+ }
1853
+ default: {
1854
+ return node;
1855
+ }
1856
+ }
1857
+ };
1858
+ const toTimelineNode = (node) => {
1859
+ if (node.__typename === "IssueComment") {
1860
+ return { kind: "comment", ...toComment(node) };
1861
+ }
1862
+ if (node.__typename === "PullRequestReview") return toReviewItem(node);
1863
+ return toEvent(node);
1864
+ };
1865
+ const isTruncated = (connection) => connection !== null && connection.totalCount > present(connection.nodes).length;
1866
+ const bodyHtmlsOf = (pullRequest) => [
1867
+ ...present(pullRequest.timelineItems?.nodes).map(
1868
+ (node) => "bodyHTML" in node ? node.bodyHTML : void 0
1869
+ ),
1870
+ ...present(pullRequest.reviewThreads?.nodes).flatMap(
1871
+ (thread) => present(thread.comments.nodes).map((comment) => comment.bodyHTML)
1872
+ )
1873
+ ];
1874
+ const toTimeline = (pullRequest) => {
1875
+ const items = [
1876
+ ...present(pullRequest.timelineItems?.nodes).map(toTimelineNode),
1877
+ ...present(pullRequest.reviewThreads?.nodes).map(toThreadItem)
1878
+ ].filter((item) => item !== null);
1879
+ return {
1880
+ timeline: items.toSorted((a, b) => a.createdAt.localeCompare(b.createdAt)),
1881
+ images: mergeImageProxies(bodyHtmlsOf(pullRequest)),
1882
+ timelineTruncated: pullRequest.timelineItems?.pageInfo.hasPreviousPage === true || isTruncated(pullRequest.reviewThreads)
1883
+ };
1884
+ };
1885
+ const toChecks = (contexts) => present(contexts?.nodes).map(toCheck).map((check, index) => ({ check, index })).toSorted(
1886
+ (a, b) => checkOrder[a.check.outcome] - checkOrder[b.check.outcome] || a.index - b.index
1887
+ ).map(({ check }) => check);
1888
+ const toFiles = (pullRequest) => present(pullRequest.files?.nodes).map((file) => ({
1889
+ path: file.path,
1890
+ additions: file.additions,
1891
+ deletions: file.deletions,
1892
+ change: fileChanges.get(file.changeType) ?? "changed",
1893
+ viewed: viewedStates.get(file.viewerViewedState) ?? "unviewed"
1894
+ }));
1895
+ const toRequestedReviewers = (pullRequest) => present(pullRequest.reviewRequests?.nodes).map(({ requestedReviewer }) => requestedReviewer).filter((reviewer) => reviewer !== null).map(toReviewer);
1896
+ const toReviewDecision = (decision) => decision === null ? null : reviewDecisions.get(decision) ?? null;
1897
+ const mergeStates = /* @__PURE__ */ new Map([
1898
+ ["BEHIND", "behind"],
1899
+ ["BLOCKED", "blocked"],
1900
+ ["CLEAN", "clean"],
1901
+ ["DIRTY", "dirty"],
1902
+ ["DRAFT", "draft"],
1903
+ ["HAS_HOOKS", "hasHooks"],
1904
+ ["UNSTABLE", "unstable"]
1905
+ ]);
1906
+ const mergeMethodNames = /* @__PURE__ */ new Map([
1907
+ ["MERGE", "merge"],
1908
+ ["REBASE", "rebase"],
1909
+ ["SQUASH", "squash"]
1910
+ ]);
1911
+ const toMergeMethods = (repository) => {
1912
+ const allowed = [];
1913
+ if (repository.mergeCommitAllowed === true) allowed.push("merge");
1914
+ if (repository.squashMergeAllowed === true) allowed.push("squash");
1915
+ if (repository.rebaseMergeAllowed === true) allowed.push("rebase");
1916
+ const preferred = mergeMethodNames.get(
1917
+ repository.viewerDefaultMergeMethod ?? ""
1918
+ );
1919
+ return preferred !== void 0 && allowed.includes(preferred) ? [preferred, ...allowed.filter((method) => method !== preferred)] : allowed;
1920
+ };
1921
+ const toCommit = ({ commit }) => ({
1922
+ oid: commit.oid,
1923
+ parentOid: present(commit.parents.nodes)[0]?.oid ?? null,
1924
+ merge: commit.parents.totalCount > 1,
1925
+ headline: commit.messageHeadline,
1926
+ body: commit.messageBody.trim(),
1927
+ author: commitAuthor(commit),
1928
+ committedAt: commit.committedDate
1929
+ });
1930
+ const viewerLastReview = (pullRequest) => {
1931
+ const review = present(pullRequest.latestReviews?.nodes).find(
1932
+ ({ viewerDidAuthor }) => viewerDidAuthor
1933
+ );
1934
+ const oid = review?.commit?.oid;
1935
+ if (review === void 0 || oid === void 0) return null;
1936
+ return { oid, submittedAt: review.submittedAt };
1937
+ };
1938
+ const toDetail = (pullRequest, repository, ownerRepo) => ({
1939
+ ...toReactable(pullRequest),
1940
+ ownerRepo,
1941
+ number: pullRequest.number,
1942
+ url: pullRequest.url,
1943
+ title: pullRequest.title,
1944
+ body: pullRequest.body,
1945
+ state: toState(pullRequest.state, pullRequest.isDraft),
1946
+ author: pullRequest.author === null ? null : toPerson(pullRequest.author),
1947
+ headRef: pullRequest.headRefName,
1948
+ baseRef: pullRequest.baseRefName,
1949
+ headSha: pullRequest.headRefOid,
1950
+ baseSha: pullRequest.baseRefOid,
1951
+ createdAt: pullRequest.createdAt,
1952
+ updatedAt: pullRequest.updatedAt,
1953
+ lastEditedAt: pullRequest.lastEditedAt,
1954
+ additions: pullRequest.additions,
1955
+ deletions: pullRequest.deletions,
1956
+ changedFiles: pullRequest.changedFiles,
1957
+ mergeable: mergeables$1.get(pullRequest.mergeable) ?? "unknown",
1958
+ mergeState: mergeStates.get(pullRequest.mergeStateStatus) ?? "unknown",
1959
+ mergeMethods: toMergeMethods(repository),
1960
+ mergeCommitMessages: {
1961
+ merge: {
1962
+ title: pullRequest.mergeCommitHeadline,
1963
+ body: pullRequest.mergeCommitBody
1964
+ },
1965
+ squash: {
1966
+ title: pullRequest.squashCommitHeadline,
1967
+ body: pullRequest.squashCommitBody
1968
+ }
1969
+ },
1970
+ viewerCanUpdate: pullRequest.viewerCanUpdate,
1971
+ viewerCanUpdateBranch: pullRequest.viewerCanUpdateBranch,
1972
+ viewerCanApplySuggestion: pullRequest.viewerCanApplySuggestion,
1973
+ reviewDecision: toReviewDecision(pullRequest.reviewDecision),
1974
+ labels: present(pullRequest.labels?.nodes).map(toLabel),
1975
+ assignees: present(pullRequest.assignees?.nodes).map(toPerson),
1976
+ requestedReviewers: toRequestedReviewers(pullRequest),
1977
+ reviews: present(pullRequest.latestReviews?.nodes).map((review) => ({
1978
+ author: review.author?.login ?? "ghost",
1979
+ state: reviewStates$1.get(review.state) ?? "commented",
1980
+ submittedAt: review.submittedAt
1981
+ })),
1982
+ commitsTotal: pullRequest.commits.totalCount
1983
+ });
1984
+ const toChecksPart = (pullRequest) => {
1985
+ const contexts = pullRequest.headCommits.nodes?.[0]?.commit.statusCheckRollup?.contexts ?? null;
1986
+ return { checks: toChecks(contexts), checksTotal: contexts?.totalCount ?? 0 };
1987
+ };
1988
+ const pullRequestCandidatesQuery = `query PullRequestCandidates($owner: String!, $name: String!, $number: Int!) {
1989
+ repositoryOwner(login: $owner) {
1990
+ ... on Organization {
1991
+ teams(first: 100, orderBy: { field: NAME, direction: ASC }) {
1992
+ nodes { slug name }
1993
+ }
1994
+ }
1995
+ }
1996
+ repository(owner: $owner, name: $name) {
1997
+ labels(first: 100, orderBy: { field: NAME, direction: ASC }) {
1998
+ nodes { name color description }
1999
+ }
2000
+ assignableUsers(first: 100) { nodes { login avatarUrl } }
2001
+ pullRequest(number: $number) {
2002
+ author { login }
2003
+ suggestedReviewers { reviewer { login avatarUrl } }
2004
+ }
2005
+ }
2006
+ }`;
2007
+ const reviewerCandidates = (pullRequest, assignable) => {
2008
+ const author = pullRequest.author?.login;
2009
+ const seen = /* @__PURE__ */ new Set();
2010
+ const unseen = (actors) => actors.flatMap((actor) => {
2011
+ if (actor.login === author || seen.has(actor.login)) return [];
2012
+ seen.add(actor.login);
2013
+ return [toPerson(actor)];
2014
+ });
2015
+ const suggested = unseen(
2016
+ present(pullRequest.suggestedReviewers).map(({ reviewer }) => reviewer).filter((reviewer) => reviewer !== null)
2017
+ );
2018
+ const others = unseen(
2019
+ assignable.toSorted((a, b) => a.login.localeCompare(b.login))
2020
+ );
2021
+ return { suggested, others };
2022
+ };
2023
+ const gitHubRead = ({
2024
+ fetchedAt,
2025
+ errors
2026
+ }) => ({
2027
+ fetchedAt,
2028
+ checkedAt: fetchedAt,
2029
+ source: "github",
2030
+ errors
2031
+ });
2032
+ const readPart = ({
2033
+ data,
2034
+ errors,
2035
+ ownerRepo,
2036
+ number
2037
+ }) => {
2038
+ const repository = data?.repository ?? null;
2039
+ const pullRequest = repository?.pullRequest ?? null;
2040
+ if (repository === null || pullRequest === null) {
2041
+ const reason = errors.length > 0 ? errors.join("; ") : "not found";
2042
+ throw new Error(
2043
+ `no pull request #${String(number)} in ${ownerRepo}: ${reason}`
2044
+ );
2045
+ }
2046
+ return { pullRequest, repository };
2047
+ };
2048
+ const parsePullRequestDetail = (params) => {
2049
+ const { pullRequest, repository } = readPart(params);
2050
+ return {
2051
+ pullRequest: toDetail(pullRequest, repository, params.ownerRepo),
2052
+ viewer: params.data?.viewer?.login ?? "",
2053
+ images: imageProxiesOf(pullRequest.bodyHTML),
2054
+ ...gitHubRead(params)
2055
+ };
2056
+ };
2057
+ const parsePullRequestChecks = (params) => ({
2058
+ ...toChecksPart(readPart(params).pullRequest),
2059
+ ...gitHubRead(params)
2060
+ });
2061
+ const parsePullRequestTimeline = (params) => ({
2062
+ ...toTimeline(readPart(params).pullRequest),
2063
+ ...gitHubRead(params)
2064
+ });
2065
+ const parsePullRequestCommits = (params) => ({
2066
+ commits: present(readPart(params).pullRequest.commits.nodes).map(toCommit),
2067
+ ...gitHubRead(params)
2068
+ });
2069
+ const parsePullRequestFiles = (params) => {
2070
+ const { pullRequest } = readPart(params);
2071
+ return {
2072
+ files: toFiles(pullRequest),
2073
+ viewerLastReview: viewerLastReview(pullRequest),
2074
+ ...gitHubRead(params)
2075
+ };
2076
+ };
2077
+ const readGitHub = async (transport, { ownerRepo, number }, query, parse) => {
2078
+ const split = splitOwnerRepo(ownerRepo);
2079
+ if (split === null) throw new Error(`${ownerRepo} is not owner/repo`);
2080
+ const { data, errors } = await transport.graphql(
2081
+ query,
2082
+ {
2083
+ owner: split.owner,
2084
+ name: split.name,
2085
+ number
2086
+ }
2087
+ );
2088
+ return parse({
2089
+ data,
2090
+ errors,
2091
+ ownerRepo,
2092
+ number,
2093
+ fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
2094
+ });
2095
+ };
2096
+ const partReaders = {
2097
+ detail: (transport, ref) => readGitHub(transport, ref, pullRequestDetailQuery, parsePullRequestDetail),
2098
+ checks: (transport, ref) => readGitHub(transport, ref, pullRequestChecksQuery, parsePullRequestChecks),
2099
+ timeline: (transport, ref) => readGitHub(
2100
+ transport,
2101
+ ref,
2102
+ pullRequestTimelineQuery,
2103
+ parsePullRequestTimeline
2104
+ ),
2105
+ commits: (transport, ref) => readGitHub(
2106
+ transport,
2107
+ ref,
2108
+ pullRequestCommitsQuery,
2109
+ parsePullRequestCommits
2110
+ ),
2111
+ files: (transport, ref) => readGitHub(transport, ref, pullRequestFilesQuery, parsePullRequestFiles)
2112
+ };
2113
+ const fetchPullRequestPart = (transport, ref, part) => partReaders[part](transport, ref);
2114
+ const parsePullRequestCandidates = ({
2115
+ data,
2116
+ errors
2117
+ }) => {
2118
+ const repository = data?.repository ?? null;
2119
+ if (repository === null) {
2120
+ throw new Error(errors.length > 0 ? errors.join("; ") : "not found");
2121
+ }
2122
+ const assignable = present(repository.assignableUsers?.nodes);
2123
+ const reviewers = reviewerCandidates(
2124
+ repository.pullRequest ?? { author: null, suggestedReviewers: null },
2125
+ assignable
2126
+ );
2127
+ return {
2128
+ repoLabels: present(repository.labels?.nodes).map(toLabel),
2129
+ suggestedReviewers: reviewers.suggested,
2130
+ reviewerCandidates: reviewers.others,
2131
+ teamCandidates: present(data?.repositoryOwner?.teams?.nodes).map(
2132
+ ({ slug, name }) => ({ slug, name })
2133
+ ),
2134
+ assigneeCandidates: assignable.toSorted((a, b) => a.login.localeCompare(b.login)).map(toPerson),
2135
+ errors
2136
+ };
2137
+ };
2138
+ const fetchPullRequestCandidates = async (transport, {
2139
+ ownerRepo,
2140
+ number
2141
+ }) => {
2142
+ const split = splitOwnerRepo(ownerRepo);
2143
+ if (split === null) throw new Error(`${ownerRepo} is not owner/repo`);
2144
+ const { data, errors } = await transport.graphql(
2145
+ pullRequestCandidatesQuery,
2146
+ { owner: split.owner, name: split.name, number }
2147
+ );
2148
+ return parsePullRequestCandidates({ data, errors });
2149
+ };
2150
+
2151
+ const pullRequestHeadQuery = `query PullRequestHead($owner: String!, $name: String!, $number: Int!) {
2152
+ repository(owner: $owner, name: $name) {
2153
+ pullRequest(number: $number) {
2154
+ state
2155
+ headRefName
2156
+ isCrossRepository
2157
+ headRepositoryOwner { login }
2158
+ }
2159
+ }
2160
+ }`;
2161
+ const headStates = {
2162
+ CLOSED: "closed",
2163
+ MERGED: "merged",
2164
+ OPEN: "open"
2165
+ };
2166
+ const fetchPullRequestHead = async (transport, { ownerRepo, number }) => {
2167
+ const [owner = "", name = ""] = ownerRepo.split("/");
2168
+ const { data, errors } = await transport.graphql(
2169
+ pullRequestHeadQuery,
2170
+ { owner, name, number }
2171
+ );
2172
+ const pullRequest = data?.repository?.pullRequest ?? null;
2173
+ if (pullRequest === null) {
2174
+ const reason = errors.length > 0 ? errors.join("; ") : "not found";
2175
+ throw new Error(`${ownerRepo}#${String(number)}: ${reason}`);
2176
+ }
2177
+ return {
2178
+ state: headStates[pullRequest.state],
2179
+ branch: pullRequest.headRefName,
2180
+ fork: pullRequest.isCrossRepository ? pullRequest.headRepositoryOwner?.login ?? "a deleted fork" : null
2181
+ };
2182
+ };
2183
+
2184
+ const revisionsPage = 50;
2185
+ const pullRequestRevisionsQuery = `query PullRequestRevisions($id: ID!) {
2186
+ node(id: $id) {
2187
+ ... on Comment {
2188
+ includesCreatedEdit
2189
+ userContentEdits(first: ${String(revisionsPage)}) {
2190
+ totalCount
2191
+ nodes {
2192
+ editedAt
2193
+ deletedAt
2194
+ editor { login avatarUrl }
2195
+ deletedBy { login }
2196
+ diff
2197
+ }
2198
+ }
2199
+ }
2200
+ }
2201
+ }`;
2202
+ const normalizeNewlines = (text) => text?.replaceAll("\r\n", "\n") ?? null;
2203
+ const parsePullRequestRevisions = ({
2204
+ data,
2205
+ errors,
2206
+ subjectId
2207
+ }) => {
2208
+ const edits = data?.node?.userContentEdits ?? null;
2209
+ if (edits === null) {
2210
+ const reason = errors.length > 0 ? errors.join("; ") : "not found";
2211
+ throw new Error(`no edit history for ${subjectId}: ${reason}`);
2212
+ }
2213
+ const nodes = (edits.nodes ?? []).flatMap(
2214
+ (node, rawIndex) => node === null ? [] : [{ ...node, rawIndex }]
2215
+ );
2216
+ const bodies = nodes.map(
2217
+ (node) => node.deletedAt === null ? normalizeNewlines(node.diff) : null
2218
+ );
2219
+ const lastIndex = edits.totalCount - 1;
2220
+ const revisions = nodes.map((node, index) => {
2221
+ const body = bodies[index] ?? null;
2222
+ const before = bodies.slice(index + 1).find((older) => older !== null);
2223
+ return {
2224
+ editedAt: node.editedAt,
2225
+ editor: node.editor === null ? null : {
2226
+ login: node.editor.login,
2227
+ avatarUrl: node.editor.avatarUrl ?? null
2228
+ },
2229
+ original: data?.node?.includesCreatedEdit === true && node.rawIndex === lastIndex,
2230
+ body,
2231
+ deletedBy: node.deletedBy?.login ?? null,
2232
+ changes: body === null || before === void 0 ? null : diffLines({ before, after: body })
2233
+ };
2234
+ });
2235
+ return { revisions, revisionsTotal: edits.totalCount };
2236
+ };
2237
+ const fetchPullRequestRevisions = async (transport, subjectId) => {
2238
+ const { data, errors } = await transport.graphql(
2239
+ pullRequestRevisionsQuery,
2240
+ { id: subjectId }
2241
+ );
2242
+ return parsePullRequestRevisions({ data, errors, subjectId });
2243
+ };
2244
+
2245
+ const firstLine = (text) => text.split("\n", 1)[0]?.trim() ?? "";
2246
+ const isSuccess = (status) => status >= 200 && status < 300;
2247
+ const parseJson = (text) => {
2248
+ try {
2249
+ return JSON.parse(text);
2250
+ } catch {
2251
+ return null;
2252
+ }
2253
+ };
2254
+ const interpretGraphql = ({
2255
+ status,
2256
+ body
2257
+ }) => {
2258
+ const parsed = parseJson(body);
2259
+ if (parsed === null || !isSuccess(status) && parsed.data == null) {
2260
+ const detail = parsed?.message ?? firstLine(body);
2261
+ throw new Error(
2262
+ `GitHub graphql failed (HTTP ${String(status)})${detail === "" ? "" : `: ${detail}`}`
2263
+ );
2264
+ }
2265
+ return {
2266
+ data: parsed.data ?? null,
2267
+ errors: (parsed.errors ?? []).map(
2268
+ ({ message }) => message ?? "github returned an unnamed error"
2269
+ )
2270
+ };
2271
+ };
2272
+ const restErrorMessage = ({ status, body }) => {
2273
+ const parsed = parseJson(body);
2274
+ const details = (parsed?.errors ?? []).map(
2275
+ (error) => typeof error === "string" ? error : error.message ?? [error.field, error.code].filter(Boolean).join(" ")
2276
+ ).filter((detail) => detail !== "");
2277
+ const headline = parsed?.message ?? (firstLine(body) || `HTTP ${String(status)}`);
2278
+ return [headline, ...details].filter((part) => part !== "").join(": ");
2279
+ };
2280
+ const createGitHubTransport = (send) => ({
2281
+ graphql: async (query, variables) => interpretGraphql(
2282
+ await send({
2283
+ method: "POST",
2284
+ path: "graphql",
2285
+ accept: "application/json",
2286
+ body: JSON.stringify(
2287
+ variables === void 0 ? { query } : { query, variables }
2288
+ )
2289
+ })
2290
+ ),
2291
+ rest: async ({
2292
+ method,
2293
+ path,
2294
+ body
2295
+ }) => {
2296
+ const answer = await send({
2297
+ method,
2298
+ path,
2299
+ accept: "application/vnd.github+json",
2300
+ body: body === void 0 ? void 0 : JSON.stringify(body)
2301
+ });
2302
+ if (!isSuccess(answer.status)) {
2303
+ throw new Error(
2304
+ `GitHub refused ${method} ${path}: ${restErrorMessage(answer)}`
2305
+ );
2306
+ }
2307
+ return parseJson(answer.body);
2308
+ },
2309
+ text: async ({ path, accept }) => {
2310
+ const answer = await send({ method: "GET", path, accept, body: void 0 });
2311
+ if (!isSuccess(answer.status)) {
2312
+ throw new Error(
2313
+ `GitHub refused GET ${path}: ${restErrorMessage(answer)}`
2314
+ );
2315
+ }
2316
+ return answer.body;
2317
+ }
2318
+ });
2319
+
2320
+ const pullRequestParts = [
2321
+ "detail",
2322
+ "checks",
2323
+ "timeline",
2324
+ "commits",
2325
+ "files"
2326
+ ];
2327
+ const isPullRequestPart = (value) => pullRequestParts.includes(value);
2328
+
2329
+ const pullRequestPartsStaleAfter = (edit, pullRequestId) => {
2330
+ switch (edit.kind) {
2331
+ case "applySuggestions":
2332
+ case "updateBranch": {
2333
+ return pullRequestParts;
2334
+ }
2335
+ case "body": {
2336
+ return ["detail"];
2337
+ }
2338
+ case "reaction": {
2339
+ return edit.subjectId === pullRequestId ? ["detail"] : ["timeline"];
2340
+ }
2341
+ case "viewed": {
2342
+ return [];
2343
+ }
2344
+ case "comment":
2345
+ case "editComment":
2346
+ case "inlineComment":
2347
+ case "reply":
2348
+ case "resolveThread": {
2349
+ return ["timeline"];
2350
+ }
2351
+ case "review": {
2352
+ return ["detail", "timeline", "files"];
2353
+ }
2354
+ case "assignees":
2355
+ case "draft":
2356
+ case "labels":
2357
+ case "merge":
2358
+ case "reviewers":
2359
+ case "state":
2360
+ case "title": {
2361
+ return ["detail", "timeline"];
2362
+ }
2363
+ default: {
2364
+ return edit;
2365
+ }
2366
+ }
2367
+ };
2368
+ const withFileViewed = (files, path, viewed) => ({
2369
+ ...files,
2370
+ files: files.files.map(
2371
+ (file) => file.path === path ? { ...file, viewed: viewed ? "viewed" : "unviewed" } : file
2372
+ )
2373
+ });
2374
+ const pullRequestEditMovesBoard = (edit) => {
2375
+ switch (edit.kind) {
2376
+ case "applySuggestions":
2377
+ case "draft":
2378
+ case "merge":
2379
+ case "review":
2380
+ case "reviewers":
2381
+ case "state":
2382
+ case "updateBranch": {
2383
+ return true;
2384
+ }
2385
+ case "assignees":
2386
+ case "body":
2387
+ case "comment":
2388
+ case "editComment":
2389
+ case "inlineComment":
2390
+ case "labels":
2391
+ case "reaction":
2392
+ case "reply":
2393
+ case "resolveThread":
2394
+ case "title":
2395
+ case "viewed": {
2396
+ return false;
2397
+ }
2398
+ default: {
2399
+ return edit;
2400
+ }
2401
+ }
2402
+ };
2403
+
2404
+ const partFields = {
2405
+ detail: (pr) => [
2406
+ pr.title,
2407
+ pr.headRef,
2408
+ pr.baseRef,
2409
+ pr.headSha,
2410
+ pr.baseSha,
2411
+ pr.state,
2412
+ pr.reviewState,
2413
+ pr.author,
2414
+ pr.checks.conclusion,
2415
+ pr.approvedBy,
2416
+ pr.changesRequestedBy,
2417
+ pr.requestedReviewers,
2418
+ pr.reRequestedReviewers,
2419
+ pr.requestedTeams,
2420
+ pr.assignees,
2421
+ pr.labels,
2422
+ pr.mergeable,
2423
+ pr.unresolvedThreads === 0,
2424
+ pr.reviewActivityAt,
2425
+ pr.bodyEditedAt,
2426
+ pr.closedAt
2427
+ ],
2428
+ checks: (pr) => [pr.headSha, pr.checks],
2429
+ timeline: (pr) => [
2430
+ pr.headSha,
2431
+ pr.baseSha,
2432
+ pr.baseRef,
2433
+ pr.title,
2434
+ pr.state,
2435
+ pr.labels,
2436
+ pr.assignees,
2437
+ pr.requestedReviewers,
2438
+ pr.reRequestedReviewers,
2439
+ pr.requestedTeams,
2440
+ pr.approvedBy,
2441
+ pr.changesRequestedBy,
2442
+ pr.reviewActivityAt,
2443
+ pr.commentActivityAt,
2444
+ pr.reviewCommentActivityAt,
2445
+ pr.unresolvedThreads
2446
+ ],
2447
+ commits: (pr) => [pr.headSha, pr.baseSha],
2448
+ files: (pr) => [pr.headSha, pr.baseSha]
2449
+ };
2450
+ const partFingerprint = (pr, part) => JSON.stringify(partFields[part](pr));
2451
+ const changedPullRequestParts = (previous, next) => pullRequestParts.filter(
2452
+ (part) => partFingerprint(previous, part) !== partFingerprint(next, part)
2453
+ );
2454
+
2455
+ const upperOrNull = (value) => value === null ? null : value.toUpperCase();
2456
+ const checksFromReviewflow = ({
2457
+ checkRuns,
2458
+ statuses
2459
+ }, at) => {
2460
+ const nodes = [
2461
+ ...checkRuns.map((run) => ({
2462
+ __typename: "CheckRun",
2463
+ name: run.name,
2464
+ status: upperOrNull(run.status) ?? (run.conclusion === null ? "QUEUED" : "COMPLETED"),
2465
+ conclusion: upperOrNull(run.conclusion),
2466
+ title: run.title,
2467
+ detailsUrl: run.detailsUrl
2468
+ })),
2469
+ ...statuses.map((status) => ({
2470
+ __typename: "StatusContext",
2471
+ context: status.context,
2472
+ state: status.state.toUpperCase(),
2473
+ description: status.description,
2474
+ targetUrl: status.targetUrl
2475
+ }))
2476
+ ];
2477
+ const parsed = parsePullRequestChecks({
2478
+ data: {
2479
+ repository: {
2480
+ pullRequest: {
2481
+ headCommits: {
2482
+ nodes: [
2483
+ {
2484
+ commit: {
2485
+ statusCheckRollup: {
2486
+ contexts: { totalCount: nodes.length, nodes }
2487
+ }
2488
+ }
2489
+ }
2490
+ ]
2491
+ }
2492
+ }
2493
+ }
2494
+ },
2495
+ errors: [],
2496
+ ownerRepo: "",
2497
+ number: 0,
2498
+ fetchedAt: at
2499
+ });
2500
+ return { ...parsed, source: "reviewflow" };
2501
+ };
2502
+
2503
+ const reviewflowErrorCode = "ERR_REVIEWFLOW";
2504
+ const timeoutMs = 15e3;
2505
+ const maxLookupTargets = 200;
2506
+ const maxExtraPagesPerBucket = 5;
2507
+ const fail = (message, { status, retryAfterMs } = {}) => Object.assign(new Error(message), {
2508
+ code: reviewflowErrorCode,
2509
+ status,
2510
+ retryAfterMs
2511
+ });
2512
+ const isReviewflowFailure = (error) => error instanceof Error && error.code === reviewflowErrorCode;
2513
+ const isRecord$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
2514
+ const stringOrNull = (value) => typeof value === "string" ? value : null;
2515
+ const numberOr = (value, fallback) => typeof value === "number" && Number.isFinite(value) ? value : fallback;
2516
+ const prStates = /* @__PURE__ */ new Set(["closed", "draft", "merged", "open"]);
2517
+ const reviewStates = /* @__PURE__ */ new Set([
2518
+ "approved",
2519
+ "changesRequested",
2520
+ "none",
2521
+ "requested"
2522
+ ]);
2523
+ const mergeables = /* @__PURE__ */ new Set(["conflicting", "mergeable", "unknown"]);
2524
+ const checksConclusions = {
2525
+ failed: "failed",
2526
+ "in-progress": "inProgress",
2527
+ passed: "passed"
2528
+ };
2529
+ const enumOrNull = (value, allowed) => typeof value === "string" && allowed.has(value) ? value : null;
2530
+ const strings = (value) => Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
2531
+ const logins = (value) => Array.isArray(value) ? value.flatMap(
2532
+ (person) => isRecord$1(person) && typeof person.login === "string" ? [person.login] : []
2533
+ ) : [];
2534
+ const readCheckRuns = (value) => Array.isArray(value) ? value.flatMap(
2535
+ (run) => isRecord$1(run) && typeof run.name === "string" ? [
2536
+ {
2537
+ name: run.name,
2538
+ status: stringOrNull(run.status),
2539
+ conclusion: stringOrNull(run.conclusion),
2540
+ title: stringOrNull(run.title),
2541
+ detailsUrl: stringOrNull(run.detailsUrl)
2542
+ }
2543
+ ] : []
2544
+ ) : null;
2545
+ const readCommitStatuses = (value) => Array.isArray(value) ? value.flatMap(
2546
+ (status) => isRecord$1(status) && typeof status.context === "string" && typeof status.state === "string" ? [
2547
+ {
2548
+ context: status.context,
2549
+ state: status.state,
2550
+ description: stringOrNull(status.description),
2551
+ targetUrl: stringOrNull(status.targetUrl)
2552
+ }
2553
+ ] : []
2554
+ ) : null;
2555
+ const readStatusLinks = (value) => Array.isArray(value) ? value.flatMap(
2556
+ (link) => isRecord$1(link) && link.type === "success" && typeof link.name === "string" && typeof link.label === "string" && typeof link.url === "string" ? [{ name: link.name, label: link.label, url: link.url }] : []
2557
+ ) : [];
2558
+ const readLabels = (value) => Array.isArray(value) ? value.flatMap(
2559
+ (label) => isRecord$1(label) && typeof label.name === "string" ? [label.name] : []
2560
+ ) : [];
2561
+ const readChanges = (value) => isRecord$1(value) && typeof value.changedFiles === "number" && typeof value.additions === "number" && typeof value.deletions === "number" ? {
2562
+ changedFiles: value.changedFiles,
2563
+ additions: value.additions,
2564
+ deletions: value.deletions
2565
+ } : null;
2566
+ const readPrDetail = (value) => {
2567
+ if (!isRecord$1(value) || typeof value._id !== "string" || typeof value.ownerRepo !== "string" || typeof value.number !== "number" || typeof value.url !== "string" || !isRecord$1(value.checks)) {
2568
+ throw fail("reviewflow returned a pull request the client cannot read");
2569
+ }
2570
+ const { checks } = value;
2571
+ const creator = isRecord$1(value.creator) ? value.creator : null;
2572
+ const flowDates = isRecord$1(value.flowDates) ? value.flowDates : {};
2573
+ return {
2574
+ id: value._id,
2575
+ ownerRepo: value.ownerRepo,
2576
+ number: value.number,
2577
+ title: typeof value.title === "string" ? value.title : "",
2578
+ url: value.url,
2579
+ headRef: stringOrNull(value.headRef),
2580
+ baseRef: stringOrNull(value.baseRef),
2581
+ headSha: stringOrNull(value.headSha),
2582
+ baseSha: stringOrNull(value.baseSha),
2583
+ state: enumOrNull(value.state, prStates),
2584
+ reviewState: enumOrNull(
2585
+ value.reviewState,
2586
+ reviewStates
2587
+ ),
2588
+ author: stringOrNull(creator?.login),
2589
+ checks: {
2590
+ conclusion: typeof checks.conclusion === "string" ? checksConclusions[checks.conclusion] ?? "unknown" : "unknown",
2591
+ passed: numberOr(checks.passed, 0),
2592
+ failed: numberOr(checks.failed, 0),
2593
+ pending: numberOr(checks.pending, 0),
2594
+ running: numberOr(checks.runningCount, 0),
2595
+ failedNames: strings(checks.failedNames),
2596
+ checkRuns: readCheckRuns(checks.checkRuns),
2597
+ statuses: readCommitStatuses(checks.statuses)
2598
+ },
2599
+ lintFailed: value.lintFailed === true,
2600
+ approvedBy: logins(value.approvedBy),
2601
+ changesRequestedBy: logins(value.changesRequestedBy),
2602
+ requestedReviewers: logins(value.requestedReviewers),
2603
+ reRequestedReviewers: logins(value.reRequestedReviewers),
2604
+ requestedTeams: strings(value.requestedTeams),
2605
+ assignees: logins(value.assignees),
2606
+ labels: readLabels(value.labels),
2607
+ statusLinks: readStatusLinks(value.statusLinks),
2608
+ openedAt: stringOrNull(flowDates.openedAt),
2609
+ approvedAt: stringOrNull(flowDates.approvedAt),
2610
+ changes: readChanges(value.changes),
2611
+ unresolvedThreads: typeof value.unresolvedThreads === "number" ? value.unresolvedThreads : null,
2612
+ mergeable: enumOrNull(value.mergeable, mergeables) ?? "unknown",
2613
+ closedAt: stringOrNull(flowDates.closedAt),
2614
+ commentActivityAt: stringOrNull(flowDates.lastCommentActivityAt),
2615
+ reviewActivityAt: stringOrNull(flowDates.lastReviewSubmittedAt),
2616
+ reviewCommentActivityAt: stringOrNull(
2617
+ flowDates.lastReviewCommentActivityAt
2618
+ ),
2619
+ bodyEditedAt: stringOrNull(flowDates.lastBodyEditedAt),
2620
+ updatedAt: typeof value.updatedAt === "string" ? value.updatedAt : ""
2621
+ };
2622
+ };
2623
+ const lookupReasons = /* @__PURE__ */ new Set(["forbidden", "noPr", "notTracked"]);
2624
+ const readLookupResult = (value) => {
2625
+ if (isRecord$1(value) && value.found === true) {
2626
+ return { found: true, pr: readPrDetail(value.pr) };
2627
+ }
2628
+ if (isRecord$1(value) && value.found === false && typeof value.reason === "string" && lookupReasons.has(value.reason)) {
2629
+ return {
2630
+ found: false,
2631
+ reason: value.reason
2632
+ };
2633
+ }
2634
+ return { found: false, reason: "notTracked" };
2635
+ };
2636
+ const readBucketPage = (value) => {
2637
+ if (!isRecord$1(value) || typeof value.key !== "string" || !Array.isArray(value.prs)) {
2638
+ throw fail("reviewflow returned a board bucket the client cannot read");
2639
+ }
2640
+ return {
2641
+ key: value.key,
2642
+ title: typeof value.title === "string" ? value.title : value.key,
2643
+ total: numberOr(value.total, value.prs.length),
2644
+ prs: value.prs.map(readPrDetail),
2645
+ nextPage: stringOrNull(value.nextPage)
2646
+ };
2647
+ };
2648
+ const readMe = (value) => {
2649
+ if (!isRecord$1(value) || !isRecord$1(value.user) || typeof value.user.login !== "string" || !Array.isArray(value.accounts)) {
2650
+ throw fail("reviewflow returned a /me the client cannot read");
2651
+ }
2652
+ return {
2653
+ login: value.user.login,
2654
+ accounts: value.accounts.filter(isRecord$1).map((account) => ({
2655
+ login: String(account.login),
2656
+ type: account.type === "Organization" ? "Organization" : "User"
2657
+ }))
2658
+ };
2659
+ };
2660
+ const readRetryAfterMs = (response) => {
2661
+ const seconds = Number(response.headers.get("retry-after") ?? Number.NaN);
2662
+ return Number.isFinite(seconds) && seconds >= 0 ? seconds * 1e3 : void 0;
2663
+ };
2664
+ const responseFailure = async (response) => {
2665
+ const body = await response.json().catch(() => null);
2666
+ const error = isRecord$1(body) && isRecord$1(body.error) ? body.error : null;
2667
+ const detail = error === null ? "" : ` ${String(error.code)}: ${typeof error.message === "string" ? error.message : ""}`;
2668
+ const retryAfter = response.headers.get("retry-after");
2669
+ const retry = retryAfter === null ? "" : ` (retry after ${retryAfter}s)`;
2670
+ return fail(
2671
+ `reviewflow answered ${String(response.status)}${detail}${retry}`,
2672
+ { status: response.status, retryAfterMs: readRetryAfterMs(response) }
2673
+ );
2674
+ };
2675
+ const chunk = (items, size) => Array.from(
2676
+ { length: Math.ceil(items.length / size) },
2677
+ (_, index) => items.slice(index * size, (index + 1) * size)
2678
+ );
2679
+ const createReviewflowClient = ({
2680
+ url,
2681
+ token,
2682
+ fetch = globalThis.fetch
2683
+ }) => {
2684
+ const request = async (path, init = {}) => {
2685
+ const response = await fetch(`${url}/api${path}`, {
2686
+ method: init.method ?? "GET",
2687
+ headers: {
2688
+ authorization: `Bearer ${token}`,
2689
+ accept: "application/json",
2690
+ ...init.body === void 0 ? {} : { "content-type": "application/json" }
2691
+ },
2692
+ body: init.body === void 0 ? void 0 : JSON.stringify(init.body),
2693
+ signal: AbortSignal.timeout(timeoutMs)
2694
+ }).catch((error) => {
2695
+ const cause = error instanceof Error ? error.message : String(error);
2696
+ throw fail(`reviewflow could not be reached at ${url}: ${cause}`);
2697
+ });
2698
+ if (!response.ok) throw await responseFailure(response);
2699
+ return response.json().catch(() => {
2700
+ throw fail("reviewflow answered with a body that is not json");
2701
+ });
2702
+ };
2703
+ const readBucket = async (account, first) => {
2704
+ const prs = [...first.prs];
2705
+ let { nextPage } = first;
2706
+ for (let page = 0; nextPage !== null && page < maxExtraPagesPerBucket; page++) {
2707
+ const query = new URLSearchParams({
2708
+ bucket: first.key,
2709
+ account,
2710
+ page: nextPage
2711
+ });
2712
+ const next = readBucketPage(
2713
+ await request(`/board-bucket/v1?${query.toString()}`)
2714
+ );
2715
+ prs.push(...next.prs);
2716
+ ({ nextPage } = next);
2717
+ }
2718
+ const seen = /* @__PURE__ */ new Set();
2719
+ return {
2720
+ key: first.key,
2721
+ title: first.title,
2722
+ total: first.total,
2723
+ prs: prs.filter((pr) => !seen.has(pr.id) && seen.add(pr.id))
2724
+ };
2725
+ };
2726
+ return {
2727
+ me: async () => readMe(await request("/me/v1")),
2728
+ lookup: async (targets) => {
2729
+ const answers = await Promise.all(
2730
+ chunk(targets, maxLookupTargets).map(async (part) => {
2731
+ const body = await request("/prs-lookup/v1", {
2732
+ method: "POST",
2733
+ body: { targets: part }
2734
+ });
2735
+ if (!isRecord$1(body) || !Array.isArray(body.results) || body.results.length !== part.length) {
2736
+ throw fail("reviewflow returned a lookup the client cannot read");
2737
+ }
2738
+ return body.results.map(readLookupResult);
2739
+ })
2740
+ );
2741
+ return answers.flat();
2742
+ },
2743
+ pr: async (ownerRepo, number) => {
2744
+ const path = ownerRepo.split("/").map((part) => encodeURIComponent(part)).join("/");
2745
+ try {
2746
+ return readPrDetail(await request(`/pr/v1/${path}/${String(number)}`));
2747
+ } catch (error) {
2748
+ if (isReviewflowFailure(error) && (error.status === 403 || error.status === 404)) {
2749
+ return null;
2750
+ }
2751
+ throw error;
2752
+ }
2753
+ },
2754
+ board: async (account) => {
2755
+ const body = await request(
2756
+ `/board/v1?${new URLSearchParams({ account }).toString()}`
2757
+ );
2758
+ if (!isRecord$1(body) || !Array.isArray(body.groups)) {
2759
+ throw fail("reviewflow returned a board the client cannot read");
2760
+ }
2761
+ return Promise.all(
2762
+ body.groups.map(async (group) => {
2763
+ if (!isRecord$1(group) || typeof group.key !== "string" || !Array.isArray(group.buckets)) {
2764
+ throw fail(
2765
+ "reviewflow returned a board group the client cannot read"
2766
+ );
2767
+ }
2768
+ return {
2769
+ key: group.key,
2770
+ title: typeof group.title === "string" ? group.title : group.key,
2771
+ buckets: await Promise.all(
2772
+ group.buckets.map(
2773
+ (bucket) => readBucket(account, readBucketPage(bucket))
2774
+ )
2775
+ )
2776
+ };
2777
+ })
2778
+ );
2779
+ }
2780
+ };
2781
+ };
2782
+
2783
+ const streamIdleTimeoutMs = 6e4;
2784
+ const streamClosedWithinDays = 7;
2785
+ const parseFrame = (block) => {
2786
+ let event = "message";
2787
+ const data = [];
2788
+ for (const line of block.split("\n")) {
2789
+ if (line.startsWith(":")) continue;
2790
+ const separator = line.indexOf(":");
2791
+ const field = separator === -1 ? line : line.slice(0, separator);
2792
+ const value = separator === -1 ? "" : line.slice(separator + 1).replace(/^ /, "");
2793
+ if (field === "event") event = value;
2794
+ else if (field === "data") data.push(value);
2795
+ }
2796
+ return { event, data: data.join("\n") };
2797
+ };
2798
+ const parseData = (frame) => {
2799
+ try {
2800
+ return JSON.parse(frame.data);
2801
+ } catch {
2802
+ return null;
2803
+ }
2804
+ };
2805
+ const toStreamEvent = (frame, logger) => {
2806
+ switch (frame.event) {
2807
+ case "pr": {
2808
+ try {
2809
+ return { type: "pr", pr: readPrDetail(parseData(frame)) };
2810
+ } catch (error) {
2811
+ logger?.warn(
2812
+ "reviewflow streamed a pull request the client cannot read",
2813
+ { error }
2814
+ );
2815
+ return null;
2816
+ }
2817
+ }
2818
+ case "prRemoved": {
2819
+ const data = parseData(frame);
2820
+ return typeof data?._id === "string" ? { type: "prRemoved", id: data._id } : null;
2821
+ }
2822
+ case "ready":
2823
+ return { type: "ready" };
2824
+ case "scopeChanged":
2825
+ return { type: "end", reason: "scopeChanged" };
2826
+ case "reset": {
2827
+ const data = parseData(frame);
2828
+ return {
2829
+ type: "end",
2830
+ reason: `reset: ${typeof data?.reason === "string" ? data.reason : "unknown"}`
2831
+ };
2832
+ }
2833
+ default:
2834
+ return null;
2835
+ }
2836
+ };
2837
+ async function* readReviewflowStream({
2838
+ url,
2839
+ token,
2840
+ signal,
2841
+ fetch = globalThis.fetch,
2842
+ idleTimeoutMs = streamIdleTimeoutMs,
2843
+ logger
2844
+ }) {
2845
+ const controller = new AbortController();
2846
+ let reader;
2847
+ const cancel = () => {
2848
+ controller.abort();
2849
+ reader?.cancel().catch(() => void 0);
2850
+ };
2851
+ signal.addEventListener("abort", cancel, { once: true });
2852
+ let silent = false;
2853
+ let idle;
2854
+ const armIdle = () => {
2855
+ clearTimeout(idle);
2856
+ idle = setTimeout(() => {
2857
+ silent = true;
2858
+ cancel();
2859
+ }, idleTimeoutMs);
2860
+ };
2861
+ const silence = () => fail(
2862
+ `reviewflow's stream went silent for ${String(idleTimeoutMs / 1e3)}s`
2863
+ );
2864
+ try {
2865
+ armIdle();
2866
+ const query = new URLSearchParams({
2867
+ closedWithinDays: String(streamClosedWithinDays)
2868
+ });
2869
+ const response = await fetch(`${url}/api/stream/v1?${query.toString()}`, {
2870
+ headers: {
2871
+ authorization: `Bearer ${token}`,
2872
+ accept: "text/event-stream"
2873
+ },
2874
+ signal: controller.signal
2875
+ });
2876
+ if (!response.ok) throw await responseFailure(response);
2877
+ if (response.body === null) {
2878
+ throw fail("reviewflow answered the stream with no body");
2879
+ }
2880
+ reader = response.body.getReader();
2881
+ const decoder = new TextDecoder();
2882
+ let buffer = "";
2883
+ for (; ; ) {
2884
+ const { value, done } = await reader.read();
2885
+ if (done) break;
2886
+ armIdle();
2887
+ buffer = (buffer + decoder.decode(value, { stream: true })).replaceAll(
2888
+ "\r\n",
2889
+ "\n"
2890
+ );
2891
+ let end = buffer.indexOf("\n\n");
2892
+ while (end !== -1) {
2893
+ const event = toStreamEvent(parseFrame(buffer.slice(0, end)), logger);
2894
+ buffer = buffer.slice(end + 2);
2895
+ if (event !== null) {
2896
+ yield event;
2897
+ if (event.type === "end") return;
2898
+ }
2899
+ end = buffer.indexOf("\n\n");
2900
+ }
2901
+ }
2902
+ if (silent) throw silence();
2903
+ } catch (error) {
2904
+ if (signal.aborted) return;
2905
+ if (silent) throw silence();
2906
+ if (isReviewflowFailure(error)) throw error;
2907
+ const cause = error instanceof Error ? error.message : String(error);
2908
+ throw fail(`reviewflow's stream could not be read at ${url}: ${cause}`);
2909
+ } finally {
2910
+ clearTimeout(idle);
2911
+ signal.removeEventListener("abort", cancel);
2912
+ cancel();
2913
+ }
2914
+ }
2915
+
2916
+ const toBoardPr = (pr) => ({
2917
+ id: pr.id,
2918
+ ownerRepo: pr.ownerRepo,
2919
+ number: pr.number,
2920
+ title: pr.title,
2921
+ url: pr.url,
2922
+ headRef: pr.headRef,
2923
+ state: pr.state,
2924
+ reviewState: pr.reviewState,
2925
+ author: pr.author,
2926
+ checksFailed: pr.checks.failed,
2927
+ checksConclusion: pr.checks.conclusion,
2928
+ checksRunning: pr.checks.running,
2929
+ failedChecks: pr.checks.failedNames,
2930
+ lintFailed: pr.lintFailed,
2931
+ approvedBy: pr.approvedBy,
2932
+ changesRequestedBy: pr.changesRequestedBy,
2933
+ requestedReviewers: pr.requestedReviewers,
2934
+ reRequestedReviewers: pr.reRequestedReviewers,
2935
+ requestedTeams: pr.requestedTeams,
2936
+ assignees: pr.assignees,
2937
+ statusLinks: pr.statusLinks,
2938
+ changes: pr.changes,
2939
+ openedAt: pr.openedAt,
2940
+ approvedAt: pr.approvedAt,
2941
+ unresolvedThreads: pr.unresolvedThreads,
2942
+ mergeable: pr.mergeable,
2943
+ updatedAt: pr.updatedAt
2944
+ });
2945
+ const accountOfOwnerRepo = (ownerRepo) => ownerRepo.slice(0, ownerRepo.indexOf("/")).toLowerCase();
2946
+ const isPullRequestOnBoard = (board, { ownerRepo, number }) => {
2947
+ const wanted = ownerRepo.toLowerCase();
2948
+ return board?.groups.some(
2949
+ (group) => group.buckets.some(
2950
+ (bucket) => bucket.prs.some(
2951
+ (pr) => pr.number === number && pr.ownerRepo.toLowerCase() === wanted
2952
+ )
2953
+ )
2954
+ ) ?? false;
2955
+ };
2956
+ const withAccountBoard = (snapshot, account, board) => ({ ...snapshot, [account]: board });
2957
+ const readAccountBoards = async (client, accounts) => {
2958
+ const settled = await Promise.allSettled(
2959
+ accounts.map(async (account) => ({
2960
+ account,
2961
+ groups: await client.board(account)
2962
+ }))
2963
+ );
2964
+ const read = { boards: [], errors: [] };
2965
+ settled.forEach((result, index) => {
2966
+ if (result.status === "fulfilled") read.boards.push(result.value);
2967
+ else {
2968
+ const { reason } = result;
2969
+ read.errors.push({
2970
+ account: accounts[index],
2971
+ message: reason instanceof Error ? reason.message : String(reason)
2972
+ });
2973
+ }
2974
+ });
2975
+ return read;
2976
+ };
2977
+ const assembleBoard = (snapshot, viewer, accounts = Object.keys(snapshot)) => {
2978
+ const merged = /* @__PURE__ */ new Map();
2979
+ const fetchedAts = [];
2980
+ for (const account of accounts) {
2981
+ const read = snapshot[account];
2982
+ if (read === void 0) continue;
2983
+ fetchedAts.push(read.fetchedAt);
2984
+ let position = 0;
2985
+ for (const group of read.groups) {
2986
+ for (const bucket of group.buckets) {
2987
+ const existing = merged.get(bucket.key);
2988
+ const prs = bucket.prs.map((pr) => toBoardPr(pr));
2989
+ if (existing === void 0) {
2990
+ merged.set(bucket.key, {
2991
+ key: bucket.key,
2992
+ group: { key: group.key, title: group.title },
2993
+ title: bucket.title,
2994
+ total: bucket.total,
2995
+ position,
2996
+ prs,
2997
+ fetchedAt: read.fetchedAt
2998
+ });
2999
+ } else {
3000
+ existing.total += bucket.total;
3001
+ existing.position = Math.min(existing.position, position);
3002
+ const seen = new Set(existing.prs.map((pr) => pr.id));
3003
+ existing.prs.push(...prs.filter((pr) => !seen.has(pr.id)));
3004
+ }
3005
+ position++;
3006
+ }
3007
+ }
3008
+ }
3009
+ if (merged.size === 0) return { board: null, viewer };
3010
+ const groups = [];
3011
+ for (const bucket of [...merged.values()].toSorted(
3012
+ (a, b) => a.position - b.position
3013
+ )) {
3014
+ const entry = {
3015
+ key: bucket.key,
3016
+ title: bucket.title,
3017
+ total: bucket.total,
3018
+ prs: bucket.prs
3019
+ };
3020
+ const last = groups.at(-1);
3021
+ if (last?.key === bucket.group.key) last.buckets.push(entry);
3022
+ else {
3023
+ groups.push({
3024
+ key: bucket.group.key,
3025
+ title: bucket.group.title,
3026
+ buckets: [entry]
3027
+ });
3028
+ }
3029
+ }
3030
+ const [oldest] = fetchedAts.toSorted((a, b) => a.localeCompare(b));
3031
+ return { board: { fetchedAt: oldest ?? "", groups }, viewer };
3032
+ };
3033
+ const applyStreamEvent = (snapshot, event) => {
3034
+ if (event.type !== "pr" && event.type !== "prRemoved") {
3035
+ return { snapshot, changed: false, readAccounts: [] };
3036
+ }
3037
+ const id = event.type === "pr" ? event.pr.id : event.id;
3038
+ let changed = false;
3039
+ const holding = /* @__PURE__ */ new Set();
3040
+ const next = {};
3041
+ for (const [account, read] of Object.entries(snapshot)) {
3042
+ const groups = read.groups.map((group) => ({
3043
+ ...group,
3044
+ buckets: group.buckets.map((bucket) => {
3045
+ if (!bucket.prs.some((pr) => pr.id === id)) return bucket;
3046
+ holding.add(account);
3047
+ changed = true;
3048
+ if (event.type === "pr") {
3049
+ return {
3050
+ ...bucket,
3051
+ prs: bucket.prs.map((pr) => pr.id === id ? event.pr : pr)
3052
+ };
3053
+ }
3054
+ return {
3055
+ ...bucket,
3056
+ total: Math.max(0, bucket.total - 1),
3057
+ prs: bucket.prs.filter((pr) => pr.id !== id)
3058
+ };
3059
+ })
3060
+ }));
3061
+ next[account] = { ...read, groups };
3062
+ }
3063
+ const readAccounts = new Set(holding);
3064
+ if (event.type === "pr") {
3065
+ const owner = accountOfOwnerRepo(event.pr.ownerRepo);
3066
+ for (const account of Object.keys(snapshot)) {
3067
+ if (account.toLowerCase() === owner) readAccounts.add(account);
3068
+ }
3069
+ }
3070
+ return {
3071
+ snapshot: changed ? next : snapshot,
3072
+ changed,
3073
+ readAccounts: [...readAccounts]
3074
+ };
3075
+ };
3076
+
3077
+ const lingerMs = 5 * 6e4;
3078
+ const boardReadDelayMs = 3e3;
3079
+ const reconnectDelayMs = 1e3;
3080
+ const minRetryMs = 2e3;
3081
+ const maxRetryMs = 5 * 6e4;
3082
+ const messageOf = (error) => error instanceof Error ? error.message : String(error);
3083
+ const retryDelayMs = (error, failures) => {
3084
+ if (isReviewflowFailure(error)) {
3085
+ if (error.retryAfterMs !== void 0) return error.retryAfterMs;
3086
+ if (error.status === 401) return maxRetryMs;
3087
+ }
3088
+ return Math.min(minRetryMs * 2 ** (failures - 1), maxRetryMs);
3089
+ };
3090
+ const wait = async (ms, signal) => new Promise((resolve) => {
3091
+ const done = () => {
3092
+ clearTimeout(timer);
3093
+ signal.removeEventListener("abort", done);
3094
+ resolve();
3095
+ };
3096
+ const timer = setTimeout(done, ms);
3097
+ signal.addEventListener("abort", done, { once: true });
3098
+ });
3099
+ const createReviewflowFollower = ({
3100
+ connect,
3101
+ accounts,
3102
+ onPr,
3103
+ onPrRemoved,
3104
+ onAccountBoard,
3105
+ onBoardError,
3106
+ onPullRequestChanged,
3107
+ onStateChanged,
3108
+ onLog
3109
+ }) => {
3110
+ let watched = false;
3111
+ let state = "off";
3112
+ let controller = null;
3113
+ let linger;
3114
+ const boardReads = /* @__PURE__ */ new Map();
3115
+ const accountOfPr = /* @__PURE__ */ new Map();
3116
+ const lastSeen = /* @__PURE__ */ new Map();
3117
+ const setState = (next, error) => {
3118
+ if (state === next && error === void 0) return;
3119
+ state = next;
3120
+ onStateChanged?.(next, error ?? null);
3121
+ };
3122
+ const comparePr = (pr) => {
3123
+ const previous = lastSeen.get(pr.id);
3124
+ lastSeen.set(pr.id, pr);
3125
+ if (previous === void 0 || onPullRequestChanged === void 0) return;
3126
+ const parts = changedPullRequestParts(previous, pr);
3127
+ if (parts.length === 0) return;
3128
+ onPullRequestChanged({ ownerRepo: pr.ownerRepo, number: pr.number, parts });
3129
+ };
3130
+ const linked = (account) => accounts().find((owner) => owner.toLowerCase() === account.toLowerCase());
3131
+ const readBoard = async (connection, account, signal) => {
3132
+ const read = boardReads.get(account);
3133
+ if (read === void 0 || signal.aborted) return false;
3134
+ read.timer = void 0;
3135
+ read.inFlight = true;
3136
+ read.again = false;
3137
+ try {
3138
+ const groups = await connection.client.board(account);
3139
+ if (signal.aborted) return false;
3140
+ onAccountBoard(account, groups);
3141
+ } catch (error) {
3142
+ if (signal.aborted) return false;
3143
+ onBoardError?.(account, messageOf(error));
3144
+ } finally {
3145
+ read.inFlight = false;
3146
+ }
3147
+ return read.again;
3148
+ };
3149
+ const scheduleBoard = (connection, account, signal) => {
3150
+ const owner = linked(account);
3151
+ if (owner === void 0) return;
3152
+ const read = boardReads.get(owner) ?? { inFlight: false, again: false };
3153
+ boardReads.set(owner, read);
3154
+ if (read.inFlight) {
3155
+ read.again = true;
3156
+ return;
3157
+ }
3158
+ read.timer ??= setTimeout(() => {
3159
+ readBoard(connection, owner, signal).then((again) => {
3160
+ if (again) scheduleBoard(connection, owner, signal);
3161
+ }).catch((error) => {
3162
+ onLog?.({
3163
+ level: "warn",
3164
+ message: "reviewflow board read failed",
3165
+ error
3166
+ });
3167
+ });
3168
+ }, boardReadDelayMs);
3169
+ };
3170
+ const scheduleEveryBoard = (connection, signal) => {
3171
+ for (const owner of accounts()) scheduleBoard(connection, owner, signal);
3172
+ };
3173
+ const follow = async (connection, signal, onReady) => {
3174
+ const scope = [];
3175
+ let ready = false;
3176
+ for await (const event of connection.events(signal)) {
3177
+ if (event.type === "end") {
3178
+ onLog?.({
3179
+ level: "info",
3180
+ message: "reviewflow ended the stream",
3181
+ fields: { reason: event.reason }
3182
+ });
3183
+ scheduleEveryBoard(connection, signal);
3184
+ return ready;
3185
+ }
3186
+ if (event.type === "ready") {
3187
+ ready = true;
3188
+ onReady();
3189
+ onLog?.({
3190
+ level: "info",
3191
+ message: "reviewflow stream ready",
3192
+ fields: { prs: scope.length }
3193
+ });
3194
+ for (const pr of scope.splice(0)) onPr(pr, "scope");
3195
+ setState("live");
3196
+ scheduleEveryBoard(connection, signal);
3197
+ } else if (event.type === "pr") {
3198
+ accountOfPr.set(event.pr.id, accountOfOwnerRepo(event.pr.ownerRepo));
3199
+ comparePr(event.pr);
3200
+ if (ready) {
3201
+ onPr(event.pr, "change");
3202
+ scheduleBoard(
3203
+ connection,
3204
+ accountOfOwnerRepo(event.pr.ownerRepo),
3205
+ signal
3206
+ );
3207
+ } else {
3208
+ scope.push(event.pr);
3209
+ }
3210
+ } else if (ready) {
3211
+ const account = accountOfPr.get(event.id);
3212
+ accountOfPr.delete(event.id);
3213
+ lastSeen.delete(event.id);
3214
+ onPrRemoved(event.id);
3215
+ if (account === void 0) scheduleEveryBoard(connection, signal);
3216
+ else scheduleBoard(connection, account, signal);
3217
+ }
3218
+ }
3219
+ return ready;
3220
+ };
3221
+ const run = async (signal) => {
3222
+ let failures = 0;
3223
+ const resetFailures = () => {
3224
+ failures = 0;
3225
+ };
3226
+ while (!signal.aborted) {
3227
+ const connection = connect();
3228
+ if (connection === null) {
3229
+ setState("off");
3230
+ return;
3231
+ }
3232
+ let delayMs = reconnectDelayMs;
3233
+ setState("connecting");
3234
+ try {
3235
+ const reachedReady = await follow(connection, signal, resetFailures);
3236
+ if (!reachedReady && !signal.aborted) {
3237
+ failures++;
3238
+ delayMs = retryDelayMs(void 0, failures);
3239
+ onLog?.({
3240
+ level: "warn",
3241
+ message: "reviewflow ended the stream before it was ready",
3242
+ fields: { retryInMs: delayMs }
3243
+ });
3244
+ }
3245
+ } catch (error) {
3246
+ if (signal.aborted) return;
3247
+ failures++;
3248
+ delayMs = retryDelayMs(error, failures);
3249
+ onLog?.({
3250
+ level: "warn",
3251
+ message: "reviewflow stream failed",
3252
+ error,
3253
+ fields: { retryInMs: delayMs }
3254
+ });
3255
+ setState("error", messageOf(error));
3256
+ }
3257
+ if (state === "live") setState("connecting");
3258
+ await wait(delayMs, signal);
3259
+ }
3260
+ };
3261
+ const stop = () => {
3262
+ controller?.abort();
3263
+ controller = null;
3264
+ for (const read of boardReads.values()) clearTimeout(read.timer);
3265
+ boardReads.clear();
3266
+ accountOfPr.clear();
3267
+ setState("off");
3268
+ };
3269
+ const start = () => {
3270
+ if (controller !== null) return;
3271
+ const current = new AbortController();
3272
+ controller = current;
3273
+ run(current.signal).catch((error) => {
3274
+ onLog?.({
3275
+ level: "error",
3276
+ message: "reviewflow stream stopped",
3277
+ error
3278
+ });
3279
+ }).finally(() => {
3280
+ if (controller === current) controller = null;
3281
+ });
3282
+ };
3283
+ return {
3284
+ setWatched: (value) => {
3285
+ watched = value;
3286
+ clearTimeout(linger);
3287
+ linger = void 0;
3288
+ if (value) start();
3289
+ else if (controller !== null) linger = setTimeout(stop, lingerMs);
3290
+ },
3291
+ restart: () => {
3292
+ const wasOpen = controller !== null;
3293
+ stop();
3294
+ lastSeen.clear();
3295
+ if (watched || wasOpen) start();
3296
+ },
3297
+ state: () => state,
3298
+ close: () => {
3299
+ watched = false;
3300
+ clearTimeout(linger);
3301
+ stop();
3302
+ }
3303
+ };
3304
+ };
3305
+
3306
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
3307
+ const insertCheckoutEntry = (source, ownerRepo) => {
3308
+ let parsed;
3309
+ if (source.trim() === "") {
3310
+ parsed = {};
3311
+ } else {
3312
+ try {
3313
+ parsed = JSON.parse(source);
3314
+ } catch {
3315
+ return null;
3316
+ }
3317
+ }
3318
+ if (!isRecord(parsed)) return null;
3319
+ const repos = parsed.repos ?? {};
3320
+ if (!isRecord(repos)) return null;
3321
+ const wanted = ownerRepo.toLowerCase();
3322
+ const existing = Object.keys(repos).find(
3323
+ (key2) => key2.toLowerCase() === wanted
3324
+ );
3325
+ const key = existing ?? ownerRepo;
3326
+ const next = existing === void 0 ? { ...repos, [key]: "" } : repos;
3327
+ const text = `${JSON.stringify({ ...parsed, repos: next }, null, 2)}
3328
+ `;
3329
+ const marker = `${JSON.stringify(key)}: "`;
3330
+ const reposAt = text.indexOf('\n "repos": ');
3331
+ const at = text.indexOf(marker, reposAt);
3332
+ if (at === -1) return null;
3333
+ const value = at + marker.length;
3334
+ return { source: text, cursor: value };
3335
+ };
3336
+
3337
+ export { accountOfOwnerRepo, applyPullRequestEdit, applyStreamEvent, applySuggestionsToText, assembleBoard, boardReadDelayMs, buildPrQuery, changedPullRequestParts, checkOutcomes, checksFromReviewflow, coAuthorTrailers, commitSuggestions, createGitHubTransport, createReviewflowClient, createReviewflowFollower, diffLines, diffPaths, diffThemeNames, diffThemeOptions, diffThemes, editPullRequest, fail, fetchCompareDiff, fetchPullRequestCandidates, fetchPullRequestHead, fetchPullRequestPart, fetchPullRequestRevisions, insertCheckoutEntry, interpretGraphql, isAttachablePath, isDiffTheme, isDiffWhitespace, isPullRequestOnBoard, isPullRequestPart, isPullRequestReactionContent, isReviewflowFailure, lingerMs, maxExtraPagesPerBucket, maxLookupTargets, maxRetryMs, parsePrQueryResponse, parsePullRequestCandidates, parsePullRequestChecks, parsePullRequestCommits, parsePullRequestDetail, parsePullRequestEdit, parsePullRequestFiles, parsePullRequestRevisions, parsePullRequestTimeline, parseSuggestions, partFingerprint, planSuggestions, prFields, pullRequestCandidatesQuery, pullRequestChecksQuery, pullRequestCommitsQuery, pullRequestDetailQuery, pullRequestEditMovesBoard, pullRequestFilesQuery, pullRequestHeadQuery, pullRequestParts, pullRequestPartsStaleAfter, pullRequestReactionContents, pullRequestRevisionsQuery, pullRequestTimelineQuery, readAccountBoards, readPrDetail, readReviewflowStream, reconnectDelayMs, responseFailure, restErrorMessage, reviewflowErrorCode, splitOwnerRepo, streamClosedWithinDays, streamIdleTimeoutMs, subjectsOf, suggestionCommitRefusal, suggestionOriginalLines, suggestionRangesOverlap, toBoardPr, toFetchedPr, withAccountBoard, withFileViewed };
3338
+ //# sourceMappingURL=index-node.mjs.map