nugit-cli 0.0.1-alpha

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.
@@ -0,0 +1,58 @@
1
+ import { readStackFile, parseRepoFullName, validateStackDoc, findGitRoot } from "./nugit-stack.js";
2
+ import { sortStackPrs } from "./stack-propagate.js";
3
+
4
+ /**
5
+ * @param {string | null} repoOpt owner/repo override
6
+ */
7
+ export function loadStackContext(repoOpt) {
8
+ const root = findGitRoot();
9
+ if (!root) {
10
+ throw new Error("Not inside a git repository");
11
+ }
12
+ const doc = readStackFile(root);
13
+ if (!doc) {
14
+ throw new Error("No .nugit/stack.json — run nugit init first");
15
+ }
16
+ validateStackDoc(doc);
17
+ const repoFull = repoOpt || doc.repo_full_name;
18
+ if (!repoFull) {
19
+ throw new Error("Missing repo_full_name in stack file; pass --repo owner/repo");
20
+ }
21
+ const { owner, repo } = parseRepoFullName(repoFull);
22
+ const sorted = sortStackPrs(doc.prs);
23
+ return { root, doc, owner, repo, sorted };
24
+ }
25
+
26
+ /**
27
+ * @param {ReturnType<typeof sortStackPrs>} sorted
28
+ * @param {number} fromPr
29
+ * @param {number} toPr
30
+ */
31
+ export function assertFromBelowTo(sorted, fromPr, toPr) {
32
+ const idxFrom = sorted.findIndex((p) => p.pr_number === fromPr);
33
+ const idxTo = sorted.findIndex((p) => p.pr_number === toPr);
34
+ if (idxFrom < 0) {
35
+ throw new Error(`PR #${fromPr} is not in the stack`);
36
+ }
37
+ if (idxTo < 0) {
38
+ throw new Error(`PR #${toPr} is not in the stack`);
39
+ }
40
+ if (idxFrom >= idxTo) {
41
+ throw new Error(`--from-pr (#${fromPr}) must be below --to-pr (#${toPr}) in stack order`);
42
+ }
43
+ }
44
+
45
+ /**
46
+ * @param {ReturnType<typeof sortStackPrs>} sorted
47
+ * @param {number} fromPr
48
+ */
49
+ export function defaultFixPr(sorted, fromPr) {
50
+ const idx = sorted.findIndex((p) => p.pr_number === fromPr);
51
+ if (idx < 0) {
52
+ throw new Error(`PR #${fromPr} is not in the stack`);
53
+ }
54
+ if (idx + 1 >= sorted.length) {
55
+ throw new Error(`PR #${fromPr} is the stack tip; there is no upper PR for the fix`);
56
+ }
57
+ return sorted[idx + 1].pr_number;
58
+ }
@@ -0,0 +1,422 @@
1
+ import { execFileSync, execSync } from "child_process";
2
+ import fs from "fs";
3
+ import path from "path";
4
+ import { stackJsonPath, validateStackDoc } from "./nugit-stack.js";
5
+
6
+ /** @param {unknown[]} prs */
7
+ export function sortStackPrs(prs) {
8
+ if (!Array.isArray(prs)) {
9
+ return [];
10
+ }
11
+ return [...prs].sort((a, b) => {
12
+ const pa = a && typeof a === "object" ? /** @type {{ position?: number }} */ (a).position : 0;
13
+ const pb = b && typeof b === "object" ? /** @type {{ position?: number }} */ (b).position : 0;
14
+ return (pa ?? 0) - (pb ?? 0);
15
+ });
16
+ }
17
+
18
+ /**
19
+ * Build layer metadata for the PR at `position` in an ordered stack.
20
+ * @param {Array<{ position: number, pr_number: number, head_branch?: string, base_branch?: string }>} sortedPrs
21
+ * @param {number} position
22
+ */
23
+ export function buildLayer(sortedPrs, position) {
24
+ const idx = sortedPrs.findIndex((p) => p.position === position);
25
+ if (idx < 0) {
26
+ throw new Error(`No PR with position ${position} in stack`);
27
+ }
28
+ const self = sortedPrs[idx];
29
+ const stack_size = sortedPrs.length;
30
+
31
+ /** @type {{ type: 'branch', ref: string } | { type: 'stack_pr', pr_number: number, head_branch: string }} */
32
+ let below;
33
+ if (idx === 0) {
34
+ below = { type: "branch", ref: self.base_branch || "" };
35
+ } else {
36
+ const low = sortedPrs[idx - 1];
37
+ below = {
38
+ type: "stack_pr",
39
+ pr_number: low.pr_number,
40
+ head_branch: low.head_branch || ""
41
+ };
42
+ }
43
+
44
+ /** @type {{ type: 'stack_pr', pr_number: number, head_branch: string } | null} */
45
+ let above = null;
46
+ if (idx + 1 < sortedPrs.length) {
47
+ const high = sortedPrs[idx + 1];
48
+ above = {
49
+ type: "stack_pr",
50
+ pr_number: high.pr_number,
51
+ head_branch: high.head_branch || ""
52
+ };
53
+ }
54
+
55
+ const tipPr = sortedPrs[sortedPrs.length - 1];
56
+ const tip = {
57
+ pr_number: tipPr.pr_number,
58
+ head_branch: tipPr.head_branch || ""
59
+ };
60
+
61
+ return { position, stack_size, below, above, tip };
62
+ }
63
+
64
+ /**
65
+ * @param {Record<string, unknown>} doc validated stack doc
66
+ * @param {ReturnType<typeof sortStackPrs>} sortedPrs
67
+ * @param {number} position
68
+ */
69
+ export function documentForHeadBranch(doc, sortedPrs, position) {
70
+ const idx = sortedPrs.findIndex((p) => p.position === position);
71
+ if (idx < 0) {
72
+ throw new Error(`No PR with position ${position} in stack`);
73
+ }
74
+ /** Prefix: this branch only includes PRs from bottom through this layer. */
75
+ const prsCopy = sortedPrs.slice(0, idx + 1).map((p) => ({ ...p }));
76
+ const layer = buildLayer(sortedPrs, position);
77
+ const { layer: _drop, ...rest } = doc;
78
+ return {
79
+ ...rest,
80
+ prs: prsCopy,
81
+ layer
82
+ };
83
+ }
84
+
85
+ function execGit(root, args, dryRun) {
86
+ if (dryRun) {
87
+ console.error(`[dry-run] git ${args.map((a) => (/\s/.test(a) ? JSON.stringify(a) : a)).join(" ")}`);
88
+ return "";
89
+ }
90
+ return execFileSync("git", args, { cwd: root, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
91
+ }
92
+
93
+ /**
94
+ * Clear stuck merge/rebase state so a later `git checkout` can run.
95
+ * @param {string} root
96
+ */
97
+ export function abortStaleGitOperations(root) {
98
+ for (const args of /** @type {const} */ ([["merge", "--abort"], ["rebase", "--abort"]])) {
99
+ try {
100
+ execFileSync("git", args, { cwd: root, stdio: "ignore" });
101
+ } catch {
102
+ /* not in merge/rebase */
103
+ }
104
+ }
105
+ }
106
+
107
+ /** Relative path always with `/` for git and comparisons */
108
+ export const STACK_JSON_REL = path.join(".nugit", "stack.json").replace(/\\/g, "/");
109
+
110
+ /**
111
+ * @param {string} root
112
+ */
113
+ function listUnmergedPaths(root) {
114
+ const out = execSync("git diff --name-only --diff-filter=U", { cwd: root, encoding: "utf8" })
115
+ .trim()
116
+ .split("\n")
117
+ .filter(Boolean)
118
+ .map((p) => p.replace(/\\/g, "/"));
119
+ return out;
120
+ }
121
+
122
+ /**
123
+ * When merging the lower head into the upper, `.nugit/stack.json` often conflicts (different prefixes).
124
+ * We take the incoming branch's version and complete the merge; propagate immediately overwrites the file.
125
+ * @param {string} root
126
+ * @param {string} lowerHeadBranch
127
+ * @returns {boolean} true if merge was completed
128
+ */
129
+ function resolveStackJsonMergeWithTheirs(root, lowerHeadBranch) {
130
+ const um = listUnmergedPaths(root);
131
+ if (um.length !== 1 || um[0] !== STACK_JSON_REL) {
132
+ return false;
133
+ }
134
+ execGit(root, ["checkout", "--theirs", STACK_JSON_REL], false);
135
+ execGit(root, ["add", STACK_JSON_REL], false);
136
+ execGit(root, ["commit", "--no-edit"], false);
137
+ console.error(
138
+ `Auto-resolved ${STACK_JSON_REL} merge (used ${lowerHeadBranch}); replacing with propagated metadata next.`
139
+ );
140
+ return true;
141
+ }
142
+
143
+ /**
144
+ * Merge the stacked branch below into the current branch so upper heads include
145
+ * the latest `.nugit/stack.json` (and any other commits) from the layer under them.
146
+ * Without this, committing on test0 first leaves test1/test2 missing that commit → GitHub PR conflicts.
147
+ * @param {string} root
148
+ * @param {string} lowerHeadBranch local branch name (e.g. test-stack0)
149
+ * @param {boolean} dryRun
150
+ */
151
+ export function mergeLowerStackHead(root, lowerHeadBranch, dryRun) {
152
+ if (!lowerHeadBranch) {
153
+ return;
154
+ }
155
+ if (dryRun) {
156
+ console.error(`[dry-run] git merge --no-edit ${lowerHeadBranch} # absorb lower stacked head`);
157
+ return;
158
+ }
159
+ try {
160
+ const out = execGit(root, ["merge", "--no-edit", lowerHeadBranch], false);
161
+ if (out) {
162
+ console.error(out);
163
+ }
164
+ } catch (err) {
165
+ if (resolveStackJsonMergeWithTheirs(root, lowerHeadBranch)) {
166
+ return;
167
+ }
168
+ const msg = err && typeof err === "object" && "stderr" in err ? String(/** @type {{ stderr?: Buffer }} */ (err).stderr) : String(err);
169
+ throw new Error(
170
+ `git merge ${lowerHeadBranch} failed while propagating (upper branch must include the lower head). ` +
171
+ `Resolve conflicts, commit the merge, then re-run \`nugit stack propagate\`. Underlying: ${msg.trim() || err}`
172
+ );
173
+ }
174
+ }
175
+
176
+ /**
177
+ * @param {string} root
178
+ */
179
+ export function assertCleanWorkingTree(root) {
180
+ const out = execSync("git status --porcelain", { cwd: root, encoding: "utf8" });
181
+ if (out.trim()) {
182
+ throw new Error("Working tree is not clean; commit or stash before nugit stack propagate");
183
+ }
184
+ }
185
+
186
+ const BOOTSTRAP_COMMIT_MESSAGE = "Nugit stack creation";
187
+
188
+ /**
189
+ * If the only dirty path is `.nugit/stack.json`, commit it so propagate can run.
190
+ * @param {string} root
191
+ * @param {boolean} dryRun
192
+ * @param {string} [message]
193
+ * @returns {boolean} whether a commit was made (or would be made in dry-run)
194
+ */
195
+ export function tryBootstrapCommitStackJson(root, dryRun, message = BOOTSTRAP_COMMIT_MESSAGE) {
196
+ const full = execSync("git status --porcelain", { cwd: root, encoding: "utf8" }).trim();
197
+ if (!full) {
198
+ return false;
199
+ }
200
+
201
+ const besidesStack = execFileSync(
202
+ "git",
203
+ ["status", "--porcelain", "--", ".", ":(exclude).nugit/stack.json"],
204
+ { cwd: root, encoding: "utf8" }
205
+ ).trim();
206
+
207
+ if (besidesStack) {
208
+ throw new Error(
209
+ "Working tree has changes outside `.nugit/stack.json`; commit or stash them before propagate.\n" + besidesStack
210
+ );
211
+ }
212
+
213
+ const stackDirty = execFileSync("git", ["status", "--porcelain", "--", ".nugit/stack.json"], {
214
+ cwd: root,
215
+ encoding: "utf8"
216
+ }).trim();
217
+
218
+ if (!stackDirty) {
219
+ throw new Error(
220
+ "Working tree is not clean but `.nugit/stack.json` is not among the changes; commit or stash manually.\n" + full
221
+ );
222
+ }
223
+
224
+ if (dryRun) {
225
+ console.error(`[dry-run] git add ${STACK_JSON_REL} && git commit -m ${JSON.stringify(message)}`);
226
+ return true;
227
+ }
228
+
229
+ execGit(root, ["add", STACK_JSON_REL], false);
230
+ execGit(root, ["commit", "-m", message], false);
231
+ console.error(`Committed ${STACK_JSON_REL} (${message})`);
232
+ return true;
233
+ }
234
+
235
+ /**
236
+ * @param {string} root
237
+ * @returns {{ kind: 'branch' | 'detached', ref: string }}
238
+ */
239
+ export function getCurrentHead(root) {
240
+ try {
241
+ const sym = execSync("git symbolic-ref -q --short HEAD", {
242
+ cwd: root,
243
+ encoding: "utf8"
244
+ }).trim();
245
+ if (sym) {
246
+ return { kind: "branch", ref: sym };
247
+ }
248
+ } catch {
249
+ /* detached */
250
+ }
251
+ const sha = execSync("git rev-parse HEAD", { cwd: root, encoding: "utf8" }).trim();
252
+ return { kind: "detached", ref: sha };
253
+ }
254
+
255
+ /**
256
+ * @param {string} root
257
+ * @param {string} remote
258
+ * @param {string} branch
259
+ * @param {boolean} dryRun
260
+ */
261
+ export function checkoutStackHead(root, remote, branch, dryRun) {
262
+ execGit(root, ["fetch", remote, branch], dryRun);
263
+ if (dryRun) {
264
+ return;
265
+ }
266
+ // After `git merge`, the index can still block `git checkout` to the next head ("resolve your
267
+ // index first"). Sync to HEAD before switching branches.
268
+ abortStaleGitOperations(root);
269
+ execGit(root, ["reset", "--hard", "HEAD"], false);
270
+ try {
271
+ execGit(root, ["checkout", branch], false);
272
+ } catch {
273
+ execGit(root, ["checkout", "-B", branch, `${remote}/${branch}`], false);
274
+ }
275
+ }
276
+
277
+ /**
278
+ * @param {string} root
279
+ * @param {string} ref
280
+ * @param {boolean} dryRun
281
+ */
282
+ export function checkoutRef(root, ref, dryRun) {
283
+ if (dryRun) {
284
+ execGit(root, ["checkout", ref], dryRun);
285
+ return;
286
+ }
287
+ try {
288
+ execGit(root, ["checkout", ref], false);
289
+ } catch {
290
+ abortStaleGitOperations(root);
291
+ execGit(root, ["reset", "--hard", "HEAD"], false);
292
+ execGit(root, ["checkout", ref], false);
293
+ }
294
+ }
295
+
296
+ /**
297
+ * @param {string} root
298
+ * @param {string} fileRel path relative to root
299
+ */
300
+ function fileContentAtHead(root, fileRel) {
301
+ try {
302
+ return execFileSync("git", ["show", `HEAD:${fileRel}`], {
303
+ cwd: root,
304
+ encoding: "utf8",
305
+ stdio: ["ignore", "pipe", "ignore"]
306
+ });
307
+ } catch {
308
+ return null;
309
+ }
310
+ }
311
+
312
+ /**
313
+ * @param {object} opts
314
+ * @param {string} opts.root
315
+ * @param {string} [opts.message]
316
+ * @param {boolean} [opts.push]
317
+ * @param {boolean} [opts.dryRun]
318
+ * @param {string} [opts.remote]
319
+ * @param {boolean} [opts.noMergeLower] if true, skip merging the branch below into each upper head (not recommended)
320
+ * @param {boolean} [opts.bootstrapCommit] if false, skip auto-commit of dirty `.nugit/stack.json` before propagating
321
+ */
322
+ export async function runStackPropagate(opts) {
323
+ const root = opts.root;
324
+ const message = opts.message || "nugit: propagate stack metadata";
325
+ const push = Boolean(opts.push);
326
+ const dryRun = Boolean(opts.dryRun);
327
+ const remote = opts.remote || "origin";
328
+ const noMergeLower = Boolean(opts.noMergeLower);
329
+ const bootstrapCommit = opts.bootstrapCommit !== false;
330
+
331
+ const raw = JSON.parse(fs.readFileSync(stackJsonPath(root), "utf8"));
332
+ validateStackDoc(raw);
333
+ /** @type {Record<string, unknown>} */
334
+ const doc = raw;
335
+ const sorted = sortStackPrs(doc.prs);
336
+ if (sorted.length === 0) {
337
+ throw new Error("Stack has no PRs; nothing to propagate");
338
+ }
339
+
340
+ /** @type {boolean} */
341
+ let bootstrappedDry = false;
342
+ if (!dryRun && bootstrapCommit) {
343
+ tryBootstrapCommitStackJson(root, false);
344
+ }
345
+ if (dryRun && bootstrapCommit) {
346
+ bootstrappedDry = tryBootstrapCommitStackJson(root, true);
347
+ }
348
+
349
+ if (!dryRun) {
350
+ assertCleanWorkingTree(root);
351
+ } else if (!bootstrappedDry) {
352
+ assertCleanWorkingTree(root);
353
+ }
354
+
355
+ const start = getCurrentHead(root);
356
+
357
+ /** @type {string | null} */
358
+ let prevHeadBranch = null;
359
+
360
+ try {
361
+ for (const entry of sorted) {
362
+ const headBranch =
363
+ entry && typeof entry === "object" ? String(/** @type {{ head_branch?: string }} */ (entry).head_branch || "").trim() : "";
364
+ if (!headBranch) {
365
+ console.error(`Skipping position ${entry?.position}: missing head_branch`);
366
+ continue;
367
+ }
368
+ const pos = /** @type {{ position: number }} */ (entry).position;
369
+ const toWrite = documentForHeadBranch(doc, sorted, pos);
370
+ validateStackDoc(toWrite);
371
+ const json = JSON.stringify(toWrite, null, 2) + "\n";
372
+
373
+ if (dryRun) {
374
+ console.error(`[dry-run] checkout ${headBranch}`);
375
+ if (!noMergeLower && prevHeadBranch) {
376
+ mergeLowerStackHead(root, prevHeadBranch, true);
377
+ }
378
+ console.error(
379
+ `[dry-run] write ${STACK_JSON_REL} (${pos + 1} prs prefix of ${sorted.length}, layer position ${pos})`
380
+ );
381
+ prevHeadBranch = headBranch;
382
+ continue;
383
+ }
384
+
385
+ checkoutStackHead(root, remote, headBranch, false);
386
+
387
+ if (!noMergeLower && prevHeadBranch) {
388
+ mergeLowerStackHead(root, prevHeadBranch, false);
389
+ console.error(`Merged ${prevHeadBranch} into ${headBranch} before writing stack metadata`);
390
+ }
391
+
392
+ const existing = fileContentAtHead(root, STACK_JSON_REL);
393
+ if (existing === json) {
394
+ console.error(`Skip ${headBranch}: .nugit/stack.json already matches`);
395
+ prevHeadBranch = headBranch;
396
+ continue;
397
+ }
398
+
399
+ const dir = path.join(root, ".nugit");
400
+ fs.mkdirSync(dir, { recursive: true });
401
+ fs.writeFileSync(stackJsonPath(root), json);
402
+ execGit(root, ["add", STACK_JSON_REL], false);
403
+ execGit(root, ["commit", "-m", message], false);
404
+ console.error(`Committed ${STACK_JSON_REL} on ${headBranch}`);
405
+
406
+ if (push) {
407
+ execGit(root, ["push", remote, headBranch], false);
408
+ console.error(`Pushed ${remote}/${headBranch}`);
409
+ }
410
+
411
+ prevHeadBranch = headBranch;
412
+ }
413
+ } finally {
414
+ if (!dryRun) {
415
+ checkoutRef(root, start.ref, false);
416
+ }
417
+ }
418
+
419
+ if (dryRun) {
420
+ console.error(`[dry-run] would restore checkout ${start.ref}`);
421
+ }
422
+ }
@@ -0,0 +1,126 @@
1
+ import { githubGetPull, githubListPullFiles, githubRestJson } from "../github-rest.js";
2
+
3
+ /**
4
+ * @param {unknown[]} items
5
+ * @param {number} batchSize
6
+ * @param {(item: unknown, index: number) => Promise<unknown>} fn
7
+ */
8
+ async function mapInBatches(items, batchSize, fn) {
9
+ const out = [];
10
+ for (let i = 0; i < items.length; i += batchSize) {
11
+ const batch = items.slice(i, i + batchSize);
12
+ const part = await Promise.all(batch.map((item, j) => fn(item, i + j)));
13
+ out.push(...part);
14
+ }
15
+ return out;
16
+ }
17
+
18
+ /**
19
+ * Faster view-oriented comment fetch: first page only.
20
+ * @param {string} owner
21
+ * @param {string} repo
22
+ * @param {number} issueNumber
23
+ */
24
+ async function githubListIssueCommentsFirstPage(owner, repo, issueNumber) {
25
+ const o = encodeURIComponent(owner);
26
+ const r = encodeURIComponent(repo);
27
+ const n = encodeURIComponent(String(issueNumber));
28
+ const out = await githubRestJson("GET", `/repos/${o}/${r}/issues/${n}/comments?per_page=50&page=1`);
29
+ return Array.isArray(out) ? out : [];
30
+ }
31
+
32
+ /**
33
+ * Faster view-oriented review comment fetch: first page only.
34
+ * @param {string} owner
35
+ * @param {string} repo
36
+ * @param {number} pullNumber
37
+ */
38
+ async function githubListPullReviewCommentsFirstPage(owner, repo, pullNumber) {
39
+ const o = encodeURIComponent(owner);
40
+ const r = encodeURIComponent(repo);
41
+ const n = encodeURIComponent(String(pullNumber));
42
+ const out = await githubRestJson("GET", `/repos/${o}/${r}/pulls/${n}/comments?per_page=50&page=1`);
43
+ return Array.isArray(out) ? out : [];
44
+ }
45
+
46
+ /**
47
+ * Pull reviews first page (for approval/changes requested signal in TUI).
48
+ * @param {string} owner
49
+ * @param {string} repo
50
+ * @param {number} pullNumber
51
+ */
52
+ async function githubListPullReviewsFirstPage(owner, repo, pullNumber) {
53
+ const o = encodeURIComponent(owner);
54
+ const r = encodeURIComponent(repo);
55
+ const n = encodeURIComponent(String(pullNumber));
56
+ const out = await githubRestJson("GET", `/repos/${o}/${r}/pulls/${n}/reviews?per_page=50&page=1`);
57
+ return Array.isArray(out) ? out : [];
58
+ }
59
+
60
+ /**
61
+ * Reduce reviews to a single state for UI badges.
62
+ * @param {unknown[]} reviews
63
+ */
64
+ function summarizeReviewState(reviews) {
65
+ /** @type {Map<string, string>} */
66
+ const byUser = new Map();
67
+ for (const rv of reviews) {
68
+ if (!rv || typeof rv !== "object") continue;
69
+ const r = /** @type {Record<string, unknown>} */ (rv);
70
+ const user = r.user && typeof r.user === "object" ? /** @type {Record<string, unknown>} */ (r.user) : {};
71
+ const login = typeof user.login === "string" ? user.login : "";
72
+ const state = typeof r.state === "string" ? r.state.toUpperCase() : "";
73
+ if (!login || !state) continue;
74
+ byUser.set(login, state);
75
+ }
76
+ const states = [...byUser.values()];
77
+ if (states.includes("CHANGES_REQUESTED")) return "changes_requested";
78
+ if (states.includes("APPROVED")) return "approved";
79
+ if (states.includes("COMMENTED")) return "commented";
80
+ return "none";
81
+ }
82
+
83
+ /**
84
+ * @param {string} owner
85
+ * @param {string} repo
86
+ * @param {Array<{ pr_number: number, position: number, head_branch?: string, base_branch?: string }>} prEntries
87
+ * @param {number} [concurrency]
88
+ */
89
+ export async function fetchStackPrDetails(owner, repo, prEntries, concurrency = 6) {
90
+ const sorted = [...prEntries].sort((a, b) => a.position - b.position);
91
+ return mapInBatches(sorted, concurrency, async (entry) => {
92
+ const n = entry.pr_number;
93
+ try {
94
+ const pull = await githubGetPull(owner, repo, n);
95
+ const [issueRes, reviewRes, filesRes, reviewsRes] = await Promise.allSettled([
96
+ githubListIssueCommentsFirstPage(owner, repo, n),
97
+ githubListPullReviewCommentsFirstPage(owner, repo, n),
98
+ githubListPullFiles(owner, repo, n),
99
+ githubListPullReviewsFirstPage(owner, repo, n)
100
+ ]);
101
+ const issueComments = issueRes.status === "fulfilled" ? issueRes.value : [];
102
+ const reviewComments = reviewRes.status === "fulfilled" ? reviewRes.value : [];
103
+ const files = filesRes.status === "fulfilled" ? filesRes.value : [];
104
+ const reviews = reviewsRes.status === "fulfilled" ? reviewsRes.value : [];
105
+ return {
106
+ entry,
107
+ pull,
108
+ issueComments,
109
+ reviewComments,
110
+ files: Array.isArray(files) ? files : [],
111
+ reviewSummary: summarizeReviewState(reviews),
112
+ error: null
113
+ };
114
+ } catch (e) {
115
+ return {
116
+ entry,
117
+ pull: null,
118
+ issueComments: [],
119
+ reviewComments: [],
120
+ files: [],
121
+ reviewSummary: "none",
122
+ error: String(/** @type {Error} */ (e).message || e)
123
+ };
124
+ }
125
+ });
126
+ }