reslop 0.1.2 → 0.1.4

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,18 +1,24 @@
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
9
  const { itemPath, isPathInScope, REVIEW_DIR } = require('./files.js');
10
+ const { npmBin, spawnBase } = require('./sys.js');
10
11
  const deps = require('./deps.js');
11
12
  const { foldDepItems, mergeDepFile, depFileMeta } = deps;
12
13
  const { collectUsedNames, parseAuditReport, parseOutdatedReport } = deps;
13
14
  const { mergeProposedItems, MANIFEST, LOCKFILE } = deps;
14
15
 
15
- const GIT_CONFIG = ['-c', 'core.quotepath=false'];
16
+ const GIT_CONFIG = [
17
+ '-c',
18
+ 'core.quotepath=false',
19
+ '-c',
20
+ 'i18n.logOutputEncoding=utf-8',
21
+ ];
16
22
  const DIFF_OPTS = ['--no-color', '--no-ext-diff', '--no-renames', '-U3'];
17
23
  const GIT_ENCODING = 'utf8';
18
24
  const GIT_MAX_BUFFER = 32 * 1024 * 1024;
@@ -46,17 +52,114 @@ const dropReviewItems = (items) => {
46
52
  return kept;
47
53
  };
48
54
 
55
+ const abortError = () => {
56
+ const error = new Error('aborted');
57
+ error.code = 'ABORT';
58
+ return error;
59
+ };
60
+
61
+ const isAbort = (error) => !!(error && error.code === 'ABORT');
62
+
63
+ const runProc = (cmd, args, options = {}) =>
64
+ new Promise((resolve) => {
65
+ const cwd = options.cwd;
66
+ const env = options.env;
67
+ const encoding = options.encoding ?? GIT_ENCODING;
68
+ const maxBuffer = options.maxBuffer ?? GIT_MAX_BUFFER;
69
+ const timeout = options.timeout;
70
+ const input = options.input;
71
+ const signal = options.signal;
72
+ if (signal && signal.aborted) {
73
+ resolve({ status: null, error: abortError(), stdout: '', stderr: '' });
74
+ return;
75
+ }
76
+ let stdout = '';
77
+ let stderr = '';
78
+ let settled = false;
79
+ let timer = null;
80
+ const child = spawn(cmd, args, spawnBase({ cwd, env }));
81
+ const finish = (result) => {
82
+ if (settled) return;
83
+ settled = true;
84
+ if (timer) clearTimeout(timer);
85
+ resolve(result);
86
+ };
87
+ const onAbort = () => {
88
+ child.kill();
89
+ finish({ status: null, error: abortError(), stdout, stderr });
90
+ };
91
+ const append = (kind, chunk) => {
92
+ if (kind === 'out') stdout += chunk;
93
+ else stderr += chunk;
94
+ if (stdout.length + stderr.length <= maxBuffer) return;
95
+ child.kill();
96
+ const error = new Error('maxBuffer exceeded');
97
+ error.code = 'ENOBUFS';
98
+ finish({ status: null, error, stdout, stderr });
99
+ };
100
+ if (signal) signal.addEventListener('abort', onAbort);
101
+ if (child.stdout) {
102
+ child.stdout.setEncoding(encoding);
103
+ child.stdout.on('data', (chunk) => append('out', chunk));
104
+ }
105
+ if (child.stderr) {
106
+ child.stderr.setEncoding(encoding);
107
+ child.stderr.on('data', (chunk) => append('err', chunk));
108
+ }
109
+ if (timeout) {
110
+ timer = setTimeout(() => {
111
+ child.kill();
112
+ const error = new Error('timed out');
113
+ error.code = 'ETIMEDOUT';
114
+ finish({ status: null, error, stdout, stderr });
115
+ }, timeout);
116
+ }
117
+ child.on('error', (error) => {
118
+ finish({ status: null, error, stdout, stderr });
119
+ });
120
+ child.on('close', (status) => {
121
+ finish({ status, stdout, stderr });
122
+ });
123
+ if (child.stdin) {
124
+ try {
125
+ if (input) child.stdin.end(input);
126
+ else child.stdin.end();
127
+ } catch {
128
+ // closed
129
+ }
130
+ }
131
+ });
132
+
133
+ const gitResult = (result) => {
134
+ if (!result.error) return result;
135
+ if (isAbort(result.error)) throw result.error;
136
+ if (result.error.code === 'ENOENT') throw new Error('git not found');
137
+ throw result.error;
138
+ };
139
+
49
140
  const runGit = (args, cwd, input) => {
50
141
  const env = { ...process.env, GIT_OPTIONAL_LOCKS: '0' };
51
142
  const encoding = GIT_ENCODING;
52
143
  const maxBuffer = GIT_MAX_BUFFER;
53
- const options = { cwd, encoding, input, maxBuffer, env };
144
+ const options = spawnBase({ cwd, encoding, input, maxBuffer, env });
54
145
  const result = spawnSync('git', args, options);
55
- if (result.error) {
56
- if (result.error.code === 'ENOENT') throw new Error('git not found');
57
- throw result.error;
58
- }
59
- return result;
146
+ return gitResult(result);
147
+ };
148
+
149
+ const runGitAsync = (args, cwd, extra = {}) => {
150
+ const env = { ...process.env, GIT_OPTIONAL_LOCKS: '0' };
151
+ const encoding = GIT_ENCODING;
152
+ const maxBuffer = GIT_MAX_BUFFER;
153
+ const input = extra.input;
154
+ const signal = extra.signal;
155
+ return runProc('git', args, {
156
+ cwd,
157
+ env,
158
+ encoding,
159
+ maxBuffer,
160
+ input,
161
+ signal,
162
+ }).then(gitResult);
60
163
  };
61
164
 
62
165
  const toplevel = (cwd) => {
@@ -76,12 +179,16 @@ const isBinaryBuffer = (buf) => {
76
179
  return buf.includes(0);
77
180
  };
78
181
 
182
+ const namesFromLs = (stdout) => {
183
+ if (!stdout) return [];
184
+ return stdout.split('\0').filter(Boolean);
185
+ };
186
+
79
187
  const listUntracked = (top, paths) => {
80
188
  const args = ['ls-files', '-o', '--exclude-standard', '-z'];
81
189
  if (paths.length) args.push('--', ...paths);
82
190
  const result = requireOk(runGit(args, top));
83
- if (!result.stdout) return [];
84
- return result.stdout.split('\0').filter(Boolean);
191
+ return namesFromLs(result.stdout);
85
192
  };
86
193
 
87
194
  const binaryNewFile = (rel) => ({
@@ -94,9 +201,9 @@ const binaryNewFile = (rel) => ({
94
201
  hunks: [],
95
202
  });
96
203
 
97
- const untrackedItems = (top, paths) => {
204
+ const untrackedItemsFrom = (top, names) => {
98
205
  const files = [];
99
- for (const rel of listUntracked(top, paths)) {
206
+ for (const rel of names) {
100
207
  if (isReviewFile(rel)) continue;
101
208
  const full = path.join(top, rel);
102
209
  let buf;
@@ -174,11 +281,27 @@ const readDepSides = (top, rev, origin, rel) => {
174
281
  };
175
282
 
176
283
  const runNpmJson = (top, args, timeout) => {
177
- const result = spawnSync('npm', args, {
284
+ const result = spawnSync(
285
+ npmBin(),
286
+ args,
287
+ spawnBase({
288
+ cwd: top,
289
+ encoding: GIT_ENCODING,
290
+ maxBuffer: GIT_MAX_BUFFER,
291
+ timeout,
292
+ }),
293
+ );
294
+ if (result.error) return '';
295
+ return result.stdout || '';
296
+ };
297
+
298
+ const runNpmJsonAsync = async (top, args, timeout, signal) => {
299
+ const result = await runProc(npmBin(), args, {
178
300
  cwd: top,
179
301
  encoding: GIT_ENCODING,
180
302
  maxBuffer: GIT_MAX_BUFFER,
181
303
  timeout,
304
+ signal,
182
305
  });
183
306
  if (result.error) return '';
184
307
  return result.stdout || '';
@@ -200,30 +323,55 @@ const collectOutdated = (top) => {
200
323
  return parseOutdatedReport(text);
201
324
  };
202
325
 
326
+ const collectAuditAsync = async (top, extra, signal) => {
327
+ if (extra.auditMap !== undefined) return extra.auditMap;
328
+ const lock = path.join(top, LOCKFILE);
329
+ if (!fs.existsSync(lock)) return null;
330
+ const args = ['audit', '--json', '--package-lock-only'];
331
+ const text = await runNpmJsonAsync(top, args, AUDIT_MS, signal);
332
+ return parseAuditReport(text);
333
+ };
334
+
335
+ const collectOutdatedAsync = async (top, extra, signal) => {
336
+ if (extra.outdatedMap !== undefined) return extra.outdatedMap;
337
+ const manifest = path.join(top, MANIFEST);
338
+ if (!fs.existsSync(manifest)) return null;
339
+ const args = ['outdated', '--json', '--long'];
340
+ const text = await runNpmJsonAsync(top, args, AUDIT_MS, signal);
341
+ return parseOutdatedReport(text);
342
+ };
343
+
203
344
  const pickNpmMap = (override, collect, top) => {
204
345
  if (override !== undefined) return override;
205
346
  return collect(top);
206
347
  };
207
348
 
349
+ const hasDepFile = (items) => {
350
+ for (const item of items) {
351
+ if (depFileMeta(itemPath(item))) return true;
352
+ }
353
+ return false;
354
+ };
355
+
208
356
  const foldLoaded = (items, top, rev, extra = {}) => {
209
357
  const readSides = (origin, rel) => readDepSides(top, rev, origin, rel);
210
- let usedNames = null;
211
- let audit = null;
212
- let outdated = null;
213
358
  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) {
359
+ const hasDepDiff = hasDepFile(items);
360
+ const collect = extra.collect !== false;
361
+ let usedNames = extra.usedNames ?? null;
362
+ let audit = extra.auditMap;
363
+ let outdated = extra.outdatedMap;
364
+ if (collect && usedNames === null && (hasDepDiff || live)) {
365
+ usedNames = collectUsedNames(top);
366
+ }
367
+ if (collect && live) {
222
368
  audit = pickNpmMap(extra.auditMap, collectAudit, top);
223
369
  outdated = pickNpmMap(extra.outdatedMap, collectOutdated, top);
224
- } else if (extra.audit && hasDepDiff) {
370
+ } else if (collect && extra.audit && hasDepDiff) {
225
371
  audit = pickNpmMap(extra.auditMap, collectAudit, top);
226
372
  }
373
+ if (audit === undefined) audit = null;
374
+ if (outdated === undefined) outdated = null;
227
375
  const folded = foldDepItems(items, readSides, usedNames, audit, outdated, {
228
376
  root: top,
229
377
  });
@@ -305,12 +453,16 @@ const applyDepWorktree = (top, rel, change, origin) => {
305
453
  };
306
454
 
307
455
  const runNpm = (cwd, args) =>
308
- spawnSync('npm', args, {
309
- cwd,
310
- encoding: GIT_ENCODING,
311
- maxBuffer: GIT_MAX_BUFFER,
312
- timeout: INSTALL_MS,
313
- });
456
+ spawnSync(
457
+ npmBin(),
458
+ args,
459
+ spawnBase({
460
+ cwd,
461
+ encoding: GIT_ENCODING,
462
+ maxBuffer: GIT_MAX_BUFFER,
463
+ timeout: INSTALL_MS,
464
+ }),
465
+ );
314
466
 
315
467
  const firstDepRel = (files, kind) => {
316
468
  for (const rel of files) {
@@ -408,40 +560,73 @@ const shortRev = (top, commit) => {
408
560
  return result.stdout.trim();
409
561
  };
410
562
 
411
- const loadCommit = (top, commit, extra) => {
412
- const parent = runGit(['rev-parse', '--verify', `${commit}^`], top);
413
- let args;
563
+ const commitDiffArgs = (commit, parent, pathArgs) => {
414
564
  if (parent.status === 0) {
415
565
  const from = parent.stdout.trim();
416
- args = [...GIT_CONFIG, 'diff', ...DIFF_OPTS, from, commit, ...extra];
417
- } else {
418
- args = [
419
- ...GIT_CONFIG,
420
- 'diff-tree',
421
- '--no-commit-id',
422
- '--root',
423
- '-p',
424
- ...DIFF_OPTS,
425
- commit,
426
- ...extra,
427
- ];
566
+ return [...GIT_CONFIG, 'diff', ...DIFF_OPTS, from, commit, ...pathArgs];
428
567
  }
568
+ return [
569
+ ...GIT_CONFIG,
570
+ 'diff-tree',
571
+ '--no-commit-id',
572
+ '--root',
573
+ '-p',
574
+ ...DIFF_OPTS,
575
+ commit,
576
+ ...pathArgs,
577
+ ];
578
+ };
579
+
580
+ const pathArgsOf = (paths) => (paths.length ? ['--', ...paths] : []);
581
+
582
+ const snapshotWorktree = (top, stagedOut, unstagedOut, names) => {
583
+ const stagedDiff = parseDiff(stagedOut);
584
+ const unstagedDiff = parseDiff(unstagedOut);
585
+ const staged = itemsFromFiles(stagedDiff, 'staged');
586
+ const unstaged = itemsFromFiles(unstagedDiff, 'unstaged');
587
+ const untracked = untrackedItemsFrom(top, names);
588
+ const combined = mergeItems([staged, unstaged, untracked]);
589
+ const parsed = dropReviewItems(combined);
590
+ return { top, parsed };
591
+ };
592
+
593
+ const snapshotCommit = (top, commit, files) => {
594
+ const commitItems = itemsFromFiles(files, 'commit');
595
+ const parsed = dropReviewItems(commitItems);
596
+ const revShort = shortRev(top, commit);
597
+ return { top, parsed, rev: commit, revShort };
598
+ };
599
+
600
+ const loadCommit = (top, commit, extra) => {
601
+ const parent = runGit(['rev-parse', '--verify', `${commit}^`], top);
602
+ const args = commitDiffArgs(commit, parent, extra);
429
603
  const result = requireOk(runGit(args, top));
430
604
  return parseDiff(result.stdout);
431
605
  };
432
606
 
433
- const load = (cwd, paths = [], options = {}) => {
607
+ const needsExtras = (parsed, rev, extra) => {
608
+ const live = extra.audit === true && !rev;
609
+ if (live) return true;
610
+ if (!extra.audit) return false;
611
+ return hasDepFile(parsed);
612
+ };
613
+
614
+ const finishLoad = (snapshot, extra) => {
615
+ const options = { ...extra, paths: extra.paths ?? [] };
616
+ if (extra.deferExtras) options.collect = false;
617
+ const parsed = snapshot.parsed;
618
+ const top = snapshot.top;
619
+ const rev = snapshot.rev;
620
+ const items = foldLoaded(parsed, top, rev, options);
621
+ const pending = extra.deferExtras && needsExtras(parsed, rev, extra);
622
+ return { ...snapshot, items, pending };
623
+ };
624
+
625
+ const readSnapshotSync = (cwd, paths, options) => {
434
626
  const top = toplevel(cwd);
435
- const extra = paths.length ? ['--', ...paths] : [];
627
+ const extra = pathArgsOf(paths);
436
628
  const rev = options.commit;
437
- if (rev) {
438
- const files = loadCommit(top, rev, extra);
439
- const commitItems = itemsFromFiles(files, 'commit');
440
- const parsed = dropReviewItems(commitItems);
441
- const items = foldLoaded(parsed, top, rev, { ...options, paths });
442
- const revShort = shortRev(top, rev);
443
- return { top, items, rev, revShort };
444
- }
629
+ if (rev) return snapshotCommit(top, rev, loadCommit(top, rev, extra));
445
630
  const stagedArgs = [
446
631
  ...GIT_CONFIG,
447
632
  'diff',
@@ -452,15 +637,95 @@ const load = (cwd, paths = [], options = {}) => {
452
637
  const unstagedArgs = [...GIT_CONFIG, 'diff', ...DIFF_OPTS, ...extra];
453
638
  const stagedRes = requireOk(runGit(stagedArgs, top));
454
639
  const unstagedRes = requireOk(runGit(unstagedArgs, top));
455
- const stagedDiff = parseDiff(stagedRes.stdout);
456
- const unstagedDiff = parseDiff(unstagedRes.stdout);
457
- const staged = itemsFromFiles(stagedDiff, 'staged');
458
- const unstaged = itemsFromFiles(unstagedDiff, 'unstaged');
459
- const untracked = untrackedItems(top, paths);
460
- const combined = mergeItems([staged, unstaged, untracked]);
461
- const merged = dropReviewItems(combined);
462
- const items = foldLoaded(merged, top, undefined, { ...options, paths });
463
- return { top, items };
640
+ const names = listUntracked(top, paths);
641
+ return snapshotWorktree(top, stagedRes.stdout, unstagedRes.stdout, names);
642
+ };
643
+
644
+ const loadCommitAsync = async (top, commit, extra, signal) => {
645
+ const parent = await runGitAsync(
646
+ ['rev-parse', '--verify', `${commit}^`],
647
+ top,
648
+ { signal },
649
+ );
650
+ const args = commitDiffArgs(commit, parent, extra);
651
+ const result = requireOk(await runGitAsync(args, top, { signal }));
652
+ return parseDiff(result.stdout);
653
+ };
654
+
655
+ const readSnapshotAsync = async (cwd, paths, options) => {
656
+ const top = toplevel(cwd);
657
+ const extra = pathArgsOf(paths);
658
+ const rev = options.commit;
659
+ const signal = options.signal;
660
+ if (rev) {
661
+ const files = await loadCommitAsync(top, rev, extra, signal);
662
+ return snapshotCommit(top, rev, files);
663
+ }
664
+ const stagedArgs = [
665
+ ...GIT_CONFIG,
666
+ 'diff',
667
+ '--cached',
668
+ ...DIFF_OPTS,
669
+ ...extra,
670
+ ];
671
+ const unstagedArgs = [...GIT_CONFIG, 'diff', ...DIFF_OPTS, ...extra];
672
+ const lsArgs = ['ls-files', '-o', '--exclude-standard', '-z'];
673
+ if (paths.length) lsArgs.push('--', ...paths);
674
+ const stagedP = runGitAsync(stagedArgs, top, { signal });
675
+ const unstagedP = runGitAsync(unstagedArgs, top, { signal });
676
+ const lsP = runGitAsync(lsArgs, top, { signal });
677
+ const stagedRes = requireOk(await stagedP);
678
+ const unstagedRes = requireOk(await unstagedP);
679
+ const names = namesFromLs(requireOk(await lsP).stdout);
680
+ return snapshotWorktree(top, stagedRes.stdout, unstagedRes.stdout, names);
681
+ };
682
+
683
+ const load = (cwd, paths = [], options = {}) => {
684
+ const snapshot = readSnapshotSync(cwd, paths, options);
685
+ return finishLoad(snapshot, { ...options, paths });
686
+ };
687
+
688
+ const loadAsync = async (cwd, paths = [], options = {}) => {
689
+ const snapshot = await readSnapshotAsync(cwd, paths, options);
690
+ return finishLoad(snapshot, { ...options, paths, deferExtras: true });
691
+ };
692
+
693
+ const usedNamesPromise = (parsed, rev, extra, top) => {
694
+ const live = extra.audit === true && !rev;
695
+ if (!live && !hasDepFile(parsed)) return Promise.resolve(null);
696
+ return Promise.resolve().then(() => collectUsedNames(top));
697
+ };
698
+
699
+ const loadExtras = async (snapshot, extra = {}) => {
700
+ const top = snapshot.top;
701
+ const rev = snapshot.rev;
702
+ const parsed = snapshot.parsed;
703
+ const signal = extra.signal;
704
+ const live = extra.audit === true && !rev;
705
+ const hasDepDiff = hasDepFile(parsed);
706
+ const usedP = usedNamesPromise(parsed, rev, extra, top);
707
+ let auditP = Promise.resolve(null);
708
+ let outdatedP = Promise.resolve(null);
709
+ if (live) {
710
+ auditP = collectAuditAsync(top, extra, signal);
711
+ outdatedP = collectOutdatedAsync(top, extra, signal);
712
+ } else if (extra.audit && hasDepDiff) {
713
+ auditP = collectAuditAsync(top, extra, signal);
714
+ }
715
+ const extras = await Promise.all([usedP, auditP, outdatedP]);
716
+ const usedNames = extras[0];
717
+ const audit = extras[1];
718
+ const outdated = extras[2];
719
+ if (signal && signal.aborted) throw abortError();
720
+ const folded = foldLoaded(parsed, top, rev, {
721
+ ...extra,
722
+ paths: extra.paths ?? [],
723
+ usedNames,
724
+ auditMap: audit,
725
+ outdatedMap: outdated,
726
+ collect: false,
727
+ });
728
+ return { ...snapshot, items: folded, pending: false };
464
729
  };
465
730
 
466
731
  const addItem = (top, item) => {
@@ -594,6 +859,9 @@ const revertFile = (top, rel, items) => {
594
859
 
595
860
  const createGitRepo = () => ({
596
861
  load,
862
+ loadAsync,
863
+ loadExtras,
864
+ toplevel,
597
865
  resolveRev,
598
866
  add: addItem,
599
867
  unstage: unstageItem,
@@ -603,6 +871,8 @@ const createGitRepo = () => ({
603
871
 
604
872
  module.exports = {
605
873
  load,
874
+ loadAsync,
875
+ loadExtras,
606
876
  addItem,
607
877
  unstageItem,
608
878
  revertItem,