reslop 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/git.js CHANGED
@@ -6,16 +6,24 @@ const path = require('node:path');
6
6
 
7
7
  const diff = require('./diff.js');
8
8
  const { parseDiff, synthesizeNewFile, itemsFromFiles } = diff;
9
- const { itemPath } = require('./files.js');
10
- const { REVIEW_DIR } = require('./review.js');
9
+ const { itemPath, isPathInScope, REVIEW_DIR } = require('./files.js');
10
+ const deps = require('./deps.js');
11
+ const { foldDepItems, mergeDepFile, depFileMeta } = deps;
12
+ const { collectUsedNames, parseAuditReport, parseOutdatedReport } = deps;
13
+ const { mergeProposedItems, MANIFEST, LOCKFILE } = deps;
11
14
 
12
15
  const GIT_CONFIG = ['-c', 'core.quotepath=false'];
13
16
  const DIFF_OPTS = ['--no-color', '--no-ext-diff', '--no-renames', '-U3'];
14
17
  const GIT_ENCODING = 'utf8';
15
18
  const GIT_MAX_BUFFER = 32 * 1024 * 1024;
16
19
  const BINARY_SIZE = 1000000;
20
+ const AUDIT_MS = 15000;
21
+ const INSTALL_MS = 120000;
17
22
 
18
- const oneLine = (text) => text.trim().split('\n')[0] || 'git failed';
23
+ const oneLine = (text) => {
24
+ const line = text.trim().split('\n')[0];
25
+ return line || 'git failed';
26
+ };
19
27
 
20
28
  const requireOk = (result, fallback = 'git failed') => {
21
29
  if (result.status === 0) return result;
@@ -40,13 +48,9 @@ const dropReviewItems = (items) => {
40
48
 
41
49
  const runGit = (args, cwd, input) => {
42
50
  const env = { ...process.env, GIT_OPTIONAL_LOCKS: '0' };
43
- const options = {
44
- cwd,
45
- encoding: GIT_ENCODING,
46
- input,
47
- maxBuffer: GIT_MAX_BUFFER,
48
- env,
49
- };
51
+ const encoding = GIT_ENCODING;
52
+ const maxBuffer = GIT_MAX_BUFFER;
53
+ const options = { cwd, encoding, input, maxBuffer, env };
50
54
  const result = spawnSync('git', args, options);
51
55
  if (result.error) {
52
56
  if (result.error.code === 'ENOENT') throw new Error('git not found');
@@ -131,6 +135,264 @@ const mergeItems = (groups) => {
131
135
  return items;
132
136
  };
133
137
 
138
+ const gitText = (top, spec) => {
139
+ const result = runGit(['show', spec], top);
140
+ if (result.status !== 0) return '';
141
+ return result.stdout ?? '';
142
+ };
143
+
144
+ const worktreeText = (top, rel) => {
145
+ try {
146
+ return fs.readFileSync(path.join(top, rel), 'utf8');
147
+ } catch {
148
+ return '';
149
+ }
150
+ };
151
+
152
+ const readDepSides = (top, rev, origin, rel) => {
153
+ if (origin === 'untracked') {
154
+ const newText = worktreeText(top, rel);
155
+ return { oldText: '', newText };
156
+ }
157
+ if (origin === 'staged') {
158
+ const oldText = gitText(top, `HEAD:${rel}`);
159
+ const newText = gitText(top, `:${rel}`);
160
+ return { oldText, newText };
161
+ }
162
+ if (origin === 'unstaged') {
163
+ const indexed = gitText(top, `:${rel}`);
164
+ const oldText = indexed || gitText(top, `HEAD:${rel}`);
165
+ const newText = worktreeText(top, rel);
166
+ return { oldText, newText };
167
+ }
168
+ if (origin === 'commit' && rev) {
169
+ const oldText = gitText(top, `${rev}^:${rel}`);
170
+ const newText = gitText(top, `${rev}:${rel}`);
171
+ return { oldText, newText };
172
+ }
173
+ return { oldText: '', newText: '' };
174
+ };
175
+
176
+ const runNpmJson = (top, args, timeout) => {
177
+ const result = spawnSync('npm', args, {
178
+ cwd: top,
179
+ encoding: GIT_ENCODING,
180
+ maxBuffer: GIT_MAX_BUFFER,
181
+ timeout,
182
+ });
183
+ if (result.error) return '';
184
+ return result.stdout || '';
185
+ };
186
+
187
+ const collectAudit = (top) => {
188
+ const lock = path.join(top, LOCKFILE);
189
+ if (!fs.existsSync(lock)) return null;
190
+ const args = ['audit', '--json', '--package-lock-only'];
191
+ const text = runNpmJson(top, args, AUDIT_MS);
192
+ return parseAuditReport(text);
193
+ };
194
+
195
+ const collectOutdated = (top) => {
196
+ const manifest = path.join(top, MANIFEST);
197
+ if (!fs.existsSync(manifest)) return null;
198
+ const args = ['outdated', '--json', '--long'];
199
+ const text = runNpmJson(top, args, AUDIT_MS);
200
+ return parseOutdatedReport(text);
201
+ };
202
+
203
+ const pickNpmMap = (override, collect, top) => {
204
+ if (override !== undefined) return override;
205
+ return collect(top);
206
+ };
207
+
208
+ const foldLoaded = (items, top, rev, extra = {}) => {
209
+ const readSides = (origin, rel) => readDepSides(top, rev, origin, rel);
210
+ let usedNames = null;
211
+ let audit = null;
212
+ let outdated = null;
213
+ const live = extra.audit === true && !rev;
214
+ let hasDepDiff = false;
215
+ for (const item of items) {
216
+ if (!depFileMeta(itemPath(item))) continue;
217
+ hasDepDiff = true;
218
+ break;
219
+ }
220
+ if (hasDepDiff || live) usedNames = collectUsedNames(top);
221
+ if (live) {
222
+ audit = pickNpmMap(extra.auditMap, collectAudit, top);
223
+ outdated = pickNpmMap(extra.outdatedMap, collectOutdated, top);
224
+ } else if (extra.audit && hasDepDiff) {
225
+ audit = pickNpmMap(extra.auditMap, collectAudit, top);
226
+ }
227
+ const folded = foldDepItems(items, readSides, usedNames, audit, outdated, {
228
+ root: top,
229
+ });
230
+ if (!live) return folded;
231
+ if (!isPathInScope(MANIFEST, extra.paths ?? [])) return folded;
232
+ const pkgText = worktreeText(top, MANIFEST);
233
+ const lockText = worktreeText(top, LOCKFILE);
234
+ return mergeProposedItems(folded, pkgText, outdated, audit, {
235
+ lockText,
236
+ usedNames,
237
+ root: top,
238
+ });
239
+ };
240
+
241
+ const depPaths = (item) => item.dep?.files ?? [];
242
+
243
+ const origDepItems = (item) => item.dep?.items ?? [];
244
+
245
+ const depChange = (item) => item.dep?.change ?? null;
246
+
247
+ const writeWorktreeFile = (top, rel, text) => {
248
+ const abs = path.join(top, rel);
249
+ fs.mkdirSync(path.dirname(abs), { recursive: true });
250
+ fs.writeFileSync(abs, text);
251
+ };
252
+
253
+ const restoreWorktreeFile = (top, rel, saved, existed) => {
254
+ const abs = path.join(top, rel);
255
+ if (existed) {
256
+ fs.writeFileSync(abs, saved);
257
+ return;
258
+ }
259
+ try {
260
+ fs.unlinkSync(abs);
261
+ } catch {
262
+ // missing
263
+ }
264
+ };
265
+
266
+ const writeIndexFile = (top, rel, text) => {
267
+ const abs = path.join(top, rel);
268
+ const existed = fs.existsSync(abs);
269
+ const saved = existed ? fs.readFileSync(abs, 'utf8') : '';
270
+ writeWorktreeFile(top, rel, text);
271
+ try {
272
+ requireOk(runGit(['add', '--', rel], top));
273
+ } finally {
274
+ restoreWorktreeFile(top, rel, saved, existed);
275
+ }
276
+ };
277
+
278
+ const mergeDepLive = (top, rel, change, origin, base, side, kind) => {
279
+ const sides = readDepSides(top, null, origin, rel);
280
+ return mergeDepFile(base, sides.oldText, sides.newText, change, side, kind);
281
+ };
282
+
283
+ const applyDepIndex = (top, rel, change, origin, side) => {
284
+ const meta = depFileMeta(rel);
285
+ if (!meta) return;
286
+ const indexed = gitText(top, `:${rel}`) || gitText(top, `HEAD:${rel}`);
287
+ const text = mergeDepLive(top, rel, change, origin, indexed, side, meta.kind);
288
+ writeIndexFile(top, rel, text);
289
+ };
290
+
291
+ const applyDepWorktree = (top, rel, change, origin) => {
292
+ const meta = depFileMeta(rel);
293
+ if (!meta) return;
294
+ const worktree = worktreeText(top, rel);
295
+ const text = mergeDepLive(
296
+ top,
297
+ rel,
298
+ change,
299
+ origin,
300
+ worktree,
301
+ 'old',
302
+ meta.kind,
303
+ );
304
+ writeWorktreeFile(top, rel, text);
305
+ };
306
+
307
+ const runNpm = (cwd, args) =>
308
+ spawnSync('npm', args, {
309
+ cwd,
310
+ encoding: GIT_ENCODING,
311
+ maxBuffer: GIT_MAX_BUFFER,
312
+ timeout: INSTALL_MS,
313
+ });
314
+
315
+ const firstDepRel = (files, kind) => {
316
+ for (const rel of files) {
317
+ const meta = depFileMeta(rel);
318
+ if (meta && meta.kind === kind) return rel;
319
+ }
320
+ return '';
321
+ };
322
+
323
+ const applyProposedUpdate = (top, item) => {
324
+ const change = depChange(item);
325
+ const origin = item.origin;
326
+ const files = depPaths(item);
327
+ const lockOnly = change.section === 'resolved';
328
+ const removing = !!change.unused;
329
+ const manifest = firstDepRel(files, 'manifest');
330
+ const lockRel = firstDepRel(files, 'lockfile');
331
+ const picked = lockOnly ? lockRel : manifest;
332
+ const target = picked || files[0] || '';
333
+ const meta = depFileMeta(target);
334
+ if (!meta) return;
335
+ let saved = '';
336
+ if (!lockOnly && !removing && manifest) {
337
+ const worktree = worktreeText(top, manifest);
338
+ saved = worktree;
339
+ const text = mergeDepLive(
340
+ top,
341
+ manifest,
342
+ change,
343
+ origin,
344
+ worktree,
345
+ 'new',
346
+ 'manifest',
347
+ );
348
+ writeWorktreeFile(top, manifest, text);
349
+ }
350
+ const cwd = meta.dir === '.' ? top : path.join(top, meta.dir);
351
+ let npmArgs = ['i'];
352
+ if (removing) npmArgs = ['uninstall', change.name];
353
+ else if (lockOnly) npmArgs = ['audit', 'fix'];
354
+ const install = item.dep.install ?? ((dir) => runNpm(dir, npmArgs));
355
+ const result = install(cwd);
356
+ const fail = removing ? 'npm uninstall failed' : 'npm i failed';
357
+ if (result && (result.error || result.status !== 0)) {
358
+ if (saved && manifest) writeWorktreeFile(top, manifest, saved);
359
+ if (result.error) {
360
+ const msg = result.error.message || fail;
361
+ throw new Error(oneLine(msg));
362
+ }
363
+ const msg = result.stderr || result.stdout || fail;
364
+ throw new Error(oneLine(msg));
365
+ }
366
+ const toAdd = [];
367
+ if (!lockOnly && manifest) toAdd.push(manifest);
368
+ const dirLock = meta.dir === '.' ? LOCKFILE : `${meta.dir}/${LOCKFILE}`;
369
+ const lockPath = lockRel || dirLock;
370
+ if (fs.existsSync(path.join(top, lockPath))) toAdd.push(lockPath);
371
+ if (toAdd.length) requireOk(runGit(['add', '--', ...toAdd], top));
372
+ };
373
+
374
+ const applyDepChange = (top, item, mode) => {
375
+ const change = depChange(item);
376
+ if (change && change.propose) {
377
+ if (mode === 'add') return void applyProposedUpdate(top, item);
378
+ if (!change.unused) return;
379
+ }
380
+ const origin = item.origin;
381
+ const files = depPaths(item);
382
+ for (const rel of files) {
383
+ if (mode === 'add') {
384
+ applyDepIndex(top, rel, change, origin, 'new');
385
+ continue;
386
+ }
387
+ if (mode === 'unstage') {
388
+ applyDepIndex(top, rel, change, origin, 'old');
389
+ continue;
390
+ }
391
+ if (origin === 'staged') applyDepIndex(top, rel, change, origin, 'old');
392
+ applyDepWorktree(top, rel, change, origin);
393
+ }
394
+ };
395
+
134
396
  const resolveRev = (cwd, spec) => {
135
397
  if (!spec) return null;
136
398
  const top = toplevel(cwd);
@@ -174,7 +436,9 @@ const load = (cwd, paths = [], options = {}) => {
174
436
  const rev = options.commit;
175
437
  if (rev) {
176
438
  const files = loadCommit(top, rev, extra);
177
- const items = dropReviewItems(itemsFromFiles(files, 'commit'));
439
+ const commitItems = itemsFromFiles(files, 'commit');
440
+ const parsed = dropReviewItems(commitItems);
441
+ const items = foldLoaded(parsed, top, rev, { ...options, paths });
178
442
  const revShort = shortRev(top, rev);
179
443
  return { top, items, rev, revShort };
180
444
  }
@@ -188,15 +452,28 @@ const load = (cwd, paths = [], options = {}) => {
188
452
  const unstagedArgs = [...GIT_CONFIG, 'diff', ...DIFF_OPTS, ...extra];
189
453
  const stagedRes = requireOk(runGit(stagedArgs, top));
190
454
  const unstagedRes = requireOk(runGit(unstagedArgs, top));
191
- const staged = itemsFromFiles(parseDiff(stagedRes.stdout), 'staged');
192
- const unstaged = itemsFromFiles(parseDiff(unstagedRes.stdout), 'unstaged');
455
+ const stagedDiff = parseDiff(stagedRes.stdout);
456
+ const unstagedDiff = parseDiff(unstagedRes.stdout);
457
+ const staged = itemsFromFiles(stagedDiff, 'staged');
458
+ const unstaged = itemsFromFiles(unstagedDiff, 'unstaged');
193
459
  const untracked = untrackedItems(top, paths);
194
- const items = dropReviewItems(mergeItems([staged, unstaged, untracked]));
460
+ const combined = mergeItems([staged, unstaged, untracked]);
461
+ const merged = dropReviewItems(combined);
462
+ const items = foldLoaded(merged, top, undefined, { ...options, paths });
195
463
  return { top, items };
196
464
  };
197
465
 
198
466
  const addItem = (top, item) => {
199
467
  if (item.origin === 'staged' || item.origin === 'commit') return;
468
+ if (depChange(item)) {
469
+ applyDepChange(top, item, 'add');
470
+ return;
471
+ }
472
+ const files = depPaths(item);
473
+ if (files.length) {
474
+ requireOk(runGit(['add', '--', ...files], top));
475
+ return;
476
+ }
200
477
  if (item.origin === 'untracked' || item.file.isBinary) {
201
478
  requireOk(runGit(['add', '--', item.file.newPath], top));
202
479
  return;
@@ -206,6 +483,15 @@ const addItem = (top, item) => {
206
483
 
207
484
  const unstageItem = (top, item) => {
208
485
  if (item.origin !== 'staged') return;
486
+ if (depChange(item)) {
487
+ applyDepChange(top, item, 'unstage');
488
+ return;
489
+ }
490
+ const files = depPaths(item);
491
+ if (files.length) {
492
+ requireOk(runGit(['restore', '--staged', '--', ...files], top));
493
+ return;
494
+ }
209
495
  const rel = itemPath(item);
210
496
  if (item.file.isBinary) {
211
497
  requireOk(runGit(['restore', '--staged', '--', rel], top));
@@ -214,8 +500,62 @@ const unstageItem = (top, item) => {
214
500
  applyPatch(top, item.patchAdd, ['--reverse', '--cached']);
215
501
  };
216
502
 
503
+ const revertOnePath = (top, rel, items) => {
504
+ const origins = new Set();
505
+ let isNew = false;
506
+ for (const item of items) {
507
+ origins.add(item.origin);
508
+ if (item.file && item.file.isNew) isNew = true;
509
+ }
510
+ if (isNew) {
511
+ if (origins.has('staged')) {
512
+ requireOk(runGit(['rm', '-f', '--', rel], top));
513
+ return;
514
+ }
515
+ fs.unlinkSync(path.join(top, rel));
516
+ return;
517
+ }
518
+ if (origins.has('untracked') && origins.size === 1) {
519
+ fs.unlinkSync(path.join(top, rel));
520
+ return;
521
+ }
522
+ const args = ['restore', '-s', 'HEAD', '--worktree', '--staged', '--', rel];
523
+ requireOk(runGit(args, top));
524
+ };
525
+
526
+ const itemsForPath = (rel, items) => {
527
+ const group = [];
528
+ for (const item of items) {
529
+ if (item.dep) {
530
+ for (const orig of origDepItems(item)) {
531
+ if (itemPath(orig) === rel) group.push(orig);
532
+ }
533
+ continue;
534
+ }
535
+ if (itemPath(item) === rel) group.push(item);
536
+ }
537
+ return group;
538
+ };
539
+
217
540
  const revertItem = (top, item) => {
218
541
  if (item.origin === 'commit') return;
542
+ if (depChange(item)) {
543
+ applyDepChange(top, item, 'revert');
544
+ return;
545
+ }
546
+ if (item.dep) {
547
+ const byPath = new Map();
548
+ for (const orig of origDepItems(item)) {
549
+ const rel = itemPath(orig);
550
+ const list = byPath.get(rel);
551
+ if (list) list.push(orig);
552
+ else byPath.set(rel, [orig]);
553
+ }
554
+ for (const rel of byPath.keys()) {
555
+ revertOnePath(top, rel, byPath.get(rel));
556
+ }
557
+ return;
558
+ }
219
559
  const rel = itemPath(item);
220
560
  if (item.origin === 'untracked') {
221
561
  fs.unlinkSync(path.join(top, rel));
@@ -241,26 +581,15 @@ const revertItem = (top, item) => {
241
581
  };
242
582
 
243
583
  const revertFile = (top, rel, items) => {
244
- const origins = new Set();
245
- let isNew = false;
584
+ const paths = new Set([rel]);
246
585
  for (const item of items) {
247
- origins.add(item.origin);
248
- if (item.file && item.file.isNew) isNew = true;
249
- }
250
- if (isNew) {
251
- if (origins.has('staged')) {
252
- requireOk(runGit(['rm', '-f', '--', rel], top));
253
- return;
254
- }
255
- fs.unlinkSync(path.join(top, rel));
256
- return;
586
+ for (const file of depPaths(item)) paths.add(file);
257
587
  }
258
- if (origins.has('untracked') && origins.size === 1) {
259
- fs.unlinkSync(path.join(top, rel));
260
- return;
588
+ for (const file of paths) {
589
+ const group = itemsForPath(file, items);
590
+ const targets = group.length ? group : items;
591
+ revertOnePath(top, file, targets);
261
592
  }
262
- const args = ['restore', '-s', 'HEAD', '--worktree', '--staged', '--', rel];
263
- requireOk(runGit(args, top));
264
593
  };
265
594
 
266
595
  const createGitRepo = () => ({
@@ -273,14 +602,9 @@ const createGitRepo = () => ({
273
602
  });
274
603
 
275
604
  module.exports = {
276
- runGit,
277
- toplevel,
278
605
  load,
279
- resolveRev,
280
606
  addItem,
281
607
  unstageItem,
282
608
  revertItem,
283
- revertFile,
284
609
  createGitRepo,
285
- oneLine,
286
610
  };
package/lib/github.js CHANGED
@@ -3,8 +3,9 @@
3
3
  const { delay } = require('metautil');
4
4
 
5
5
  const diff = require('./diff.js');
6
- const { parseDiff, itemsFromFiles } = diff;
7
- const { itemPath } = require('./files.js');
6
+ const { parseDiff, itemsFromFiles, isCtxType } = diff;
7
+ const { itemPath, isPathInScope } = require('./files.js');
8
+ const { foldDepItems } = require('./deps.js');
8
9
 
9
10
  const GITHUB_API = 'https://api.github.com';
10
11
  const API_VERSION = '2022-11-28';
@@ -70,7 +71,6 @@ const parseGithubPrUrl = (text) => {
70
71
 
71
72
  const githubToken = (env) => {
72
73
  const from = env ?? process.env;
73
- if (!isRecord(from)) return '';
74
74
  return from.GITHUB_TOKEN || from.GH_TOKEN || '';
75
75
  };
76
76
 
@@ -134,18 +134,15 @@ const isRetryable = (error) => {
134
134
  const retry = async (run, options) => {
135
135
  const attempts = options.attempts ?? RETRY_ATTEMPTS;
136
136
  const delayMs = options.delayMs ?? RETRY_DELAY_MS;
137
- let lastError = null;
138
- for (let i = 0; i < attempts; i++) {
137
+ for (let i = 0; ; i++) {
139
138
  try {
140
139
  return await run();
141
140
  } catch (error) {
142
- lastError = error;
143
141
  const isLast = i === attempts - 1;
144
142
  if (isLast || !isRetryable(error)) throw error;
145
143
  await delay(delayMs * (i + 1));
146
144
  }
147
145
  }
148
- throw lastError;
149
146
  };
150
147
 
151
148
  const githubHeaders = (token, accept) => {
@@ -164,7 +161,8 @@ const headerValue = (headers, name) => {
164
161
  return headers.get(name) || '';
165
162
  }
166
163
  const wanted = name.toLowerCase();
167
- for (const [key, value] of Object.entries(headers)) {
164
+ for (const key of Object.keys(headers)) {
165
+ const value = headers[key];
168
166
  if (key.toLowerCase() === wanted) return `${value}`;
169
167
  }
170
168
  return '';
@@ -252,19 +250,6 @@ const requestGraphql = async (query, variables, options) => {
252
250
 
253
251
  const changePath = (file) => file.newPath || file.oldPath || '';
254
252
 
255
- const normalizePathSpec = (spec) =>
256
- `${spec ?? ''}`.replaceAll('\\', '/').replace(/\/+$/, '');
257
-
258
- const isPathInScope = (rel, paths) => {
259
- if (!paths.length) return true;
260
- for (const spec of paths) {
261
- const norm = normalizePathSpec(spec);
262
- if (!norm || norm === '.') return true;
263
- if (rel === norm || rel.startsWith(`${norm}/`)) return true;
264
- }
265
- return false;
266
- };
267
-
268
253
  const filterChangeFiles = (files, paths) => {
269
254
  if (!paths.length) return files;
270
255
  const kept = [];
@@ -314,14 +299,23 @@ const readRepoName = (data, pr) => {
314
299
  return `${pr.owner}/${pr.repo}`;
315
300
  };
316
301
 
302
+ const pushChangeFile = (files, seen, rel) => {
303
+ if (!rel || seen.has(rel)) return;
304
+ seen.add(rel);
305
+ files.push(rel);
306
+ };
307
+
317
308
  const changeFiles = (items) => {
318
309
  const files = [];
319
310
  const seen = new Set();
320
311
  for (const item of items) {
312
+ if (item.dep) {
313
+ const depFiles = item.dep.files ?? [];
314
+ for (const rel of depFiles) pushChangeFile(files, seen, rel);
315
+ continue;
316
+ }
321
317
  const rel = itemPath(item);
322
- if (!rel || seen.has(rel)) continue;
323
- seen.add(rel);
324
- files.push(rel);
318
+ pushChangeFile(files, seen, rel);
325
319
  }
326
320
  return files;
327
321
  };
@@ -406,8 +400,12 @@ const locateLineInItem = (item, rel, line, side) => {
406
400
  const inBlock = entry.blockId === item.blockId;
407
401
  return { inBlock };
408
402
  }
409
- if (entry.type === 'ctx' || entry.type === 'del') oldNo += 1;
410
- if (entry.type === 'ctx' || entry.type === 'add') newNo += 1;
403
+ if (isCtxType(entry.type) || entry.type === 'del') {
404
+ oldNo += 1;
405
+ }
406
+ if (isCtxType(entry.type) || entry.type === 'add') {
407
+ newNo += 1;
408
+ }
411
409
  }
412
410
  return null;
413
411
  };
@@ -546,7 +544,6 @@ const loadReviewThreads = async (pr, options) => {
546
544
  const threadComments = (thread) => {
547
545
  if (!isRecord(thread)) return [];
548
546
  const comments = thread.comments;
549
- if (Array.isArray(comments)) return comments;
550
547
  if (isRecord(comments) && Array.isArray(comments.nodes)) {
551
548
  return comments.nodes;
552
549
  }
@@ -581,20 +578,18 @@ const loadDiscussion = async (pr, options) => {
581
578
  return { reviewComments, reviews, issueComments, resolvedById };
582
579
  };
583
580
 
584
- const emptyImported = () => ({ feedback: [], todos: [] });
585
-
586
581
  const loadImportedNotes = async (pr, items, paths, options) => {
587
582
  try {
588
583
  const discussion = await loadDiscussion(pr, options);
589
584
  return discussionToNotes(discussion, items, paths);
590
585
  } catch {
591
586
  // ignore: discussion import is optional
592
- return emptyImported();
587
+ return { feedback: [], todos: [] };
593
588
  }
594
589
  };
595
590
 
596
591
  const loadPullRequest = async (pr, options = {}) => {
597
- const token = options.token ?? githubToken(options.env);
592
+ const token = options.token ?? '';
598
593
  const fetchImpl = options.fetch ?? globalThis.fetch;
599
594
  const paths = options.paths ?? [];
600
595
  const retryOpts = options.retry ?? {};
@@ -611,7 +606,8 @@ const loadPullRequest = async (pr, options = {}) => {
611
606
  });
612
607
  const parsed = parseDiff(diffPage.body);
613
608
  const files = filterChangeFiles(parsed, paths);
614
- const items = itemsFromFiles(files, 'pr');
609
+ const parsedItems = itemsFromFiles(files, 'pr');
610
+ const items = foldDepItems(parsedItems);
615
611
  const change = toChange(pr, data, items);
616
612
  const sourceLabel = `#${change.number}`;
617
613
  const imported = await loadImportedNotes(pr, items, paths, request);
@@ -619,8 +615,6 @@ const loadPullRequest = async (pr, options = {}) => {
619
615
  };
620
616
 
621
617
  module.exports = {
622
- GITHUB_API,
623
- PR_TODO_FILE,
624
618
  parseGithubPrUrl,
625
619
  githubToken,
626
620
  filterChangeFiles,