reslop 0.2.5 → 0.2.7

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.
Files changed (114) hide show
  1. package/CHANGELOG.md +22 -1
  2. package/LICENSE +1 -1
  3. package/README.md +11 -11
  4. package/bin/reslop.js +3 -8
  5. package/lib/ansi.js +72 -19
  6. package/lib/api.js +1 -1
  7. package/lib/capabilities.js +8 -8
  8. package/lib/cli.js +61 -75
  9. package/lib/clipboard.js +39 -47
  10. package/lib/dashboard/git.js +128 -0
  11. package/lib/dashboard/model.js +357 -0
  12. package/lib/dashboard/npm.js +290 -0
  13. package/lib/dashboard/tree.js +278 -0
  14. package/lib/deps-diff.js +197 -402
  15. package/lib/deps.js +113 -240
  16. package/lib/detect.js +8 -18
  17. package/lib/diff/align.js +41 -113
  18. package/lib/diff/diff.js +12 -43
  19. package/lib/diff/display.js +25 -36
  20. package/lib/diff/edit.js +6 -20
  21. package/lib/diff/inline.js +76 -144
  22. package/lib/diff/items.js +33 -47
  23. package/lib/diff/parse.js +28 -56
  24. package/lib/diff/patch.js +12 -33
  25. package/lib/diff/unit.js +79 -178
  26. package/lib/editor.js +52 -69
  27. package/lib/files.js +79 -91
  28. package/lib/find.js +61 -0
  29. package/lib/git-branches.js +254 -416
  30. package/lib/git-deps.js +148 -272
  31. package/lib/git-reword-todo.js +5 -13
  32. package/lib/git-worktree.js +161 -214
  33. package/lib/git.js +21 -49
  34. package/lib/github.js +67 -181
  35. package/lib/gitlab.js +40 -90
  36. package/lib/highlight/bash.js +55 -177
  37. package/lib/highlight/core.js +155 -141
  38. package/lib/highlight/css.js +25 -58
  39. package/lib/highlight/csv.js +19 -37
  40. package/lib/highlight/dart.js +72 -213
  41. package/lib/highlight/dot.js +14 -14
  42. package/lib/highlight/html.js +42 -63
  43. package/lib/highlight/js.js +177 -506
  44. package/lib/highlight/json.js +17 -37
  45. package/lib/highlight/log.js +19 -25
  46. package/lib/highlight/txt.js +7 -10
  47. package/lib/highlight.js +17 -46
  48. package/lib/keys.js +75 -83
  49. package/lib/manifest.js +67 -126
  50. package/lib/npm-commands.js +120 -169
  51. package/lib/remote.js +140 -190
  52. package/lib/render/branches.js +63 -61
  53. package/lib/render/chrome.js +412 -347
  54. package/lib/render/commits.js +160 -361
  55. package/lib/render/dash-activity.js +212 -0
  56. package/lib/render/dash-blocks.js +496 -0
  57. package/lib/render/dash-table.js +295 -0
  58. package/lib/render/dashboard.js +62 -0
  59. package/lib/render/diff.js +123 -258
  60. package/lib/render/files.js +60 -86
  61. package/lib/render/frame.js +82 -117
  62. package/lib/render/notes.js +150 -210
  63. package/lib/render/npm.js +213 -177
  64. package/lib/render/packages.js +93 -0
  65. package/lib/render/present.js +4 -11
  66. package/lib/render/primitives.js +77 -172
  67. package/lib/render/render.js +5 -14
  68. package/lib/render/report.js +43 -0
  69. package/lib/render/repos.js +226 -0
  70. package/lib/render/search.js +25 -0
  71. package/lib/render/tiles.js +355 -0
  72. package/lib/render/unit.js +15 -31
  73. package/lib/report-model.js +110 -33
  74. package/lib/report-parse.js +56 -37
  75. package/lib/report-render.js +102 -59
  76. package/lib/report-run.js +87 -77
  77. package/lib/report-test.js +221 -99
  78. package/lib/report-tools.js +96 -154
  79. package/lib/review-merge.js +33 -41
  80. package/lib/review.js +69 -129
  81. package/lib/runs.js +225 -0
  82. package/lib/select.js +53 -91
  83. package/lib/session/accessors.js +14 -21
  84. package/lib/session/actions.js +189 -65
  85. package/lib/session/branches.js +164 -222
  86. package/lib/session/change-actions.js +49 -119
  87. package/lib/session/commit.js +298 -352
  88. package/lib/session/compose.js +572 -715
  89. package/lib/session/dashboard.js +530 -0
  90. package/lib/session/dispatch.js +197 -171
  91. package/lib/session/files-pane.js +92 -45
  92. package/lib/session/find.js +94 -0
  93. package/lib/session/items.js +6 -17
  94. package/lib/session/lifecycle.js +420 -466
  95. package/lib/session/load.js +1 -14
  96. package/lib/session/navigation.js +4 -2
  97. package/lib/session/npm.js +403 -420
  98. package/lib/session/ops.js +5 -13
  99. package/lib/session/packages.js +143 -0
  100. package/lib/session/pointer.js +30 -14
  101. package/lib/session/progress.js +39 -30
  102. package/lib/session/review.js +1 -3
  103. package/lib/session/tasks.js +277 -236
  104. package/lib/session/templates.js +51 -59
  105. package/lib/session/terminal.js +85 -132
  106. package/lib/session/view.js +59 -80
  107. package/lib/session/watch.js +201 -212
  108. package/lib/session/workspace.js +455 -0
  109. package/lib/session.js +95 -161
  110. package/lib/update.js +44 -111
  111. package/lib/utilities.js +46 -88
  112. package/lib/workspace.js +166 -0
  113. package/lib/wrap.js +29 -55
  114. package/package.json +1 -1
@@ -3,17 +3,15 @@
3
3
  const fs = require('node:fs');
4
4
  const path = require('node:path');
5
5
 
6
- const proc = require('./utilities.js');
7
- const { runProc, runProcSync, isAbort } = proc;
6
+ const utilities = require('./utilities.js');
7
+ const { runProc, runProcSync, isAbort, oneLine } = utilities;
8
8
  const diff = require('./diff/diff.js');
9
9
  const { parseDiff, synthesizeNewFile, itemsFromFiles } = diff;
10
10
  const files = require('./files.js');
11
- const { itemPath, REVIEW_DIR } = files;
11
+ const { itemPath, isReadOnlyOrigin, lineDelta, REVIEW_DIR } = files;
12
12
  const edit = require('./diff/edit.js');
13
13
  const { replaceBlockAdds } = edit;
14
14
 
15
- const oneLine = (text, fallback = 'git failed') => proc.oneLine(text, fallback);
16
-
17
15
  const GIT_CONFIG = [
18
16
  '-c',
19
17
  'core.quotepath=false',
@@ -21,14 +19,11 @@ const GIT_CONFIG = [
21
19
  'i18n.logOutputEncoding=utf-8',
22
20
  ];
23
21
  const DIFF_OPTS = ['--no-color', '--no-ext-diff', '--no-renames', '-U3'];
24
- const GIT_ENCODING = proc.DEFAULT_ENCODING;
25
- const GIT_MAX_BUFFER = proc.DEFAULT_MAX_BUFFER;
22
+ const BINARY_SIZE = 1000000;
26
23
 
27
- const gitProcOptions = (cwd, extra = {}) => ({
24
+ const gitOptions = (cwd, extra) => ({
28
25
  cwd,
29
26
  env: { ...process.env, GIT_OPTIONAL_LOCKS: '0', ...extra.env },
30
- encoding: GIT_ENCODING,
31
- maxBuffer: GIT_MAX_BUFFER,
32
27
  input: extra.input,
33
28
  signal: extra.signal,
34
29
  });
@@ -46,37 +41,61 @@ const requireOk = (result, fallback = 'git failed') => {
46
41
  throw new Error(oneLine(msg, fallback));
47
42
  };
48
43
 
49
- const runGit = (args, cwd, input, extra = {}) => {
50
- const options = gitProcOptions(cwd, { input, env: extra.env });
51
- return gitResult(runProcSync('git', args, options));
44
+ const runGit = (args, cwd, extra = {}) =>
45
+ gitResult(runProcSync('git', args, gitOptions(cwd, extra)));
46
+
47
+ const runGitAsync = (args, cwd, extra = {}) =>
48
+ runProc('git', args, gitOptions(cwd, extra)).then(gitResult);
49
+
50
+ const command = (args, extra = {}) => ({
51
+ args,
52
+ input: extra.input,
53
+ env: extra.env,
54
+ });
55
+
56
+ const runSteps = (steps, top) => {
57
+ try {
58
+ let step = steps.next();
59
+ while (!step.done) {
60
+ step = steps.next(runGit(step.value.args, top, step.value));
61
+ }
62
+ return step.value;
63
+ } finally {
64
+ steps.return();
65
+ }
52
66
  };
53
67
 
54
- const runGitAsync = (args, cwd, extra = {}) => {
55
- const options = gitProcOptions(cwd, extra);
56
- return runProc('git', args, options).then(gitResult);
68
+ const runStepsAsync = async (steps, top, signal) => {
69
+ try {
70
+ let step = steps.next();
71
+ while (!step.done) {
72
+ const extra = { ...step.value, signal };
73
+ step = steps.next(await runGitAsync(step.value.args, top, extra));
74
+ }
75
+ return step.value;
76
+ } finally {
77
+ steps.return();
78
+ }
57
79
  };
58
80
 
59
- const pathArgsOf = (paths) => (paths.length ? ['--', ...paths] : []);
81
+ const gitOp = (op) => ({
82
+ sync: (top, ...args) => runSteps(op(...args), top),
83
+ async: (top, ...args) => runStepsAsync(op(...args), top),
84
+ });
60
85
 
61
- const cachedDiffArgs = (pathArgs) => [
62
- ...GIT_CONFIG,
63
- 'diff',
64
- '--cached',
65
- ...DIFF_OPTS,
66
- ...pathArgs,
67
- ];
86
+ const pathArgs = (paths) => (paths.length ? ['--', ...paths] : []);
68
87
 
69
- const worktreeDiffArgs = (pathArgs) => [
88
+ const diffArgs = (revs, paths) => [
70
89
  ...GIT_CONFIG,
71
90
  'diff',
72
91
  ...DIFF_OPTS,
73
- ...pathArgs,
92
+ ...revs,
93
+ ...pathArgs(paths),
74
94
  ];
75
95
 
76
- const commitDiffArgs = (commit, parent, pathArgs) => {
96
+ const commitDiffArgs = (commit, parent, paths) => {
77
97
  if (parent.status === 0) {
78
- const from = parent.stdout.trim();
79
- return [...GIT_CONFIG, 'diff', ...DIFF_OPTS, from, commit, ...pathArgs];
98
+ return diffArgs([parent.stdout.trim(), commit], paths);
80
99
  }
81
100
  return [
82
101
  ...GIT_CONFIG,
@@ -86,62 +105,38 @@ const commitDiffArgs = (commit, parent, pathArgs) => {
86
105
  '-p',
87
106
  ...DIFF_OPTS,
88
107
  commit,
89
- ...pathArgs,
108
+ ...pathArgs(paths),
90
109
  ];
91
110
  };
92
111
 
93
- const BINARY_SIZE = 1000000;
112
+ const untrackedArgs = (paths) => [
113
+ 'ls-files',
114
+ '-o',
115
+ '--exclude-standard',
116
+ '-z',
117
+ ...pathArgs(paths),
118
+ ];
94
119
 
95
- const isReviewFile = (rel) => {
96
- if (!rel) return false;
97
- const first = rel.split(/[/\\]/)[0];
98
- return first === REVIEW_DIR;
99
- };
120
+ const isReviewFile = (rel) => rel.split(/[/\\]/)[0] === REVIEW_DIR;
121
+
122
+ const isDirEntry = (rel) => rel.endsWith('/');
100
123
 
101
124
  const dropReviewItems = (items) =>
102
125
  items.filter((item) => !isReviewFile(itemPath(item)));
103
126
 
104
- const isBinaryBuffer = (buf) => {
105
- if (buf.length > BINARY_SIZE) return true;
106
- return buf.includes(0);
107
- };
108
-
109
- const namesFromLs = (stdout) => {
110
- if (!stdout) return [];
111
- return stdout.split('\0').filter(Boolean);
112
- };
113
-
114
- const untrackedLsArgs = (paths) => {
115
- const args = ['ls-files', '-o', '--exclude-standard', '-z'];
116
- if (paths.length) args.push('--', ...paths);
117
- return args;
118
- };
119
-
120
- const listUntracked = (top, paths) => {
121
- const result = requireOk(runGit(untrackedLsArgs(paths), top));
122
- return namesFromLs(result.stdout);
123
- };
124
-
125
- const filterListed = (names) =>
126
- names.filter((rel) => rel && !isReviewFile(rel));
127
-
128
- const listTreeArgs = (rev, paths) => {
129
- const args = ['ls-tree', '-r', '--name-only', '-z', rev];
130
- if (paths.length) args.push('--', ...paths);
131
- return args;
132
- };
127
+ const isBinaryBuffer = (buf) => buf.length > BINARY_SIZE || buf.includes(0);
133
128
 
134
- const listWorktreeArgs = (paths) => {
135
- const args = ['ls-files', '-co', '--exclude-standard', '-z'];
136
- if (paths.length) args.push('--', ...paths);
137
- return args;
138
- };
129
+ const namesFromLs = (stdout) => stdout.split('\0').filter(Boolean);
139
130
 
140
131
  const listScopeFiles = (top, paths = [], options = {}) => {
141
132
  const rev = options.commit;
142
- const args = rev ? listTreeArgs(rev, paths) : listWorktreeArgs(paths);
133
+ const args = rev
134
+ ? ['ls-tree', '-r', '--name-only', '-z', rev, ...pathArgs(paths)]
135
+ : ['ls-files', '-co', '--exclude-standard', '-z', ...pathArgs(paths)];
143
136
  const result = requireOk(runGit(args, top), 'ls-files failed');
144
- const names = filterListed(namesFromLs(result.stdout));
137
+ const names = namesFromLs(result.stdout).filter(
138
+ (rel) => !isReviewFile(rel) && !isDirEntry(rel),
139
+ );
145
140
  names.sort((left, right) => left.localeCompare(right, 'en'));
146
141
  return names;
147
142
  };
@@ -159,44 +154,47 @@ const binaryNewFile = (rel) => ({
159
154
  const untrackedItemsFrom = (top, names) => {
160
155
  const files = [];
161
156
  for (const rel of names) {
162
- if (isReviewFile(rel)) continue;
163
- const full = path.join(top, rel);
157
+ if (isReviewFile(rel) || isDirEntry(rel)) continue;
164
158
  let buf;
165
159
  try {
166
- buf = fs.readFileSync(full);
160
+ buf = fs.readFileSync(path.join(top, rel));
167
161
  } catch (error) {
168
- if (error.code === 'ENOENT') continue;
162
+ if (error.code === 'ENOENT' || error.code === 'EISDIR') continue;
169
163
  throw error;
170
164
  }
171
- if (isBinaryBuffer(buf)) {
172
- files.push(binaryNewFile(rel));
173
- continue;
174
- }
175
- files.push(synthesizeNewFile(rel, buf.toString('utf8')));
165
+ if (isBinaryBuffer(buf)) files.push(binaryNewFile(rel));
166
+ else files.push(synthesizeNewFile(rel, buf.toString('utf8')));
176
167
  }
177
168
  return itemsFromFiles(files, 'untracked');
178
169
  };
179
170
 
180
- const mergeItems = (groups) => {
171
+ const countUntrackedAdded = async (top) => {
172
+ try {
173
+ const result = await runGitAsync(untrackedArgs([]), top);
174
+ if (result.status !== 0) return 0;
175
+ const items = untrackedItemsFrom(top, namesFromLs(result.stdout));
176
+ let added = 0;
177
+ for (const item of items) added += lineDelta(item).added;
178
+ return added;
179
+ } catch {
180
+ return 0;
181
+ }
182
+ };
183
+
184
+ const groupByPath = (items) => {
181
185
  const byPath = new Map();
182
- for (const group of groups) {
183
- for (const item of group) {
184
- const rel = itemPath(item);
185
- const list = byPath.get(rel);
186
- if (list) {
187
- list.push(item);
188
- continue;
189
- }
190
- byPath.set(rel, [item]);
191
- }
186
+ for (const item of items) {
187
+ const rel = itemPath(item);
188
+ const list = byPath.get(rel);
189
+ if (list) list.push(item);
190
+ else byPath.set(rel, [item]);
192
191
  }
193
- return [...byPath.values()].flat();
192
+ return byPath;
194
193
  };
195
194
 
196
195
  const gitText = (top, spec) => {
197
196
  const result = runGit(['show', spec], top);
198
- if (result.status !== 0) return '';
199
- return result.stdout ?? '';
197
+ return result.status === 0 ? result.stdout : '';
200
198
  };
201
199
 
202
200
  const worktreeText = (top, rel) => {
@@ -215,24 +213,19 @@ const fileText = (top, rel, rev) => {
215
213
 
216
214
  const readDepSides = (top, rev, origin, rel) => {
217
215
  if (origin === 'untracked') {
218
- const newText = worktreeText(top, rel);
219
- return { oldText: '', newText };
216
+ return { oldText: '', newText: worktreeText(top, rel) };
220
217
  }
221
218
  if (origin === 'staged') {
222
219
  const oldText = gitText(top, `HEAD:${rel}`);
223
- const newText = gitText(top, `:${rel}`);
224
- return { oldText, newText };
220
+ return { oldText, newText: gitText(top, `:${rel}`) };
225
221
  }
226
222
  if (origin === 'unstaged') {
227
- const indexed = gitText(top, `:${rel}`);
228
- const oldText = indexed || gitText(top, `HEAD:${rel}`);
229
- const newText = worktreeText(top, rel);
230
- return { oldText, newText };
223
+ const oldText = gitText(top, `:${rel}`) || gitText(top, `HEAD:${rel}`);
224
+ return { oldText, newText: worktreeText(top, rel) };
231
225
  }
232
226
  if (origin === 'commit' && rev) {
233
227
  const oldText = gitText(top, `${rev}^:${rel}`);
234
- const newText = gitText(top, `${rev}:${rel}`);
235
- return { oldText, newText };
228
+ return { oldText, newText: gitText(top, `${rev}:${rel}`) };
236
229
  }
237
230
  return { oldText: '', newText: '' };
238
231
  };
@@ -245,106 +238,77 @@ const toplevel = (cwd) => {
245
238
 
246
239
  const resolveRev = (cwd, spec) => {
247
240
  if (!spec) return null;
248
- const top = toplevel(cwd);
249
241
  const args = ['rev-parse', '--verify', `${spec}^{commit}`];
250
- const result = runGit(args, top);
251
- if (result.status !== 0) return null;
252
- return result.stdout.trim();
242
+ const result = runGit(args, toplevel(cwd));
243
+ return result.status === 0 ? result.stdout.trim() : null;
253
244
  };
254
245
 
255
246
  const shortRev = (top, commit) => {
256
247
  const result = runGit(['rev-parse', '--short', commit], top);
257
- if (result.status !== 0) return commit.slice(0, 7);
258
- return result.stdout.trim();
248
+ return result.status === 0 ? result.stdout.trim() : commit.slice(0, 7);
259
249
  };
260
250
 
261
251
  const currentBranch = (top) => {
262
- const args = ['rev-parse', '--abbrev-ref', 'HEAD'];
263
- const result = runGit(args, top);
252
+ const result = runGit(['rev-parse', '--abbrev-ref', 'HEAD'], top);
264
253
  if (result.status !== 0) return '';
265
254
  const name = result.stdout.trim();
266
- if (!name || name === 'HEAD') return '';
267
- return name;
255
+ return name === 'HEAD' ? '' : name;
268
256
  };
269
257
 
270
- const snapshotWorktree = (top, stagedOut, unstagedOut, names) => {
271
- const stagedDiff = parseDiff(stagedOut);
272
- const unstagedDiff = parseDiff(unstagedOut);
273
- const staged = itemsFromFiles(stagedDiff, 'staged');
274
- const unstaged = itemsFromFiles(unstagedDiff, 'unstaged');
275
- const untracked = untrackedItemsFrom(top, names);
276
- const combined = mergeItems([staged, unstaged, untracked]);
277
- const parsed = dropReviewItems(combined);
278
- const branch = currentBranch(top);
279
- return { top, parsed, branch };
258
+ const snapshotWorktree = (top, results) => {
259
+ const outputs = results.map((result) => requireOk(result).stdout);
260
+ const staged = itemsFromFiles(parseDiff(outputs[0]), 'staged');
261
+ const unstaged = itemsFromFiles(parseDiff(outputs[1]), 'unstaged');
262
+ const untracked = untrackedItemsFrom(top, namesFromLs(outputs[2]));
263
+ const grouped = groupByPath([...staged, ...unstaged, ...untracked]);
264
+ const parsed = dropReviewItems([...grouped.values()].flat());
265
+ return { top, parsed, branch: currentBranch(top) };
280
266
  };
281
267
 
282
268
  const snapshotCommit = (top, commit, files) => {
283
- const commitItems = itemsFromFiles(files, 'commit');
284
- const parsed = dropReviewItems(commitItems);
269
+ const parsed = dropReviewItems(itemsFromFiles(files, 'commit'));
285
270
  const revShort = shortRev(top, commit);
286
- const branch = currentBranch(top);
287
- return { top, parsed, rev: commit, revShort, branch };
271
+ return { top, parsed, rev: commit, revShort, branch: currentBranch(top) };
288
272
  };
289
273
 
290
- const loadCommit = (top, commit, extra) => {
291
- const parent = runGit(['rev-parse', '--verify', `${commit}^`], top);
292
- const args = commitDiffArgs(commit, parent, extra);
293
- const result = requireOk(runGit(args, top));
294
- return parseDiff(result.stdout);
295
- };
274
+ function* commitDiff(commit, paths) {
275
+ const parent = yield command(['rev-parse', '--verify', `${commit}^`]);
276
+ const result = yield command(commitDiffArgs(commit, parent, paths));
277
+ return parseDiff(requireOk(result).stdout);
278
+ }
296
279
 
297
- const loadCommitAsync = async (top, commit, extra, signal) => {
298
- const parent = await runGitAsync(
299
- ['rev-parse', '--verify', `${commit}^`],
300
- top,
301
- { signal },
302
- );
303
- const args = commitDiffArgs(commit, parent, extra);
304
- const result = requireOk(await runGitAsync(args, top, { signal }));
305
- return parseDiff(result.stdout);
306
- };
280
+ const worktreeCommands = (paths) => [
281
+ diffArgs(['--cached'], paths),
282
+ diffArgs([], paths),
283
+ untrackedArgs(paths),
284
+ ];
307
285
 
308
286
  const readSnapshot = (cwd, paths, options) => {
309
287
  const top = toplevel(cwd);
310
- const extra = pathArgsOf(paths);
311
288
  const rev = options.commit;
312
- if (rev) return snapshotCommit(top, rev, loadCommit(top, rev, extra));
313
- const stagedRes = requireOk(runGit(cachedDiffArgs(extra), top));
314
- const unstagedRes = requireOk(runGit(worktreeDiffArgs(extra), top));
315
- const names = listUntracked(top, paths);
316
- return snapshotWorktree(top, stagedRes.stdout, unstagedRes.stdout, names);
289
+ if (rev) {
290
+ return snapshotCommit(top, rev, runSteps(commitDiff(rev, paths), top));
291
+ }
292
+ const results = worktreeCommands(paths).map((args) => runGit(args, top));
293
+ return snapshotWorktree(top, results);
317
294
  };
318
295
 
319
296
  const readSnapshotAsync = async (cwd, paths, options) => {
320
297
  const top = toplevel(cwd);
321
- const extra = pathArgsOf(paths);
322
- const rev = options.commit;
323
- const signal = options.signal;
298
+ const { commit: rev, signal } = options;
324
299
  if (rev) {
325
- const files = await loadCommitAsync(top, rev, extra, signal);
300
+ const files = await runStepsAsync(commitDiff(rev, paths), top, signal);
326
301
  return snapshotCommit(top, rev, files);
327
302
  }
328
- const stagedArgs = cachedDiffArgs(extra);
329
- const unstagedArgs = worktreeDiffArgs(extra);
330
- const lsArgs = untrackedLsArgs(paths);
331
- const stagedP = runGitAsync(stagedArgs, top, { signal });
332
- const unstagedP = runGitAsync(unstagedArgs, top, { signal });
333
- const lsP = runGitAsync(lsArgs, top, { signal });
334
- const extras = await Promise.all([stagedP, unstagedP, lsP]);
335
- const stagedRes = extras[0];
336
- const unstagedRes = extras[1];
337
- const lsRes = extras[2];
338
- requireOk(stagedRes);
339
- requireOk(unstagedRes);
340
- const names = namesFromLs(requireOk(lsRes).stdout);
341
- return snapshotWorktree(top, stagedRes.stdout, unstagedRes.stdout, names);
303
+ const running = worktreeCommands(paths).map((args) =>
304
+ runGitAsync(args, top, { signal }),
305
+ );
306
+ return snapshotWorktree(top, await Promise.all(running));
342
307
  };
343
308
 
344
309
  const applyPatch = (cwd, patch, flags) => {
345
310
  const args = ['apply', ...flags, '--whitespace=nowarn', '-'];
346
- const result = runGit(args, cwd, patch);
347
- requireOk(result, 'apply failed');
311
+ requireOk(runGit(args, cwd, { input: patch }), 'apply failed');
348
312
  };
349
313
 
350
314
  const addItem = (top, item) => {
@@ -357,35 +321,26 @@ const addItem = (top, item) => {
357
321
 
358
322
  const unstageItem = (top, item) => {
359
323
  if (item.origin !== 'staged') return;
360
- const rel = itemPath(item);
361
324
  if (item.file.isBinary) {
362
- return void requireOk(runGit(['restore', '--staged', '--', rel], top));
325
+ const args = ['restore', '--staged', '--', itemPath(item)];
326
+ return void requireOk(runGit(args, top));
363
327
  }
364
328
  applyPatch(top, item.patchAdd, ['--reverse', '--cached']);
365
329
  };
366
330
 
367
331
  const revertOnePath = (top, rel, items) => {
368
- const origins = new Set();
369
- let isNew = false;
370
- for (const item of items) {
371
- origins.add(item.origin);
372
- if (item.file && item.file.isNew) isNew = true;
373
- }
374
- if (isNew) {
375
- if (origins.has('staged')) {
376
- return void requireOk(runGit(['rm', '-f', '--', rel], top));
377
- }
378
- return void fs.unlinkSync(path.join(top, rel));
379
- }
380
- if (origins.has('untracked') && origins.size === 1) {
381
- return void fs.unlinkSync(path.join(top, rel));
332
+ const origins = new Set(items.map((item) => item.origin));
333
+ const isNew = items.some((item) => item.file?.isNew);
334
+ if (isNew && origins.has('staged')) {
335
+ return void requireOk(runGit(['rm', '-f', '--', rel], top));
382
336
  }
337
+ const onlyUntracked = origins.size === 1 && origins.has('untracked');
338
+ if (isNew || onlyUntracked) return void fs.unlinkSync(path.join(top, rel));
383
339
  const args = ['restore', '-s', 'HEAD', '--worktree', '--staged', '--', rel];
384
340
  requireOk(runGit(args, top));
385
341
  };
386
342
 
387
343
  const revertItem = (top, item) => {
388
- if (item.origin === 'commit') return;
389
344
  const rel = itemPath(item);
390
345
  if (item.origin === 'untracked') {
391
346
  return void fs.unlinkSync(path.join(top, rel));
@@ -393,8 +348,7 @@ const revertItem = (top, item) => {
393
348
  if (item.file.isBinary) {
394
349
  const args = ['restore', '-s', 'HEAD', '--worktree'];
395
350
  if (item.origin === 'staged') args.push('--staged');
396
- args.push('--', rel);
397
- return void requireOk(runGit(args, top));
351
+ return void requireOk(runGit([...args, '--', rel], top));
398
352
  }
399
353
  if (item.origin === 'unstaged') {
400
354
  return void applyPatch(top, item.patchRevert, ['--reverse']);
@@ -407,12 +361,6 @@ const revertItem = (top, item) => {
407
361
  }
408
362
  };
409
363
 
410
- const indexText = (top, rel) => {
411
- const result = runGit(['show', `:${rel}`], top);
412
- requireOk(result, 'show failed');
413
- return result.stdout ?? '';
414
- };
415
-
416
364
  const indexMode = (top, rel) => {
417
365
  const result = requireOk(runGit(['ls-files', '--stage', '--', rel], top));
418
366
  const line = result.stdout.trim();
@@ -428,17 +376,17 @@ const indexMode = (top, rel) => {
428
376
 
429
377
  const writeIndex = (top, rel, text) => {
430
378
  const hashArgs = ['hash-object', '-w', `--path=${rel}`, '--stdin'];
431
- const hashed = runGit(hashArgs, top, text);
432
- requireOk(hashed, 'hash-object failed');
433
- const sha = (hashed.stdout ?? '').trim();
379
+ const hashed = runGit(hashArgs, top, { input: text });
380
+ const sha = requireOk(hashed, 'hash-object failed').stdout.trim();
434
381
  const mode = indexMode(top, rel);
435
382
  const args = ['update-index', '--add', '--cacheinfo', mode, sha, rel];
436
383
  requireOk(runGit(args, top), 'update-index failed');
437
384
  };
438
385
 
439
- const editStaged = (top, rel, abs, hunk, blockId, text) => {
440
- const source = indexText(top, rel);
441
- const next = replaceBlockAdds(source, hunk, blockId, text);
386
+ const editStaged = (top, rel, abs, item, text) => {
387
+ const shown = requireOk(runGit(['show', `:${rel}`], top), 'show failed');
388
+ const source = shown.stdout;
389
+ const next = replaceBlockAdds(source, item.hunk, item.blockId, text);
442
390
  writeIndex(top, rel, next);
443
391
  let work = null;
444
392
  try {
@@ -446,28 +394,24 @@ const editStaged = (top, rel, abs, hunk, blockId, text) => {
446
394
  } catch (error) {
447
395
  if (error.code !== 'ENOENT') throw error;
448
396
  }
449
- if (work === null) {
450
- return void fs.writeFileSync(abs, next);
451
- }
452
- if (work === source) fs.writeFileSync(abs, next);
397
+ if (work === null || work === source) fs.writeFileSync(abs, next);
453
398
  };
454
399
 
455
- const editWorktree = (abs, hunk, blockId, text) => {
400
+ const editWorktree = (abs, item, text) => {
456
401
  const source = fs.readFileSync(abs, 'utf8');
457
- const next = replaceBlockAdds(source, hunk, blockId, text);
458
- if (next === source) return;
459
- fs.writeFileSync(abs, next);
402
+ const next = replaceBlockAdds(source, item.hunk, item.blockId, text);
403
+ if (next !== source) fs.writeFileSync(abs, next);
460
404
  };
461
405
 
462
406
  const editItem = (top, item, text) => {
463
- if (item.origin === 'commit' || item.origin === 'pr') return;
407
+ if (isReadOnlyOrigin(item.origin)) return;
464
408
  const rel = itemPath(item);
465
409
  if (!rel || !item.hunk) return;
466
410
  const abs = path.join(top, rel);
467
411
  if (item.origin === 'staged') {
468
- return void editStaged(top, rel, abs, item.hunk, item.blockId, text);
412
+ return void editStaged(top, rel, abs, item, text);
469
413
  }
470
- editWorktree(abs, item.hunk, item.blockId, text);
414
+ editWorktree(abs, item, text);
471
415
  };
472
416
 
473
417
  const writeFile = (top, rel, text) => {
@@ -483,10 +427,12 @@ const stagePath = (top, rel) => {
483
427
  };
484
428
 
485
429
  module.exports = {
486
- oneLine,
487
430
  requireOk,
488
431
  runGit,
489
432
  runGitAsync,
433
+ command,
434
+ gitOp,
435
+ groupByPath,
490
436
  gitText,
491
437
  worktreeText,
492
438
  readDepSides,
@@ -495,6 +441,7 @@ module.exports = {
495
441
  resolveRev,
496
442
  currentBranch,
497
443
  fileText,
444
+ countUntrackedAdded,
498
445
  listScopeFiles,
499
446
  readSnapshot,
500
447
  readSnapshotAsync,