@reviewflow/sdk 0.0.0-stage → 6.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +3328 -0
  6. package/build/index-node.mjs +3328 -0
  7. package/package.json +135 -3
  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,3328 @@
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
+ start_line: comment.startLine,
1027
+ start_side: comment.startSide
1028
+ }
1029
+ });
1030
+ const reviewEventNames = {
1031
+ approve: "APPROVE",
1032
+ comment: "COMMENT",
1033
+ requestChanges: "REQUEST_CHANGES"
1034
+ };
1035
+ const resolveCommittedThreads = async (transport, threadIds, logger) => {
1036
+ const failures = [];
1037
+ for (const threadId of threadIds) {
1038
+ try {
1039
+ await runMutation(
1040
+ transport,
1041
+ resolveThreadMutation,
1042
+ { threadId },
1043
+ "resolving the thread"
1044
+ );
1045
+ } catch (error) {
1046
+ logger?.warn("thread of a committed suggestion left unresolved", {
1047
+ threadId,
1048
+ error
1049
+ });
1050
+ failures.push(error instanceof Error ? error.message : String(error));
1051
+ }
1052
+ }
1053
+ if (failures.length === 0) return null;
1054
+ return `The suggestions were committed, but ${String(failures.length)} thread${failures.length === 1 ? "" : "s"} stayed unresolved: ${failures.join("; ")}`;
1055
+ };
1056
+ const editPullRequest = async (transport, { ownerRepo, number, edit }, options = {}) => {
1057
+ const split = splitOwnerRepo(ownerRepo);
1058
+ if (split === null) throw new Error(`${ownerRepo} is not owner/repo`);
1059
+ const repoPath = `repos/${encodeURIComponent(split.owner)}/${encodeURIComponent(split.name)}`;
1060
+ const pullPath = `${repoPath}/pulls/${String(number)}`;
1061
+ const issuePath = `${repoPath}/issues/${String(number)}`;
1062
+ switch (edit.kind) {
1063
+ case "title": {
1064
+ await transport.rest({
1065
+ method: "PATCH",
1066
+ path: pullPath,
1067
+ body: { title: edit.title }
1068
+ });
1069
+ return null;
1070
+ }
1071
+ case "labels": {
1072
+ if (edit.add.length > 0) {
1073
+ await transport.rest({
1074
+ method: "POST",
1075
+ path: `${issuePath}/labels`,
1076
+ body: { labels: edit.add }
1077
+ });
1078
+ }
1079
+ for (const label of edit.remove) {
1080
+ await transport.rest({
1081
+ method: "DELETE",
1082
+ path: `${issuePath}/labels/${encodeURIComponent(label)}`
1083
+ });
1084
+ }
1085
+ return null;
1086
+ }
1087
+ case "reviewers": {
1088
+ if (edit.add.length > 0 || edit.addTeams.length > 0) {
1089
+ await transport.rest({
1090
+ method: "POST",
1091
+ path: `${pullPath}/requested_reviewers`,
1092
+ body: { reviewers: edit.add, team_reviewers: edit.addTeams }
1093
+ });
1094
+ }
1095
+ if (edit.remove.length > 0 || edit.removeTeams.length > 0) {
1096
+ await transport.rest({
1097
+ method: "DELETE",
1098
+ path: `${pullPath}/requested_reviewers`,
1099
+ body: { reviewers: edit.remove, team_reviewers: edit.removeTeams }
1100
+ });
1101
+ }
1102
+ return null;
1103
+ }
1104
+ case "draft": {
1105
+ await setDraft(transport, { ...split, number, draft: edit.draft });
1106
+ return null;
1107
+ }
1108
+ case "body": {
1109
+ await transport.rest({
1110
+ method: "PATCH",
1111
+ path: pullPath,
1112
+ body: { body: edit.body }
1113
+ });
1114
+ return null;
1115
+ }
1116
+ case "assignees": {
1117
+ if (edit.add.length > 0) {
1118
+ await transport.rest({
1119
+ method: "POST",
1120
+ path: `${issuePath}/assignees`,
1121
+ body: { assignees: edit.add }
1122
+ });
1123
+ }
1124
+ if (edit.remove.length > 0) {
1125
+ await transport.rest({
1126
+ method: "DELETE",
1127
+ path: `${issuePath}/assignees`,
1128
+ body: { assignees: edit.remove }
1129
+ });
1130
+ }
1131
+ return null;
1132
+ }
1133
+ case "state": {
1134
+ await transport.rest({
1135
+ method: "PATCH",
1136
+ path: pullPath,
1137
+ body: { state: edit.state }
1138
+ });
1139
+ return null;
1140
+ }
1141
+ case "merge": {
1142
+ await transport.rest({
1143
+ method: "PUT",
1144
+ path: `${pullPath}/merge`,
1145
+ body: {
1146
+ merge_method: edit.method,
1147
+ sha: edit.expectedHeadSha,
1148
+ ...edit.commitMessage === void 0 ? {} : {
1149
+ commit_title: edit.commitMessage.title,
1150
+ commit_message: edit.commitMessage.body
1151
+ }
1152
+ }
1153
+ });
1154
+ return null;
1155
+ }
1156
+ case "updateBranch": {
1157
+ await transport.rest({
1158
+ method: "PUT",
1159
+ path: `${pullPath}/update-branch`,
1160
+ body: { expected_head_sha: edit.expectedHeadSha }
1161
+ });
1162
+ return null;
1163
+ }
1164
+ case "comment": {
1165
+ await transport.rest({
1166
+ method: "POST",
1167
+ path: `${issuePath}/comments`,
1168
+ body: { body: edit.body }
1169
+ });
1170
+ return null;
1171
+ }
1172
+ case "editComment": {
1173
+ const id = String(edit.databaseId);
1174
+ if (edit.target === "review") {
1175
+ await transport.rest({
1176
+ method: "PUT",
1177
+ path: `${pullPath}/reviews/${id}`,
1178
+ body: { body: edit.body }
1179
+ });
1180
+ return null;
1181
+ }
1182
+ await transport.rest({
1183
+ method: "PATCH",
1184
+ path: edit.target === "issueComment" ? `${repoPath}/issues/comments/${id}` : `${repoPath}/pulls/comments/${id}`,
1185
+ body: { body: edit.body }
1186
+ });
1187
+ return null;
1188
+ }
1189
+ case "reply": {
1190
+ await transport.rest({
1191
+ method: "POST",
1192
+ path: `${pullPath}/comments/${String(edit.inReplyTo)}/replies`,
1193
+ body: { body: edit.body }
1194
+ });
1195
+ return null;
1196
+ }
1197
+ case "reaction": {
1198
+ await runMutation(
1199
+ transport,
1200
+ edit.reacted ? addReactionMutation : removeReactionMutation,
1201
+ {
1202
+ subjectId: edit.subjectId,
1203
+ content: reactionContentNames[edit.content]
1204
+ },
1205
+ "the reaction"
1206
+ );
1207
+ return null;
1208
+ }
1209
+ case "resolveThread": {
1210
+ await runMutation(
1211
+ transport,
1212
+ edit.resolved ? resolveThreadMutation : unresolveThreadMutation,
1213
+ { threadId: edit.threadId },
1214
+ "the thread change"
1215
+ );
1216
+ return null;
1217
+ }
1218
+ case "viewed": {
1219
+ await setViewed(transport, {
1220
+ ...split,
1221
+ number,
1222
+ path: edit.path,
1223
+ viewed: edit.viewed
1224
+ });
1225
+ return null;
1226
+ }
1227
+ case "review": {
1228
+ if (edit.event !== "comment") {
1229
+ await assertVerdictOnHead(transport, {
1230
+ ...split,
1231
+ number,
1232
+ commitId: edit.commitId
1233
+ });
1234
+ }
1235
+ await transport.rest({
1236
+ method: "POST",
1237
+ path: `${pullPath}/reviews`,
1238
+ body: {
1239
+ commit_id: edit.commitId,
1240
+ ...edit.body === "" ? {} : { body: edit.body },
1241
+ event: reviewEventNames[edit.event],
1242
+ comments: edit.comments.map(restComment)
1243
+ }
1244
+ });
1245
+ return null;
1246
+ }
1247
+ case "applySuggestions": {
1248
+ return resolveCommittedThreads(
1249
+ transport,
1250
+ await commitSuggestions(transport, { ownerRepo, number, edit }),
1251
+ options.logger
1252
+ );
1253
+ }
1254
+ case "inlineComment": {
1255
+ await transport.rest({
1256
+ method: "POST",
1257
+ path: `${pullPath}/comments`,
1258
+ body: { commit_id: edit.commitId, ...restComment(edit.comment) }
1259
+ });
1260
+ return null;
1261
+ }
1262
+ default: {
1263
+ throw new Error("unknown edit");
1264
+ }
1265
+ }
1266
+ };
1267
+ const applyPullRequestEdit = async (transport, request, options = {}) => {
1268
+ const followUpError = await editPullRequest(transport, request, options);
1269
+ const { edit } = request;
1270
+ if (edit.kind !== "viewed") return followUpError;
1271
+ try {
1272
+ options.onFileViewed?.(edit.path, edit.viewed);
1273
+ } catch (error) {
1274
+ options.logger?.error("viewed mark not applied to the last read", {
1275
+ error
1276
+ });
1277
+ }
1278
+ return followUpError;
1279
+ };
1280
+
1281
+ const fetchCompareDiff = (transport, { ownerRepo, base, head }) => {
1282
+ const [owner, name, ...rest] = ownerRepo.split("/");
1283
+ if (owner === void 0 || name === void 0 || rest.length > 0) {
1284
+ throw new Error(`${ownerRepo} is not owner/repo`);
1285
+ }
1286
+ return transport.text({
1287
+ path: `repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/compare/${base}...${head}`,
1288
+ accept: "application/vnd.github.diff"
1289
+ });
1290
+ };
1291
+
1292
+ const maxDiffCells = 16e4;
1293
+ const toLines = (value) => {
1294
+ if (value === "") return [];
1295
+ const lines = value.split("\n");
1296
+ if (lines.at(-1) === "") lines.pop();
1297
+ return lines;
1298
+ };
1299
+ const context = (text) => ({ kind: "context", text });
1300
+ const removed = (text) => ({ kind: "removed", text });
1301
+ const added = (text) => ({ kind: "added", text });
1302
+ const wholeSides = (before, after) => [
1303
+ ...before.map(removed),
1304
+ ...after.map(added)
1305
+ ];
1306
+ const shortestEdit = (before, after) => {
1307
+ const height = before.length;
1308
+ const width = after.length + 1;
1309
+ const common = new Uint32Array((height + 1) * width);
1310
+ for (let row2 = height - 1; row2 >= 0; row2--) {
1311
+ const beforeLine = before[row2];
1312
+ for (let column2 = after.length - 1; column2 >= 0; column2--) {
1313
+ common[row2 * width + column2] = beforeLine === after[column2] ? (common[(row2 + 1) * width + column2 + 1] ?? 0) + 1 : Math.max(
1314
+ common[(row2 + 1) * width + column2] ?? 0,
1315
+ common[row2 * width + column2 + 1] ?? 0
1316
+ );
1317
+ }
1318
+ }
1319
+ const lines = [];
1320
+ let row = 0;
1321
+ let column = 0;
1322
+ while (row < height && column < after.length) {
1323
+ if (before[row] === after[column]) {
1324
+ lines.push(context(before[row] ?? ""));
1325
+ row++;
1326
+ column++;
1327
+ continue;
1328
+ }
1329
+ if ((common[(row + 1) * width + column] ?? 0) >= (common[row * width + column + 1] ?? 0)) {
1330
+ lines.push(removed(before[row] ?? ""));
1331
+ row++;
1332
+ continue;
1333
+ }
1334
+ lines.push(added(after[column] ?? ""));
1335
+ column++;
1336
+ }
1337
+ return [
1338
+ ...lines,
1339
+ ...before.slice(row).map(removed),
1340
+ ...after.slice(column).map(added)
1341
+ ];
1342
+ };
1343
+ const diffLines = ({
1344
+ before,
1345
+ after
1346
+ }) => {
1347
+ const beforeLines = toLines(before);
1348
+ const afterLines = toLines(after);
1349
+ let head = 0;
1350
+ while (head < beforeLines.length && head < afterLines.length && beforeLines[head] === afterLines[head]) {
1351
+ head++;
1352
+ }
1353
+ let tail = 0;
1354
+ while (tail < beforeLines.length - head && tail < afterLines.length - head && beforeLines[beforeLines.length - 1 - tail] === afterLines[afterLines.length - 1 - tail]) {
1355
+ tail++;
1356
+ }
1357
+ const beforeMiddle = beforeLines.slice(head, beforeLines.length - tail);
1358
+ const afterMiddle = afterLines.slice(head, afterLines.length - tail);
1359
+ return [
1360
+ ...beforeLines.slice(0, head).map(context),
1361
+ ...beforeMiddle.length * afterMiddle.length > maxDiffCells ? wholeSides(beforeMiddle, afterMiddle) : shortestEdit(beforeMiddle, afterMiddle),
1362
+ ...beforeLines.slice(beforeLines.length - tail).map(context)
1363
+ ];
1364
+ };
1365
+
1366
+ const imgTag = /<img\b[^>]*>/gi;
1367
+ const decodeAttribute = (value) => value.replaceAll("&quot;", '"').replaceAll("&#39;", "'").replaceAll("&lt;", "<").replaceAll("&gt;", ">").replaceAll("&amp;", "&");
1368
+ const attribute = (tag, name) => {
1369
+ const match = new RegExp(`\\s${name}="([^"]*)"`, "i").exec(tag);
1370
+ return match?.[1] === void 0 ? null : decodeAttribute(match[1]);
1371
+ };
1372
+ const imageProxiesOf = (html) => {
1373
+ const proxies = {};
1374
+ for (const [tag] of (html ?? "").matchAll(imgTag)) {
1375
+ const src = attribute(tag, "src");
1376
+ const canonical = attribute(tag, "data-canonical-src");
1377
+ if (src !== null && canonical !== null && src !== canonical) {
1378
+ proxies[canonical] = src;
1379
+ }
1380
+ }
1381
+ return proxies;
1382
+ };
1383
+ const mergeImageProxies = (htmls) => Object.assign({}, ...htmls.map(imageProxiesOf));
1384
+
1385
+ const timelineItemTypes = [
1386
+ "ISSUE_COMMENT",
1387
+ "PULL_REQUEST_REVIEW",
1388
+ "PULL_REQUEST_COMMIT",
1389
+ "ASSIGNED_EVENT",
1390
+ "UNASSIGNED_EVENT",
1391
+ "LABELED_EVENT",
1392
+ "UNLABELED_EVENT",
1393
+ "REVIEW_REQUESTED_EVENT",
1394
+ "REVIEW_REQUEST_REMOVED_EVENT",
1395
+ "CLOSED_EVENT",
1396
+ "REOPENED_EVENT",
1397
+ "MERGED_EVENT",
1398
+ "READY_FOR_REVIEW_EVENT",
1399
+ "CONVERT_TO_DRAFT_EVENT",
1400
+ "HEAD_REF_FORCE_PUSHED_EVENT",
1401
+ "BASE_REF_CHANGED_EVENT",
1402
+ "RENAMED_TITLE_EVENT",
1403
+ "REVIEW_DISMISSED_EVENT",
1404
+ "AUTO_MERGE_ENABLED_EVENT",
1405
+ "AUTO_MERGE_DISABLED_EVENT"
1406
+ ].join(", ");
1407
+ const assigneeFields = "__typename ... on User { login } ... on Bot { login } ... on Mannequin { login } ... on Organization { login }";
1408
+ const reviewerFields = "__typename ... on User { login } ... on Bot { login } ... on Mannequin { login } ... on Team { name }";
1409
+ const reactionFields = "viewerCanReact reactionGroups { content viewerHasReacted reactors { totalCount } }";
1410
+ const pullRequestQuery = ({
1411
+ name,
1412
+ viewer = false,
1413
+ repositoryFields = "",
1414
+ fields
1415
+ }) => `query ${name}($owner: String!, $name: String!, $number: Int!) {${viewer ? "\n viewer { login }" : ""}
1416
+ repository(owner: $owner, name: $name) {${repositoryFields}
1417
+ pullRequest(number: $number) {${fields}
1418
+ }
1419
+ }
1420
+ }`;
1421
+ const pullRequestDetailQuery = pullRequestQuery({
1422
+ name: "PullRequestDetail",
1423
+ viewer: true,
1424
+ repositoryFields: `
1425
+ mergeCommitAllowed
1426
+ squashMergeAllowed
1427
+ rebaseMergeAllowed
1428
+ viewerDefaultMergeMethod`,
1429
+ fields: `
1430
+ id
1431
+ number
1432
+ url
1433
+ title
1434
+ body
1435
+ bodyHTML
1436
+ state
1437
+ isDraft
1438
+ author { login avatarUrl }
1439
+ headRefName
1440
+ baseRefName
1441
+ headRefOid
1442
+ baseRefOid
1443
+ createdAt
1444
+ updatedAt
1445
+ lastEditedAt
1446
+ additions
1447
+ deletions
1448
+ changedFiles
1449
+ mergeable
1450
+ mergeStateStatus
1451
+ viewerCanUpdate
1452
+ viewerCanUpdateBranch
1453
+ viewerCanApplySuggestion
1454
+ mergeCommitHeadline: viewerMergeHeadlineText(mergeType: MERGE)
1455
+ mergeCommitBody: viewerMergeBodyText(mergeType: MERGE)
1456
+ squashCommitHeadline: viewerMergeHeadlineText(mergeType: SQUASH)
1457
+ squashCommitBody: viewerMergeBodyText(mergeType: SQUASH)
1458
+ ${reactionFields}
1459
+ reviewDecision
1460
+ labels(first: 50) { nodes { name color description } }
1461
+ assignees(first: 20) { nodes { login avatarUrl } }
1462
+ reviewRequests(first: 50) {
1463
+ nodes {
1464
+ requestedReviewer {
1465
+ __typename
1466
+ ... on User { login avatarUrl }
1467
+ ... on Bot { login avatarUrl }
1468
+ ... on Mannequin { login avatarUrl }
1469
+ ... on Team { slug name }
1470
+ }
1471
+ }
1472
+ }
1473
+ latestReviews(first: 50) { nodes { author { login } state submittedAt } }
1474
+ commits { totalCount }`
1475
+ });
1476
+ const pullRequestChecksQuery = pullRequestQuery({
1477
+ name: "PullRequestChecks",
1478
+ fields: `
1479
+ headCommits: commits(last: 1) {
1480
+ nodes {
1481
+ commit {
1482
+ statusCheckRollup {
1483
+ contexts(first: 100) {
1484
+ totalCount
1485
+ nodes {
1486
+ __typename
1487
+ ... on CheckRun { name status conclusion title detailsUrl }
1488
+ ... on StatusContext { context state description targetUrl }
1489
+ }
1490
+ }
1491
+ }
1492
+ }
1493
+ }
1494
+ }`
1495
+ });
1496
+ const pullRequestTimelineQuery = pullRequestQuery({
1497
+ name: "PullRequestTimeline",
1498
+ fields: `
1499
+ timelineItems(last: 100, itemTypes: [${timelineItemTypes}]) {
1500
+ filteredCount
1501
+ pageInfo { hasPreviousPage }
1502
+ nodes {
1503
+ __typename
1504
+ ... on IssueComment {
1505
+ id databaseId author { login avatarUrl } body bodyHTML createdAt lastEditedAt url viewerCanUpdate
1506
+ ${reactionFields}
1507
+ }
1508
+ ... on PullRequestReview {
1509
+ id databaseId author { login avatarUrl } state body bodyHTML submittedAt createdAt lastEditedAt url
1510
+ viewerCanUpdate
1511
+ ${reactionFields}
1512
+ commit { oid }
1513
+ }
1514
+ ... on PullRequestCommit {
1515
+ commit {
1516
+ abbreviatedOid
1517
+ messageHeadline
1518
+ committedDate
1519
+ author { name user { login avatarUrl } }
1520
+ }
1521
+ }
1522
+ ... on AssignedEvent { createdAt actor { login avatarUrl } assignee { ${assigneeFields} } }
1523
+ ... on UnassignedEvent { createdAt actor { login avatarUrl } assignee { ${assigneeFields} } }
1524
+ ... on LabeledEvent { createdAt actor { login avatarUrl } label { name color description } }
1525
+ ... on UnlabeledEvent { createdAt actor { login avatarUrl } label { name color description } }
1526
+ ... on ReviewRequestedEvent { createdAt actor { login avatarUrl } requestedReviewer { ${reviewerFields} } }
1527
+ ... on ReviewRequestRemovedEvent { createdAt actor { login avatarUrl } requestedReviewer { ${reviewerFields} } }
1528
+ ... on ClosedEvent { createdAt actor { login avatarUrl } }
1529
+ ... on ReopenedEvent { createdAt actor { login avatarUrl } }
1530
+ ... on MergedEvent { createdAt actor { login avatarUrl } }
1531
+ ... on ReadyForReviewEvent { createdAt actor { login avatarUrl } }
1532
+ ... on ConvertToDraftEvent { createdAt actor { login avatarUrl } }
1533
+ ... on AutoMergeEnabledEvent { createdAt actor { login avatarUrl } }
1534
+ ... on AutoMergeDisabledEvent { createdAt actor { login avatarUrl } }
1535
+ ... on HeadRefForcePushedEvent { createdAt actor { login avatarUrl } afterCommit { abbreviatedOid } }
1536
+ ... on BaseRefChangedEvent { createdAt actor { login avatarUrl } currentRefName previousRefName }
1537
+ ... on RenamedTitleEvent { createdAt actor { login avatarUrl } currentTitle previousTitle }
1538
+ ... on ReviewDismissedEvent { createdAt actor { login avatarUrl } review { author { login } } }
1539
+ }
1540
+ }
1541
+ reviewThreads(first: 100) {
1542
+ totalCount
1543
+ nodes {
1544
+ id
1545
+ isResolved
1546
+ isOutdated
1547
+ path
1548
+ line
1549
+ startLine
1550
+ diffSide
1551
+ startDiffSide
1552
+ originalLine
1553
+ originalStartLine
1554
+ viewerCanResolve
1555
+ viewerCanUnresolve
1556
+ viewerCanReply
1557
+ comments(first: 20) {
1558
+ totalCount
1559
+ nodes {
1560
+ id databaseId author { login avatarUrl } body bodyHTML createdAt lastEditedAt url diffHunk viewerCanUpdate
1561
+ ${reactionFields}
1562
+ }
1563
+ }
1564
+ }
1565
+ }`
1566
+ });
1567
+ const pullRequestCommitsQuery = pullRequestQuery({
1568
+ name: "PullRequestCommits",
1569
+ fields: `
1570
+ commits(first: 100) {
1571
+ totalCount
1572
+ nodes {
1573
+ commit {
1574
+ oid
1575
+ parents(first: 1) { totalCount nodes { oid } }
1576
+ messageHeadline
1577
+ messageBody
1578
+ committedDate
1579
+ author { name user { login avatarUrl } }
1580
+ }
1581
+ }
1582
+ }`
1583
+ });
1584
+ const pullRequestFilesQuery = pullRequestQuery({
1585
+ name: "PullRequestFiles",
1586
+ fields: `
1587
+ files(first: 100) {
1588
+ nodes { path additions deletions changeType viewerViewedState }
1589
+ }
1590
+ latestReviews(first: 100) {
1591
+ nodes { viewerDidAuthor submittedAt commit { oid } }
1592
+ }`
1593
+ });
1594
+ const present = (items) => (items ?? []).filter((item) => item !== null);
1595
+ const toPerson = ({ login, avatarUrl }) => ({
1596
+ login,
1597
+ avatarUrl: avatarUrl ?? null
1598
+ });
1599
+ const toLabel = ({ name, color, description }) => ({
1600
+ name,
1601
+ color,
1602
+ description: description === "" ? null : description
1603
+ });
1604
+ const toState = (state, isDraft) => {
1605
+ if (state === "MERGED") return "merged";
1606
+ if (state === "CLOSED") return "closed";
1607
+ return isDraft ? "draft" : "open";
1608
+ };
1609
+ const reviewDecisions = /* @__PURE__ */ new Map([
1610
+ ["APPROVED", "approved"],
1611
+ ["CHANGES_REQUESTED", "changesRequested"],
1612
+ ["REVIEW_REQUIRED", "reviewRequired"]
1613
+ ]);
1614
+ const mergeables$1 = /* @__PURE__ */ new Map([
1615
+ ["CONFLICTING", "conflicting"],
1616
+ ["MERGEABLE", "mergeable"]
1617
+ ]);
1618
+ const reviewStates$1 = /* @__PURE__ */ new Map([
1619
+ ["APPROVED", "approved"],
1620
+ ["CHANGES_REQUESTED", "changesRequested"],
1621
+ ["COMMENTED", "commented"],
1622
+ ["DISMISSED", "dismissed"],
1623
+ ["PENDING", "pending"]
1624
+ ]);
1625
+ const fileChanges = /* @__PURE__ */ new Map([
1626
+ ["ADDED", "added"],
1627
+ ["CHANGED", "changed"],
1628
+ ["COPIED", "copied"],
1629
+ ["DELETED", "deleted"],
1630
+ ["MODIFIED", "modified"],
1631
+ ["RENAMED", "renamed"]
1632
+ ]);
1633
+ const viewedStates = /* @__PURE__ */ new Map([
1634
+ ["DISMISSED", "dismissed"],
1635
+ ["VIEWED", "viewed"]
1636
+ ]);
1637
+ const toReviewer = (reviewer) => reviewer.__typename === "Team" ? { kind: "team", slug: reviewer.slug, name: reviewer.name } : {
1638
+ kind: "user",
1639
+ login: reviewer.login,
1640
+ avatarUrl: reviewer.avatarUrl
1641
+ };
1642
+ const nonEmpty = (text) => text === null || text === "" ? null : text;
1643
+ const toCheck = (context) => {
1644
+ if (context.__typename === "StatusContext") {
1645
+ return {
1646
+ name: context.context,
1647
+ outcome: checkOutcomes.get(context.state) ?? "pending",
1648
+ running: false,
1649
+ description: nonEmpty(context.description),
1650
+ url: context.targetUrl
1651
+ };
1652
+ }
1653
+ return {
1654
+ name: context.name,
1655
+ outcome: context.status === "COMPLETED" && context.conclusion !== null ? checkOutcomes.get(context.conclusion) ?? "pending" : "pending",
1656
+ running: context.status === "IN_PROGRESS",
1657
+ description: nonEmpty(context.title),
1658
+ url: context.detailsUrl
1659
+ };
1660
+ };
1661
+ const checkOrder = {
1662
+ failed: 0,
1663
+ pending: 1,
1664
+ passed: 2
1665
+ };
1666
+ const ghost = { login: "ghost", avatarUrl: null };
1667
+ const toAuthor = (actor) => actor === null ? ghost : toPerson(actor);
1668
+ const reactionContents = /* @__PURE__ */ new Map([
1669
+ ["CONFUSED", "confused"],
1670
+ ["EYES", "eyes"],
1671
+ ["HEART", "heart"],
1672
+ ["HOORAY", "hooray"],
1673
+ ["LAUGH", "laugh"],
1674
+ ["ROCKET", "rocket"],
1675
+ ["THUMBS_DOWN", "thumbsDown"],
1676
+ ["THUMBS_UP", "thumbsUp"]
1677
+ ]);
1678
+ const toReactable = (subject) => ({
1679
+ id: subject.id,
1680
+ viewerCanReact: subject.viewerCanReact,
1681
+ reactions: (subject.reactionGroups ?? []).flatMap((group) => {
1682
+ const content = reactionContents.get(group.content);
1683
+ if (content === void 0 || group.reactors.totalCount === 0) return [];
1684
+ return [
1685
+ {
1686
+ content,
1687
+ count: group.reactors.totalCount,
1688
+ viewerHasReacted: group.viewerHasReacted
1689
+ }
1690
+ ];
1691
+ })
1692
+ });
1693
+ const toComment = (comment) => ({
1694
+ ...toReactable(comment),
1695
+ databaseId: comment.databaseId,
1696
+ author: toAuthor(comment.author),
1697
+ body: comment.body,
1698
+ createdAt: comment.createdAt,
1699
+ lastEditedAt: comment.lastEditedAt,
1700
+ url: comment.url,
1701
+ viewerCanUpdate: comment.viewerCanUpdate
1702
+ });
1703
+ const toReviewItem = (review) => {
1704
+ if (review.state === "PENDING") return null;
1705
+ if (review.state === "COMMENTED" && review.body.trim() === "") return null;
1706
+ return {
1707
+ kind: "review",
1708
+ ...toReactable(review),
1709
+ databaseId: review.databaseId,
1710
+ author: toAuthor(review.author),
1711
+ state: reviewStates$1.get(review.state) ?? "commented",
1712
+ body: review.body,
1713
+ createdAt: review.submittedAt ?? review.createdAt,
1714
+ lastEditedAt: review.lastEditedAt,
1715
+ url: review.url,
1716
+ viewerCanUpdate: review.viewerCanUpdate && review.body !== "",
1717
+ commitOid: review.commit?.oid ?? null
1718
+ };
1719
+ };
1720
+ const toSide = (side) => side === "LEFT" ? "LEFT" : "RIGHT";
1721
+ const toThreadItem = (thread) => {
1722
+ const comments = present(thread.comments.nodes);
1723
+ const [first] = comments;
1724
+ if (first === void 0) return null;
1725
+ return {
1726
+ kind: "thread",
1727
+ id: thread.id,
1728
+ path: thread.path,
1729
+ line: thread.line,
1730
+ startLine: thread.startLine,
1731
+ side: toSide(thread.diffSide),
1732
+ startSide: thread.startDiffSide === null ? null : toSide(thread.startDiffSide),
1733
+ originalLine: thread.originalLine,
1734
+ originalStartLine: thread.originalStartLine,
1735
+ resolved: thread.isResolved,
1736
+ outdated: thread.isOutdated,
1737
+ viewerCanResolve: thread.viewerCanResolve,
1738
+ viewerCanUnresolve: thread.viewerCanUnresolve,
1739
+ viewerCanReply: thread.viewerCanReply,
1740
+ diffHunk: first.diffHunk,
1741
+ comments: comments.map(toComment),
1742
+ commentsTotal: thread.comments.totalCount,
1743
+ createdAt: first.createdAt
1744
+ };
1745
+ };
1746
+ const nameOf = (named) => {
1747
+ if (named === null) return null;
1748
+ return "login" in named ? named.login : named.name;
1749
+ };
1750
+ const commitAuthor = (commit) => {
1751
+ const { author } = commit;
1752
+ if (author === null) return null;
1753
+ if (author.user !== null) return toPerson(author.user);
1754
+ return author.name === null ? null : { login: author.name, avatarUrl: null };
1755
+ };
1756
+ const simpleActions = {
1757
+ AutoMergeDisabledEvent: "autoMergeDisabled",
1758
+ AutoMergeEnabledEvent: "autoMergeEnabled",
1759
+ ClosedEvent: "closed",
1760
+ ConvertToDraftEvent: "convertedToDraft",
1761
+ MergedEvent: "merged",
1762
+ ReadyForReviewEvent: "readyForReview",
1763
+ ReopenedEvent: "reopened"
1764
+ };
1765
+ const event = (action, { actor, createdAt }, extra = {}) => ({
1766
+ kind: "event",
1767
+ action,
1768
+ actor: actor === null ? null : toPerson(actor),
1769
+ subject: null,
1770
+ label: null,
1771
+ detail: null,
1772
+ createdAt,
1773
+ ...extra
1774
+ });
1775
+ const toEvent = (node) => {
1776
+ switch (node.__typename) {
1777
+ case "PullRequestCommit": {
1778
+ const { commit } = node;
1779
+ return event(
1780
+ "committed",
1781
+ { actor: commitAuthor(commit), createdAt: commit.committedDate },
1782
+ { subject: commit.messageHeadline, detail: commit.abbreviatedOid }
1783
+ );
1784
+ }
1785
+ case "AssignedEvent":
1786
+ case "UnassignedEvent": {
1787
+ return event(
1788
+ node.__typename === "AssignedEvent" ? "assigned" : "unassigned",
1789
+ node,
1790
+ { subject: nameOf(node.assignee) }
1791
+ );
1792
+ }
1793
+ case "LabeledEvent":
1794
+ case "UnlabeledEvent": {
1795
+ return event(
1796
+ node.__typename === "LabeledEvent" ? "labeled" : "unlabeled",
1797
+ node,
1798
+ {
1799
+ subject: node.label?.name ?? null,
1800
+ label: node.label === null ? null : toLabel(node.label)
1801
+ }
1802
+ );
1803
+ }
1804
+ case "ReviewRequestedEvent":
1805
+ case "ReviewRequestRemovedEvent": {
1806
+ return event(
1807
+ node.__typename === "ReviewRequestedEvent" ? "reviewRequested" : "reviewRequestRemoved",
1808
+ node,
1809
+ { subject: nameOf(node.requestedReviewer) }
1810
+ );
1811
+ }
1812
+ case "HeadRefForcePushedEvent": {
1813
+ return event("headRefForcePushed", node, {
1814
+ detail: node.afterCommit?.abbreviatedOid ?? null
1815
+ });
1816
+ }
1817
+ case "BaseRefChangedEvent": {
1818
+ return event("baseRefChanged", node, {
1819
+ subject: node.currentRefName,
1820
+ detail: node.previousRefName
1821
+ });
1822
+ }
1823
+ case "RenamedTitleEvent": {
1824
+ return event("renamed", node, {
1825
+ subject: node.currentTitle,
1826
+ detail: node.previousTitle
1827
+ });
1828
+ }
1829
+ case "ReviewDismissedEvent": {
1830
+ return event("reviewDismissed", node, {
1831
+ subject: node.review?.author?.login ?? null
1832
+ });
1833
+ }
1834
+ case "AutoMergeDisabledEvent":
1835
+ case "AutoMergeEnabledEvent":
1836
+ case "ClosedEvent":
1837
+ case "ConvertToDraftEvent":
1838
+ case "MergedEvent":
1839
+ case "ReadyForReviewEvent":
1840
+ case "ReopenedEvent": {
1841
+ return event(simpleActions[node.__typename], node);
1842
+ }
1843
+ default: {
1844
+ return node;
1845
+ }
1846
+ }
1847
+ };
1848
+ const toTimelineNode = (node) => {
1849
+ if (node.__typename === "IssueComment") {
1850
+ return { kind: "comment", ...toComment(node) };
1851
+ }
1852
+ if (node.__typename === "PullRequestReview") return toReviewItem(node);
1853
+ return toEvent(node);
1854
+ };
1855
+ const isTruncated = (connection) => connection !== null && connection.totalCount > present(connection.nodes).length;
1856
+ const bodyHtmlsOf = (pullRequest) => [
1857
+ ...present(pullRequest.timelineItems?.nodes).map(
1858
+ (node) => "bodyHTML" in node ? node.bodyHTML : void 0
1859
+ ),
1860
+ ...present(pullRequest.reviewThreads?.nodes).flatMap(
1861
+ (thread) => present(thread.comments.nodes).map((comment) => comment.bodyHTML)
1862
+ )
1863
+ ];
1864
+ const toTimeline = (pullRequest) => {
1865
+ const items = [
1866
+ ...present(pullRequest.timelineItems?.nodes).map(toTimelineNode),
1867
+ ...present(pullRequest.reviewThreads?.nodes).map(toThreadItem)
1868
+ ].filter((item) => item !== null);
1869
+ return {
1870
+ timeline: items.toSorted((a, b) => a.createdAt.localeCompare(b.createdAt)),
1871
+ images: mergeImageProxies(bodyHtmlsOf(pullRequest)),
1872
+ timelineTruncated: pullRequest.timelineItems?.pageInfo.hasPreviousPage === true || isTruncated(pullRequest.reviewThreads)
1873
+ };
1874
+ };
1875
+ const toChecks = (contexts) => present(contexts?.nodes).map(toCheck).map((check, index) => ({ check, index })).toSorted(
1876
+ (a, b) => checkOrder[a.check.outcome] - checkOrder[b.check.outcome] || a.index - b.index
1877
+ ).map(({ check }) => check);
1878
+ const toFiles = (pullRequest) => present(pullRequest.files?.nodes).map((file) => ({
1879
+ path: file.path,
1880
+ additions: file.additions,
1881
+ deletions: file.deletions,
1882
+ change: fileChanges.get(file.changeType) ?? "changed",
1883
+ viewed: viewedStates.get(file.viewerViewedState) ?? "unviewed"
1884
+ }));
1885
+ const toRequestedReviewers = (pullRequest) => present(pullRequest.reviewRequests?.nodes).map(({ requestedReviewer }) => requestedReviewer).filter((reviewer) => reviewer !== null).map(toReviewer);
1886
+ const toReviewDecision = (decision) => decision === null ? null : reviewDecisions.get(decision) ?? null;
1887
+ const mergeStates = /* @__PURE__ */ new Map([
1888
+ ["BEHIND", "behind"],
1889
+ ["BLOCKED", "blocked"],
1890
+ ["CLEAN", "clean"],
1891
+ ["DIRTY", "dirty"],
1892
+ ["DRAFT", "draft"],
1893
+ ["HAS_HOOKS", "hasHooks"],
1894
+ ["UNSTABLE", "unstable"]
1895
+ ]);
1896
+ const mergeMethodNames = /* @__PURE__ */ new Map([
1897
+ ["MERGE", "merge"],
1898
+ ["REBASE", "rebase"],
1899
+ ["SQUASH", "squash"]
1900
+ ]);
1901
+ const toMergeMethods = (repository) => {
1902
+ const allowed = [];
1903
+ if (repository.mergeCommitAllowed === true) allowed.push("merge");
1904
+ if (repository.squashMergeAllowed === true) allowed.push("squash");
1905
+ if (repository.rebaseMergeAllowed === true) allowed.push("rebase");
1906
+ const preferred = mergeMethodNames.get(
1907
+ repository.viewerDefaultMergeMethod ?? ""
1908
+ );
1909
+ return preferred !== void 0 && allowed.includes(preferred) ? [preferred, ...allowed.filter((method) => method !== preferred)] : allowed;
1910
+ };
1911
+ const toCommit = ({ commit }) => ({
1912
+ oid: commit.oid,
1913
+ parentOid: present(commit.parents.nodes)[0]?.oid ?? null,
1914
+ merge: commit.parents.totalCount > 1,
1915
+ headline: commit.messageHeadline,
1916
+ body: commit.messageBody.trim(),
1917
+ author: commitAuthor(commit),
1918
+ committedAt: commit.committedDate
1919
+ });
1920
+ const viewerLastReview = (pullRequest) => {
1921
+ const review = present(pullRequest.latestReviews?.nodes).find(
1922
+ ({ viewerDidAuthor }) => viewerDidAuthor
1923
+ );
1924
+ const oid = review?.commit?.oid;
1925
+ if (review === void 0 || oid === void 0) return null;
1926
+ return { oid, submittedAt: review.submittedAt };
1927
+ };
1928
+ const toDetail = (pullRequest, repository, ownerRepo) => ({
1929
+ ...toReactable(pullRequest),
1930
+ ownerRepo,
1931
+ number: pullRequest.number,
1932
+ url: pullRequest.url,
1933
+ title: pullRequest.title,
1934
+ body: pullRequest.body,
1935
+ state: toState(pullRequest.state, pullRequest.isDraft),
1936
+ author: pullRequest.author === null ? null : toPerson(pullRequest.author),
1937
+ headRef: pullRequest.headRefName,
1938
+ baseRef: pullRequest.baseRefName,
1939
+ headSha: pullRequest.headRefOid,
1940
+ baseSha: pullRequest.baseRefOid,
1941
+ createdAt: pullRequest.createdAt,
1942
+ updatedAt: pullRequest.updatedAt,
1943
+ lastEditedAt: pullRequest.lastEditedAt,
1944
+ additions: pullRequest.additions,
1945
+ deletions: pullRequest.deletions,
1946
+ changedFiles: pullRequest.changedFiles,
1947
+ mergeable: mergeables$1.get(pullRequest.mergeable) ?? "unknown",
1948
+ mergeState: mergeStates.get(pullRequest.mergeStateStatus) ?? "unknown",
1949
+ mergeMethods: toMergeMethods(repository),
1950
+ mergeCommitMessages: {
1951
+ merge: {
1952
+ title: pullRequest.mergeCommitHeadline,
1953
+ body: pullRequest.mergeCommitBody
1954
+ },
1955
+ squash: {
1956
+ title: pullRequest.squashCommitHeadline,
1957
+ body: pullRequest.squashCommitBody
1958
+ }
1959
+ },
1960
+ viewerCanUpdate: pullRequest.viewerCanUpdate,
1961
+ viewerCanUpdateBranch: pullRequest.viewerCanUpdateBranch,
1962
+ viewerCanApplySuggestion: pullRequest.viewerCanApplySuggestion,
1963
+ reviewDecision: toReviewDecision(pullRequest.reviewDecision),
1964
+ labels: present(pullRequest.labels?.nodes).map(toLabel),
1965
+ assignees: present(pullRequest.assignees?.nodes).map(toPerson),
1966
+ requestedReviewers: toRequestedReviewers(pullRequest),
1967
+ reviews: present(pullRequest.latestReviews?.nodes).map((review) => ({
1968
+ author: review.author?.login ?? "ghost",
1969
+ state: reviewStates$1.get(review.state) ?? "commented",
1970
+ submittedAt: review.submittedAt
1971
+ })),
1972
+ commitsTotal: pullRequest.commits.totalCount
1973
+ });
1974
+ const toChecksPart = (pullRequest) => {
1975
+ const contexts = pullRequest.headCommits.nodes?.[0]?.commit.statusCheckRollup?.contexts ?? null;
1976
+ return { checks: toChecks(contexts), checksTotal: contexts?.totalCount ?? 0 };
1977
+ };
1978
+ const pullRequestCandidatesQuery = `query PullRequestCandidates($owner: String!, $name: String!, $number: Int!) {
1979
+ repositoryOwner(login: $owner) {
1980
+ ... on Organization {
1981
+ teams(first: 100, orderBy: { field: NAME, direction: ASC }) {
1982
+ nodes { slug name }
1983
+ }
1984
+ }
1985
+ }
1986
+ repository(owner: $owner, name: $name) {
1987
+ labels(first: 100, orderBy: { field: NAME, direction: ASC }) {
1988
+ nodes { name color description }
1989
+ }
1990
+ assignableUsers(first: 100) { nodes { login avatarUrl } }
1991
+ pullRequest(number: $number) {
1992
+ author { login }
1993
+ suggestedReviewers { reviewer { login avatarUrl } }
1994
+ }
1995
+ }
1996
+ }`;
1997
+ const reviewerCandidates = (pullRequest, assignable) => {
1998
+ const author = pullRequest.author?.login;
1999
+ const seen = /* @__PURE__ */ new Set();
2000
+ const unseen = (actors) => actors.flatMap((actor) => {
2001
+ if (actor.login === author || seen.has(actor.login)) return [];
2002
+ seen.add(actor.login);
2003
+ return [toPerson(actor)];
2004
+ });
2005
+ const suggested = unseen(
2006
+ present(pullRequest.suggestedReviewers).map(({ reviewer }) => reviewer).filter((reviewer) => reviewer !== null)
2007
+ );
2008
+ const others = unseen(
2009
+ assignable.toSorted((a, b) => a.login.localeCompare(b.login))
2010
+ );
2011
+ return { suggested, others };
2012
+ };
2013
+ const gitHubRead = ({
2014
+ fetchedAt,
2015
+ errors
2016
+ }) => ({
2017
+ fetchedAt,
2018
+ checkedAt: fetchedAt,
2019
+ source: "github",
2020
+ errors
2021
+ });
2022
+ const readPart = ({
2023
+ data,
2024
+ errors,
2025
+ ownerRepo,
2026
+ number
2027
+ }) => {
2028
+ const repository = data?.repository ?? null;
2029
+ const pullRequest = repository?.pullRequest ?? null;
2030
+ if (repository === null || pullRequest === null) {
2031
+ const reason = errors.length > 0 ? errors.join("; ") : "not found";
2032
+ throw new Error(
2033
+ `no pull request #${String(number)} in ${ownerRepo}: ${reason}`
2034
+ );
2035
+ }
2036
+ return { pullRequest, repository };
2037
+ };
2038
+ const parsePullRequestDetail = (params) => {
2039
+ const { pullRequest, repository } = readPart(params);
2040
+ return {
2041
+ pullRequest: toDetail(pullRequest, repository, params.ownerRepo),
2042
+ viewer: params.data?.viewer?.login ?? "",
2043
+ images: imageProxiesOf(pullRequest.bodyHTML),
2044
+ ...gitHubRead(params)
2045
+ };
2046
+ };
2047
+ const parsePullRequestChecks = (params) => ({
2048
+ ...toChecksPart(readPart(params).pullRequest),
2049
+ ...gitHubRead(params)
2050
+ });
2051
+ const parsePullRequestTimeline = (params) => ({
2052
+ ...toTimeline(readPart(params).pullRequest),
2053
+ ...gitHubRead(params)
2054
+ });
2055
+ const parsePullRequestCommits = (params) => ({
2056
+ commits: present(readPart(params).pullRequest.commits.nodes).map(toCommit),
2057
+ ...gitHubRead(params)
2058
+ });
2059
+ const parsePullRequestFiles = (params) => {
2060
+ const { pullRequest } = readPart(params);
2061
+ return {
2062
+ files: toFiles(pullRequest),
2063
+ viewerLastReview: viewerLastReview(pullRequest),
2064
+ ...gitHubRead(params)
2065
+ };
2066
+ };
2067
+ const readGitHub = async (transport, { ownerRepo, number }, query, parse) => {
2068
+ const split = splitOwnerRepo(ownerRepo);
2069
+ if (split === null) throw new Error(`${ownerRepo} is not owner/repo`);
2070
+ const { data, errors } = await transport.graphql(
2071
+ query,
2072
+ {
2073
+ owner: split.owner,
2074
+ name: split.name,
2075
+ number
2076
+ }
2077
+ );
2078
+ return parse({
2079
+ data,
2080
+ errors,
2081
+ ownerRepo,
2082
+ number,
2083
+ fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
2084
+ });
2085
+ };
2086
+ const partReaders = {
2087
+ detail: (transport, ref) => readGitHub(transport, ref, pullRequestDetailQuery, parsePullRequestDetail),
2088
+ checks: (transport, ref) => readGitHub(transport, ref, pullRequestChecksQuery, parsePullRequestChecks),
2089
+ timeline: (transport, ref) => readGitHub(
2090
+ transport,
2091
+ ref,
2092
+ pullRequestTimelineQuery,
2093
+ parsePullRequestTimeline
2094
+ ),
2095
+ commits: (transport, ref) => readGitHub(
2096
+ transport,
2097
+ ref,
2098
+ pullRequestCommitsQuery,
2099
+ parsePullRequestCommits
2100
+ ),
2101
+ files: (transport, ref) => readGitHub(transport, ref, pullRequestFilesQuery, parsePullRequestFiles)
2102
+ };
2103
+ const fetchPullRequestPart = (transport, ref, part) => partReaders[part](transport, ref);
2104
+ const parsePullRequestCandidates = ({
2105
+ data,
2106
+ errors
2107
+ }) => {
2108
+ const repository = data?.repository ?? null;
2109
+ if (repository === null) {
2110
+ throw new Error(errors.length > 0 ? errors.join("; ") : "not found");
2111
+ }
2112
+ const assignable = present(repository.assignableUsers?.nodes);
2113
+ const reviewers = reviewerCandidates(
2114
+ repository.pullRequest ?? { author: null, suggestedReviewers: null },
2115
+ assignable
2116
+ );
2117
+ return {
2118
+ repoLabels: present(repository.labels?.nodes).map(toLabel),
2119
+ suggestedReviewers: reviewers.suggested,
2120
+ reviewerCandidates: reviewers.others,
2121
+ teamCandidates: present(data?.repositoryOwner?.teams?.nodes).map(
2122
+ ({ slug, name }) => ({ slug, name })
2123
+ ),
2124
+ assigneeCandidates: assignable.toSorted((a, b) => a.login.localeCompare(b.login)).map(toPerson),
2125
+ errors
2126
+ };
2127
+ };
2128
+ const fetchPullRequestCandidates = async (transport, {
2129
+ ownerRepo,
2130
+ number
2131
+ }) => {
2132
+ const split = splitOwnerRepo(ownerRepo);
2133
+ if (split === null) throw new Error(`${ownerRepo} is not owner/repo`);
2134
+ const { data, errors } = await transport.graphql(
2135
+ pullRequestCandidatesQuery,
2136
+ { owner: split.owner, name: split.name, number }
2137
+ );
2138
+ return parsePullRequestCandidates({ data, errors });
2139
+ };
2140
+
2141
+ const pullRequestHeadQuery = `query PullRequestHead($owner: String!, $name: String!, $number: Int!) {
2142
+ repository(owner: $owner, name: $name) {
2143
+ pullRequest(number: $number) {
2144
+ state
2145
+ headRefName
2146
+ isCrossRepository
2147
+ headRepositoryOwner { login }
2148
+ }
2149
+ }
2150
+ }`;
2151
+ const headStates = {
2152
+ CLOSED: "closed",
2153
+ MERGED: "merged",
2154
+ OPEN: "open"
2155
+ };
2156
+ const fetchPullRequestHead = async (transport, { ownerRepo, number }) => {
2157
+ const [owner = "", name = ""] = ownerRepo.split("/");
2158
+ const { data, errors } = await transport.graphql(
2159
+ pullRequestHeadQuery,
2160
+ { owner, name, number }
2161
+ );
2162
+ const pullRequest = data?.repository?.pullRequest ?? null;
2163
+ if (pullRequest === null) {
2164
+ const reason = errors.length > 0 ? errors.join("; ") : "not found";
2165
+ throw new Error(`${ownerRepo}#${String(number)}: ${reason}`);
2166
+ }
2167
+ return {
2168
+ state: headStates[pullRequest.state],
2169
+ branch: pullRequest.headRefName,
2170
+ fork: pullRequest.isCrossRepository ? pullRequest.headRepositoryOwner?.login ?? "a deleted fork" : null
2171
+ };
2172
+ };
2173
+
2174
+ const revisionsPage = 50;
2175
+ const pullRequestRevisionsQuery = `query PullRequestRevisions($id: ID!) {
2176
+ node(id: $id) {
2177
+ ... on Comment {
2178
+ includesCreatedEdit
2179
+ userContentEdits(first: ${String(revisionsPage)}) {
2180
+ totalCount
2181
+ nodes {
2182
+ editedAt
2183
+ deletedAt
2184
+ editor { login avatarUrl }
2185
+ deletedBy { login }
2186
+ diff
2187
+ }
2188
+ }
2189
+ }
2190
+ }
2191
+ }`;
2192
+ const normalizeNewlines = (text) => text?.replaceAll("\r\n", "\n") ?? null;
2193
+ const parsePullRequestRevisions = ({
2194
+ data,
2195
+ errors,
2196
+ subjectId
2197
+ }) => {
2198
+ const edits = data?.node?.userContentEdits ?? null;
2199
+ if (edits === null) {
2200
+ const reason = errors.length > 0 ? errors.join("; ") : "not found";
2201
+ throw new Error(`no edit history for ${subjectId}: ${reason}`);
2202
+ }
2203
+ const nodes = (edits.nodes ?? []).flatMap(
2204
+ (node, rawIndex) => node === null ? [] : [{ ...node, rawIndex }]
2205
+ );
2206
+ const bodies = nodes.map(
2207
+ (node) => node.deletedAt === null ? normalizeNewlines(node.diff) : null
2208
+ );
2209
+ const lastIndex = edits.totalCount - 1;
2210
+ const revisions = nodes.map((node, index) => {
2211
+ const body = bodies[index] ?? null;
2212
+ const before = bodies.slice(index + 1).find((older) => older !== null);
2213
+ return {
2214
+ editedAt: node.editedAt,
2215
+ editor: node.editor === null ? null : {
2216
+ login: node.editor.login,
2217
+ avatarUrl: node.editor.avatarUrl ?? null
2218
+ },
2219
+ original: data?.node?.includesCreatedEdit === true && node.rawIndex === lastIndex,
2220
+ body,
2221
+ deletedBy: node.deletedBy?.login ?? null,
2222
+ changes: body === null || before === void 0 ? null : diffLines({ before, after: body })
2223
+ };
2224
+ });
2225
+ return { revisions, revisionsTotal: edits.totalCount };
2226
+ };
2227
+ const fetchPullRequestRevisions = async (transport, subjectId) => {
2228
+ const { data, errors } = await transport.graphql(
2229
+ pullRequestRevisionsQuery,
2230
+ { id: subjectId }
2231
+ );
2232
+ return parsePullRequestRevisions({ data, errors, subjectId });
2233
+ };
2234
+
2235
+ const firstLine = (text) => text.split("\n", 1)[0]?.trim() ?? "";
2236
+ const isSuccess = (status) => status >= 200 && status < 300;
2237
+ const parseJson = (text) => {
2238
+ try {
2239
+ return JSON.parse(text);
2240
+ } catch {
2241
+ return null;
2242
+ }
2243
+ };
2244
+ const interpretGraphql = ({
2245
+ status,
2246
+ body
2247
+ }) => {
2248
+ const parsed = parseJson(body);
2249
+ if (parsed === null || !isSuccess(status) && parsed.data == null) {
2250
+ const detail = parsed?.message ?? firstLine(body);
2251
+ throw new Error(
2252
+ `GitHub graphql failed (HTTP ${String(status)})${detail === "" ? "" : `: ${detail}`}`
2253
+ );
2254
+ }
2255
+ return {
2256
+ data: parsed.data ?? null,
2257
+ errors: (parsed.errors ?? []).map(
2258
+ ({ message }) => message ?? "github returned an unnamed error"
2259
+ )
2260
+ };
2261
+ };
2262
+ const restErrorMessage = ({ status, body }) => {
2263
+ const parsed = parseJson(body);
2264
+ const details = (parsed?.errors ?? []).map(
2265
+ (error) => typeof error === "string" ? error : error.message ?? [error.field, error.code].filter(Boolean).join(" ")
2266
+ ).filter((detail) => detail !== "");
2267
+ const headline = parsed?.message ?? (firstLine(body) || `HTTP ${String(status)}`);
2268
+ return [headline, ...details].filter((part) => part !== "").join(": ");
2269
+ };
2270
+ const createGitHubTransport = (send) => ({
2271
+ graphql: async (query, variables) => interpretGraphql(
2272
+ await send({
2273
+ method: "POST",
2274
+ path: "graphql",
2275
+ accept: "application/json",
2276
+ body: JSON.stringify(
2277
+ variables === void 0 ? { query } : { query, variables }
2278
+ )
2279
+ })
2280
+ ),
2281
+ rest: async ({
2282
+ method,
2283
+ path,
2284
+ body
2285
+ }) => {
2286
+ const answer = await send({
2287
+ method,
2288
+ path,
2289
+ accept: "application/vnd.github+json",
2290
+ body: body === void 0 ? void 0 : JSON.stringify(body)
2291
+ });
2292
+ if (!isSuccess(answer.status)) {
2293
+ throw new Error(
2294
+ `GitHub refused ${method} ${path}: ${restErrorMessage(answer)}`
2295
+ );
2296
+ }
2297
+ return parseJson(answer.body);
2298
+ },
2299
+ text: async ({ path, accept }) => {
2300
+ const answer = await send({ method: "GET", path, accept, body: void 0 });
2301
+ if (!isSuccess(answer.status)) {
2302
+ throw new Error(
2303
+ `GitHub refused GET ${path}: ${restErrorMessage(answer)}`
2304
+ );
2305
+ }
2306
+ return answer.body;
2307
+ }
2308
+ });
2309
+
2310
+ const pullRequestParts = [
2311
+ "detail",
2312
+ "checks",
2313
+ "timeline",
2314
+ "commits",
2315
+ "files"
2316
+ ];
2317
+ const isPullRequestPart = (value) => pullRequestParts.includes(value);
2318
+
2319
+ const pullRequestPartsStaleAfter = (edit, pullRequestId) => {
2320
+ switch (edit.kind) {
2321
+ case "applySuggestions":
2322
+ case "updateBranch": {
2323
+ return pullRequestParts;
2324
+ }
2325
+ case "body": {
2326
+ return ["detail"];
2327
+ }
2328
+ case "reaction": {
2329
+ return edit.subjectId === pullRequestId ? ["detail"] : ["timeline"];
2330
+ }
2331
+ case "viewed": {
2332
+ return [];
2333
+ }
2334
+ case "comment":
2335
+ case "editComment":
2336
+ case "inlineComment":
2337
+ case "reply":
2338
+ case "resolveThread": {
2339
+ return ["timeline"];
2340
+ }
2341
+ case "review": {
2342
+ return ["detail", "timeline", "files"];
2343
+ }
2344
+ case "assignees":
2345
+ case "draft":
2346
+ case "labels":
2347
+ case "merge":
2348
+ case "reviewers":
2349
+ case "state":
2350
+ case "title": {
2351
+ return ["detail", "timeline"];
2352
+ }
2353
+ default: {
2354
+ return edit;
2355
+ }
2356
+ }
2357
+ };
2358
+ const withFileViewed = (files, path, viewed) => ({
2359
+ ...files,
2360
+ files: files.files.map(
2361
+ (file) => file.path === path ? { ...file, viewed: viewed ? "viewed" : "unviewed" } : file
2362
+ )
2363
+ });
2364
+ const pullRequestEditMovesBoard = (edit) => {
2365
+ switch (edit.kind) {
2366
+ case "applySuggestions":
2367
+ case "draft":
2368
+ case "merge":
2369
+ case "review":
2370
+ case "reviewers":
2371
+ case "state":
2372
+ case "updateBranch": {
2373
+ return true;
2374
+ }
2375
+ case "assignees":
2376
+ case "body":
2377
+ case "comment":
2378
+ case "editComment":
2379
+ case "inlineComment":
2380
+ case "labels":
2381
+ case "reaction":
2382
+ case "reply":
2383
+ case "resolveThread":
2384
+ case "title":
2385
+ case "viewed": {
2386
+ return false;
2387
+ }
2388
+ default: {
2389
+ return edit;
2390
+ }
2391
+ }
2392
+ };
2393
+
2394
+ const partFields = {
2395
+ detail: (pr) => [
2396
+ pr.title,
2397
+ pr.headRef,
2398
+ pr.baseRef,
2399
+ pr.headSha,
2400
+ pr.baseSha,
2401
+ pr.state,
2402
+ pr.reviewState,
2403
+ pr.author,
2404
+ pr.checks.conclusion,
2405
+ pr.approvedBy,
2406
+ pr.changesRequestedBy,
2407
+ pr.requestedReviewers,
2408
+ pr.reRequestedReviewers,
2409
+ pr.requestedTeams,
2410
+ pr.assignees,
2411
+ pr.labels,
2412
+ pr.mergeable,
2413
+ pr.unresolvedThreads === 0,
2414
+ pr.reviewActivityAt,
2415
+ pr.bodyEditedAt,
2416
+ pr.closedAt
2417
+ ],
2418
+ checks: (pr) => [pr.headSha, pr.checks],
2419
+ timeline: (pr) => [
2420
+ pr.headSha,
2421
+ pr.baseSha,
2422
+ pr.baseRef,
2423
+ pr.title,
2424
+ pr.state,
2425
+ pr.labels,
2426
+ pr.assignees,
2427
+ pr.requestedReviewers,
2428
+ pr.reRequestedReviewers,
2429
+ pr.requestedTeams,
2430
+ pr.approvedBy,
2431
+ pr.changesRequestedBy,
2432
+ pr.reviewActivityAt,
2433
+ pr.commentActivityAt,
2434
+ pr.reviewCommentActivityAt,
2435
+ pr.unresolvedThreads
2436
+ ],
2437
+ commits: (pr) => [pr.headSha, pr.baseSha],
2438
+ files: (pr) => [pr.headSha, pr.baseSha]
2439
+ };
2440
+ const partFingerprint = (pr, part) => JSON.stringify(partFields[part](pr));
2441
+ const changedPullRequestParts = (previous, next) => pullRequestParts.filter(
2442
+ (part) => partFingerprint(previous, part) !== partFingerprint(next, part)
2443
+ );
2444
+
2445
+ const upperOrNull = (value) => value === null ? null : value.toUpperCase();
2446
+ const checksFromReviewflow = ({
2447
+ checkRuns,
2448
+ statuses
2449
+ }, at) => {
2450
+ const nodes = [
2451
+ ...checkRuns.map((run) => ({
2452
+ __typename: "CheckRun",
2453
+ name: run.name,
2454
+ status: upperOrNull(run.status) ?? (run.conclusion === null ? "QUEUED" : "COMPLETED"),
2455
+ conclusion: upperOrNull(run.conclusion),
2456
+ title: run.title,
2457
+ detailsUrl: run.detailsUrl
2458
+ })),
2459
+ ...statuses.map((status) => ({
2460
+ __typename: "StatusContext",
2461
+ context: status.context,
2462
+ state: status.state.toUpperCase(),
2463
+ description: status.description,
2464
+ targetUrl: status.targetUrl
2465
+ }))
2466
+ ];
2467
+ const parsed = parsePullRequestChecks({
2468
+ data: {
2469
+ repository: {
2470
+ pullRequest: {
2471
+ headCommits: {
2472
+ nodes: [
2473
+ {
2474
+ commit: {
2475
+ statusCheckRollup: {
2476
+ contexts: { totalCount: nodes.length, nodes }
2477
+ }
2478
+ }
2479
+ }
2480
+ ]
2481
+ }
2482
+ }
2483
+ }
2484
+ },
2485
+ errors: [],
2486
+ ownerRepo: "",
2487
+ number: 0,
2488
+ fetchedAt: at
2489
+ });
2490
+ return { ...parsed, source: "reviewflow" };
2491
+ };
2492
+
2493
+ const reviewflowErrorCode = "ERR_REVIEWFLOW";
2494
+ const timeoutMs = 15e3;
2495
+ const maxLookupTargets = 200;
2496
+ const maxExtraPagesPerBucket = 5;
2497
+ const fail = (message, { status, retryAfterMs } = {}) => Object.assign(new Error(message), {
2498
+ code: reviewflowErrorCode,
2499
+ status,
2500
+ retryAfterMs
2501
+ });
2502
+ const isReviewflowFailure = (error) => error instanceof Error && error.code === reviewflowErrorCode;
2503
+ const isRecord$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
2504
+ const stringOrNull = (value) => typeof value === "string" ? value : null;
2505
+ const numberOr = (value, fallback) => typeof value === "number" && Number.isFinite(value) ? value : fallback;
2506
+ const prStates = /* @__PURE__ */ new Set(["closed", "draft", "merged", "open"]);
2507
+ const reviewStates = /* @__PURE__ */ new Set([
2508
+ "approved",
2509
+ "changesRequested",
2510
+ "none",
2511
+ "requested"
2512
+ ]);
2513
+ const mergeables = /* @__PURE__ */ new Set(["conflicting", "mergeable", "unknown"]);
2514
+ const checksConclusions = {
2515
+ failed: "failed",
2516
+ "in-progress": "inProgress",
2517
+ passed: "passed"
2518
+ };
2519
+ const enumOrNull = (value, allowed) => typeof value === "string" && allowed.has(value) ? value : null;
2520
+ const strings = (value) => Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
2521
+ const logins = (value) => Array.isArray(value) ? value.flatMap(
2522
+ (person) => isRecord$1(person) && typeof person.login === "string" ? [person.login] : []
2523
+ ) : [];
2524
+ const readCheckRuns = (value) => Array.isArray(value) ? value.flatMap(
2525
+ (run) => isRecord$1(run) && typeof run.name === "string" ? [
2526
+ {
2527
+ name: run.name,
2528
+ status: stringOrNull(run.status),
2529
+ conclusion: stringOrNull(run.conclusion),
2530
+ title: stringOrNull(run.title),
2531
+ detailsUrl: stringOrNull(run.detailsUrl)
2532
+ }
2533
+ ] : []
2534
+ ) : null;
2535
+ const readCommitStatuses = (value) => Array.isArray(value) ? value.flatMap(
2536
+ (status) => isRecord$1(status) && typeof status.context === "string" && typeof status.state === "string" ? [
2537
+ {
2538
+ context: status.context,
2539
+ state: status.state,
2540
+ description: stringOrNull(status.description),
2541
+ targetUrl: stringOrNull(status.targetUrl)
2542
+ }
2543
+ ] : []
2544
+ ) : null;
2545
+ const readStatusLinks = (value) => Array.isArray(value) ? value.flatMap(
2546
+ (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 }] : []
2547
+ ) : [];
2548
+ const readLabels = (value) => Array.isArray(value) ? value.flatMap(
2549
+ (label) => isRecord$1(label) && typeof label.name === "string" ? [label.name] : []
2550
+ ) : [];
2551
+ const readChanges = (value) => isRecord$1(value) && typeof value.changedFiles === "number" && typeof value.additions === "number" && typeof value.deletions === "number" ? {
2552
+ changedFiles: value.changedFiles,
2553
+ additions: value.additions,
2554
+ deletions: value.deletions
2555
+ } : null;
2556
+ const readPrDetail = (value) => {
2557
+ if (!isRecord$1(value) || typeof value._id !== "string" || typeof value.ownerRepo !== "string" || typeof value.number !== "number" || typeof value.url !== "string" || !isRecord$1(value.checks)) {
2558
+ throw fail("reviewflow returned a pull request the client cannot read");
2559
+ }
2560
+ const { checks } = value;
2561
+ const creator = isRecord$1(value.creator) ? value.creator : null;
2562
+ const flowDates = isRecord$1(value.flowDates) ? value.flowDates : {};
2563
+ return {
2564
+ id: value._id,
2565
+ ownerRepo: value.ownerRepo,
2566
+ number: value.number,
2567
+ title: typeof value.title === "string" ? value.title : "",
2568
+ url: value.url,
2569
+ headRef: stringOrNull(value.headRef),
2570
+ baseRef: stringOrNull(value.baseRef),
2571
+ headSha: stringOrNull(value.headSha),
2572
+ baseSha: stringOrNull(value.baseSha),
2573
+ state: enumOrNull(value.state, prStates),
2574
+ reviewState: enumOrNull(
2575
+ value.reviewState,
2576
+ reviewStates
2577
+ ),
2578
+ author: stringOrNull(creator?.login),
2579
+ checks: {
2580
+ conclusion: typeof checks.conclusion === "string" ? checksConclusions[checks.conclusion] ?? "unknown" : "unknown",
2581
+ passed: numberOr(checks.passed, 0),
2582
+ failed: numberOr(checks.failed, 0),
2583
+ pending: numberOr(checks.pending, 0),
2584
+ running: numberOr(checks.runningCount, 0),
2585
+ failedNames: strings(checks.failedNames),
2586
+ checkRuns: readCheckRuns(checks.checkRuns),
2587
+ statuses: readCommitStatuses(checks.statuses)
2588
+ },
2589
+ lintFailed: value.lintFailed === true,
2590
+ approvedBy: logins(value.approvedBy),
2591
+ changesRequestedBy: logins(value.changesRequestedBy),
2592
+ requestedReviewers: logins(value.requestedReviewers),
2593
+ reRequestedReviewers: logins(value.reRequestedReviewers),
2594
+ requestedTeams: strings(value.requestedTeams),
2595
+ assignees: logins(value.assignees),
2596
+ labels: readLabels(value.labels),
2597
+ statusLinks: readStatusLinks(value.statusLinks),
2598
+ openedAt: stringOrNull(flowDates.openedAt),
2599
+ approvedAt: stringOrNull(flowDates.approvedAt),
2600
+ changes: readChanges(value.changes),
2601
+ unresolvedThreads: typeof value.unresolvedThreads === "number" ? value.unresolvedThreads : null,
2602
+ mergeable: enumOrNull(value.mergeable, mergeables) ?? "unknown",
2603
+ closedAt: stringOrNull(flowDates.closedAt),
2604
+ commentActivityAt: stringOrNull(flowDates.lastCommentActivityAt),
2605
+ reviewActivityAt: stringOrNull(flowDates.lastReviewSubmittedAt),
2606
+ reviewCommentActivityAt: stringOrNull(
2607
+ flowDates.lastReviewCommentActivityAt
2608
+ ),
2609
+ bodyEditedAt: stringOrNull(flowDates.lastBodyEditedAt),
2610
+ updatedAt: typeof value.updatedAt === "string" ? value.updatedAt : ""
2611
+ };
2612
+ };
2613
+ const lookupReasons = /* @__PURE__ */ new Set(["forbidden", "noPr", "notTracked"]);
2614
+ const readLookupResult = (value) => {
2615
+ if (isRecord$1(value) && value.found === true) {
2616
+ return { found: true, pr: readPrDetail(value.pr) };
2617
+ }
2618
+ if (isRecord$1(value) && value.found === false && typeof value.reason === "string" && lookupReasons.has(value.reason)) {
2619
+ return {
2620
+ found: false,
2621
+ reason: value.reason
2622
+ };
2623
+ }
2624
+ return { found: false, reason: "notTracked" };
2625
+ };
2626
+ const readBucketPage = (value) => {
2627
+ if (!isRecord$1(value) || typeof value.key !== "string" || !Array.isArray(value.prs)) {
2628
+ throw fail("reviewflow returned a board bucket the client cannot read");
2629
+ }
2630
+ return {
2631
+ key: value.key,
2632
+ title: typeof value.title === "string" ? value.title : value.key,
2633
+ total: numberOr(value.total, value.prs.length),
2634
+ prs: value.prs.map(readPrDetail),
2635
+ nextPage: stringOrNull(value.nextPage)
2636
+ };
2637
+ };
2638
+ const readMe = (value) => {
2639
+ if (!isRecord$1(value) || !isRecord$1(value.user) || typeof value.user.login !== "string" || !Array.isArray(value.accounts)) {
2640
+ throw fail("reviewflow returned a /me the client cannot read");
2641
+ }
2642
+ return {
2643
+ login: value.user.login,
2644
+ accounts: value.accounts.filter(isRecord$1).map((account) => ({
2645
+ login: String(account.login),
2646
+ type: account.type === "Organization" ? "Organization" : "User"
2647
+ }))
2648
+ };
2649
+ };
2650
+ const readRetryAfterMs = (response) => {
2651
+ const seconds = Number(response.headers.get("retry-after") ?? Number.NaN);
2652
+ return Number.isFinite(seconds) && seconds >= 0 ? seconds * 1e3 : void 0;
2653
+ };
2654
+ const responseFailure = async (response) => {
2655
+ const body = await response.json().catch(() => null);
2656
+ const error = isRecord$1(body) && isRecord$1(body.error) ? body.error : null;
2657
+ const detail = error === null ? "" : ` ${String(error.code)}: ${typeof error.message === "string" ? error.message : ""}`;
2658
+ const retryAfter = response.headers.get("retry-after");
2659
+ const retry = retryAfter === null ? "" : ` (retry after ${retryAfter}s)`;
2660
+ return fail(
2661
+ `reviewflow answered ${String(response.status)}${detail}${retry}`,
2662
+ { status: response.status, retryAfterMs: readRetryAfterMs(response) }
2663
+ );
2664
+ };
2665
+ const chunk = (items, size) => Array.from(
2666
+ { length: Math.ceil(items.length / size) },
2667
+ (_, index) => items.slice(index * size, (index + 1) * size)
2668
+ );
2669
+ const createReviewflowClient = ({
2670
+ url,
2671
+ token,
2672
+ fetch = globalThis.fetch
2673
+ }) => {
2674
+ const request = async (path, init = {}) => {
2675
+ const response = await fetch(`${url}/api${path}`, {
2676
+ method: init.method ?? "GET",
2677
+ headers: {
2678
+ authorization: `Bearer ${token}`,
2679
+ accept: "application/json",
2680
+ ...init.body === void 0 ? {} : { "content-type": "application/json" }
2681
+ },
2682
+ body: init.body === void 0 ? void 0 : JSON.stringify(init.body),
2683
+ signal: AbortSignal.timeout(timeoutMs)
2684
+ }).catch((error) => {
2685
+ const cause = error instanceof Error ? error.message : String(error);
2686
+ throw fail(`reviewflow could not be reached at ${url}: ${cause}`);
2687
+ });
2688
+ if (!response.ok) throw await responseFailure(response);
2689
+ return response.json().catch(() => {
2690
+ throw fail("reviewflow answered with a body that is not json");
2691
+ });
2692
+ };
2693
+ const readBucket = async (account, first) => {
2694
+ const prs = [...first.prs];
2695
+ let { nextPage } = first;
2696
+ for (let page = 0; nextPage !== null && page < maxExtraPagesPerBucket; page++) {
2697
+ const query = new URLSearchParams({
2698
+ bucket: first.key,
2699
+ account,
2700
+ page: nextPage
2701
+ });
2702
+ const next = readBucketPage(
2703
+ await request(`/board-bucket/v1?${query.toString()}`)
2704
+ );
2705
+ prs.push(...next.prs);
2706
+ ({ nextPage } = next);
2707
+ }
2708
+ const seen = /* @__PURE__ */ new Set();
2709
+ return {
2710
+ key: first.key,
2711
+ title: first.title,
2712
+ total: first.total,
2713
+ prs: prs.filter((pr) => !seen.has(pr.id) && seen.add(pr.id))
2714
+ };
2715
+ };
2716
+ return {
2717
+ me: async () => readMe(await request("/me/v1")),
2718
+ lookup: async (targets) => {
2719
+ const answers = await Promise.all(
2720
+ chunk(targets, maxLookupTargets).map(async (part) => {
2721
+ const body = await request("/prs-lookup/v1", {
2722
+ method: "POST",
2723
+ body: { targets: part }
2724
+ });
2725
+ if (!isRecord$1(body) || !Array.isArray(body.results) || body.results.length !== part.length) {
2726
+ throw fail("reviewflow returned a lookup the client cannot read");
2727
+ }
2728
+ return body.results.map(readLookupResult);
2729
+ })
2730
+ );
2731
+ return answers.flat();
2732
+ },
2733
+ pr: async (ownerRepo, number) => {
2734
+ const path = ownerRepo.split("/").map((part) => encodeURIComponent(part)).join("/");
2735
+ try {
2736
+ return readPrDetail(await request(`/pr/v1/${path}/${String(number)}`));
2737
+ } catch (error) {
2738
+ if (isReviewflowFailure(error) && (error.status === 403 || error.status === 404)) {
2739
+ return null;
2740
+ }
2741
+ throw error;
2742
+ }
2743
+ },
2744
+ board: async (account) => {
2745
+ const body = await request(
2746
+ `/board/v1?${new URLSearchParams({ account }).toString()}`
2747
+ );
2748
+ if (!isRecord$1(body) || !Array.isArray(body.groups)) {
2749
+ throw fail("reviewflow returned a board the client cannot read");
2750
+ }
2751
+ return Promise.all(
2752
+ body.groups.map(async (group) => {
2753
+ if (!isRecord$1(group) || typeof group.key !== "string" || !Array.isArray(group.buckets)) {
2754
+ throw fail(
2755
+ "reviewflow returned a board group the client cannot read"
2756
+ );
2757
+ }
2758
+ return {
2759
+ key: group.key,
2760
+ title: typeof group.title === "string" ? group.title : group.key,
2761
+ buckets: await Promise.all(
2762
+ group.buckets.map(
2763
+ (bucket) => readBucket(account, readBucketPage(bucket))
2764
+ )
2765
+ )
2766
+ };
2767
+ })
2768
+ );
2769
+ }
2770
+ };
2771
+ };
2772
+
2773
+ const streamIdleTimeoutMs = 6e4;
2774
+ const streamClosedWithinDays = 7;
2775
+ const parseFrame = (block) => {
2776
+ let event = "message";
2777
+ const data = [];
2778
+ for (const line of block.split("\n")) {
2779
+ if (line.startsWith(":")) continue;
2780
+ const separator = line.indexOf(":");
2781
+ const field = separator === -1 ? line : line.slice(0, separator);
2782
+ const value = separator === -1 ? "" : line.slice(separator + 1).replace(/^ /, "");
2783
+ if (field === "event") event = value;
2784
+ else if (field === "data") data.push(value);
2785
+ }
2786
+ return { event, data: data.join("\n") };
2787
+ };
2788
+ const parseData = (frame) => {
2789
+ try {
2790
+ return JSON.parse(frame.data);
2791
+ } catch {
2792
+ return null;
2793
+ }
2794
+ };
2795
+ const toStreamEvent = (frame, logger) => {
2796
+ switch (frame.event) {
2797
+ case "pr": {
2798
+ try {
2799
+ return { type: "pr", pr: readPrDetail(parseData(frame)) };
2800
+ } catch (error) {
2801
+ logger?.warn(
2802
+ "reviewflow streamed a pull request the client cannot read",
2803
+ { error }
2804
+ );
2805
+ return null;
2806
+ }
2807
+ }
2808
+ case "prRemoved": {
2809
+ const data = parseData(frame);
2810
+ return typeof data?._id === "string" ? { type: "prRemoved", id: data._id } : null;
2811
+ }
2812
+ case "ready":
2813
+ return { type: "ready" };
2814
+ case "scopeChanged":
2815
+ return { type: "end", reason: "scopeChanged" };
2816
+ case "reset": {
2817
+ const data = parseData(frame);
2818
+ return {
2819
+ type: "end",
2820
+ reason: `reset: ${typeof data?.reason === "string" ? data.reason : "unknown"}`
2821
+ };
2822
+ }
2823
+ default:
2824
+ return null;
2825
+ }
2826
+ };
2827
+ async function* readReviewflowStream({
2828
+ url,
2829
+ token,
2830
+ signal,
2831
+ fetch = globalThis.fetch,
2832
+ idleTimeoutMs = streamIdleTimeoutMs,
2833
+ logger
2834
+ }) {
2835
+ const controller = new AbortController();
2836
+ let reader;
2837
+ const cancel = () => {
2838
+ controller.abort();
2839
+ reader?.cancel().catch(() => void 0);
2840
+ };
2841
+ signal.addEventListener("abort", cancel, { once: true });
2842
+ let silent = false;
2843
+ let idle;
2844
+ const armIdle = () => {
2845
+ clearTimeout(idle);
2846
+ idle = setTimeout(() => {
2847
+ silent = true;
2848
+ cancel();
2849
+ }, idleTimeoutMs);
2850
+ };
2851
+ const silence = () => fail(
2852
+ `reviewflow's stream went silent for ${String(idleTimeoutMs / 1e3)}s`
2853
+ );
2854
+ try {
2855
+ armIdle();
2856
+ const query = new URLSearchParams({
2857
+ closedWithinDays: String(streamClosedWithinDays)
2858
+ });
2859
+ const response = await fetch(`${url}/api/stream/v1?${query.toString()}`, {
2860
+ headers: {
2861
+ authorization: `Bearer ${token}`,
2862
+ accept: "text/event-stream"
2863
+ },
2864
+ signal: controller.signal
2865
+ });
2866
+ if (!response.ok) throw await responseFailure(response);
2867
+ if (response.body === null) {
2868
+ throw fail("reviewflow answered the stream with no body");
2869
+ }
2870
+ reader = response.body.getReader();
2871
+ const decoder = new TextDecoder();
2872
+ let buffer = "";
2873
+ for (; ; ) {
2874
+ const { value, done } = await reader.read();
2875
+ if (done) break;
2876
+ armIdle();
2877
+ buffer = (buffer + decoder.decode(value, { stream: true })).replaceAll(
2878
+ "\r\n",
2879
+ "\n"
2880
+ );
2881
+ let end = buffer.indexOf("\n\n");
2882
+ while (end !== -1) {
2883
+ const event = toStreamEvent(parseFrame(buffer.slice(0, end)), logger);
2884
+ buffer = buffer.slice(end + 2);
2885
+ if (event !== null) {
2886
+ yield event;
2887
+ if (event.type === "end") return;
2888
+ }
2889
+ end = buffer.indexOf("\n\n");
2890
+ }
2891
+ }
2892
+ if (silent) throw silence();
2893
+ } catch (error) {
2894
+ if (signal.aborted) return;
2895
+ if (silent) throw silence();
2896
+ if (isReviewflowFailure(error)) throw error;
2897
+ const cause = error instanceof Error ? error.message : String(error);
2898
+ throw fail(`reviewflow's stream could not be read at ${url}: ${cause}`);
2899
+ } finally {
2900
+ clearTimeout(idle);
2901
+ signal.removeEventListener("abort", cancel);
2902
+ cancel();
2903
+ }
2904
+ }
2905
+
2906
+ const toBoardPr = (pr) => ({
2907
+ id: pr.id,
2908
+ ownerRepo: pr.ownerRepo,
2909
+ number: pr.number,
2910
+ title: pr.title,
2911
+ url: pr.url,
2912
+ headRef: pr.headRef,
2913
+ state: pr.state,
2914
+ reviewState: pr.reviewState,
2915
+ author: pr.author,
2916
+ checksFailed: pr.checks.failed,
2917
+ checksConclusion: pr.checks.conclusion,
2918
+ checksRunning: pr.checks.running,
2919
+ failedChecks: pr.checks.failedNames,
2920
+ lintFailed: pr.lintFailed,
2921
+ approvedBy: pr.approvedBy,
2922
+ changesRequestedBy: pr.changesRequestedBy,
2923
+ requestedReviewers: pr.requestedReviewers,
2924
+ reRequestedReviewers: pr.reRequestedReviewers,
2925
+ requestedTeams: pr.requestedTeams,
2926
+ assignees: pr.assignees,
2927
+ statusLinks: pr.statusLinks,
2928
+ changes: pr.changes,
2929
+ openedAt: pr.openedAt,
2930
+ approvedAt: pr.approvedAt,
2931
+ unresolvedThreads: pr.unresolvedThreads,
2932
+ mergeable: pr.mergeable,
2933
+ updatedAt: pr.updatedAt
2934
+ });
2935
+ const accountOfOwnerRepo = (ownerRepo) => ownerRepo.slice(0, ownerRepo.indexOf("/")).toLowerCase();
2936
+ const isPullRequestOnBoard = (board, { ownerRepo, number }) => {
2937
+ const wanted = ownerRepo.toLowerCase();
2938
+ return board?.groups.some(
2939
+ (group) => group.buckets.some(
2940
+ (bucket) => bucket.prs.some(
2941
+ (pr) => pr.number === number && pr.ownerRepo.toLowerCase() === wanted
2942
+ )
2943
+ )
2944
+ ) ?? false;
2945
+ };
2946
+ const withAccountBoard = (snapshot, account, board) => ({ ...snapshot, [account]: board });
2947
+ const readAccountBoards = async (client, accounts) => {
2948
+ const settled = await Promise.allSettled(
2949
+ accounts.map(async (account) => ({
2950
+ account,
2951
+ groups: await client.board(account)
2952
+ }))
2953
+ );
2954
+ const read = { boards: [], errors: [] };
2955
+ settled.forEach((result, index) => {
2956
+ if (result.status === "fulfilled") read.boards.push(result.value);
2957
+ else {
2958
+ const { reason } = result;
2959
+ read.errors.push({
2960
+ account: accounts[index],
2961
+ message: reason instanceof Error ? reason.message : String(reason)
2962
+ });
2963
+ }
2964
+ });
2965
+ return read;
2966
+ };
2967
+ const assembleBoard = (snapshot, viewer, accounts = Object.keys(snapshot)) => {
2968
+ const merged = /* @__PURE__ */ new Map();
2969
+ const fetchedAts = [];
2970
+ for (const account of accounts) {
2971
+ const read = snapshot[account];
2972
+ if (read === void 0) continue;
2973
+ fetchedAts.push(read.fetchedAt);
2974
+ let position = 0;
2975
+ for (const group of read.groups) {
2976
+ for (const bucket of group.buckets) {
2977
+ const existing = merged.get(bucket.key);
2978
+ const prs = bucket.prs.map((pr) => toBoardPr(pr));
2979
+ if (existing === void 0) {
2980
+ merged.set(bucket.key, {
2981
+ key: bucket.key,
2982
+ group: { key: group.key, title: group.title },
2983
+ title: bucket.title,
2984
+ total: bucket.total,
2985
+ position,
2986
+ prs,
2987
+ fetchedAt: read.fetchedAt
2988
+ });
2989
+ } else {
2990
+ existing.total += bucket.total;
2991
+ existing.position = Math.min(existing.position, position);
2992
+ const seen = new Set(existing.prs.map((pr) => pr.id));
2993
+ existing.prs.push(...prs.filter((pr) => !seen.has(pr.id)));
2994
+ }
2995
+ position++;
2996
+ }
2997
+ }
2998
+ }
2999
+ if (merged.size === 0) return { board: null, viewer };
3000
+ const groups = [];
3001
+ for (const bucket of [...merged.values()].toSorted(
3002
+ (a, b) => a.position - b.position
3003
+ )) {
3004
+ const entry = {
3005
+ key: bucket.key,
3006
+ title: bucket.title,
3007
+ total: bucket.total,
3008
+ prs: bucket.prs
3009
+ };
3010
+ const last = groups.at(-1);
3011
+ if (last?.key === bucket.group.key) last.buckets.push(entry);
3012
+ else {
3013
+ groups.push({
3014
+ key: bucket.group.key,
3015
+ title: bucket.group.title,
3016
+ buckets: [entry]
3017
+ });
3018
+ }
3019
+ }
3020
+ const [oldest] = fetchedAts.toSorted((a, b) => a.localeCompare(b));
3021
+ return { board: { fetchedAt: oldest ?? "", groups }, viewer };
3022
+ };
3023
+ const applyStreamEvent = (snapshot, event) => {
3024
+ if (event.type !== "pr" && event.type !== "prRemoved") {
3025
+ return { snapshot, changed: false, readAccounts: [] };
3026
+ }
3027
+ const id = event.type === "pr" ? event.pr.id : event.id;
3028
+ let changed = false;
3029
+ const holding = /* @__PURE__ */ new Set();
3030
+ const next = {};
3031
+ for (const [account, read] of Object.entries(snapshot)) {
3032
+ const groups = read.groups.map((group) => ({
3033
+ ...group,
3034
+ buckets: group.buckets.map((bucket) => {
3035
+ if (!bucket.prs.some((pr) => pr.id === id)) return bucket;
3036
+ holding.add(account);
3037
+ changed = true;
3038
+ if (event.type === "pr") {
3039
+ return {
3040
+ ...bucket,
3041
+ prs: bucket.prs.map((pr) => pr.id === id ? event.pr : pr)
3042
+ };
3043
+ }
3044
+ return {
3045
+ ...bucket,
3046
+ total: Math.max(0, bucket.total - 1),
3047
+ prs: bucket.prs.filter((pr) => pr.id !== id)
3048
+ };
3049
+ })
3050
+ }));
3051
+ next[account] = { ...read, groups };
3052
+ }
3053
+ const readAccounts = new Set(holding);
3054
+ if (event.type === "pr") {
3055
+ const owner = accountOfOwnerRepo(event.pr.ownerRepo);
3056
+ for (const account of Object.keys(snapshot)) {
3057
+ if (account.toLowerCase() === owner) readAccounts.add(account);
3058
+ }
3059
+ }
3060
+ return {
3061
+ snapshot: changed ? next : snapshot,
3062
+ changed,
3063
+ readAccounts: [...readAccounts]
3064
+ };
3065
+ };
3066
+
3067
+ const lingerMs = 5 * 6e4;
3068
+ const boardReadDelayMs = 3e3;
3069
+ const reconnectDelayMs = 1e3;
3070
+ const minRetryMs = 2e3;
3071
+ const maxRetryMs = 5 * 6e4;
3072
+ const messageOf = (error) => error instanceof Error ? error.message : String(error);
3073
+ const retryDelayMs = (error, failures) => {
3074
+ if (isReviewflowFailure(error)) {
3075
+ if (error.retryAfterMs !== void 0) return error.retryAfterMs;
3076
+ if (error.status === 401) return maxRetryMs;
3077
+ }
3078
+ return Math.min(minRetryMs * 2 ** (failures - 1), maxRetryMs);
3079
+ };
3080
+ const wait = async (ms, signal) => new Promise((resolve) => {
3081
+ const done = () => {
3082
+ clearTimeout(timer);
3083
+ signal.removeEventListener("abort", done);
3084
+ resolve();
3085
+ };
3086
+ const timer = setTimeout(done, ms);
3087
+ signal.addEventListener("abort", done, { once: true });
3088
+ });
3089
+ const createReviewflowFollower = ({
3090
+ connect,
3091
+ accounts,
3092
+ onPr,
3093
+ onPrRemoved,
3094
+ onAccountBoard,
3095
+ onBoardError,
3096
+ onPullRequestChanged,
3097
+ onStateChanged,
3098
+ onLog
3099
+ }) => {
3100
+ let watched = false;
3101
+ let state = "off";
3102
+ let controller = null;
3103
+ let linger;
3104
+ const boardReads = /* @__PURE__ */ new Map();
3105
+ const accountOfPr = /* @__PURE__ */ new Map();
3106
+ const lastSeen = /* @__PURE__ */ new Map();
3107
+ const setState = (next, error) => {
3108
+ if (state === next && error === void 0) return;
3109
+ state = next;
3110
+ onStateChanged?.(next, error ?? null);
3111
+ };
3112
+ const comparePr = (pr) => {
3113
+ const previous = lastSeen.get(pr.id);
3114
+ lastSeen.set(pr.id, pr);
3115
+ if (previous === void 0 || onPullRequestChanged === void 0) return;
3116
+ const parts = changedPullRequestParts(previous, pr);
3117
+ if (parts.length === 0) return;
3118
+ onPullRequestChanged({ ownerRepo: pr.ownerRepo, number: pr.number, parts });
3119
+ };
3120
+ const linked = (account) => accounts().find((owner) => owner.toLowerCase() === account.toLowerCase());
3121
+ const readBoard = async (connection, account, signal) => {
3122
+ const read = boardReads.get(account);
3123
+ if (read === void 0 || signal.aborted) return false;
3124
+ read.timer = void 0;
3125
+ read.inFlight = true;
3126
+ read.again = false;
3127
+ try {
3128
+ const groups = await connection.client.board(account);
3129
+ if (signal.aborted) return false;
3130
+ onAccountBoard(account, groups);
3131
+ } catch (error) {
3132
+ if (signal.aborted) return false;
3133
+ onBoardError?.(account, messageOf(error));
3134
+ } finally {
3135
+ read.inFlight = false;
3136
+ }
3137
+ return read.again;
3138
+ };
3139
+ const scheduleBoard = (connection, account, signal) => {
3140
+ const owner = linked(account);
3141
+ if (owner === void 0) return;
3142
+ const read = boardReads.get(owner) ?? { inFlight: false, again: false };
3143
+ boardReads.set(owner, read);
3144
+ if (read.inFlight) {
3145
+ read.again = true;
3146
+ return;
3147
+ }
3148
+ read.timer ??= setTimeout(() => {
3149
+ readBoard(connection, owner, signal).then((again) => {
3150
+ if (again) scheduleBoard(connection, owner, signal);
3151
+ }).catch((error) => {
3152
+ onLog?.({
3153
+ level: "warn",
3154
+ message: "reviewflow board read failed",
3155
+ error
3156
+ });
3157
+ });
3158
+ }, boardReadDelayMs);
3159
+ };
3160
+ const scheduleEveryBoard = (connection, signal) => {
3161
+ for (const owner of accounts()) scheduleBoard(connection, owner, signal);
3162
+ };
3163
+ const follow = async (connection, signal, onReady) => {
3164
+ const scope = [];
3165
+ let ready = false;
3166
+ for await (const event of connection.events(signal)) {
3167
+ if (event.type === "end") {
3168
+ onLog?.({
3169
+ level: "info",
3170
+ message: "reviewflow ended the stream",
3171
+ fields: { reason: event.reason }
3172
+ });
3173
+ scheduleEveryBoard(connection, signal);
3174
+ return ready;
3175
+ }
3176
+ if (event.type === "ready") {
3177
+ ready = true;
3178
+ onReady();
3179
+ onLog?.({
3180
+ level: "info",
3181
+ message: "reviewflow stream ready",
3182
+ fields: { prs: scope.length }
3183
+ });
3184
+ for (const pr of scope.splice(0)) onPr(pr, "scope");
3185
+ setState("live");
3186
+ scheduleEveryBoard(connection, signal);
3187
+ } else if (event.type === "pr") {
3188
+ accountOfPr.set(event.pr.id, accountOfOwnerRepo(event.pr.ownerRepo));
3189
+ comparePr(event.pr);
3190
+ if (ready) {
3191
+ onPr(event.pr, "change");
3192
+ scheduleBoard(
3193
+ connection,
3194
+ accountOfOwnerRepo(event.pr.ownerRepo),
3195
+ signal
3196
+ );
3197
+ } else {
3198
+ scope.push(event.pr);
3199
+ }
3200
+ } else if (ready) {
3201
+ const account = accountOfPr.get(event.id);
3202
+ accountOfPr.delete(event.id);
3203
+ lastSeen.delete(event.id);
3204
+ onPrRemoved(event.id);
3205
+ if (account === void 0) scheduleEveryBoard(connection, signal);
3206
+ else scheduleBoard(connection, account, signal);
3207
+ }
3208
+ }
3209
+ return ready;
3210
+ };
3211
+ const run = async (signal) => {
3212
+ let failures = 0;
3213
+ const resetFailures = () => {
3214
+ failures = 0;
3215
+ };
3216
+ while (!signal.aborted) {
3217
+ const connection = connect();
3218
+ if (connection === null) {
3219
+ setState("off");
3220
+ return;
3221
+ }
3222
+ let delayMs = reconnectDelayMs;
3223
+ setState("connecting");
3224
+ try {
3225
+ const reachedReady = await follow(connection, signal, resetFailures);
3226
+ if (!reachedReady && !signal.aborted) {
3227
+ failures++;
3228
+ delayMs = retryDelayMs(void 0, failures);
3229
+ onLog?.({
3230
+ level: "warn",
3231
+ message: "reviewflow ended the stream before it was ready",
3232
+ fields: { retryInMs: delayMs }
3233
+ });
3234
+ }
3235
+ } catch (error) {
3236
+ if (signal.aborted) return;
3237
+ failures++;
3238
+ delayMs = retryDelayMs(error, failures);
3239
+ onLog?.({
3240
+ level: "warn",
3241
+ message: "reviewflow stream failed",
3242
+ error,
3243
+ fields: { retryInMs: delayMs }
3244
+ });
3245
+ setState("error", messageOf(error));
3246
+ }
3247
+ if (state === "live") setState("connecting");
3248
+ await wait(delayMs, signal);
3249
+ }
3250
+ };
3251
+ const stop = () => {
3252
+ controller?.abort();
3253
+ controller = null;
3254
+ for (const read of boardReads.values()) clearTimeout(read.timer);
3255
+ boardReads.clear();
3256
+ accountOfPr.clear();
3257
+ setState("off");
3258
+ };
3259
+ const start = () => {
3260
+ if (controller !== null) return;
3261
+ const current = new AbortController();
3262
+ controller = current;
3263
+ run(current.signal).catch((error) => {
3264
+ onLog?.({
3265
+ level: "error",
3266
+ message: "reviewflow stream stopped",
3267
+ error
3268
+ });
3269
+ }).finally(() => {
3270
+ if (controller === current) controller = null;
3271
+ });
3272
+ };
3273
+ return {
3274
+ setWatched: (value) => {
3275
+ watched = value;
3276
+ clearTimeout(linger);
3277
+ linger = void 0;
3278
+ if (value) start();
3279
+ else if (controller !== null) linger = setTimeout(stop, lingerMs);
3280
+ },
3281
+ restart: () => {
3282
+ const wasOpen = controller !== null;
3283
+ stop();
3284
+ lastSeen.clear();
3285
+ if (watched || wasOpen) start();
3286
+ },
3287
+ state: () => state,
3288
+ close: () => {
3289
+ watched = false;
3290
+ clearTimeout(linger);
3291
+ stop();
3292
+ }
3293
+ };
3294
+ };
3295
+
3296
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
3297
+ const insertCheckoutEntry = (source, ownerRepo) => {
3298
+ let parsed;
3299
+ if (source.trim() === "") {
3300
+ parsed = {};
3301
+ } else {
3302
+ try {
3303
+ parsed = JSON.parse(source);
3304
+ } catch {
3305
+ return null;
3306
+ }
3307
+ }
3308
+ if (!isRecord(parsed)) return null;
3309
+ const repos = parsed.repos ?? {};
3310
+ if (!isRecord(repos)) return null;
3311
+ const wanted = ownerRepo.toLowerCase();
3312
+ const existing = Object.keys(repos).find(
3313
+ (key2) => key2.toLowerCase() === wanted
3314
+ );
3315
+ const key = existing ?? ownerRepo;
3316
+ const next = existing === void 0 ? { ...repos, [key]: "" } : repos;
3317
+ const text = `${JSON.stringify({ ...parsed, repos: next }, null, 2)}
3318
+ `;
3319
+ const marker = `${JSON.stringify(key)}: "`;
3320
+ const reposAt = text.indexOf('\n "repos": ');
3321
+ const at = text.indexOf(marker, reposAt);
3322
+ if (at === -1) return null;
3323
+ const value = at + marker.length;
3324
+ return { source: text, cursor: value };
3325
+ };
3326
+
3327
+ 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 };
3328
+ //# sourceMappingURL=index-browser.es.js.map