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/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 { 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;
@@ -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 options = {
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
- const result = spawnSync('git', args, options);
51
- if (result.error) {
52
- if (result.error.code === 'ENOENT') throw new Error('git not found');
53
- throw result.error;
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
- if (!result.stdout) return [];
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 untrackedItems = (top, paths) => {
198
+ const untrackedItemsFrom = (top, names) => {
94
199
  const files = [];
95
- for (const rel of listUntracked(top, paths)) {
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 loadCommit = (top, commit, extra) => {
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
- args = [...GIT_CONFIG, 'diff', ...DIFF_OPTS, from, commit, ...extra];
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 load = (cwd, paths = [], options = {}) => {
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.length ? ['--', ...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 staged = itemsFromFiles(parseDiff(stagedRes.stdout), 'staged');
192
- const unstaged = itemsFromFiles(parseDiff(unstagedRes.stdout), 'unstaged');
193
- const untracked = untrackedItems(top, paths);
194
- const items = dropReviewItems(mergeItems([staged, unstaged, untracked]));
195
- return { top, items };
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 origins = new Set();
245
- let isNew = false;
835
+ const paths = new Set([rel]);
246
836
  for (const item of items) {
247
- origins.add(item.origin);
248
- if (item.file && item.file.isNew) isNew = true;
837
+ for (const file of depPaths(item)) paths.add(file);
249
838
  }
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;
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
- resolveRev,
860
+ loadAsync,
861
+ loadExtras,
280
862
  addItem,
281
863
  unstageItem,
282
864
  revertItem,
283
- revertFile,
284
865
  createGitRepo,
285
- oneLine,
286
866
  };