reslop 0.2.3 → 0.2.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.
Files changed (80) hide show
  1. package/CHANGELOG.md +14 -1
  2. package/README.md +8 -3
  3. package/bin/reslop.js +4 -2
  4. package/lib/api.js +2 -1
  5. package/lib/cli.js +19 -10
  6. package/lib/clipboard.js +6 -3
  7. package/lib/deps-diff.js +61 -115
  8. package/lib/deps.js +24 -61
  9. package/lib/detect.js +4 -2
  10. package/lib/diff/align.js +2 -1
  11. package/lib/diff/diff.js +6 -3
  12. package/lib/diff/display.js +2 -1
  13. package/lib/diff/inline.js +2 -1
  14. package/lib/diff/patch.js +2 -1
  15. package/lib/diff/unit.js +4 -2
  16. package/lib/editor.js +22 -4
  17. package/lib/files.js +38 -12
  18. package/lib/git-branches.js +107 -154
  19. package/lib/git-deps.js +20 -60
  20. package/lib/git-worktree.js +508 -0
  21. package/lib/git.js +23 -580
  22. package/lib/github.js +2 -1
  23. package/lib/gitlab.js +2 -1
  24. package/lib/highlight/bash.js +2 -1
  25. package/lib/highlight/css.js +2 -1
  26. package/lib/highlight/csv.js +2 -1
  27. package/lib/highlight/dot.js +2 -1
  28. package/lib/highlight/html.js +2 -1
  29. package/lib/highlight/json.js +2 -1
  30. package/lib/highlight/log.js +2 -1
  31. package/lib/highlight/txt.js +2 -1
  32. package/lib/highlight.js +2 -1
  33. package/lib/keys.js +2 -2
  34. package/lib/manifest.js +4 -2
  35. package/lib/npm-commands.js +83 -4
  36. package/lib/remote.js +20 -13
  37. package/lib/render/branches.js +6 -15
  38. package/lib/render/chrome.js +27 -7
  39. package/lib/render/commits.js +158 -46
  40. package/lib/render/diff.js +130 -47
  41. package/lib/render/files.js +4 -12
  42. package/lib/render/frame.js +3 -9
  43. package/lib/render/notes.js +4 -11
  44. package/lib/render/npm.js +1 -13
  45. package/lib/render/present.js +2 -1
  46. package/lib/render/primitives.js +28 -15
  47. package/lib/render/render.js +2 -1
  48. package/lib/render/unit.js +14 -11
  49. package/lib/report-model.js +6 -4
  50. package/lib/report-parse.js +8 -13
  51. package/lib/report-render.js +38 -21
  52. package/lib/report-run.js +12 -10
  53. package/lib/report-test.js +5 -4
  54. package/lib/report-tools.js +44 -18
  55. package/lib/review-merge.js +2 -1
  56. package/lib/review.js +6 -2
  57. package/lib/session/actions.js +49 -65
  58. package/lib/session/branches.js +13 -49
  59. package/lib/session/change-actions.js +2 -1
  60. package/lib/session/commit.js +34 -49
  61. package/lib/session/compose.js +37 -11
  62. package/lib/session/dispatch.js +17 -25
  63. package/lib/session/files-pane.js +6 -27
  64. package/lib/session/items.js +6 -3
  65. package/lib/session/lifecycle.js +8 -4
  66. package/lib/session/load.js +4 -2
  67. package/lib/session/navigation.js +1 -0
  68. package/lib/session/npm.js +54 -1
  69. package/lib/session/ops.js +15 -1
  70. package/lib/session/pointer.js +22 -3
  71. package/lib/session/review.js +2 -1
  72. package/lib/session/terminal.js +4 -2
  73. package/lib/session/update.js +2 -1
  74. package/lib/session/view.js +29 -3
  75. package/lib/session.js +37 -23
  76. package/lib/source.js +6 -3
  77. package/lib/update.js +5 -9
  78. package/lib/utilities.js +4 -2
  79. package/lib/wrap.js +3 -1
  80. package/package.json +1 -1
package/lib/git-deps.js CHANGED
@@ -8,12 +8,14 @@ const { foldDepItems, mergeDepFile, depFileMeta } = deps;
8
8
  const { parseAuditReport, parseOutdatedReport } = deps;
9
9
  const { mergeProposedItems, MANIFEST, LOCKFILE } = deps;
10
10
  const { collectUsedNames, collectExportMeta } = deps;
11
- const { itemPath, isPathInScope } = require('./files.js');
11
+ const files = require('./files.js');
12
+ const { itemPath, isPathInScope } = files;
12
13
  const proc = require('./utilities.js');
13
14
  const { abortError, npmBin, npmOpts, runProc, runProcSync } = proc;
14
- const git = require('./git.js');
15
+ const git = require('./git-worktree.js');
15
16
  const { oneLine, requireOk, runGit } = git;
16
17
  const { gitText, worktreeText, readDepSides, revertOnePath } = git;
18
+ const { writeFile, writeIndex } = git;
17
19
 
18
20
  const AUDIT_MS = 15000;
19
21
  const INSTALL_MS = 120000;
@@ -30,12 +32,12 @@ const npmProcOptions = (cwd, extra = {}) => ({
30
32
  const runNpm = (cwd, args) =>
31
33
  runProcSync(npmBin(), args, npmProcOptions(cwd, { timeout: INSTALL_MS }));
32
34
 
33
- const runNpmAsync = (cwd, args, extra = {}) =>
34
- runProc(
35
- npmBin(),
36
- args,
37
- npmProcOptions(cwd, { timeout: INSTALL_MS, signal: extra.signal }),
38
- );
35
+ const runNpmAsync = (cwd, args, extra = {}) => {
36
+ const bin = npmBin();
37
+ const signal = extra.signal;
38
+ const options = npmProcOptions(cwd, { timeout: INSTALL_MS, signal });
39
+ return runProc(bin, args, options);
40
+ };
39
41
 
40
42
  const runNpmJson = (top, args, timeout) => {
41
43
  const result = runProcSync(npmBin(), args, npmProcOptions(top, { timeout }));
@@ -104,18 +106,6 @@ const hasProposed = (item) => {
104
106
  return !!(change && change.propose);
105
107
  };
106
108
 
107
- const relatedFiles = (items) => {
108
- let size = 0;
109
- for (const item of items) size += depPaths(item).length;
110
- const files = new Array(size);
111
- let i = 0;
112
- for (const item of items) {
113
- const paths = depPaths(item);
114
- for (let j = 0; j < paths.length; j++) files[i++] = paths[j];
115
- }
116
- return files;
117
- };
118
-
119
109
  const itemsForPath = (rel, items) => {
120
110
  const group = [];
121
111
  for (const item of items) {
@@ -287,12 +277,6 @@ const loadExtras = async (snapshot, extra = {}) => {
287
277
  };
288
278
  };
289
279
 
290
- const writeWorktreeFile = (top, rel, text) => {
291
- const abs = path.join(top, rel);
292
- fs.mkdirSync(path.dirname(abs), { recursive: true });
293
- fs.writeFileSync(abs, text);
294
- };
295
-
296
280
  const captureFile = (top, rel) => {
297
281
  const abs = path.join(top, rel);
298
282
  try {
@@ -327,16 +311,6 @@ const restoreFiles = (top, owned) => {
327
311
  for (const saved of owned) restoreFile(top, saved);
328
312
  };
329
313
 
330
- const writeIndexFile = (top, rel, text) => {
331
- const saved = captureFile(top, rel);
332
- writeWorktreeFile(top, rel, text);
333
- try {
334
- requireOk(runGit(['add', '--', rel], top));
335
- } finally {
336
- restoreFile(top, saved);
337
- }
338
- };
339
-
340
314
  const mergeDepLive = (top, rel, change, origin, base, side, kind) => {
341
315
  const sides = readDepSides(top, null, origin, rel);
342
316
  return mergeDepFile(base, sides.oldText, sides.newText, change, side, kind);
@@ -347,7 +321,7 @@ const applyDepIndex = (top, rel, change, origin, side) => {
347
321
  if (!meta) return;
348
322
  const indexed = gitText(top, `:${rel}`) || gitText(top, `HEAD:${rel}`);
349
323
  const text = mergeDepLive(top, rel, change, origin, indexed, side, meta.kind);
350
- writeIndexFile(top, rel, text);
324
+ writeIndex(top, rel, text);
351
325
  };
352
326
 
353
327
  const applyDepWorktree = (top, rel, change, origin) => {
@@ -363,7 +337,7 @@ const applyDepWorktree = (top, rel, change, origin) => {
363
337
  'old',
364
338
  meta.kind,
365
339
  );
366
- writeWorktreeFile(top, rel, text);
340
+ writeFile(top, rel, text);
367
341
  };
368
342
 
369
343
  const ownedRelsOf = (plan) => {
@@ -399,7 +373,7 @@ const applyPreparation = (top, item, plan) => {
399
373
  'new',
400
374
  'manifest',
401
375
  );
402
- writeWorktreeFile(top, plan.manifest, text);
376
+ writeFile(top, plan.manifest, text);
403
377
  };
404
378
 
405
379
  const stagePlan = (top, plan) => {
@@ -418,26 +392,14 @@ const finishPlan = (top, plan, result, stage) => {
418
392
  stageFn(top, plan);
419
393
  };
420
394
 
421
- const syncInstaller = (plan, extra) => {
422
- if (extra.run) return extra.run;
423
- if (plan.runner) return plan.runner;
424
- return (dir) => runNpm(dir, plan.npmArgs);
425
- };
426
-
427
- const asyncInstaller = (plan, extra) => {
428
- if (extra.run) return extra.run;
429
- if (plan.runner) return (dir) => Promise.resolve(plan.runner(dir));
430
- return (dir) => runNpmAsync(dir, plan.npmArgs);
431
- };
432
-
433
395
  const applyProposedUpdate = (top, item, extra = {}) => {
434
396
  const plan = proposedNpmPlan(top, item);
435
397
  if (!plan) return;
436
398
  const owned = captureFiles(top, ownedRelsOf(plan));
437
399
  try {
438
400
  applyPreparation(top, item, plan);
439
- const install = syncInstaller(plan, extra);
440
- const result = install(plan.cwd);
401
+ const install = extra.run ?? plan.runner ?? runNpm;
402
+ const result = install(plan.cwd, plan.npmArgs);
441
403
  finishPlan(top, plan, result, extra.stage);
442
404
  } catch (error) {
443
405
  restoreFiles(top, owned);
@@ -451,8 +413,8 @@ const applyProposedUpdateAsync = async (top, item, extra = {}) => {
451
413
  const owned = captureFiles(top, ownedRelsOf(plan));
452
414
  try {
453
415
  applyPreparation(top, item, plan);
454
- const run = asyncInstaller(plan, extra);
455
- const result = await run(plan.cwd);
416
+ const run = extra.run ?? plan.runner ?? runNpmAsync;
417
+ const result = await run(plan.cwd, plan.npmArgs);
456
418
  finishPlan(top, plan, result, extra.stage);
457
419
  } catch (error) {
458
420
  restoreFiles(top, owned);
@@ -525,7 +487,9 @@ const revertDepItem = (top, item) => {
525
487
 
526
488
  const revertFile = (top, rel, items) => {
527
489
  const paths = new Set([rel]);
528
- for (const file of relatedFiles(items)) paths.add(file);
490
+ for (const item of items) {
491
+ for (const file of depPaths(item)) paths.add(file);
492
+ }
529
493
  for (const file of paths) {
530
494
  const group = itemsForPath(file, items);
531
495
  const targets = group.length ? group : items;
@@ -533,12 +497,8 @@ const revertFile = (top, rel, items) => {
533
497
  }
534
498
  };
535
499
 
536
- const handles = (item) => !!(item && item.dep);
537
-
538
500
  module.exports = {
539
501
  hasProposed,
540
- handles,
541
- relatedFiles,
542
502
  itemsForPath,
543
503
  finishLoad,
544
504
  loadExtras,
@@ -0,0 +1,508 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+
6
+ const proc = require('./utilities.js');
7
+ const { runProc, runProcSync, isAbort } = proc;
8
+ const diff = require('./diff/diff.js');
9
+ const { parseDiff, synthesizeNewFile, itemsFromFiles } = diff;
10
+ const files = require('./files.js');
11
+ const { itemPath, REVIEW_DIR } = files;
12
+ const edit = require('./diff/edit.js');
13
+ const { replaceBlockAdds } = edit;
14
+
15
+ const oneLine = (text, fallback = 'git failed') => proc.oneLine(text, fallback);
16
+
17
+ const GIT_CONFIG = [
18
+ '-c',
19
+ 'core.quotepath=false',
20
+ '-c',
21
+ 'i18n.logOutputEncoding=utf-8',
22
+ ];
23
+ 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;
26
+
27
+ const gitProcOptions = (cwd, extra = {}) => ({
28
+ cwd,
29
+ env: { ...process.env, GIT_OPTIONAL_LOCKS: '0', ...extra.env },
30
+ encoding: GIT_ENCODING,
31
+ maxBuffer: GIT_MAX_BUFFER,
32
+ input: extra.input,
33
+ signal: extra.signal,
34
+ });
35
+
36
+ const gitResult = (result) => {
37
+ if (!result.error) return result;
38
+ if (isAbort(result.error)) throw result.error;
39
+ if (result.error.code === 'ENOENT') throw new Error('git not found');
40
+ throw result.error;
41
+ };
42
+
43
+ const requireOk = (result, fallback = 'git failed') => {
44
+ if (result.status === 0) return result;
45
+ const msg = result.stderr || result.stdout || fallback;
46
+ throw new Error(oneLine(msg, fallback));
47
+ };
48
+
49
+ const runGit = (args, cwd, input, extra = {}) => {
50
+ const options = gitProcOptions(cwd, { input, env: extra.env });
51
+ return gitResult(runProcSync('git', args, options));
52
+ };
53
+
54
+ const runGitAsync = (args, cwd, extra = {}) => {
55
+ const options = gitProcOptions(cwd, extra);
56
+ return runProc('git', args, options).then(gitResult);
57
+ };
58
+
59
+ const pathArgsOf = (paths) => (paths.length ? ['--', ...paths] : []);
60
+
61
+ const cachedDiffArgs = (pathArgs) => [
62
+ ...GIT_CONFIG,
63
+ 'diff',
64
+ '--cached',
65
+ ...DIFF_OPTS,
66
+ ...pathArgs,
67
+ ];
68
+
69
+ const worktreeDiffArgs = (pathArgs) => [
70
+ ...GIT_CONFIG,
71
+ 'diff',
72
+ ...DIFF_OPTS,
73
+ ...pathArgs,
74
+ ];
75
+
76
+ const commitDiffArgs = (commit, parent, pathArgs) => {
77
+ if (parent.status === 0) {
78
+ const from = parent.stdout.trim();
79
+ return [...GIT_CONFIG, 'diff', ...DIFF_OPTS, from, commit, ...pathArgs];
80
+ }
81
+ return [
82
+ ...GIT_CONFIG,
83
+ 'diff-tree',
84
+ '--no-commit-id',
85
+ '--root',
86
+ '-p',
87
+ ...DIFF_OPTS,
88
+ commit,
89
+ ...pathArgs,
90
+ ];
91
+ };
92
+
93
+ const BINARY_SIZE = 1000000;
94
+
95
+ const isReviewFile = (rel) => {
96
+ if (!rel) return false;
97
+ const first = rel.split(/[/\\]/)[0];
98
+ return first === REVIEW_DIR;
99
+ };
100
+
101
+ const dropReviewItems = (items) =>
102
+ items.filter((item) => !isReviewFile(itemPath(item)));
103
+
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
+ };
133
+
134
+ const listWorktreeArgs = (paths) => {
135
+ const args = ['ls-files', '-co', '--exclude-standard', '-z'];
136
+ if (paths.length) args.push('--', ...paths);
137
+ return args;
138
+ };
139
+
140
+ const listScopeFiles = (top, paths = [], options = {}) => {
141
+ const rev = options.commit;
142
+ const args = rev ? listTreeArgs(rev, paths) : listWorktreeArgs(paths);
143
+ const result = requireOk(runGit(args, top), 'ls-files failed');
144
+ const names = filterListed(namesFromLs(result.stdout));
145
+ names.sort((left, right) => left.localeCompare(right, 'en'));
146
+ return names;
147
+ };
148
+
149
+ const binaryNewFile = (rel) => ({
150
+ oldPath: rel,
151
+ newPath: rel,
152
+ isNew: true,
153
+ isDeleted: false,
154
+ isBinary: true,
155
+ preamble: [`diff --git a/${rel} b/${rel}`, 'new file mode 100644'],
156
+ hunks: [],
157
+ });
158
+
159
+ const untrackedItemsFrom = (top, names) => {
160
+ const files = [];
161
+ for (const rel of names) {
162
+ if (isReviewFile(rel)) continue;
163
+ const full = path.join(top, rel);
164
+ let buf;
165
+ try {
166
+ buf = fs.readFileSync(full);
167
+ } catch (error) {
168
+ if (error.code === 'ENOENT') continue;
169
+ throw error;
170
+ }
171
+ if (isBinaryBuffer(buf)) {
172
+ files.push(binaryNewFile(rel));
173
+ continue;
174
+ }
175
+ files.push(synthesizeNewFile(rel, buf.toString('utf8')));
176
+ }
177
+ return itemsFromFiles(files, 'untracked');
178
+ };
179
+
180
+ const mergeItems = (groups) => {
181
+ 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
+ }
192
+ }
193
+ return [...byPath.values()].flat();
194
+ };
195
+
196
+ const gitText = (top, spec) => {
197
+ const result = runGit(['show', spec], top);
198
+ if (result.status !== 0) return '';
199
+ return result.stdout ?? '';
200
+ };
201
+
202
+ const worktreeText = (top, rel) => {
203
+ try {
204
+ return fs.readFileSync(path.join(top, rel), 'utf8');
205
+ } catch {
206
+ return '';
207
+ }
208
+ };
209
+
210
+ const fileText = (top, rel, rev) => {
211
+ if (!rel) return '';
212
+ if (rev) return gitText(top, `${rev}:${rel}`);
213
+ return worktreeText(top, rel);
214
+ };
215
+
216
+ const readDepSides = (top, rev, origin, rel) => {
217
+ if (origin === 'untracked') {
218
+ const newText = worktreeText(top, rel);
219
+ return { oldText: '', newText };
220
+ }
221
+ if (origin === 'staged') {
222
+ const oldText = gitText(top, `HEAD:${rel}`);
223
+ const newText = gitText(top, `:${rel}`);
224
+ return { oldText, newText };
225
+ }
226
+ 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 };
231
+ }
232
+ if (origin === 'commit' && rev) {
233
+ const oldText = gitText(top, `${rev}^:${rel}`);
234
+ const newText = gitText(top, `${rev}:${rel}`);
235
+ return { oldText, newText };
236
+ }
237
+ return { oldText: '', newText: '' };
238
+ };
239
+
240
+ const toplevel = (cwd) => {
241
+ const result = runGit(['rev-parse', '--show-toplevel'], cwd);
242
+ if (result.status !== 0) throw new Error('not a git repository');
243
+ return result.stdout.trim();
244
+ };
245
+
246
+ const resolveRev = (cwd, spec) => {
247
+ if (!spec) return null;
248
+ const top = toplevel(cwd);
249
+ 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();
253
+ };
254
+
255
+ const shortRev = (top, commit) => {
256
+ const result = runGit(['rev-parse', '--short', commit], top);
257
+ if (result.status !== 0) return commit.slice(0, 7);
258
+ return result.stdout.trim();
259
+ };
260
+
261
+ const currentBranch = (top) => {
262
+ const args = ['rev-parse', '--abbrev-ref', 'HEAD'];
263
+ const result = runGit(args, top);
264
+ if (result.status !== 0) return '';
265
+ const name = result.stdout.trim();
266
+ if (!name || name === 'HEAD') return '';
267
+ return name;
268
+ };
269
+
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 };
280
+ };
281
+
282
+ const snapshotCommit = (top, commit, files) => {
283
+ const commitItems = itemsFromFiles(files, 'commit');
284
+ const parsed = dropReviewItems(commitItems);
285
+ const revShort = shortRev(top, commit);
286
+ const branch = currentBranch(top);
287
+ return { top, parsed, rev: commit, revShort, branch };
288
+ };
289
+
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
+ };
296
+
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
+ };
307
+
308
+ const readSnapshot = (cwd, paths, options) => {
309
+ const top = toplevel(cwd);
310
+ const extra = pathArgsOf(paths);
311
+ 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);
317
+ };
318
+
319
+ const readSnapshotAsync = async (cwd, paths, options) => {
320
+ const top = toplevel(cwd);
321
+ const extra = pathArgsOf(paths);
322
+ const rev = options.commit;
323
+ const signal = options.signal;
324
+ if (rev) {
325
+ const files = await loadCommitAsync(top, rev, extra, signal);
326
+ return snapshotCommit(top, rev, files);
327
+ }
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);
342
+ };
343
+
344
+ const applyPatch = (cwd, patch, flags) => {
345
+ const args = ['apply', ...flags, '--whitespace=nowarn', '-'];
346
+ const result = runGit(args, cwd, patch);
347
+ requireOk(result, 'apply failed');
348
+ };
349
+
350
+ const addItem = (top, item) => {
351
+ if (item.origin === 'staged' || item.origin === 'commit') return;
352
+ if (item.origin === 'untracked' || item.file.isBinary) {
353
+ return void requireOk(runGit(['add', '--', item.file.newPath], top));
354
+ }
355
+ applyPatch(top, item.patchAdd, ['--cached']);
356
+ };
357
+
358
+ const unstageItem = (top, item) => {
359
+ if (item.origin !== 'staged') return;
360
+ const rel = itemPath(item);
361
+ if (item.file.isBinary) {
362
+ return void requireOk(runGit(['restore', '--staged', '--', rel], top));
363
+ }
364
+ applyPatch(top, item.patchAdd, ['--reverse', '--cached']);
365
+ };
366
+
367
+ 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));
382
+ }
383
+ const args = ['restore', '-s', 'HEAD', '--worktree', '--staged', '--', rel];
384
+ requireOk(runGit(args, top));
385
+ };
386
+
387
+ const revertItem = (top, item) => {
388
+ if (item.origin === 'commit') return;
389
+ const rel = itemPath(item);
390
+ if (item.origin === 'untracked') {
391
+ return void fs.unlinkSync(path.join(top, rel));
392
+ }
393
+ if (item.file.isBinary) {
394
+ const args = ['restore', '-s', 'HEAD', '--worktree'];
395
+ if (item.origin === 'staged') args.push('--staged');
396
+ args.push('--', rel);
397
+ return void requireOk(runGit(args, top));
398
+ }
399
+ if (item.origin === 'unstaged') {
400
+ return void applyPatch(top, item.patchRevert, ['--reverse']);
401
+ }
402
+ applyPatch(top, item.patchRevert, ['--reverse', '--cached']);
403
+ try {
404
+ applyPatch(top, item.patchRevert, ['--reverse']);
405
+ } catch (error) {
406
+ if (!error.message.includes('does not apply')) throw error;
407
+ }
408
+ };
409
+
410
+ const indexText = (top, rel) => {
411
+ const result = runGit(['show', `:${rel}`], top);
412
+ requireOk(result, 'show failed');
413
+ return result.stdout ?? '';
414
+ };
415
+
416
+ const indexMode = (top, rel) => {
417
+ const result = requireOk(runGit(['ls-files', '--stage', '--', rel], top));
418
+ const line = result.stdout.trim();
419
+ if (line) return line.split(' ')[0];
420
+ try {
421
+ const stat = fs.statSync(path.join(top, rel));
422
+ if (stat.mode & 0o111) return '100755';
423
+ } catch (error) {
424
+ if (error.code !== 'ENOENT') throw error;
425
+ }
426
+ return '100644';
427
+ };
428
+
429
+ const writeIndex = (top, rel, text) => {
430
+ 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();
434
+ const mode = indexMode(top, rel);
435
+ const args = ['update-index', '--add', '--cacheinfo', mode, sha, rel];
436
+ requireOk(runGit(args, top), 'update-index failed');
437
+ };
438
+
439
+ const editStaged = (top, rel, abs, hunk, blockId, text) => {
440
+ const source = indexText(top, rel);
441
+ const next = replaceBlockAdds(source, hunk, blockId, text);
442
+ writeIndex(top, rel, next);
443
+ let work = null;
444
+ try {
445
+ work = fs.readFileSync(abs, 'utf8');
446
+ } catch (error) {
447
+ if (error.code !== 'ENOENT') throw error;
448
+ }
449
+ if (work === null) {
450
+ return void fs.writeFileSync(abs, next);
451
+ }
452
+ if (work === source) fs.writeFileSync(abs, next);
453
+ };
454
+
455
+ const editWorktree = (abs, hunk, blockId, text) => {
456
+ const source = fs.readFileSync(abs, 'utf8');
457
+ const next = replaceBlockAdds(source, hunk, blockId, text);
458
+ if (next === source) return;
459
+ fs.writeFileSync(abs, next);
460
+ };
461
+
462
+ const editItem = (top, item, text) => {
463
+ if (item.origin === 'commit' || item.origin === 'pr') return;
464
+ const rel = itemPath(item);
465
+ if (!rel || !item.hunk) return;
466
+ const abs = path.join(top, rel);
467
+ if (item.origin === 'staged') {
468
+ return void editStaged(top, rel, abs, item.hunk, item.blockId, text);
469
+ }
470
+ editWorktree(abs, item.hunk, item.blockId, text);
471
+ };
472
+
473
+ const writeFile = (top, rel, text) => {
474
+ if (!rel) return;
475
+ const abs = path.join(top, rel);
476
+ fs.mkdirSync(path.dirname(abs), { recursive: true });
477
+ fs.writeFileSync(abs, text);
478
+ };
479
+
480
+ const stagePath = (top, rel) => {
481
+ if (!rel) return;
482
+ requireOk(runGit(['add', '--', rel], top));
483
+ };
484
+
485
+ module.exports = {
486
+ oneLine,
487
+ requireOk,
488
+ runGit,
489
+ runGitAsync,
490
+ gitText,
491
+ worktreeText,
492
+ readDepSides,
493
+ revertOnePath,
494
+ toplevel,
495
+ resolveRev,
496
+ currentBranch,
497
+ fileText,
498
+ listScopeFiles,
499
+ readSnapshot,
500
+ readSnapshotAsync,
501
+ editItem,
502
+ writeFile,
503
+ writeIndex,
504
+ stagePath,
505
+ addItem,
506
+ unstageItem,
507
+ revertItem,
508
+ };