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/CHANGELOG.md +13 -6
- package/README.md +11 -9
- package/bin/reslop.js +2 -2
- package/lib/ansi.js +0 -1
- package/lib/cli.js +44 -43
- package/lib/clipboard.js +1 -1
- package/lib/deps.js +1440 -0
- package/lib/detect.js +4 -4
- package/lib/diff.js +31 -55
- package/lib/editor.js +1 -1
- package/lib/files.js +22 -3
- package/lib/git.js +360 -36
- package/lib/github.js +28 -34
- package/lib/highlight.js +126 -140
- package/lib/keys.js +29 -39
- package/lib/render.js +172 -129
- package/lib/review.js +22 -42
- package/lib/select.js +16 -18
- package/lib/session.js +97 -208
- package/lib/source.js +7 -3
- package/package.json +1 -1
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
|
|
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) =>
|
|
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
|
|
44
|
-
|
|
45
|
-
|
|
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
|
|
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
|
|
192
|
-
const
|
|
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
|
|
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
|
|
245
|
-
let isNew = false;
|
|
584
|
+
const paths = new Set([rel]);
|
|
246
585
|
for (const item of items) {
|
|
247
|
-
|
|
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
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
410
|
-
|
|
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
|
|
587
|
+
return { feedback: [], todos: [] };
|
|
593
588
|
}
|
|
594
589
|
};
|
|
595
590
|
|
|
596
591
|
const loadPullRequest = async (pr, options = {}) => {
|
|
597
|
-
const token = options.token ??
|
|
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
|
|
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,
|