reslop 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -1
- package/README.md +18 -17
- package/lib/cli.js +22 -8
- package/lib/detect.js +2 -0
- package/lib/files.js +36 -12
- package/lib/git.js +312 -56
- package/lib/highlight.js +160 -5
- package/lib/keys.js +8 -2
- package/lib/render.js +78 -24
- package/lib/session.js +207 -22
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased][unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.1.3][] - 2026-09-12
|
|
6
|
+
|
|
7
|
+
- Showed added/removed and staged/total counts in the file list
|
|
8
|
+
- Highlighted JSX and TSX syntax, including
|
|
9
|
+
- Added vim-style j/k navigation and Ctrl scrolling
|
|
10
|
+
- Improved TUI/UX with background git and npm checks to avoid a freeze
|
|
11
|
+
|
|
5
12
|
## [0.1.2][] - 2026-09-11
|
|
6
13
|
|
|
7
14
|
- Review each dependency once across package.json and package-lock
|
|
@@ -26,7 +33,8 @@
|
|
|
26
33
|
- Feedback and todos that produce a repair plan for an agent
|
|
27
34
|
- Intra-line highlighting in unified, mixed, and side-by-side layouts
|
|
28
35
|
|
|
29
|
-
[unreleased]: https://github.com/tshemsedinov/reslop/compare/v0.1.
|
|
36
|
+
[unreleased]: https://github.com/tshemsedinov/reslop/compare/v0.1.3...HEAD
|
|
37
|
+
[0.1.3]: https://github.com/tshemsedinov/reslop/compare/v0.1.2...v0.1.3
|
|
30
38
|
[0.1.2]: https://github.com/tshemsedinov/reslop/compare/v0.1.1...v0.1.2
|
|
31
39
|
[0.1.1]: https://github.com/tshemsedinov/reslop/compare/v0.1.0...v0.1.1
|
|
32
40
|
[0.1.0]: https://github.com/tshemsedinov/reslop/releases/tag/v0.1.0
|
package/README.md
CHANGED
|
@@ -48,23 +48,24 @@ Reviews go in `.review/YYYY-MM-DD-NN.md` with frontmatter `status`:
|
|
|
48
48
|
|
|
49
49
|
## Keys and buttons
|
|
50
50
|
|
|
51
|
-
| Key
|
|
52
|
-
|
|
|
53
|
-
| `a`
|
|
54
|
-
| `u`
|
|
55
|
-
| `r`
|
|
56
|
-
| `
|
|
57
|
-
|
|
|
58
|
-
|
|
|
59
|
-
| `
|
|
60
|
-
|
|
|
61
|
-
| `
|
|
62
|
-
| `
|
|
63
|
-
| `
|
|
64
|
-
|
|
|
65
|
-
|
|
|
66
|
-
| `
|
|
67
|
-
|
|
|
51
|
+
| Key | Action |
|
|
52
|
+
| ------------------- | ---------------------------------------- |
|
|
53
|
+
| `a` | Stage this block (`git add`) |
|
|
54
|
+
| `u` | Unstage this block (keep worktree) |
|
|
55
|
+
| `r` | Restore this block to the last commit |
|
|
56
|
+
| `f` | Feedback on this diff block |
|
|
57
|
+
| `t` | New todo for this file |
|
|
58
|
+
| `l` | File list (git status of this scope) |
|
|
59
|
+
| `m` | Cycle unified / mixed / side-by-side |
|
|
60
|
+
| `⏎` | Open the selected file |
|
|
61
|
+
| `Esc` | Quit file list or a tool |
|
|
62
|
+
| `→` / `j` | Next remaining block / next file |
|
|
63
|
+
| `←` / `k` | Previous remaining block / previous file |
|
|
64
|
+
| `Ctrl-e` / `Ctrl-y` | Scroll one line down / up |
|
|
65
|
+
| `Ctrl-f` / `Ctrl-b` | Scroll page down / up |
|
|
66
|
+
| `Ctrl-d` / `Ctrl-u` | Scroll half-page down / up |
|
|
67
|
+
| `⌫` / `Del` | Remove the selected todo |
|
|
68
|
+
| `q` | Quit |
|
|
68
69
|
|
|
69
70
|
## License
|
|
70
71
|
|
package/lib/cli.js
CHANGED
|
@@ -99,9 +99,9 @@ const sessionOptions = (proc, deps, args, env, extra) => ({
|
|
|
99
99
|
...extra,
|
|
100
100
|
});
|
|
101
101
|
|
|
102
|
-
const startSession = (options) => {
|
|
102
|
+
const startSession = (options, extra = {}) => {
|
|
103
103
|
const session = new Session(options);
|
|
104
|
-
session.load();
|
|
104
|
+
if (extra.load) session.load();
|
|
105
105
|
return session;
|
|
106
106
|
};
|
|
107
107
|
|
|
@@ -110,13 +110,15 @@ const openLocalSession = (proc, deps, args, selected, env) => {
|
|
|
110
110
|
const repo = deps.repo ?? createGitRepo();
|
|
111
111
|
const statSync = deps.statSync ?? fs.statSync;
|
|
112
112
|
const scope = resolveScope(cwd, selected.paths, repo, statSync);
|
|
113
|
-
|
|
113
|
+
const session = startSession(
|
|
114
114
|
sessionOptions(proc, deps, args, env, {
|
|
115
115
|
repo,
|
|
116
116
|
paths: scope.paths,
|
|
117
117
|
rev: scope.rev,
|
|
118
118
|
}),
|
|
119
119
|
);
|
|
120
|
+
session.ensureRepo();
|
|
121
|
+
return session;
|
|
120
122
|
};
|
|
121
123
|
|
|
122
124
|
const openPullRequestSession = async (proc, deps, args, selected, env) => {
|
|
@@ -140,6 +142,7 @@ const openPullRequestSession = async (proc, deps, args, selected, env) => {
|
|
|
140
142
|
sourceLabel: loaded.sourceLabel,
|
|
141
143
|
change,
|
|
142
144
|
}),
|
|
145
|
+
{ load: true },
|
|
143
146
|
);
|
|
144
147
|
};
|
|
145
148
|
|
|
@@ -152,6 +155,13 @@ const loadSession = async (proc, deps, args) => {
|
|
|
152
155
|
return openLocalSession(proc, deps, args, selected, env);
|
|
153
156
|
};
|
|
154
157
|
|
|
158
|
+
const runUi = async (session, proc) => {
|
|
159
|
+
const code = await session.startUi();
|
|
160
|
+
if (session.loadError) return fail(proc.stderr, session.loadError);
|
|
161
|
+
if (session.emptyReview) proc.stdout.write('nothing to review\n');
|
|
162
|
+
return code;
|
|
163
|
+
};
|
|
164
|
+
|
|
155
165
|
const run = async (proc, deps = {}) => {
|
|
156
166
|
const stdout = proc.stdout;
|
|
157
167
|
const stderr = proc.stderr;
|
|
@@ -167,15 +177,19 @@ const run = async (proc, deps = {}) => {
|
|
|
167
177
|
} catch (error) {
|
|
168
178
|
return fail(stderr, error);
|
|
169
179
|
}
|
|
180
|
+
const tty = proc.stdin.isTTY === true && stdout.isTTY === true;
|
|
181
|
+
if (tty) return runUi(session, proc);
|
|
182
|
+
try {
|
|
183
|
+
await session.loadReady();
|
|
184
|
+
} catch (error) {
|
|
185
|
+
return fail(stderr, error);
|
|
186
|
+
}
|
|
170
187
|
if (!session.items.length) {
|
|
171
188
|
stdout.write('nothing to review\n');
|
|
172
189
|
return 0;
|
|
173
190
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
return 1;
|
|
177
|
-
}
|
|
178
|
-
return session.startUi();
|
|
191
|
+
stderr.write('reslop: interactive terminal required\n');
|
|
192
|
+
return 1;
|
|
179
193
|
};
|
|
180
194
|
|
|
181
195
|
module.exports = {
|
package/lib/detect.js
CHANGED
package/lib/files.js
CHANGED
|
@@ -33,26 +33,50 @@ const fileStatus = (origins) => {
|
|
|
33
33
|
return key;
|
|
34
34
|
};
|
|
35
35
|
|
|
36
|
+
const lineDelta = ({ hunk, blockId }) => {
|
|
37
|
+
const delta = { added: 0, removed: 0 };
|
|
38
|
+
if (!hunk) return delta;
|
|
39
|
+
const { lines } = hunk;
|
|
40
|
+
if (!lines) return delta;
|
|
41
|
+
const scoped = typeof blockId === 'number';
|
|
42
|
+
for (const { type, blockId: lineBlock } of lines) {
|
|
43
|
+
if (scoped && lineBlock !== blockId) continue;
|
|
44
|
+
if (type === 'add') delta.added += 1;
|
|
45
|
+
else if (type === 'del') delta.removed += 1;
|
|
46
|
+
}
|
|
47
|
+
return delta;
|
|
48
|
+
};
|
|
49
|
+
|
|
36
50
|
const fileEntries = (items) => {
|
|
37
51
|
const groups = new Map();
|
|
38
52
|
for (let i = 0; i < items.length; i++) {
|
|
39
53
|
const item = items[i];
|
|
40
54
|
const rel = itemPath(item);
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
55
|
+
if (!groups.has(rel)) {
|
|
56
|
+
groups.set(rel, {
|
|
57
|
+
path: rel,
|
|
58
|
+
origins: [],
|
|
59
|
+
firstIndex: i,
|
|
60
|
+
remaining: 0,
|
|
61
|
+
added: 0,
|
|
62
|
+
removed: 0,
|
|
63
|
+
staged: 0,
|
|
64
|
+
unstaged: 0,
|
|
65
|
+
});
|
|
45
66
|
}
|
|
46
|
-
group.
|
|
67
|
+
const group = groups.get(rel);
|
|
68
|
+
const { origin } = item;
|
|
69
|
+
group.origins.push(origin);
|
|
47
70
|
group.remaining += 1;
|
|
71
|
+
const delta = lineDelta(item);
|
|
72
|
+
group.added += delta.added;
|
|
73
|
+
group.removed += delta.removed;
|
|
74
|
+
if (origin === 'staged') group.staged += 1;
|
|
75
|
+
if (origin === 'unstaged') group.unstaged += 1;
|
|
48
76
|
}
|
|
49
|
-
const entries = [];
|
|
50
|
-
for (const group of
|
|
51
|
-
|
|
52
|
-
const status = fileStatus(group.origins);
|
|
53
|
-
const firstIndex = group.firstIndex;
|
|
54
|
-
const remaining = group.remaining;
|
|
55
|
-
entries.push({ path, status, firstIndex, remaining });
|
|
77
|
+
const entries = [...groups.values()];
|
|
78
|
+
for (const group of entries) {
|
|
79
|
+
group.status = fileStatus(group.origins);
|
|
56
80
|
}
|
|
57
81
|
return entries;
|
|
58
82
|
};
|
package/lib/git.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
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
|
|
|
@@ -46,17 +46,114 @@ const dropReviewItems = (items) => {
|
|
|
46
46
|
return kept;
|
|
47
47
|
};
|
|
48
48
|
|
|
49
|
+
const abortError = () => {
|
|
50
|
+
const error = new Error('aborted');
|
|
51
|
+
error.code = 'ABORT';
|
|
52
|
+
return error;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const isAbort = (error) => !!(error && error.code === 'ABORT');
|
|
56
|
+
|
|
57
|
+
const runProc = (cmd, args, options = {}) =>
|
|
58
|
+
new Promise((resolve) => {
|
|
59
|
+
const cwd = options.cwd;
|
|
60
|
+
const env = options.env;
|
|
61
|
+
const encoding = options.encoding ?? GIT_ENCODING;
|
|
62
|
+
const maxBuffer = options.maxBuffer ?? GIT_MAX_BUFFER;
|
|
63
|
+
const timeout = options.timeout;
|
|
64
|
+
const input = options.input;
|
|
65
|
+
const signal = options.signal;
|
|
66
|
+
if (signal && signal.aborted) {
|
|
67
|
+
resolve({ status: null, error: abortError(), stdout: '', stderr: '' });
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
let stdout = '';
|
|
71
|
+
let stderr = '';
|
|
72
|
+
let settled = false;
|
|
73
|
+
let timer = null;
|
|
74
|
+
const child = spawn(cmd, args, { cwd, env });
|
|
75
|
+
const finish = (result) => {
|
|
76
|
+
if (settled) return;
|
|
77
|
+
settled = true;
|
|
78
|
+
if (timer) clearTimeout(timer);
|
|
79
|
+
resolve(result);
|
|
80
|
+
};
|
|
81
|
+
const onAbort = () => {
|
|
82
|
+
child.kill();
|
|
83
|
+
finish({ status: null, error: abortError(), stdout, stderr });
|
|
84
|
+
};
|
|
85
|
+
const append = (kind, chunk) => {
|
|
86
|
+
if (kind === 'out') stdout += chunk;
|
|
87
|
+
else stderr += chunk;
|
|
88
|
+
if (stdout.length + stderr.length <= maxBuffer) return;
|
|
89
|
+
child.kill();
|
|
90
|
+
const error = new Error('maxBuffer exceeded');
|
|
91
|
+
error.code = 'ENOBUFS';
|
|
92
|
+
finish({ status: null, error, stdout, stderr });
|
|
93
|
+
};
|
|
94
|
+
if (signal) signal.addEventListener('abort', onAbort);
|
|
95
|
+
if (child.stdout) {
|
|
96
|
+
child.stdout.setEncoding(encoding);
|
|
97
|
+
child.stdout.on('data', (chunk) => append('out', chunk));
|
|
98
|
+
}
|
|
99
|
+
if (child.stderr) {
|
|
100
|
+
child.stderr.setEncoding(encoding);
|
|
101
|
+
child.stderr.on('data', (chunk) => append('err', chunk));
|
|
102
|
+
}
|
|
103
|
+
if (timeout) {
|
|
104
|
+
timer = setTimeout(() => {
|
|
105
|
+
child.kill();
|
|
106
|
+
const error = new Error('timed out');
|
|
107
|
+
error.code = 'ETIMEDOUT';
|
|
108
|
+
finish({ status: null, error, stdout, stderr });
|
|
109
|
+
}, timeout);
|
|
110
|
+
}
|
|
111
|
+
child.on('error', (error) => {
|
|
112
|
+
finish({ status: null, error, stdout, stderr });
|
|
113
|
+
});
|
|
114
|
+
child.on('close', (status) => {
|
|
115
|
+
finish({ status, stdout, stderr });
|
|
116
|
+
});
|
|
117
|
+
if (child.stdin) {
|
|
118
|
+
try {
|
|
119
|
+
if (input) child.stdin.end(input);
|
|
120
|
+
else child.stdin.end();
|
|
121
|
+
} catch {
|
|
122
|
+
// closed
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const gitResult = (result) => {
|
|
128
|
+
if (!result.error) return result;
|
|
129
|
+
if (isAbort(result.error)) throw result.error;
|
|
130
|
+
if (result.error.code === 'ENOENT') throw new Error('git not found');
|
|
131
|
+
throw result.error;
|
|
132
|
+
};
|
|
133
|
+
|
|
49
134
|
const runGit = (args, cwd, input) => {
|
|
50
135
|
const env = { ...process.env, GIT_OPTIONAL_LOCKS: '0' };
|
|
51
136
|
const encoding = GIT_ENCODING;
|
|
52
137
|
const maxBuffer = GIT_MAX_BUFFER;
|
|
53
138
|
const options = { cwd, encoding, input, maxBuffer, env };
|
|
54
139
|
const result = spawnSync('git', args, options);
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
140
|
+
return gitResult(result);
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const runGitAsync = (args, cwd, extra = {}) => {
|
|
144
|
+
const env = { ...process.env, GIT_OPTIONAL_LOCKS: '0' };
|
|
145
|
+
const encoding = GIT_ENCODING;
|
|
146
|
+
const maxBuffer = GIT_MAX_BUFFER;
|
|
147
|
+
const input = extra.input;
|
|
148
|
+
const signal = extra.signal;
|
|
149
|
+
return runProc('git', args, {
|
|
150
|
+
cwd,
|
|
151
|
+
env,
|
|
152
|
+
encoding,
|
|
153
|
+
maxBuffer,
|
|
154
|
+
input,
|
|
155
|
+
signal,
|
|
156
|
+
}).then(gitResult);
|
|
60
157
|
};
|
|
61
158
|
|
|
62
159
|
const toplevel = (cwd) => {
|
|
@@ -76,12 +173,16 @@ const isBinaryBuffer = (buf) => {
|
|
|
76
173
|
return buf.includes(0);
|
|
77
174
|
};
|
|
78
175
|
|
|
176
|
+
const namesFromLs = (stdout) => {
|
|
177
|
+
if (!stdout) return [];
|
|
178
|
+
return stdout.split('\0').filter(Boolean);
|
|
179
|
+
};
|
|
180
|
+
|
|
79
181
|
const listUntracked = (top, paths) => {
|
|
80
182
|
const args = ['ls-files', '-o', '--exclude-standard', '-z'];
|
|
81
183
|
if (paths.length) args.push('--', ...paths);
|
|
82
184
|
const result = requireOk(runGit(args, top));
|
|
83
|
-
|
|
84
|
-
return result.stdout.split('\0').filter(Boolean);
|
|
185
|
+
return namesFromLs(result.stdout);
|
|
85
186
|
};
|
|
86
187
|
|
|
87
188
|
const binaryNewFile = (rel) => ({
|
|
@@ -94,9 +195,9 @@ const binaryNewFile = (rel) => ({
|
|
|
94
195
|
hunks: [],
|
|
95
196
|
});
|
|
96
197
|
|
|
97
|
-
const
|
|
198
|
+
const untrackedItemsFrom = (top, names) => {
|
|
98
199
|
const files = [];
|
|
99
|
-
for (const rel of
|
|
200
|
+
for (const rel of names) {
|
|
100
201
|
if (isReviewFile(rel)) continue;
|
|
101
202
|
const full = path.join(top, rel);
|
|
102
203
|
let buf;
|
|
@@ -184,6 +285,18 @@ const runNpmJson = (top, args, timeout) => {
|
|
|
184
285
|
return result.stdout || '';
|
|
185
286
|
};
|
|
186
287
|
|
|
288
|
+
const runNpmJsonAsync = async (top, args, timeout, signal) => {
|
|
289
|
+
const result = await runProc('npm', args, {
|
|
290
|
+
cwd: top,
|
|
291
|
+
encoding: GIT_ENCODING,
|
|
292
|
+
maxBuffer: GIT_MAX_BUFFER,
|
|
293
|
+
timeout,
|
|
294
|
+
signal,
|
|
295
|
+
});
|
|
296
|
+
if (result.error) return '';
|
|
297
|
+
return result.stdout || '';
|
|
298
|
+
};
|
|
299
|
+
|
|
187
300
|
const collectAudit = (top) => {
|
|
188
301
|
const lock = path.join(top, LOCKFILE);
|
|
189
302
|
if (!fs.existsSync(lock)) return null;
|
|
@@ -200,30 +313,55 @@ const collectOutdated = (top) => {
|
|
|
200
313
|
return parseOutdatedReport(text);
|
|
201
314
|
};
|
|
202
315
|
|
|
316
|
+
const collectAuditAsync = async (top, extra, signal) => {
|
|
317
|
+
if (extra.auditMap !== undefined) return extra.auditMap;
|
|
318
|
+
const lock = path.join(top, LOCKFILE);
|
|
319
|
+
if (!fs.existsSync(lock)) return null;
|
|
320
|
+
const args = ['audit', '--json', '--package-lock-only'];
|
|
321
|
+
const text = await runNpmJsonAsync(top, args, AUDIT_MS, signal);
|
|
322
|
+
return parseAuditReport(text);
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
const collectOutdatedAsync = async (top, extra, signal) => {
|
|
326
|
+
if (extra.outdatedMap !== undefined) return extra.outdatedMap;
|
|
327
|
+
const manifest = path.join(top, MANIFEST);
|
|
328
|
+
if (!fs.existsSync(manifest)) return null;
|
|
329
|
+
const args = ['outdated', '--json', '--long'];
|
|
330
|
+
const text = await runNpmJsonAsync(top, args, AUDIT_MS, signal);
|
|
331
|
+
return parseOutdatedReport(text);
|
|
332
|
+
};
|
|
333
|
+
|
|
203
334
|
const pickNpmMap = (override, collect, top) => {
|
|
204
335
|
if (override !== undefined) return override;
|
|
205
336
|
return collect(top);
|
|
206
337
|
};
|
|
207
338
|
|
|
339
|
+
const hasDepFile = (items) => {
|
|
340
|
+
for (const item of items) {
|
|
341
|
+
if (depFileMeta(itemPath(item))) return true;
|
|
342
|
+
}
|
|
343
|
+
return false;
|
|
344
|
+
};
|
|
345
|
+
|
|
208
346
|
const foldLoaded = (items, top, rev, extra = {}) => {
|
|
209
347
|
const readSides = (origin, rel) => readDepSides(top, rev, origin, rel);
|
|
210
|
-
let usedNames = null;
|
|
211
|
-
let audit = null;
|
|
212
|
-
let outdated = null;
|
|
213
348
|
const live = extra.audit === true && !rev;
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
349
|
+
const hasDepDiff = hasDepFile(items);
|
|
350
|
+
const collect = extra.collect !== false;
|
|
351
|
+
let usedNames = extra.usedNames ?? null;
|
|
352
|
+
let audit = extra.auditMap;
|
|
353
|
+
let outdated = extra.outdatedMap;
|
|
354
|
+
if (collect && usedNames === null && (hasDepDiff || live)) {
|
|
355
|
+
usedNames = collectUsedNames(top);
|
|
356
|
+
}
|
|
357
|
+
if (collect && live) {
|
|
222
358
|
audit = pickNpmMap(extra.auditMap, collectAudit, top);
|
|
223
359
|
outdated = pickNpmMap(extra.outdatedMap, collectOutdated, top);
|
|
224
|
-
} else if (extra.audit && hasDepDiff) {
|
|
360
|
+
} else if (collect && extra.audit && hasDepDiff) {
|
|
225
361
|
audit = pickNpmMap(extra.auditMap, collectAudit, top);
|
|
226
362
|
}
|
|
363
|
+
if (audit === undefined) audit = null;
|
|
364
|
+
if (outdated === undefined) outdated = null;
|
|
227
365
|
const folded = foldDepItems(items, readSides, usedNames, audit, outdated, {
|
|
228
366
|
root: top,
|
|
229
367
|
});
|
|
@@ -408,40 +546,73 @@ const shortRev = (top, commit) => {
|
|
|
408
546
|
return result.stdout.trim();
|
|
409
547
|
};
|
|
410
548
|
|
|
411
|
-
const
|
|
412
|
-
const parent = runGit(['rev-parse', '--verify', `${commit}^`], top);
|
|
413
|
-
let args;
|
|
549
|
+
const commitDiffArgs = (commit, parent, pathArgs) => {
|
|
414
550
|
if (parent.status === 0) {
|
|
415
551
|
const from = parent.stdout.trim();
|
|
416
|
-
|
|
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
|
-
];
|
|
552
|
+
return [...GIT_CONFIG, 'diff', ...DIFF_OPTS, from, commit, ...pathArgs];
|
|
428
553
|
}
|
|
554
|
+
return [
|
|
555
|
+
...GIT_CONFIG,
|
|
556
|
+
'diff-tree',
|
|
557
|
+
'--no-commit-id',
|
|
558
|
+
'--root',
|
|
559
|
+
'-p',
|
|
560
|
+
...DIFF_OPTS,
|
|
561
|
+
commit,
|
|
562
|
+
...pathArgs,
|
|
563
|
+
];
|
|
564
|
+
};
|
|
565
|
+
|
|
566
|
+
const pathArgsOf = (paths) => (paths.length ? ['--', ...paths] : []);
|
|
567
|
+
|
|
568
|
+
const snapshotWorktree = (top, stagedOut, unstagedOut, names) => {
|
|
569
|
+
const stagedDiff = parseDiff(stagedOut);
|
|
570
|
+
const unstagedDiff = parseDiff(unstagedOut);
|
|
571
|
+
const staged = itemsFromFiles(stagedDiff, 'staged');
|
|
572
|
+
const unstaged = itemsFromFiles(unstagedDiff, 'unstaged');
|
|
573
|
+
const untracked = untrackedItemsFrom(top, names);
|
|
574
|
+
const combined = mergeItems([staged, unstaged, untracked]);
|
|
575
|
+
const parsed = dropReviewItems(combined);
|
|
576
|
+
return { top, parsed };
|
|
577
|
+
};
|
|
578
|
+
|
|
579
|
+
const snapshotCommit = (top, commit, files) => {
|
|
580
|
+
const commitItems = itemsFromFiles(files, 'commit');
|
|
581
|
+
const parsed = dropReviewItems(commitItems);
|
|
582
|
+
const revShort = shortRev(top, commit);
|
|
583
|
+
return { top, parsed, rev: commit, revShort };
|
|
584
|
+
};
|
|
585
|
+
|
|
586
|
+
const loadCommit = (top, commit, extra) => {
|
|
587
|
+
const parent = runGit(['rev-parse', '--verify', `${commit}^`], top);
|
|
588
|
+
const args = commitDiffArgs(commit, parent, extra);
|
|
429
589
|
const result = requireOk(runGit(args, top));
|
|
430
590
|
return parseDiff(result.stdout);
|
|
431
591
|
};
|
|
432
592
|
|
|
433
|
-
const
|
|
593
|
+
const needsExtras = (parsed, rev, extra) => {
|
|
594
|
+
const live = extra.audit === true && !rev;
|
|
595
|
+
if (live) return true;
|
|
596
|
+
if (!extra.audit) return false;
|
|
597
|
+
return hasDepFile(parsed);
|
|
598
|
+
};
|
|
599
|
+
|
|
600
|
+
const finishLoad = (snapshot, extra) => {
|
|
601
|
+
const options = { ...extra, paths: extra.paths ?? [] };
|
|
602
|
+
if (extra.deferExtras) options.collect = false;
|
|
603
|
+
const parsed = snapshot.parsed;
|
|
604
|
+
const top = snapshot.top;
|
|
605
|
+
const rev = snapshot.rev;
|
|
606
|
+
const items = foldLoaded(parsed, top, rev, options);
|
|
607
|
+
const pending = extra.deferExtras && needsExtras(parsed, rev, extra);
|
|
608
|
+
return { ...snapshot, items, pending };
|
|
609
|
+
};
|
|
610
|
+
|
|
611
|
+
const readSnapshotSync = (cwd, paths, options) => {
|
|
434
612
|
const top = toplevel(cwd);
|
|
435
|
-
const extra = paths
|
|
613
|
+
const extra = pathArgsOf(paths);
|
|
436
614
|
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
|
-
}
|
|
615
|
+
if (rev) return snapshotCommit(top, rev, loadCommit(top, rev, extra));
|
|
445
616
|
const stagedArgs = [
|
|
446
617
|
...GIT_CONFIG,
|
|
447
618
|
'diff',
|
|
@@ -452,15 +623,95 @@ const load = (cwd, paths = [], options = {}) => {
|
|
|
452
623
|
const unstagedArgs = [...GIT_CONFIG, 'diff', ...DIFF_OPTS, ...extra];
|
|
453
624
|
const stagedRes = requireOk(runGit(stagedArgs, top));
|
|
454
625
|
const unstagedRes = requireOk(runGit(unstagedArgs, top));
|
|
455
|
-
const
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
const
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
626
|
+
const names = listUntracked(top, paths);
|
|
627
|
+
return snapshotWorktree(top, stagedRes.stdout, unstagedRes.stdout, names);
|
|
628
|
+
};
|
|
629
|
+
|
|
630
|
+
const loadCommitAsync = async (top, commit, extra, signal) => {
|
|
631
|
+
const parent = await runGitAsync(
|
|
632
|
+
['rev-parse', '--verify', `${commit}^`],
|
|
633
|
+
top,
|
|
634
|
+
{ signal },
|
|
635
|
+
);
|
|
636
|
+
const args = commitDiffArgs(commit, parent, extra);
|
|
637
|
+
const result = requireOk(await runGitAsync(args, top, { signal }));
|
|
638
|
+
return parseDiff(result.stdout);
|
|
639
|
+
};
|
|
640
|
+
|
|
641
|
+
const readSnapshotAsync = async (cwd, paths, options) => {
|
|
642
|
+
const top = toplevel(cwd);
|
|
643
|
+
const extra = pathArgsOf(paths);
|
|
644
|
+
const rev = options.commit;
|
|
645
|
+
const signal = options.signal;
|
|
646
|
+
if (rev) {
|
|
647
|
+
const files = await loadCommitAsync(top, rev, extra, signal);
|
|
648
|
+
return snapshotCommit(top, rev, files);
|
|
649
|
+
}
|
|
650
|
+
const stagedArgs = [
|
|
651
|
+
...GIT_CONFIG,
|
|
652
|
+
'diff',
|
|
653
|
+
'--cached',
|
|
654
|
+
...DIFF_OPTS,
|
|
655
|
+
...extra,
|
|
656
|
+
];
|
|
657
|
+
const unstagedArgs = [...GIT_CONFIG, 'diff', ...DIFF_OPTS, ...extra];
|
|
658
|
+
const lsArgs = ['ls-files', '-o', '--exclude-standard', '-z'];
|
|
659
|
+
if (paths.length) lsArgs.push('--', ...paths);
|
|
660
|
+
const stagedP = runGitAsync(stagedArgs, top, { signal });
|
|
661
|
+
const unstagedP = runGitAsync(unstagedArgs, top, { signal });
|
|
662
|
+
const lsP = runGitAsync(lsArgs, top, { signal });
|
|
663
|
+
const stagedRes = requireOk(await stagedP);
|
|
664
|
+
const unstagedRes = requireOk(await unstagedP);
|
|
665
|
+
const names = namesFromLs(requireOk(await lsP).stdout);
|
|
666
|
+
return snapshotWorktree(top, stagedRes.stdout, unstagedRes.stdout, names);
|
|
667
|
+
};
|
|
668
|
+
|
|
669
|
+
const load = (cwd, paths = [], options = {}) => {
|
|
670
|
+
const snapshot = readSnapshotSync(cwd, paths, options);
|
|
671
|
+
return finishLoad(snapshot, { ...options, paths });
|
|
672
|
+
};
|
|
673
|
+
|
|
674
|
+
const loadAsync = async (cwd, paths = [], options = {}) => {
|
|
675
|
+
const snapshot = await readSnapshotAsync(cwd, paths, options);
|
|
676
|
+
return finishLoad(snapshot, { ...options, paths, deferExtras: true });
|
|
677
|
+
};
|
|
678
|
+
|
|
679
|
+
const usedNamesPromise = (parsed, rev, extra, top) => {
|
|
680
|
+
const live = extra.audit === true && !rev;
|
|
681
|
+
if (!live && !hasDepFile(parsed)) return Promise.resolve(null);
|
|
682
|
+
return Promise.resolve().then(() => collectUsedNames(top));
|
|
683
|
+
};
|
|
684
|
+
|
|
685
|
+
const loadExtras = async (snapshot, extra = {}) => {
|
|
686
|
+
const top = snapshot.top;
|
|
687
|
+
const rev = snapshot.rev;
|
|
688
|
+
const parsed = snapshot.parsed;
|
|
689
|
+
const signal = extra.signal;
|
|
690
|
+
const live = extra.audit === true && !rev;
|
|
691
|
+
const hasDepDiff = hasDepFile(parsed);
|
|
692
|
+
const usedP = usedNamesPromise(parsed, rev, extra, top);
|
|
693
|
+
let auditP = Promise.resolve(null);
|
|
694
|
+
let outdatedP = Promise.resolve(null);
|
|
695
|
+
if (live) {
|
|
696
|
+
auditP = collectAuditAsync(top, extra, signal);
|
|
697
|
+
outdatedP = collectOutdatedAsync(top, extra, signal);
|
|
698
|
+
} else if (extra.audit && hasDepDiff) {
|
|
699
|
+
auditP = collectAuditAsync(top, extra, signal);
|
|
700
|
+
}
|
|
701
|
+
const extras = await Promise.all([usedP, auditP, outdatedP]);
|
|
702
|
+
const usedNames = extras[0];
|
|
703
|
+
const audit = extras[1];
|
|
704
|
+
const outdated = extras[2];
|
|
705
|
+
if (signal && signal.aborted) throw abortError();
|
|
706
|
+
const folded = foldLoaded(parsed, top, rev, {
|
|
707
|
+
...extra,
|
|
708
|
+
paths: extra.paths ?? [],
|
|
709
|
+
usedNames,
|
|
710
|
+
auditMap: audit,
|
|
711
|
+
outdatedMap: outdated,
|
|
712
|
+
collect: false,
|
|
713
|
+
});
|
|
714
|
+
return { ...snapshot, items: folded, pending: false };
|
|
464
715
|
};
|
|
465
716
|
|
|
466
717
|
const addItem = (top, item) => {
|
|
@@ -594,6 +845,9 @@ const revertFile = (top, rel, items) => {
|
|
|
594
845
|
|
|
595
846
|
const createGitRepo = () => ({
|
|
596
847
|
load,
|
|
848
|
+
loadAsync,
|
|
849
|
+
loadExtras,
|
|
850
|
+
toplevel,
|
|
597
851
|
resolveRev,
|
|
598
852
|
add: addItem,
|
|
599
853
|
unstage: unstageItem,
|
|
@@ -603,6 +857,8 @@ const createGitRepo = () => ({
|
|
|
603
857
|
|
|
604
858
|
module.exports = {
|
|
605
859
|
load,
|
|
860
|
+
loadAsync,
|
|
861
|
+
loadExtras,
|
|
606
862
|
addItem,
|
|
607
863
|
unstageItem,
|
|
608
864
|
revertItem,
|