reslop 0.1.1 → 0.1.3
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 +21 -6
- package/README.md +29 -26
- package/bin/reslop.js +2 -2
- package/lib/ansi.js +0 -1
- package/lib/cli.js +62 -47
- package/lib/clipboard.js +1 -1
- package/lib/deps.js +1440 -0
- package/lib/detect.js +6 -4
- package/lib/diff.js +31 -55
- package/lib/editor.js +1 -1
- package/lib/files.js +58 -15
- package/lib/git.js +649 -69
- package/lib/github.js +28 -34
- package/lib/highlight.js +282 -141
- package/lib/keys.js +35 -39
- package/lib/render.js +250 -153
- package/lib/review.js +22 -42
- package/lib/select.js +16 -18
- package/lib/session.js +301 -227
- package/lib/source.js +7 -3
- package/package.json +1 -1
package/lib/git.js
CHANGED
|
@@ -1,21 +1,29 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const { spawnSync } = require('node:child_process');
|
|
3
|
+
const { spawn, spawnSync } = require('node:child_process');
|
|
4
4
|
const fs = require('node:fs');
|
|
5
5
|
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;
|
|
@@ -38,21 +46,114 @@ const dropReviewItems = (items) => {
|
|
|
38
46
|
return kept;
|
|
39
47
|
};
|
|
40
48
|
|
|
49
|
+
const abortError = () => {
|
|
50
|
+
const error = new Error('aborted');
|
|
51
|
+
error.code = 'ABORT';
|
|
52
|
+
return error;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const isAbort = (error) => !!(error && error.code === 'ABORT');
|
|
56
|
+
|
|
57
|
+
const runProc = (cmd, args, options = {}) =>
|
|
58
|
+
new Promise((resolve) => {
|
|
59
|
+
const cwd = options.cwd;
|
|
60
|
+
const env = options.env;
|
|
61
|
+
const encoding = options.encoding ?? GIT_ENCODING;
|
|
62
|
+
const maxBuffer = options.maxBuffer ?? GIT_MAX_BUFFER;
|
|
63
|
+
const timeout = options.timeout;
|
|
64
|
+
const input = options.input;
|
|
65
|
+
const signal = options.signal;
|
|
66
|
+
if (signal && signal.aborted) {
|
|
67
|
+
resolve({ status: null, error: abortError(), stdout: '', stderr: '' });
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
let stdout = '';
|
|
71
|
+
let stderr = '';
|
|
72
|
+
let settled = false;
|
|
73
|
+
let timer = null;
|
|
74
|
+
const child = spawn(cmd, args, { cwd, env });
|
|
75
|
+
const finish = (result) => {
|
|
76
|
+
if (settled) return;
|
|
77
|
+
settled = true;
|
|
78
|
+
if (timer) clearTimeout(timer);
|
|
79
|
+
resolve(result);
|
|
80
|
+
};
|
|
81
|
+
const onAbort = () => {
|
|
82
|
+
child.kill();
|
|
83
|
+
finish({ status: null, error: abortError(), stdout, stderr });
|
|
84
|
+
};
|
|
85
|
+
const append = (kind, chunk) => {
|
|
86
|
+
if (kind === 'out') stdout += chunk;
|
|
87
|
+
else stderr += chunk;
|
|
88
|
+
if (stdout.length + stderr.length <= maxBuffer) return;
|
|
89
|
+
child.kill();
|
|
90
|
+
const error = new Error('maxBuffer exceeded');
|
|
91
|
+
error.code = 'ENOBUFS';
|
|
92
|
+
finish({ status: null, error, stdout, stderr });
|
|
93
|
+
};
|
|
94
|
+
if (signal) signal.addEventListener('abort', onAbort);
|
|
95
|
+
if (child.stdout) {
|
|
96
|
+
child.stdout.setEncoding(encoding);
|
|
97
|
+
child.stdout.on('data', (chunk) => append('out', chunk));
|
|
98
|
+
}
|
|
99
|
+
if (child.stderr) {
|
|
100
|
+
child.stderr.setEncoding(encoding);
|
|
101
|
+
child.stderr.on('data', (chunk) => append('err', chunk));
|
|
102
|
+
}
|
|
103
|
+
if (timeout) {
|
|
104
|
+
timer = setTimeout(() => {
|
|
105
|
+
child.kill();
|
|
106
|
+
const error = new Error('timed out');
|
|
107
|
+
error.code = 'ETIMEDOUT';
|
|
108
|
+
finish({ status: null, error, stdout, stderr });
|
|
109
|
+
}, timeout);
|
|
110
|
+
}
|
|
111
|
+
child.on('error', (error) => {
|
|
112
|
+
finish({ status: null, error, stdout, stderr });
|
|
113
|
+
});
|
|
114
|
+
child.on('close', (status) => {
|
|
115
|
+
finish({ status, stdout, stderr });
|
|
116
|
+
});
|
|
117
|
+
if (child.stdin) {
|
|
118
|
+
try {
|
|
119
|
+
if (input) child.stdin.end(input);
|
|
120
|
+
else child.stdin.end();
|
|
121
|
+
} catch {
|
|
122
|
+
// closed
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const gitResult = (result) => {
|
|
128
|
+
if (!result.error) return result;
|
|
129
|
+
if (isAbort(result.error)) throw result.error;
|
|
130
|
+
if (result.error.code === 'ENOENT') throw new Error('git not found');
|
|
131
|
+
throw result.error;
|
|
132
|
+
};
|
|
133
|
+
|
|
41
134
|
const runGit = (args, cwd, input) => {
|
|
42
135
|
const env = { ...process.env, GIT_OPTIONAL_LOCKS: '0' };
|
|
43
|
-
const
|
|
136
|
+
const encoding = GIT_ENCODING;
|
|
137
|
+
const maxBuffer = GIT_MAX_BUFFER;
|
|
138
|
+
const options = { cwd, encoding, input, maxBuffer, env };
|
|
139
|
+
const result = spawnSync('git', args, options);
|
|
140
|
+
return gitResult(result);
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const runGitAsync = (args, cwd, extra = {}) => {
|
|
144
|
+
const env = { ...process.env, GIT_OPTIONAL_LOCKS: '0' };
|
|
145
|
+
const encoding = GIT_ENCODING;
|
|
146
|
+
const maxBuffer = GIT_MAX_BUFFER;
|
|
147
|
+
const input = extra.input;
|
|
148
|
+
const signal = extra.signal;
|
|
149
|
+
return runProc('git', args, {
|
|
44
150
|
cwd,
|
|
45
|
-
encoding: GIT_ENCODING,
|
|
46
|
-
input,
|
|
47
|
-
maxBuffer: GIT_MAX_BUFFER,
|
|
48
151
|
env,
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
55
|
-
return result;
|
|
152
|
+
encoding,
|
|
153
|
+
maxBuffer,
|
|
154
|
+
input,
|
|
155
|
+
signal,
|
|
156
|
+
}).then(gitResult);
|
|
56
157
|
};
|
|
57
158
|
|
|
58
159
|
const toplevel = (cwd) => {
|
|
@@ -72,12 +173,16 @@ const isBinaryBuffer = (buf) => {
|
|
|
72
173
|
return buf.includes(0);
|
|
73
174
|
};
|
|
74
175
|
|
|
176
|
+
const namesFromLs = (stdout) => {
|
|
177
|
+
if (!stdout) return [];
|
|
178
|
+
return stdout.split('\0').filter(Boolean);
|
|
179
|
+
};
|
|
180
|
+
|
|
75
181
|
const listUntracked = (top, paths) => {
|
|
76
182
|
const args = ['ls-files', '-o', '--exclude-standard', '-z'];
|
|
77
183
|
if (paths.length) args.push('--', ...paths);
|
|
78
184
|
const result = requireOk(runGit(args, top));
|
|
79
|
-
|
|
80
|
-
return result.stdout.split('\0').filter(Boolean);
|
|
185
|
+
return namesFromLs(result.stdout);
|
|
81
186
|
};
|
|
82
187
|
|
|
83
188
|
const binaryNewFile = (rel) => ({
|
|
@@ -90,9 +195,9 @@ const binaryNewFile = (rel) => ({
|
|
|
90
195
|
hunks: [],
|
|
91
196
|
});
|
|
92
197
|
|
|
93
|
-
const
|
|
198
|
+
const untrackedItemsFrom = (top, names) => {
|
|
94
199
|
const files = [];
|
|
95
|
-
for (const rel of
|
|
200
|
+
for (const rel of names) {
|
|
96
201
|
if (isReviewFile(rel)) continue;
|
|
97
202
|
const full = path.join(top, rel);
|
|
98
203
|
let buf;
|
|
@@ -131,6 +236,301 @@ const mergeItems = (groups) => {
|
|
|
131
236
|
return items;
|
|
132
237
|
};
|
|
133
238
|
|
|
239
|
+
const gitText = (top, spec) => {
|
|
240
|
+
const result = runGit(['show', spec], top);
|
|
241
|
+
if (result.status !== 0) return '';
|
|
242
|
+
return result.stdout ?? '';
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
const worktreeText = (top, rel) => {
|
|
246
|
+
try {
|
|
247
|
+
return fs.readFileSync(path.join(top, rel), 'utf8');
|
|
248
|
+
} catch {
|
|
249
|
+
return '';
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const readDepSides = (top, rev, origin, rel) => {
|
|
254
|
+
if (origin === 'untracked') {
|
|
255
|
+
const newText = worktreeText(top, rel);
|
|
256
|
+
return { oldText: '', newText };
|
|
257
|
+
}
|
|
258
|
+
if (origin === 'staged') {
|
|
259
|
+
const oldText = gitText(top, `HEAD:${rel}`);
|
|
260
|
+
const newText = gitText(top, `:${rel}`);
|
|
261
|
+
return { oldText, newText };
|
|
262
|
+
}
|
|
263
|
+
if (origin === 'unstaged') {
|
|
264
|
+
const indexed = gitText(top, `:${rel}`);
|
|
265
|
+
const oldText = indexed || gitText(top, `HEAD:${rel}`);
|
|
266
|
+
const newText = worktreeText(top, rel);
|
|
267
|
+
return { oldText, newText };
|
|
268
|
+
}
|
|
269
|
+
if (origin === 'commit' && rev) {
|
|
270
|
+
const oldText = gitText(top, `${rev}^:${rel}`);
|
|
271
|
+
const newText = gitText(top, `${rev}:${rel}`);
|
|
272
|
+
return { oldText, newText };
|
|
273
|
+
}
|
|
274
|
+
return { oldText: '', newText: '' };
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
const runNpmJson = (top, args, timeout) => {
|
|
278
|
+
const result = spawnSync('npm', args, {
|
|
279
|
+
cwd: top,
|
|
280
|
+
encoding: GIT_ENCODING,
|
|
281
|
+
maxBuffer: GIT_MAX_BUFFER,
|
|
282
|
+
timeout,
|
|
283
|
+
});
|
|
284
|
+
if (result.error) return '';
|
|
285
|
+
return result.stdout || '';
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
const runNpmJsonAsync = async (top, args, timeout, signal) => {
|
|
289
|
+
const result = await runProc('npm', args, {
|
|
290
|
+
cwd: top,
|
|
291
|
+
encoding: GIT_ENCODING,
|
|
292
|
+
maxBuffer: GIT_MAX_BUFFER,
|
|
293
|
+
timeout,
|
|
294
|
+
signal,
|
|
295
|
+
});
|
|
296
|
+
if (result.error) return '';
|
|
297
|
+
return result.stdout || '';
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
const collectAudit = (top) => {
|
|
301
|
+
const lock = path.join(top, LOCKFILE);
|
|
302
|
+
if (!fs.existsSync(lock)) return null;
|
|
303
|
+
const args = ['audit', '--json', '--package-lock-only'];
|
|
304
|
+
const text = runNpmJson(top, args, AUDIT_MS);
|
|
305
|
+
return parseAuditReport(text);
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
const collectOutdated = (top) => {
|
|
309
|
+
const manifest = path.join(top, MANIFEST);
|
|
310
|
+
if (!fs.existsSync(manifest)) return null;
|
|
311
|
+
const args = ['outdated', '--json', '--long'];
|
|
312
|
+
const text = runNpmJson(top, args, AUDIT_MS);
|
|
313
|
+
return parseOutdatedReport(text);
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
const collectAuditAsync = async (top, extra, signal) => {
|
|
317
|
+
if (extra.auditMap !== undefined) return extra.auditMap;
|
|
318
|
+
const lock = path.join(top, LOCKFILE);
|
|
319
|
+
if (!fs.existsSync(lock)) return null;
|
|
320
|
+
const args = ['audit', '--json', '--package-lock-only'];
|
|
321
|
+
const text = await runNpmJsonAsync(top, args, AUDIT_MS, signal);
|
|
322
|
+
return parseAuditReport(text);
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
const collectOutdatedAsync = async (top, extra, signal) => {
|
|
326
|
+
if (extra.outdatedMap !== undefined) return extra.outdatedMap;
|
|
327
|
+
const manifest = path.join(top, MANIFEST);
|
|
328
|
+
if (!fs.existsSync(manifest)) return null;
|
|
329
|
+
const args = ['outdated', '--json', '--long'];
|
|
330
|
+
const text = await runNpmJsonAsync(top, args, AUDIT_MS, signal);
|
|
331
|
+
return parseOutdatedReport(text);
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
const pickNpmMap = (override, collect, top) => {
|
|
335
|
+
if (override !== undefined) return override;
|
|
336
|
+
return collect(top);
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
const hasDepFile = (items) => {
|
|
340
|
+
for (const item of items) {
|
|
341
|
+
if (depFileMeta(itemPath(item))) return true;
|
|
342
|
+
}
|
|
343
|
+
return false;
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
const foldLoaded = (items, top, rev, extra = {}) => {
|
|
347
|
+
const readSides = (origin, rel) => readDepSides(top, rev, origin, rel);
|
|
348
|
+
const live = extra.audit === true && !rev;
|
|
349
|
+
const hasDepDiff = hasDepFile(items);
|
|
350
|
+
const collect = extra.collect !== false;
|
|
351
|
+
let usedNames = extra.usedNames ?? null;
|
|
352
|
+
let audit = extra.auditMap;
|
|
353
|
+
let outdated = extra.outdatedMap;
|
|
354
|
+
if (collect && usedNames === null && (hasDepDiff || live)) {
|
|
355
|
+
usedNames = collectUsedNames(top);
|
|
356
|
+
}
|
|
357
|
+
if (collect && live) {
|
|
358
|
+
audit = pickNpmMap(extra.auditMap, collectAudit, top);
|
|
359
|
+
outdated = pickNpmMap(extra.outdatedMap, collectOutdated, top);
|
|
360
|
+
} else if (collect && extra.audit && hasDepDiff) {
|
|
361
|
+
audit = pickNpmMap(extra.auditMap, collectAudit, top);
|
|
362
|
+
}
|
|
363
|
+
if (audit === undefined) audit = null;
|
|
364
|
+
if (outdated === undefined) outdated = null;
|
|
365
|
+
const folded = foldDepItems(items, readSides, usedNames, audit, outdated, {
|
|
366
|
+
root: top,
|
|
367
|
+
});
|
|
368
|
+
if (!live) return folded;
|
|
369
|
+
if (!isPathInScope(MANIFEST, extra.paths ?? [])) return folded;
|
|
370
|
+
const pkgText = worktreeText(top, MANIFEST);
|
|
371
|
+
const lockText = worktreeText(top, LOCKFILE);
|
|
372
|
+
return mergeProposedItems(folded, pkgText, outdated, audit, {
|
|
373
|
+
lockText,
|
|
374
|
+
usedNames,
|
|
375
|
+
root: top,
|
|
376
|
+
});
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
const depPaths = (item) => item.dep?.files ?? [];
|
|
380
|
+
|
|
381
|
+
const origDepItems = (item) => item.dep?.items ?? [];
|
|
382
|
+
|
|
383
|
+
const depChange = (item) => item.dep?.change ?? null;
|
|
384
|
+
|
|
385
|
+
const writeWorktreeFile = (top, rel, text) => {
|
|
386
|
+
const abs = path.join(top, rel);
|
|
387
|
+
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
388
|
+
fs.writeFileSync(abs, text);
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
const restoreWorktreeFile = (top, rel, saved, existed) => {
|
|
392
|
+
const abs = path.join(top, rel);
|
|
393
|
+
if (existed) {
|
|
394
|
+
fs.writeFileSync(abs, saved);
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
try {
|
|
398
|
+
fs.unlinkSync(abs);
|
|
399
|
+
} catch {
|
|
400
|
+
// missing
|
|
401
|
+
}
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
const writeIndexFile = (top, rel, text) => {
|
|
405
|
+
const abs = path.join(top, rel);
|
|
406
|
+
const existed = fs.existsSync(abs);
|
|
407
|
+
const saved = existed ? fs.readFileSync(abs, 'utf8') : '';
|
|
408
|
+
writeWorktreeFile(top, rel, text);
|
|
409
|
+
try {
|
|
410
|
+
requireOk(runGit(['add', '--', rel], top));
|
|
411
|
+
} finally {
|
|
412
|
+
restoreWorktreeFile(top, rel, saved, existed);
|
|
413
|
+
}
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
const mergeDepLive = (top, rel, change, origin, base, side, kind) => {
|
|
417
|
+
const sides = readDepSides(top, null, origin, rel);
|
|
418
|
+
return mergeDepFile(base, sides.oldText, sides.newText, change, side, kind);
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
const applyDepIndex = (top, rel, change, origin, side) => {
|
|
422
|
+
const meta = depFileMeta(rel);
|
|
423
|
+
if (!meta) return;
|
|
424
|
+
const indexed = gitText(top, `:${rel}`) || gitText(top, `HEAD:${rel}`);
|
|
425
|
+
const text = mergeDepLive(top, rel, change, origin, indexed, side, meta.kind);
|
|
426
|
+
writeIndexFile(top, rel, text);
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
const applyDepWorktree = (top, rel, change, origin) => {
|
|
430
|
+
const meta = depFileMeta(rel);
|
|
431
|
+
if (!meta) return;
|
|
432
|
+
const worktree = worktreeText(top, rel);
|
|
433
|
+
const text = mergeDepLive(
|
|
434
|
+
top,
|
|
435
|
+
rel,
|
|
436
|
+
change,
|
|
437
|
+
origin,
|
|
438
|
+
worktree,
|
|
439
|
+
'old',
|
|
440
|
+
meta.kind,
|
|
441
|
+
);
|
|
442
|
+
writeWorktreeFile(top, rel, text);
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
const runNpm = (cwd, args) =>
|
|
446
|
+
spawnSync('npm', args, {
|
|
447
|
+
cwd,
|
|
448
|
+
encoding: GIT_ENCODING,
|
|
449
|
+
maxBuffer: GIT_MAX_BUFFER,
|
|
450
|
+
timeout: INSTALL_MS,
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
const firstDepRel = (files, kind) => {
|
|
454
|
+
for (const rel of files) {
|
|
455
|
+
const meta = depFileMeta(rel);
|
|
456
|
+
if (meta && meta.kind === kind) return rel;
|
|
457
|
+
}
|
|
458
|
+
return '';
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
const applyProposedUpdate = (top, item) => {
|
|
462
|
+
const change = depChange(item);
|
|
463
|
+
const origin = item.origin;
|
|
464
|
+
const files = depPaths(item);
|
|
465
|
+
const lockOnly = change.section === 'resolved';
|
|
466
|
+
const removing = !!change.unused;
|
|
467
|
+
const manifest = firstDepRel(files, 'manifest');
|
|
468
|
+
const lockRel = firstDepRel(files, 'lockfile');
|
|
469
|
+
const picked = lockOnly ? lockRel : manifest;
|
|
470
|
+
const target = picked || files[0] || '';
|
|
471
|
+
const meta = depFileMeta(target);
|
|
472
|
+
if (!meta) return;
|
|
473
|
+
let saved = '';
|
|
474
|
+
if (!lockOnly && !removing && manifest) {
|
|
475
|
+
const worktree = worktreeText(top, manifest);
|
|
476
|
+
saved = worktree;
|
|
477
|
+
const text = mergeDepLive(
|
|
478
|
+
top,
|
|
479
|
+
manifest,
|
|
480
|
+
change,
|
|
481
|
+
origin,
|
|
482
|
+
worktree,
|
|
483
|
+
'new',
|
|
484
|
+
'manifest',
|
|
485
|
+
);
|
|
486
|
+
writeWorktreeFile(top, manifest, text);
|
|
487
|
+
}
|
|
488
|
+
const cwd = meta.dir === '.' ? top : path.join(top, meta.dir);
|
|
489
|
+
let npmArgs = ['i'];
|
|
490
|
+
if (removing) npmArgs = ['uninstall', change.name];
|
|
491
|
+
else if (lockOnly) npmArgs = ['audit', 'fix'];
|
|
492
|
+
const install = item.dep.install ?? ((dir) => runNpm(dir, npmArgs));
|
|
493
|
+
const result = install(cwd);
|
|
494
|
+
const fail = removing ? 'npm uninstall failed' : 'npm i failed';
|
|
495
|
+
if (result && (result.error || result.status !== 0)) {
|
|
496
|
+
if (saved && manifest) writeWorktreeFile(top, manifest, saved);
|
|
497
|
+
if (result.error) {
|
|
498
|
+
const msg = result.error.message || fail;
|
|
499
|
+
throw new Error(oneLine(msg));
|
|
500
|
+
}
|
|
501
|
+
const msg = result.stderr || result.stdout || fail;
|
|
502
|
+
throw new Error(oneLine(msg));
|
|
503
|
+
}
|
|
504
|
+
const toAdd = [];
|
|
505
|
+
if (!lockOnly && manifest) toAdd.push(manifest);
|
|
506
|
+
const dirLock = meta.dir === '.' ? LOCKFILE : `${meta.dir}/${LOCKFILE}`;
|
|
507
|
+
const lockPath = lockRel || dirLock;
|
|
508
|
+
if (fs.existsSync(path.join(top, lockPath))) toAdd.push(lockPath);
|
|
509
|
+
if (toAdd.length) requireOk(runGit(['add', '--', ...toAdd], top));
|
|
510
|
+
};
|
|
511
|
+
|
|
512
|
+
const applyDepChange = (top, item, mode) => {
|
|
513
|
+
const change = depChange(item);
|
|
514
|
+
if (change && change.propose) {
|
|
515
|
+
if (mode === 'add') return void applyProposedUpdate(top, item);
|
|
516
|
+
if (!change.unused) return;
|
|
517
|
+
}
|
|
518
|
+
const origin = item.origin;
|
|
519
|
+
const files = depPaths(item);
|
|
520
|
+
for (const rel of files) {
|
|
521
|
+
if (mode === 'add') {
|
|
522
|
+
applyDepIndex(top, rel, change, origin, 'new');
|
|
523
|
+
continue;
|
|
524
|
+
}
|
|
525
|
+
if (mode === 'unstage') {
|
|
526
|
+
applyDepIndex(top, rel, change, origin, 'old');
|
|
527
|
+
continue;
|
|
528
|
+
}
|
|
529
|
+
if (origin === 'staged') applyDepIndex(top, rel, change, origin, 'old');
|
|
530
|
+
applyDepWorktree(top, rel, change, origin);
|
|
531
|
+
}
|
|
532
|
+
};
|
|
533
|
+
|
|
134
534
|
const resolveRev = (cwd, spec) => {
|
|
135
535
|
if (!spec) return null;
|
|
136
536
|
const top = toplevel(cwd);
|
|
@@ -146,38 +546,73 @@ const shortRev = (top, commit) => {
|
|
|
146
546
|
return result.stdout.trim();
|
|
147
547
|
};
|
|
148
548
|
|
|
149
|
-
const
|
|
150
|
-
const parent = runGit(['rev-parse', '--verify', `${commit}^`], top);
|
|
151
|
-
let args;
|
|
549
|
+
const commitDiffArgs = (commit, parent, pathArgs) => {
|
|
152
550
|
if (parent.status === 0) {
|
|
153
551
|
const from = parent.stdout.trim();
|
|
154
|
-
|
|
155
|
-
} else {
|
|
156
|
-
args = [
|
|
157
|
-
...GIT_CONFIG,
|
|
158
|
-
'diff-tree',
|
|
159
|
-
'--no-commit-id',
|
|
160
|
-
'--root',
|
|
161
|
-
'-p',
|
|
162
|
-
...DIFF_OPTS,
|
|
163
|
-
commit,
|
|
164
|
-
...extra,
|
|
165
|
-
];
|
|
552
|
+
return [...GIT_CONFIG, 'diff', ...DIFF_OPTS, from, commit, ...pathArgs];
|
|
166
553
|
}
|
|
554
|
+
return [
|
|
555
|
+
...GIT_CONFIG,
|
|
556
|
+
'diff-tree',
|
|
557
|
+
'--no-commit-id',
|
|
558
|
+
'--root',
|
|
559
|
+
'-p',
|
|
560
|
+
...DIFF_OPTS,
|
|
561
|
+
commit,
|
|
562
|
+
...pathArgs,
|
|
563
|
+
];
|
|
564
|
+
};
|
|
565
|
+
|
|
566
|
+
const pathArgsOf = (paths) => (paths.length ? ['--', ...paths] : []);
|
|
567
|
+
|
|
568
|
+
const snapshotWorktree = (top, stagedOut, unstagedOut, names) => {
|
|
569
|
+
const stagedDiff = parseDiff(stagedOut);
|
|
570
|
+
const unstagedDiff = parseDiff(unstagedOut);
|
|
571
|
+
const staged = itemsFromFiles(stagedDiff, 'staged');
|
|
572
|
+
const unstaged = itemsFromFiles(unstagedDiff, 'unstaged');
|
|
573
|
+
const untracked = untrackedItemsFrom(top, names);
|
|
574
|
+
const combined = mergeItems([staged, unstaged, untracked]);
|
|
575
|
+
const parsed = dropReviewItems(combined);
|
|
576
|
+
return { top, parsed };
|
|
577
|
+
};
|
|
578
|
+
|
|
579
|
+
const snapshotCommit = (top, commit, files) => {
|
|
580
|
+
const commitItems = itemsFromFiles(files, 'commit');
|
|
581
|
+
const parsed = dropReviewItems(commitItems);
|
|
582
|
+
const revShort = shortRev(top, commit);
|
|
583
|
+
return { top, parsed, rev: commit, revShort };
|
|
584
|
+
};
|
|
585
|
+
|
|
586
|
+
const loadCommit = (top, commit, extra) => {
|
|
587
|
+
const parent = runGit(['rev-parse', '--verify', `${commit}^`], top);
|
|
588
|
+
const args = commitDiffArgs(commit, parent, extra);
|
|
167
589
|
const result = requireOk(runGit(args, top));
|
|
168
590
|
return parseDiff(result.stdout);
|
|
169
591
|
};
|
|
170
592
|
|
|
171
|
-
const
|
|
593
|
+
const needsExtras = (parsed, rev, extra) => {
|
|
594
|
+
const live = extra.audit === true && !rev;
|
|
595
|
+
if (live) return true;
|
|
596
|
+
if (!extra.audit) return false;
|
|
597
|
+
return hasDepFile(parsed);
|
|
598
|
+
};
|
|
599
|
+
|
|
600
|
+
const finishLoad = (snapshot, extra) => {
|
|
601
|
+
const options = { ...extra, paths: extra.paths ?? [] };
|
|
602
|
+
if (extra.deferExtras) options.collect = false;
|
|
603
|
+
const parsed = snapshot.parsed;
|
|
604
|
+
const top = snapshot.top;
|
|
605
|
+
const rev = snapshot.rev;
|
|
606
|
+
const items = foldLoaded(parsed, top, rev, options);
|
|
607
|
+
const pending = extra.deferExtras && needsExtras(parsed, rev, extra);
|
|
608
|
+
return { ...snapshot, items, pending };
|
|
609
|
+
};
|
|
610
|
+
|
|
611
|
+
const readSnapshotSync = (cwd, paths, options) => {
|
|
172
612
|
const top = toplevel(cwd);
|
|
173
|
-
const extra = paths
|
|
613
|
+
const extra = pathArgsOf(paths);
|
|
174
614
|
const rev = options.commit;
|
|
175
|
-
if (rev)
|
|
176
|
-
const files = loadCommit(top, rev, extra);
|
|
177
|
-
const items = dropReviewItems(itemsFromFiles(files, 'commit'));
|
|
178
|
-
const revShort = shortRev(top, rev);
|
|
179
|
-
return { top, items, rev, revShort };
|
|
180
|
-
}
|
|
615
|
+
if (rev) return snapshotCommit(top, rev, loadCommit(top, rev, extra));
|
|
181
616
|
const stagedArgs = [
|
|
182
617
|
...GIT_CONFIG,
|
|
183
618
|
'diff',
|
|
@@ -188,15 +623,108 @@ const load = (cwd, paths = [], options = {}) => {
|
|
|
188
623
|
const unstagedArgs = [...GIT_CONFIG, 'diff', ...DIFF_OPTS, ...extra];
|
|
189
624
|
const stagedRes = requireOk(runGit(stagedArgs, top));
|
|
190
625
|
const unstagedRes = requireOk(runGit(unstagedArgs, top));
|
|
191
|
-
const
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
626
|
+
const names = listUntracked(top, paths);
|
|
627
|
+
return snapshotWorktree(top, stagedRes.stdout, unstagedRes.stdout, names);
|
|
628
|
+
};
|
|
629
|
+
|
|
630
|
+
const loadCommitAsync = async (top, commit, extra, signal) => {
|
|
631
|
+
const parent = await runGitAsync(
|
|
632
|
+
['rev-parse', '--verify', `${commit}^`],
|
|
633
|
+
top,
|
|
634
|
+
{ signal },
|
|
635
|
+
);
|
|
636
|
+
const args = commitDiffArgs(commit, parent, extra);
|
|
637
|
+
const result = requireOk(await runGitAsync(args, top, { signal }));
|
|
638
|
+
return parseDiff(result.stdout);
|
|
639
|
+
};
|
|
640
|
+
|
|
641
|
+
const readSnapshotAsync = async (cwd, paths, options) => {
|
|
642
|
+
const top = toplevel(cwd);
|
|
643
|
+
const extra = pathArgsOf(paths);
|
|
644
|
+
const rev = options.commit;
|
|
645
|
+
const signal = options.signal;
|
|
646
|
+
if (rev) {
|
|
647
|
+
const files = await loadCommitAsync(top, rev, extra, signal);
|
|
648
|
+
return snapshotCommit(top, rev, files);
|
|
649
|
+
}
|
|
650
|
+
const stagedArgs = [
|
|
651
|
+
...GIT_CONFIG,
|
|
652
|
+
'diff',
|
|
653
|
+
'--cached',
|
|
654
|
+
...DIFF_OPTS,
|
|
655
|
+
...extra,
|
|
656
|
+
];
|
|
657
|
+
const unstagedArgs = [...GIT_CONFIG, 'diff', ...DIFF_OPTS, ...extra];
|
|
658
|
+
const lsArgs = ['ls-files', '-o', '--exclude-standard', '-z'];
|
|
659
|
+
if (paths.length) lsArgs.push('--', ...paths);
|
|
660
|
+
const stagedP = runGitAsync(stagedArgs, top, { signal });
|
|
661
|
+
const unstagedP = runGitAsync(unstagedArgs, top, { signal });
|
|
662
|
+
const lsP = runGitAsync(lsArgs, top, { signal });
|
|
663
|
+
const stagedRes = requireOk(await stagedP);
|
|
664
|
+
const unstagedRes = requireOk(await unstagedP);
|
|
665
|
+
const names = namesFromLs(requireOk(await lsP).stdout);
|
|
666
|
+
return snapshotWorktree(top, stagedRes.stdout, unstagedRes.stdout, names);
|
|
667
|
+
};
|
|
668
|
+
|
|
669
|
+
const load = (cwd, paths = [], options = {}) => {
|
|
670
|
+
const snapshot = readSnapshotSync(cwd, paths, options);
|
|
671
|
+
return finishLoad(snapshot, { ...options, paths });
|
|
672
|
+
};
|
|
673
|
+
|
|
674
|
+
const loadAsync = async (cwd, paths = [], options = {}) => {
|
|
675
|
+
const snapshot = await readSnapshotAsync(cwd, paths, options);
|
|
676
|
+
return finishLoad(snapshot, { ...options, paths, deferExtras: true });
|
|
677
|
+
};
|
|
678
|
+
|
|
679
|
+
const usedNamesPromise = (parsed, rev, extra, top) => {
|
|
680
|
+
const live = extra.audit === true && !rev;
|
|
681
|
+
if (!live && !hasDepFile(parsed)) return Promise.resolve(null);
|
|
682
|
+
return Promise.resolve().then(() => collectUsedNames(top));
|
|
683
|
+
};
|
|
684
|
+
|
|
685
|
+
const loadExtras = async (snapshot, extra = {}) => {
|
|
686
|
+
const top = snapshot.top;
|
|
687
|
+
const rev = snapshot.rev;
|
|
688
|
+
const parsed = snapshot.parsed;
|
|
689
|
+
const signal = extra.signal;
|
|
690
|
+
const live = extra.audit === true && !rev;
|
|
691
|
+
const hasDepDiff = hasDepFile(parsed);
|
|
692
|
+
const usedP = usedNamesPromise(parsed, rev, extra, top);
|
|
693
|
+
let auditP = Promise.resolve(null);
|
|
694
|
+
let outdatedP = Promise.resolve(null);
|
|
695
|
+
if (live) {
|
|
696
|
+
auditP = collectAuditAsync(top, extra, signal);
|
|
697
|
+
outdatedP = collectOutdatedAsync(top, extra, signal);
|
|
698
|
+
} else if (extra.audit && hasDepDiff) {
|
|
699
|
+
auditP = collectAuditAsync(top, extra, signal);
|
|
700
|
+
}
|
|
701
|
+
const extras = await Promise.all([usedP, auditP, outdatedP]);
|
|
702
|
+
const usedNames = extras[0];
|
|
703
|
+
const audit = extras[1];
|
|
704
|
+
const outdated = extras[2];
|
|
705
|
+
if (signal && signal.aborted) throw abortError();
|
|
706
|
+
const folded = foldLoaded(parsed, top, rev, {
|
|
707
|
+
...extra,
|
|
708
|
+
paths: extra.paths ?? [],
|
|
709
|
+
usedNames,
|
|
710
|
+
auditMap: audit,
|
|
711
|
+
outdatedMap: outdated,
|
|
712
|
+
collect: false,
|
|
713
|
+
});
|
|
714
|
+
return { ...snapshot, items: folded, pending: false };
|
|
196
715
|
};
|
|
197
716
|
|
|
198
717
|
const addItem = (top, item) => {
|
|
199
718
|
if (item.origin === 'staged' || item.origin === 'commit') return;
|
|
719
|
+
if (depChange(item)) {
|
|
720
|
+
applyDepChange(top, item, 'add');
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
const files = depPaths(item);
|
|
724
|
+
if (files.length) {
|
|
725
|
+
requireOk(runGit(['add', '--', ...files], top));
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
200
728
|
if (item.origin === 'untracked' || item.file.isBinary) {
|
|
201
729
|
requireOk(runGit(['add', '--', item.file.newPath], top));
|
|
202
730
|
return;
|
|
@@ -206,6 +734,15 @@ const addItem = (top, item) => {
|
|
|
206
734
|
|
|
207
735
|
const unstageItem = (top, item) => {
|
|
208
736
|
if (item.origin !== 'staged') return;
|
|
737
|
+
if (depChange(item)) {
|
|
738
|
+
applyDepChange(top, item, 'unstage');
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
const files = depPaths(item);
|
|
742
|
+
if (files.length) {
|
|
743
|
+
requireOk(runGit(['restore', '--staged', '--', ...files], top));
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
209
746
|
const rel = itemPath(item);
|
|
210
747
|
if (item.file.isBinary) {
|
|
211
748
|
requireOk(runGit(['restore', '--staged', '--', rel], top));
|
|
@@ -214,8 +751,62 @@ const unstageItem = (top, item) => {
|
|
|
214
751
|
applyPatch(top, item.patchAdd, ['--reverse', '--cached']);
|
|
215
752
|
};
|
|
216
753
|
|
|
754
|
+
const revertOnePath = (top, rel, items) => {
|
|
755
|
+
const origins = new Set();
|
|
756
|
+
let isNew = false;
|
|
757
|
+
for (const item of items) {
|
|
758
|
+
origins.add(item.origin);
|
|
759
|
+
if (item.file && item.file.isNew) isNew = true;
|
|
760
|
+
}
|
|
761
|
+
if (isNew) {
|
|
762
|
+
if (origins.has('staged')) {
|
|
763
|
+
requireOk(runGit(['rm', '-f', '--', rel], top));
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
fs.unlinkSync(path.join(top, rel));
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
if (origins.has('untracked') && origins.size === 1) {
|
|
770
|
+
fs.unlinkSync(path.join(top, rel));
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
const args = ['restore', '-s', 'HEAD', '--worktree', '--staged', '--', rel];
|
|
774
|
+
requireOk(runGit(args, top));
|
|
775
|
+
};
|
|
776
|
+
|
|
777
|
+
const itemsForPath = (rel, items) => {
|
|
778
|
+
const group = [];
|
|
779
|
+
for (const item of items) {
|
|
780
|
+
if (item.dep) {
|
|
781
|
+
for (const orig of origDepItems(item)) {
|
|
782
|
+
if (itemPath(orig) === rel) group.push(orig);
|
|
783
|
+
}
|
|
784
|
+
continue;
|
|
785
|
+
}
|
|
786
|
+
if (itemPath(item) === rel) group.push(item);
|
|
787
|
+
}
|
|
788
|
+
return group;
|
|
789
|
+
};
|
|
790
|
+
|
|
217
791
|
const revertItem = (top, item) => {
|
|
218
792
|
if (item.origin === 'commit') return;
|
|
793
|
+
if (depChange(item)) {
|
|
794
|
+
applyDepChange(top, item, 'revert');
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
if (item.dep) {
|
|
798
|
+
const byPath = new Map();
|
|
799
|
+
for (const orig of origDepItems(item)) {
|
|
800
|
+
const rel = itemPath(orig);
|
|
801
|
+
const list = byPath.get(rel);
|
|
802
|
+
if (list) list.push(orig);
|
|
803
|
+
else byPath.set(rel, [orig]);
|
|
804
|
+
}
|
|
805
|
+
for (const rel of byPath.keys()) {
|
|
806
|
+
revertOnePath(top, rel, byPath.get(rel));
|
|
807
|
+
}
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
219
810
|
const rel = itemPath(item);
|
|
220
811
|
if (item.origin === 'untracked') {
|
|
221
812
|
fs.unlinkSync(path.join(top, rel));
|
|
@@ -241,30 +832,22 @@ const revertItem = (top, item) => {
|
|
|
241
832
|
};
|
|
242
833
|
|
|
243
834
|
const revertFile = (top, rel, items) => {
|
|
244
|
-
const
|
|
245
|
-
let isNew = false;
|
|
835
|
+
const paths = new Set([rel]);
|
|
246
836
|
for (const item of items) {
|
|
247
|
-
|
|
248
|
-
if (item.file && item.file.isNew) isNew = true;
|
|
837
|
+
for (const file of depPaths(item)) paths.add(file);
|
|
249
838
|
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
}
|
|
255
|
-
fs.unlinkSync(path.join(top, rel));
|
|
256
|
-
return;
|
|
257
|
-
}
|
|
258
|
-
if (origins.has('untracked') && origins.size === 1) {
|
|
259
|
-
fs.unlinkSync(path.join(top, rel));
|
|
260
|
-
return;
|
|
839
|
+
for (const file of paths) {
|
|
840
|
+
const group = itemsForPath(file, items);
|
|
841
|
+
const targets = group.length ? group : items;
|
|
842
|
+
revertOnePath(top, file, targets);
|
|
261
843
|
}
|
|
262
|
-
const args = ['restore', '-s', 'HEAD', '--worktree', '--staged', '--', rel];
|
|
263
|
-
requireOk(runGit(args, top));
|
|
264
844
|
};
|
|
265
845
|
|
|
266
846
|
const createGitRepo = () => ({
|
|
267
847
|
load,
|
|
848
|
+
loadAsync,
|
|
849
|
+
loadExtras,
|
|
850
|
+
toplevel,
|
|
268
851
|
resolveRev,
|
|
269
852
|
add: addItem,
|
|
270
853
|
unstage: unstageItem,
|
|
@@ -273,14 +856,11 @@ const createGitRepo = () => ({
|
|
|
273
856
|
});
|
|
274
857
|
|
|
275
858
|
module.exports = {
|
|
276
|
-
runGit,
|
|
277
|
-
toplevel,
|
|
278
859
|
load,
|
|
279
|
-
|
|
860
|
+
loadAsync,
|
|
861
|
+
loadExtras,
|
|
280
862
|
addItem,
|
|
281
863
|
unstageItem,
|
|
282
864
|
revertItem,
|
|
283
|
-
revertFile,
|
|
284
865
|
createGitRepo,
|
|
285
|
-
oneLine,
|
|
286
866
|
};
|