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.
- package/CHANGELOG.md +14 -1
- package/README.md +8 -3
- package/bin/reslop.js +4 -2
- package/lib/api.js +2 -1
- package/lib/cli.js +19 -10
- package/lib/clipboard.js +6 -3
- package/lib/deps-diff.js +61 -115
- package/lib/deps.js +24 -61
- package/lib/detect.js +4 -2
- package/lib/diff/align.js +2 -1
- package/lib/diff/diff.js +6 -3
- package/lib/diff/display.js +2 -1
- package/lib/diff/inline.js +2 -1
- package/lib/diff/patch.js +2 -1
- package/lib/diff/unit.js +4 -2
- package/lib/editor.js +22 -4
- package/lib/files.js +38 -12
- package/lib/git-branches.js +107 -154
- package/lib/git-deps.js +20 -60
- package/lib/git-worktree.js +508 -0
- package/lib/git.js +23 -580
- package/lib/github.js +2 -1
- package/lib/gitlab.js +2 -1
- package/lib/highlight/bash.js +2 -1
- package/lib/highlight/css.js +2 -1
- package/lib/highlight/csv.js +2 -1
- package/lib/highlight/dot.js +2 -1
- package/lib/highlight/html.js +2 -1
- package/lib/highlight/json.js +2 -1
- package/lib/highlight/log.js +2 -1
- package/lib/highlight/txt.js +2 -1
- package/lib/highlight.js +2 -1
- package/lib/keys.js +2 -2
- package/lib/manifest.js +4 -2
- package/lib/npm-commands.js +83 -4
- package/lib/remote.js +20 -13
- package/lib/render/branches.js +6 -15
- package/lib/render/chrome.js +27 -7
- package/lib/render/commits.js +158 -46
- package/lib/render/diff.js +130 -47
- package/lib/render/files.js +4 -12
- package/lib/render/frame.js +3 -9
- package/lib/render/notes.js +4 -11
- package/lib/render/npm.js +1 -13
- package/lib/render/present.js +2 -1
- package/lib/render/primitives.js +28 -15
- package/lib/render/render.js +2 -1
- package/lib/render/unit.js +14 -11
- package/lib/report-model.js +6 -4
- package/lib/report-parse.js +8 -13
- package/lib/report-render.js +38 -21
- package/lib/report-run.js +12 -10
- package/lib/report-test.js +5 -4
- package/lib/report-tools.js +44 -18
- package/lib/review-merge.js +2 -1
- package/lib/review.js +6 -2
- package/lib/session/actions.js +49 -65
- package/lib/session/branches.js +13 -49
- package/lib/session/change-actions.js +2 -1
- package/lib/session/commit.js +34 -49
- package/lib/session/compose.js +37 -11
- package/lib/session/dispatch.js +17 -25
- package/lib/session/files-pane.js +6 -27
- package/lib/session/items.js +6 -3
- package/lib/session/lifecycle.js +8 -4
- package/lib/session/load.js +4 -2
- package/lib/session/navigation.js +1 -0
- package/lib/session/npm.js +54 -1
- package/lib/session/ops.js +15 -1
- package/lib/session/pointer.js +22 -3
- package/lib/session/review.js +2 -1
- package/lib/session/terminal.js +4 -2
- package/lib/session/update.js +2 -1
- package/lib/session/view.js +29 -3
- package/lib/session.js +37 -23
- package/lib/source.js +6 -3
- package/lib/update.js +5 -9
- package/lib/utilities.js +4 -2
- package/lib/wrap.js +3 -1
- 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
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
+
};
|