reslop 0.1.0 → 0.1.2
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 +25 -9
- package/README.md +25 -40
- package/bin/reslop.js +2 -2
- package/lib/ansi.js +0 -1
- package/lib/api.js +5 -0
- package/lib/cli.js +84 -79
- package/lib/clipboard.js +1 -1
- package/lib/deps.js +1440 -0
- package/lib/detect.js +4 -4
- package/lib/diff.js +40 -63
- package/lib/editor.js +1 -1
- package/lib/files.js +23 -4
- package/lib/git.js +367 -18
- package/lib/github.js +625 -0
- package/lib/highlight.js +149 -166
- package/lib/keys.js +34 -41
- package/lib/render.js +218 -143
- package/lib/review.js +52 -45
- package/lib/select.js +21 -31
- package/lib/session.js +298 -235
- package/lib/source.js +30 -0
- package/package.json +37 -18
package/lib/git.js
CHANGED
|
@@ -6,16 +6,24 @@ const path = require('node:path');
|
|
|
6
6
|
|
|
7
7
|
const diff = require('./diff.js');
|
|
8
8
|
const { parseDiff, synthesizeNewFile, itemsFromFiles } = diff;
|
|
9
|
-
const { itemPath } = require('./files.js');
|
|
10
|
-
const
|
|
9
|
+
const { itemPath, isPathInScope, REVIEW_DIR } = require('./files.js');
|
|
10
|
+
const deps = require('./deps.js');
|
|
11
|
+
const { foldDepItems, mergeDepFile, depFileMeta } = deps;
|
|
12
|
+
const { collectUsedNames, parseAuditReport, parseOutdatedReport } = deps;
|
|
13
|
+
const { mergeProposedItems, MANIFEST, LOCKFILE } = deps;
|
|
11
14
|
|
|
12
15
|
const GIT_CONFIG = ['-c', 'core.quotepath=false'];
|
|
13
16
|
const DIFF_OPTS = ['--no-color', '--no-ext-diff', '--no-renames', '-U3'];
|
|
14
17
|
const GIT_ENCODING = 'utf8';
|
|
15
18
|
const GIT_MAX_BUFFER = 32 * 1024 * 1024;
|
|
16
19
|
const BINARY_SIZE = 1000000;
|
|
20
|
+
const AUDIT_MS = 15000;
|
|
21
|
+
const INSTALL_MS = 120000;
|
|
17
22
|
|
|
18
|
-
const oneLine = (text) =>
|
|
23
|
+
const oneLine = (text) => {
|
|
24
|
+
const line = text.trim().split('\n')[0];
|
|
25
|
+
return line || 'git failed';
|
|
26
|
+
};
|
|
19
27
|
|
|
20
28
|
const requireOk = (result, fallback = 'git failed') => {
|
|
21
29
|
if (result.status === 0) return result;
|
|
@@ -40,13 +48,9 @@ const dropReviewItems = (items) => {
|
|
|
40
48
|
|
|
41
49
|
const runGit = (args, cwd, input) => {
|
|
42
50
|
const env = { ...process.env, GIT_OPTIONAL_LOCKS: '0' };
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
input,
|
|
47
|
-
maxBuffer: GIT_MAX_BUFFER,
|
|
48
|
-
env,
|
|
49
|
-
};
|
|
51
|
+
const encoding = GIT_ENCODING;
|
|
52
|
+
const maxBuffer = GIT_MAX_BUFFER;
|
|
53
|
+
const options = { cwd, encoding, input, maxBuffer, env };
|
|
50
54
|
const result = spawnSync('git', args, options);
|
|
51
55
|
if (result.error) {
|
|
52
56
|
if (result.error.code === 'ENOENT') throw new Error('git not found');
|
|
@@ -131,6 +135,264 @@ const mergeItems = (groups) => {
|
|
|
131
135
|
return items;
|
|
132
136
|
};
|
|
133
137
|
|
|
138
|
+
const gitText = (top, spec) => {
|
|
139
|
+
const result = runGit(['show', spec], top);
|
|
140
|
+
if (result.status !== 0) return '';
|
|
141
|
+
return result.stdout ?? '';
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const worktreeText = (top, rel) => {
|
|
145
|
+
try {
|
|
146
|
+
return fs.readFileSync(path.join(top, rel), 'utf8');
|
|
147
|
+
} catch {
|
|
148
|
+
return '';
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const readDepSides = (top, rev, origin, rel) => {
|
|
153
|
+
if (origin === 'untracked') {
|
|
154
|
+
const newText = worktreeText(top, rel);
|
|
155
|
+
return { oldText: '', newText };
|
|
156
|
+
}
|
|
157
|
+
if (origin === 'staged') {
|
|
158
|
+
const oldText = gitText(top, `HEAD:${rel}`);
|
|
159
|
+
const newText = gitText(top, `:${rel}`);
|
|
160
|
+
return { oldText, newText };
|
|
161
|
+
}
|
|
162
|
+
if (origin === 'unstaged') {
|
|
163
|
+
const indexed = gitText(top, `:${rel}`);
|
|
164
|
+
const oldText = indexed || gitText(top, `HEAD:${rel}`);
|
|
165
|
+
const newText = worktreeText(top, rel);
|
|
166
|
+
return { oldText, newText };
|
|
167
|
+
}
|
|
168
|
+
if (origin === 'commit' && rev) {
|
|
169
|
+
const oldText = gitText(top, `${rev}^:${rel}`);
|
|
170
|
+
const newText = gitText(top, `${rev}:${rel}`);
|
|
171
|
+
return { oldText, newText };
|
|
172
|
+
}
|
|
173
|
+
return { oldText: '', newText: '' };
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const runNpmJson = (top, args, timeout) => {
|
|
177
|
+
const result = spawnSync('npm', args, {
|
|
178
|
+
cwd: top,
|
|
179
|
+
encoding: GIT_ENCODING,
|
|
180
|
+
maxBuffer: GIT_MAX_BUFFER,
|
|
181
|
+
timeout,
|
|
182
|
+
});
|
|
183
|
+
if (result.error) return '';
|
|
184
|
+
return result.stdout || '';
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const collectAudit = (top) => {
|
|
188
|
+
const lock = path.join(top, LOCKFILE);
|
|
189
|
+
if (!fs.existsSync(lock)) return null;
|
|
190
|
+
const args = ['audit', '--json', '--package-lock-only'];
|
|
191
|
+
const text = runNpmJson(top, args, AUDIT_MS);
|
|
192
|
+
return parseAuditReport(text);
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const collectOutdated = (top) => {
|
|
196
|
+
const manifest = path.join(top, MANIFEST);
|
|
197
|
+
if (!fs.existsSync(manifest)) return null;
|
|
198
|
+
const args = ['outdated', '--json', '--long'];
|
|
199
|
+
const text = runNpmJson(top, args, AUDIT_MS);
|
|
200
|
+
return parseOutdatedReport(text);
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const pickNpmMap = (override, collect, top) => {
|
|
204
|
+
if (override !== undefined) return override;
|
|
205
|
+
return collect(top);
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const foldLoaded = (items, top, rev, extra = {}) => {
|
|
209
|
+
const readSides = (origin, rel) => readDepSides(top, rev, origin, rel);
|
|
210
|
+
let usedNames = null;
|
|
211
|
+
let audit = null;
|
|
212
|
+
let outdated = null;
|
|
213
|
+
const live = extra.audit === true && !rev;
|
|
214
|
+
let hasDepDiff = false;
|
|
215
|
+
for (const item of items) {
|
|
216
|
+
if (!depFileMeta(itemPath(item))) continue;
|
|
217
|
+
hasDepDiff = true;
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
if (hasDepDiff || live) usedNames = collectUsedNames(top);
|
|
221
|
+
if (live) {
|
|
222
|
+
audit = pickNpmMap(extra.auditMap, collectAudit, top);
|
|
223
|
+
outdated = pickNpmMap(extra.outdatedMap, collectOutdated, top);
|
|
224
|
+
} else if (extra.audit && hasDepDiff) {
|
|
225
|
+
audit = pickNpmMap(extra.auditMap, collectAudit, top);
|
|
226
|
+
}
|
|
227
|
+
const folded = foldDepItems(items, readSides, usedNames, audit, outdated, {
|
|
228
|
+
root: top,
|
|
229
|
+
});
|
|
230
|
+
if (!live) return folded;
|
|
231
|
+
if (!isPathInScope(MANIFEST, extra.paths ?? [])) return folded;
|
|
232
|
+
const pkgText = worktreeText(top, MANIFEST);
|
|
233
|
+
const lockText = worktreeText(top, LOCKFILE);
|
|
234
|
+
return mergeProposedItems(folded, pkgText, outdated, audit, {
|
|
235
|
+
lockText,
|
|
236
|
+
usedNames,
|
|
237
|
+
root: top,
|
|
238
|
+
});
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
const depPaths = (item) => item.dep?.files ?? [];
|
|
242
|
+
|
|
243
|
+
const origDepItems = (item) => item.dep?.items ?? [];
|
|
244
|
+
|
|
245
|
+
const depChange = (item) => item.dep?.change ?? null;
|
|
246
|
+
|
|
247
|
+
const writeWorktreeFile = (top, rel, text) => {
|
|
248
|
+
const abs = path.join(top, rel);
|
|
249
|
+
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
250
|
+
fs.writeFileSync(abs, text);
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const restoreWorktreeFile = (top, rel, saved, existed) => {
|
|
254
|
+
const abs = path.join(top, rel);
|
|
255
|
+
if (existed) {
|
|
256
|
+
fs.writeFileSync(abs, saved);
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
try {
|
|
260
|
+
fs.unlinkSync(abs);
|
|
261
|
+
} catch {
|
|
262
|
+
// missing
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
const writeIndexFile = (top, rel, text) => {
|
|
267
|
+
const abs = path.join(top, rel);
|
|
268
|
+
const existed = fs.existsSync(abs);
|
|
269
|
+
const saved = existed ? fs.readFileSync(abs, 'utf8') : '';
|
|
270
|
+
writeWorktreeFile(top, rel, text);
|
|
271
|
+
try {
|
|
272
|
+
requireOk(runGit(['add', '--', rel], top));
|
|
273
|
+
} finally {
|
|
274
|
+
restoreWorktreeFile(top, rel, saved, existed);
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
const mergeDepLive = (top, rel, change, origin, base, side, kind) => {
|
|
279
|
+
const sides = readDepSides(top, null, origin, rel);
|
|
280
|
+
return mergeDepFile(base, sides.oldText, sides.newText, change, side, kind);
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
const applyDepIndex = (top, rel, change, origin, side) => {
|
|
284
|
+
const meta = depFileMeta(rel);
|
|
285
|
+
if (!meta) return;
|
|
286
|
+
const indexed = gitText(top, `:${rel}`) || gitText(top, `HEAD:${rel}`);
|
|
287
|
+
const text = mergeDepLive(top, rel, change, origin, indexed, side, meta.kind);
|
|
288
|
+
writeIndexFile(top, rel, text);
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
const applyDepWorktree = (top, rel, change, origin) => {
|
|
292
|
+
const meta = depFileMeta(rel);
|
|
293
|
+
if (!meta) return;
|
|
294
|
+
const worktree = worktreeText(top, rel);
|
|
295
|
+
const text = mergeDepLive(
|
|
296
|
+
top,
|
|
297
|
+
rel,
|
|
298
|
+
change,
|
|
299
|
+
origin,
|
|
300
|
+
worktree,
|
|
301
|
+
'old',
|
|
302
|
+
meta.kind,
|
|
303
|
+
);
|
|
304
|
+
writeWorktreeFile(top, rel, text);
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
const runNpm = (cwd, args) =>
|
|
308
|
+
spawnSync('npm', args, {
|
|
309
|
+
cwd,
|
|
310
|
+
encoding: GIT_ENCODING,
|
|
311
|
+
maxBuffer: GIT_MAX_BUFFER,
|
|
312
|
+
timeout: INSTALL_MS,
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
const firstDepRel = (files, kind) => {
|
|
316
|
+
for (const rel of files) {
|
|
317
|
+
const meta = depFileMeta(rel);
|
|
318
|
+
if (meta && meta.kind === kind) return rel;
|
|
319
|
+
}
|
|
320
|
+
return '';
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
const applyProposedUpdate = (top, item) => {
|
|
324
|
+
const change = depChange(item);
|
|
325
|
+
const origin = item.origin;
|
|
326
|
+
const files = depPaths(item);
|
|
327
|
+
const lockOnly = change.section === 'resolved';
|
|
328
|
+
const removing = !!change.unused;
|
|
329
|
+
const manifest = firstDepRel(files, 'manifest');
|
|
330
|
+
const lockRel = firstDepRel(files, 'lockfile');
|
|
331
|
+
const picked = lockOnly ? lockRel : manifest;
|
|
332
|
+
const target = picked || files[0] || '';
|
|
333
|
+
const meta = depFileMeta(target);
|
|
334
|
+
if (!meta) return;
|
|
335
|
+
let saved = '';
|
|
336
|
+
if (!lockOnly && !removing && manifest) {
|
|
337
|
+
const worktree = worktreeText(top, manifest);
|
|
338
|
+
saved = worktree;
|
|
339
|
+
const text = mergeDepLive(
|
|
340
|
+
top,
|
|
341
|
+
manifest,
|
|
342
|
+
change,
|
|
343
|
+
origin,
|
|
344
|
+
worktree,
|
|
345
|
+
'new',
|
|
346
|
+
'manifest',
|
|
347
|
+
);
|
|
348
|
+
writeWorktreeFile(top, manifest, text);
|
|
349
|
+
}
|
|
350
|
+
const cwd = meta.dir === '.' ? top : path.join(top, meta.dir);
|
|
351
|
+
let npmArgs = ['i'];
|
|
352
|
+
if (removing) npmArgs = ['uninstall', change.name];
|
|
353
|
+
else if (lockOnly) npmArgs = ['audit', 'fix'];
|
|
354
|
+
const install = item.dep.install ?? ((dir) => runNpm(dir, npmArgs));
|
|
355
|
+
const result = install(cwd);
|
|
356
|
+
const fail = removing ? 'npm uninstall failed' : 'npm i failed';
|
|
357
|
+
if (result && (result.error || result.status !== 0)) {
|
|
358
|
+
if (saved && manifest) writeWorktreeFile(top, manifest, saved);
|
|
359
|
+
if (result.error) {
|
|
360
|
+
const msg = result.error.message || fail;
|
|
361
|
+
throw new Error(oneLine(msg));
|
|
362
|
+
}
|
|
363
|
+
const msg = result.stderr || result.stdout || fail;
|
|
364
|
+
throw new Error(oneLine(msg));
|
|
365
|
+
}
|
|
366
|
+
const toAdd = [];
|
|
367
|
+
if (!lockOnly && manifest) toAdd.push(manifest);
|
|
368
|
+
const dirLock = meta.dir === '.' ? LOCKFILE : `${meta.dir}/${LOCKFILE}`;
|
|
369
|
+
const lockPath = lockRel || dirLock;
|
|
370
|
+
if (fs.existsSync(path.join(top, lockPath))) toAdd.push(lockPath);
|
|
371
|
+
if (toAdd.length) requireOk(runGit(['add', '--', ...toAdd], top));
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
const applyDepChange = (top, item, mode) => {
|
|
375
|
+
const change = depChange(item);
|
|
376
|
+
if (change && change.propose) {
|
|
377
|
+
if (mode === 'add') return void applyProposedUpdate(top, item);
|
|
378
|
+
if (!change.unused) return;
|
|
379
|
+
}
|
|
380
|
+
const origin = item.origin;
|
|
381
|
+
const files = depPaths(item);
|
|
382
|
+
for (const rel of files) {
|
|
383
|
+
if (mode === 'add') {
|
|
384
|
+
applyDepIndex(top, rel, change, origin, 'new');
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
if (mode === 'unstage') {
|
|
388
|
+
applyDepIndex(top, rel, change, origin, 'old');
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
if (origin === 'staged') applyDepIndex(top, rel, change, origin, 'old');
|
|
392
|
+
applyDepWorktree(top, rel, change, origin);
|
|
393
|
+
}
|
|
394
|
+
};
|
|
395
|
+
|
|
134
396
|
const resolveRev = (cwd, spec) => {
|
|
135
397
|
if (!spec) return null;
|
|
136
398
|
const top = toplevel(cwd);
|
|
@@ -174,7 +436,9 @@ const load = (cwd, paths = [], options = {}) => {
|
|
|
174
436
|
const rev = options.commit;
|
|
175
437
|
if (rev) {
|
|
176
438
|
const files = loadCommit(top, rev, extra);
|
|
177
|
-
const
|
|
439
|
+
const commitItems = itemsFromFiles(files, 'commit');
|
|
440
|
+
const parsed = dropReviewItems(commitItems);
|
|
441
|
+
const items = foldLoaded(parsed, top, rev, { ...options, paths });
|
|
178
442
|
const revShort = shortRev(top, rev);
|
|
179
443
|
return { top, items, rev, revShort };
|
|
180
444
|
}
|
|
@@ -188,15 +452,28 @@ const load = (cwd, paths = [], options = {}) => {
|
|
|
188
452
|
const unstagedArgs = [...GIT_CONFIG, 'diff', ...DIFF_OPTS, ...extra];
|
|
189
453
|
const stagedRes = requireOk(runGit(stagedArgs, top));
|
|
190
454
|
const unstagedRes = requireOk(runGit(unstagedArgs, top));
|
|
191
|
-
const
|
|
192
|
-
const
|
|
455
|
+
const stagedDiff = parseDiff(stagedRes.stdout);
|
|
456
|
+
const unstagedDiff = parseDiff(unstagedRes.stdout);
|
|
457
|
+
const staged = itemsFromFiles(stagedDiff, 'staged');
|
|
458
|
+
const unstaged = itemsFromFiles(unstagedDiff, 'unstaged');
|
|
193
459
|
const untracked = untrackedItems(top, paths);
|
|
194
|
-
const
|
|
460
|
+
const combined = mergeItems([staged, unstaged, untracked]);
|
|
461
|
+
const merged = dropReviewItems(combined);
|
|
462
|
+
const items = foldLoaded(merged, top, undefined, { ...options, paths });
|
|
195
463
|
return { top, items };
|
|
196
464
|
};
|
|
197
465
|
|
|
198
466
|
const addItem = (top, item) => {
|
|
199
467
|
if (item.origin === 'staged' || item.origin === 'commit') return;
|
|
468
|
+
if (depChange(item)) {
|
|
469
|
+
applyDepChange(top, item, 'add');
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
const files = depPaths(item);
|
|
473
|
+
if (files.length) {
|
|
474
|
+
requireOk(runGit(['add', '--', ...files], top));
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
200
477
|
if (item.origin === 'untracked' || item.file.isBinary) {
|
|
201
478
|
requireOk(runGit(['add', '--', item.file.newPath], top));
|
|
202
479
|
return;
|
|
@@ -206,6 +483,15 @@ const addItem = (top, item) => {
|
|
|
206
483
|
|
|
207
484
|
const unstageItem = (top, item) => {
|
|
208
485
|
if (item.origin !== 'staged') return;
|
|
486
|
+
if (depChange(item)) {
|
|
487
|
+
applyDepChange(top, item, 'unstage');
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
const files = depPaths(item);
|
|
491
|
+
if (files.length) {
|
|
492
|
+
requireOk(runGit(['restore', '--staged', '--', ...files], top));
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
209
495
|
const rel = itemPath(item);
|
|
210
496
|
if (item.file.isBinary) {
|
|
211
497
|
requireOk(runGit(['restore', '--staged', '--', rel], top));
|
|
@@ -214,8 +500,62 @@ const unstageItem = (top, item) => {
|
|
|
214
500
|
applyPatch(top, item.patchAdd, ['--reverse', '--cached']);
|
|
215
501
|
};
|
|
216
502
|
|
|
503
|
+
const revertOnePath = (top, rel, items) => {
|
|
504
|
+
const origins = new Set();
|
|
505
|
+
let isNew = false;
|
|
506
|
+
for (const item of items) {
|
|
507
|
+
origins.add(item.origin);
|
|
508
|
+
if (item.file && item.file.isNew) isNew = true;
|
|
509
|
+
}
|
|
510
|
+
if (isNew) {
|
|
511
|
+
if (origins.has('staged')) {
|
|
512
|
+
requireOk(runGit(['rm', '-f', '--', rel], top));
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
fs.unlinkSync(path.join(top, rel));
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
if (origins.has('untracked') && origins.size === 1) {
|
|
519
|
+
fs.unlinkSync(path.join(top, rel));
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
const args = ['restore', '-s', 'HEAD', '--worktree', '--staged', '--', rel];
|
|
523
|
+
requireOk(runGit(args, top));
|
|
524
|
+
};
|
|
525
|
+
|
|
526
|
+
const itemsForPath = (rel, items) => {
|
|
527
|
+
const group = [];
|
|
528
|
+
for (const item of items) {
|
|
529
|
+
if (item.dep) {
|
|
530
|
+
for (const orig of origDepItems(item)) {
|
|
531
|
+
if (itemPath(orig) === rel) group.push(orig);
|
|
532
|
+
}
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
if (itemPath(item) === rel) group.push(item);
|
|
536
|
+
}
|
|
537
|
+
return group;
|
|
538
|
+
};
|
|
539
|
+
|
|
217
540
|
const revertItem = (top, item) => {
|
|
218
541
|
if (item.origin === 'commit') return;
|
|
542
|
+
if (depChange(item)) {
|
|
543
|
+
applyDepChange(top, item, 'revert');
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
if (item.dep) {
|
|
547
|
+
const byPath = new Map();
|
|
548
|
+
for (const orig of origDepItems(item)) {
|
|
549
|
+
const rel = itemPath(orig);
|
|
550
|
+
const list = byPath.get(rel);
|
|
551
|
+
if (list) list.push(orig);
|
|
552
|
+
else byPath.set(rel, [orig]);
|
|
553
|
+
}
|
|
554
|
+
for (const rel of byPath.keys()) {
|
|
555
|
+
revertOnePath(top, rel, byPath.get(rel));
|
|
556
|
+
}
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
219
559
|
const rel = itemPath(item);
|
|
220
560
|
if (item.origin === 'untracked') {
|
|
221
561
|
fs.unlinkSync(path.join(top, rel));
|
|
@@ -240,22 +580,31 @@ const revertItem = (top, item) => {
|
|
|
240
580
|
}
|
|
241
581
|
};
|
|
242
582
|
|
|
583
|
+
const revertFile = (top, rel, items) => {
|
|
584
|
+
const paths = new Set([rel]);
|
|
585
|
+
for (const item of items) {
|
|
586
|
+
for (const file of depPaths(item)) paths.add(file);
|
|
587
|
+
}
|
|
588
|
+
for (const file of paths) {
|
|
589
|
+
const group = itemsForPath(file, items);
|
|
590
|
+
const targets = group.length ? group : items;
|
|
591
|
+
revertOnePath(top, file, targets);
|
|
592
|
+
}
|
|
593
|
+
};
|
|
594
|
+
|
|
243
595
|
const createGitRepo = () => ({
|
|
244
596
|
load,
|
|
245
597
|
resolveRev,
|
|
246
598
|
add: addItem,
|
|
247
599
|
unstage: unstageItem,
|
|
248
600
|
revert: revertItem,
|
|
601
|
+
revertFile,
|
|
249
602
|
});
|
|
250
603
|
|
|
251
604
|
module.exports = {
|
|
252
|
-
runGit,
|
|
253
|
-
toplevel,
|
|
254
605
|
load,
|
|
255
|
-
resolveRev,
|
|
256
606
|
addItem,
|
|
257
607
|
unstageItem,
|
|
258
608
|
revertItem,
|
|
259
609
|
createGitRepo,
|
|
260
|
-
oneLine,
|
|
261
610
|
};
|