reslop 0.2.3 → 0.2.5

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 (89) hide show
  1. package/CHANGELOG.md +27 -1
  2. package/README.md +8 -4
  3. package/bin/reslop.js +4 -2
  4. package/lib/api.js +2 -1
  5. package/lib/capabilities.js +0 -4
  6. package/lib/cli.js +19 -10
  7. package/lib/clipboard.js +45 -4
  8. package/lib/deps-diff.js +61 -118
  9. package/lib/deps.js +166 -235
  10. package/lib/detect.js +4 -2
  11. package/lib/diff/align.js +2 -1
  12. package/lib/diff/diff.js +6 -3
  13. package/lib/diff/display.js +2 -1
  14. package/lib/diff/inline.js +68 -2
  15. package/lib/diff/patch.js +2 -1
  16. package/lib/diff/unit.js +15 -30
  17. package/lib/editor.js +91 -13
  18. package/lib/files.js +54 -27
  19. package/lib/git-branches.js +107 -154
  20. package/lib/git-deps.js +78 -155
  21. package/lib/git-worktree.js +508 -0
  22. package/lib/git.js +23 -580
  23. package/lib/github.js +9 -8
  24. package/lib/gitlab.js +5 -4
  25. package/lib/highlight/bash.js +2 -1
  26. package/lib/highlight/core.js +11 -0
  27. package/lib/highlight/css.js +2 -1
  28. package/lib/highlight/csv.js +2 -1
  29. package/lib/highlight/dot.js +3 -12
  30. package/lib/highlight/html.js +2 -1
  31. package/lib/highlight/json.js +2 -1
  32. package/lib/highlight/log.js +5 -11
  33. package/lib/highlight/txt.js +3 -12
  34. package/lib/highlight.js +2 -1
  35. package/lib/keys.js +22 -12
  36. package/lib/manifest.js +4 -6
  37. package/lib/npm-commands.js +82 -24
  38. package/lib/remote.js +21 -20
  39. package/lib/render/branches.js +9 -27
  40. package/lib/render/chrome.js +109 -35
  41. package/lib/render/commits.js +216 -216
  42. package/lib/render/diff.js +160 -47
  43. package/lib/render/files.js +9 -26
  44. package/lib/render/frame.js +97 -118
  45. package/lib/render/notes.js +74 -26
  46. package/lib/render/npm.js +104 -30
  47. package/lib/render/present.js +2 -1
  48. package/lib/render/primitives.js +128 -52
  49. package/lib/render/render.js +2 -1
  50. package/lib/render/unit.js +17 -11
  51. package/lib/report-model.js +6 -4
  52. package/lib/report-parse.js +8 -14
  53. package/lib/report-render.js +38 -21
  54. package/lib/report-run.js +12 -11
  55. package/lib/report-test.js +45 -51
  56. package/lib/report-tools.js +44 -25
  57. package/lib/review-merge.js +102 -131
  58. package/lib/review.js +69 -78
  59. package/lib/select.js +52 -0
  60. package/lib/session/actions.js +72 -87
  61. package/lib/session/branches.js +18 -52
  62. package/lib/session/change-actions.js +175 -147
  63. package/lib/session/commit.js +65 -61
  64. package/lib/session/compose.js +152 -83
  65. package/lib/session/dispatch.js +53 -46
  66. package/lib/session/edit-click.js +57 -0
  67. package/lib/session/files-pane.js +30 -51
  68. package/lib/session/items.js +73 -90
  69. package/lib/session/lifecycle.js +37 -98
  70. package/lib/session/load.js +52 -57
  71. package/lib/session/navigation.js +105 -95
  72. package/lib/session/npm.js +205 -71
  73. package/lib/session/ops.js +200 -45
  74. package/lib/session/pointer.js +33 -8
  75. package/lib/session/progress.js +9 -6
  76. package/lib/session/review.js +35 -53
  77. package/lib/session/tasks.js +292 -0
  78. package/lib/session/terminal.js +17 -19
  79. package/lib/session/unit.js +3 -4
  80. package/lib/session/update.js +76 -96
  81. package/lib/session/view.js +45 -16
  82. package/lib/session/watch.js +47 -27
  83. package/lib/session.js +66 -97
  84. package/lib/source.js +6 -3
  85. package/lib/update.js +5 -13
  86. package/lib/utilities.js +25 -3
  87. package/lib/wrap.js +18 -1
  88. package/package.json +1 -1
  89. package/lib/session/todos.js +0 -293
@@ -4,7 +4,10 @@ const fs = require('node:fs');
4
4
  const os = require('node:os');
5
5
  const path = require('node:path');
6
6
 
7
- const { oneLine, requireOk, runGit, runGitAsync } = require('./git.js');
7
+ const files = require('./files.js');
8
+ const gitWorktree = require('./git-worktree.js');
9
+ const { shortAge } = files;
10
+ const { oneLine, requireOk, runGit, runGitAsync } = gitWorktree;
8
11
 
9
12
  const BRANCH_FORMAT = [
10
13
  '%(HEAD)',
@@ -72,27 +75,56 @@ const parseCommitRecord = (record, index) => {
72
75
  shortSha: read('%h') || sha.slice(0, 7),
73
76
  author: read('%an'),
74
77
  email: read('%ae'),
75
- date: read('%ar'),
78
+ date: shortAge(read('%ar')),
76
79
  when: read('%ci'),
77
80
  refs: formatCommitRefs(read('%D')),
78
81
  subject,
79
82
  body,
83
+ added: 0,
84
+ removed: 0,
80
85
  head: index === 0,
81
86
  };
82
87
  };
83
88
 
89
+ const STAT_LEAD = /^\n* *\d+ files? changed[^\n]*\n?/;
90
+
91
+ const takeStat = (chunk) => {
92
+ const match = STAT_LEAD.exec(chunk);
93
+ if (!match) return { stat: '', rest: chunk };
94
+ return { stat: match[0], rest: chunk.slice(match[0].length) };
95
+ };
96
+
97
+ const countIn = (line, word) => {
98
+ const match = new RegExp(`(\\d+) ${word}`).exec(line);
99
+ if (!match) return 0;
100
+ return parseInt(match[1], 10);
101
+ };
102
+
103
+ const parseShortstat = (line) => ({
104
+ added: countIn(line, 'insertion'),
105
+ removed: countIn(line, 'deletion'),
106
+ });
107
+
84
108
  const listCommits = (top) => {
85
109
  const args = [
86
110
  'log',
87
111
  '-z',
112
+ '--shortstat',
88
113
  `--format=${COMMIT_FORMAT}`,
89
114
  `--max-count=${COMMIT_LIMIT}`,
90
115
  ];
91
116
  const result = requireOk(runGit(args, top), 'log failed');
92
117
  const commits = [];
93
- for (const record of result.stdout.split('\0')) {
94
- if (!record) continue;
95
- const entry = parseCommitRecord(record, commits.length);
118
+ for (const chunk of result.stdout.split('\0')) {
119
+ const taken = takeStat(chunk);
120
+ if (taken.stat && commits.length) {
121
+ const stat = parseShortstat(taken.stat);
122
+ const last = commits[commits.length - 1];
123
+ last.added = stat.added;
124
+ last.removed = stat.removed;
125
+ }
126
+ if (!taken.rest) continue;
127
+ const entry = parseCommitRecord(taken.rest, commits.length);
96
128
  if (entry) commits.push(entry);
97
129
  }
98
130
  return commits;
@@ -113,48 +145,40 @@ const dropHeadCommit = (top) => {
113
145
  requireOk(runGit(['reset', '--soft', 'HEAD~1'], top), 'drop failed');
114
146
  };
115
147
 
116
- const rebaseFailed = (result) => {
117
- if (result.status === 0) return '';
118
- return result.stderr || result.stdout || 'rebase failed';
119
- };
120
-
121
- const dropOlderCommit = (top, sha) => {
122
- const onto = `${sha}^`;
123
- const result = runGit(['rebase', '--onto', onto, sha], top);
124
- const failed = rebaseFailed(result);
125
- if (!failed) return;
148
+ const runRebase = (top, args, extra = {}) => {
149
+ const result = runGit(args, top, undefined, extra);
150
+ if (result.status === 0) return;
126
151
  runGit(['rebase', '--abort'], top);
127
- throw new Error(oneLine(failed));
152
+ requireOk(result, 'rebase failed');
128
153
  };
129
154
 
130
- const dropOlderCommitAsync = async (top, sha) => {
131
- const onto = `${sha}^`;
132
- const result = await runGitAsync(['rebase', '--onto', onto, sha], top);
133
- const failed = rebaseFailed(result);
134
- if (!failed) return;
155
+ const runRebaseAsync = async (top, args, extra = {}) => {
156
+ const result = await runGitAsync(args, top, extra);
157
+ if (result.status === 0) return;
135
158
  await runGitAsync(['rebase', '--abort'], top);
136
- throw new Error(oneLine(failed));
159
+ requireOk(result, 'rebase failed');
137
160
  };
138
161
 
139
- const dropCommit = (top, sha) => {
140
- const spec = namedCommit(sha);
141
- const target = resolveCommit(top, spec);
162
+ const dropTarget = (top, sha) => {
163
+ const target = resolveCommit(top, namedCommit(sha));
142
164
  const parent = runGit(['rev-parse', '--verify', `${target}^`], top);
143
165
  if (parent.status !== 0) throw new Error('cannot drop root commit');
144
- const head = resolveCommit(top, 'HEAD');
145
- if (target === head) return void dropHeadCommit(top);
146
- dropOlderCommit(top, target);
166
+ return target;
167
+ };
168
+
169
+ const dropCommit = (top, sha) => {
170
+ const target = dropTarget(top, sha);
171
+ if (target === resolveCommit(top, 'HEAD')) return void dropHeadCommit(top);
172
+ runRebase(top, ['rebase', '--onto', `${target}^`, target]);
147
173
  };
148
174
 
149
175
  const dropCommitAsync = async (top, sha) => {
150
- const spec = namedCommit(sha);
151
- const target = resolveCommit(top, spec);
152
- const parentArgs = ['rev-parse', '--verify', `${target}^`];
153
- const parent = await runGitAsync(parentArgs, top);
154
- if (parent.status !== 0) throw new Error('cannot drop root commit');
155
- const head = resolveCommit(top, 'HEAD');
156
- if (target === head) return void dropHeadCommit(top);
157
- await dropOlderCommitAsync(top, target);
176
+ const target = dropTarget(top, sha);
177
+ if (target === resolveCommit(top, 'HEAD')) {
178
+ requireOk(await runGitAsync(['reset', '--soft', 'HEAD~1'], top));
179
+ return;
180
+ }
181
+ await runRebaseAsync(top, ['rebase', '--onto', `${target}^`, target]);
158
182
  };
159
183
 
160
184
  const FIXUP_MARK = 'fixup!';
@@ -197,61 +221,33 @@ const ancestorWithSubject = (top, sha, want) => {
197
221
  return '';
198
222
  };
199
223
 
200
- const applyAutosquash = (top, ontoArgs) => {
201
- const result = runGit([...AUTOSQUASH_ARGS, ...ontoArgs], top);
202
- const failed = rebaseFailed(result);
203
- if (!failed) return;
204
- runGit(['rebase', '--abort'], top);
205
- throw new Error(oneLine(failed));
206
- };
207
-
208
- const applyAutosquashAsync = async (top, ontoArgs) => {
209
- const result = await runGitAsync([...AUTOSQUASH_ARGS, ...ontoArgs], top);
210
- const failed = rebaseFailed(result);
211
- if (!failed) return;
212
- await runGitAsync(['rebase', '--abort'], top);
213
- throw new Error(oneLine(failed));
214
- };
215
-
216
- const applyFixupOnto = (top, original, applyRange) => {
217
- const parent = runGit(['rev-parse', '--verify', `${original}^`], top);
218
- if (parent.status !== 0) return applyRange(top, ['--root']);
219
- return applyRange(top, [(parent.stdout ?? '').trim()]);
224
+ const rebaseRange = (top, sha) => {
225
+ const parent = runGit(['rev-parse', '--verify', `${sha}^`], top);
226
+ return parent.status === 0 ? [parent.stdout.trim()] : ['--root'];
220
227
  };
221
228
 
222
- const applyFixup = (top, sha) => {
223
- const spec = namedCommit(sha);
224
- const commit = resolveCommit(top, spec);
229
+ const fixupArgs = (top, sha) => {
230
+ const commit = resolveCommit(top, namedCommit(sha));
225
231
  const subject = commitSubject(top, commit);
226
232
  if (!isFixupSubject(subject)) throw new Error('not a fixup commit');
227
233
  const want = fixupRest(subject);
228
234
  if (!want) throw new Error('no fixup target');
229
235
  const original = ancestorWithSubject(top, commit, want);
230
236
  if (!original) throw new Error('no fixup target');
231
- applyFixupOnto(top, original, applyAutosquash);
237
+ return [...AUTOSQUASH_ARGS, ...rebaseRange(top, original)];
232
238
  };
233
239
 
234
- const applyFixupAsync = async (top, sha) => {
235
- const spec = namedCommit(sha);
236
- const commit = resolveCommit(top, spec);
237
- const subject = commitSubject(top, commit);
238
- if (!isFixupSubject(subject)) throw new Error('not a fixup commit');
239
- const want = fixupRest(subject);
240
- if (!want) throw new Error('no fixup target');
241
- const original = ancestorWithSubject(top, commit, want);
242
- if (!original) throw new Error('no fixup target');
243
- await applyFixupOnto(top, original, applyAutosquashAsync);
244
- };
240
+ const applyFixup = (top, sha) => runRebase(top, fixupArgs(top, sha));
241
+
242
+ const applyFixupAsync = async (top, sha) =>
243
+ runRebaseAsync(top, fixupArgs(top, sha));
245
244
 
246
245
  const messageBody = (message) => {
247
246
  const text = `${message ?? ''}`;
248
247
  return text.endsWith('\n') ? text : `${text}\n`;
249
248
  };
250
249
 
251
- const rewordHead = (top, message) => {
252
- const args = ['commit', '--amend', '--only', '-F', '-'];
253
- requireOk(runGit(args, top, messageBody(message)));
254
- };
250
+ const REWORD_HEAD_ARGS = ['commit', '--amend', '--only', '-F', '-'];
255
251
 
256
252
  const REWORD_TODO = path.join(__dirname, 'git-reword-todo.js');
257
253
 
@@ -270,32 +266,11 @@ const prepareRewordEditor = (message) => {
270
266
  };
271
267
  };
272
268
 
273
- const rewordRange = (top, sha, env) => {
274
- const parent = runGit(['rev-parse', '--verify', `${sha}^`], top);
275
- const onto =
276
- parent.status !== 0 ? ['--root'] : [(parent.stdout ?? '').trim()];
277
- return runGit(['rebase', '--interactive', ...onto], top, undefined, { env });
278
- };
279
-
280
- const rewordRangeAsync = async (top, sha, env) => {
281
- const parent = runGit(['rev-parse', '--verify', `${sha}^`], top);
282
- const onto =
283
- parent.status !== 0 ? ['--root'] : [(parent.stdout ?? '').trim()];
284
- return runGitAsync(['rebase', '--interactive', ...onto], top, { env });
285
- };
286
-
287
- const finishRewordRebase = (top, result) => {
288
- const failed = rebaseFailed(result);
289
- if (!failed) return;
290
- runGit(['rebase', '--abort'], top);
291
- throw new Error(oneLine(failed));
292
- };
293
-
294
269
  const rewordOlder = (top, sha, message) => {
295
270
  const prepared = prepareRewordEditor(message);
296
271
  try {
297
- const result = rewordRange(top, sha, prepared.env);
298
- finishRewordRebase(top, result);
272
+ const args = ['rebase', '--interactive', ...rebaseRange(top, sha)];
273
+ runRebase(top, args, { env: prepared.env });
299
274
  } finally {
300
275
  fs.rmSync(prepared.dir, { recursive: true, force: true });
301
276
  }
@@ -304,29 +279,29 @@ const rewordOlder = (top, sha, message) => {
304
279
  const rewordOlderAsync = async (top, sha, message) => {
305
280
  const prepared = prepareRewordEditor(message);
306
281
  try {
307
- const result = await rewordRangeAsync(top, sha, prepared.env);
308
- const failed = rebaseFailed(result);
309
- if (!failed) return;
310
- await runGitAsync(['rebase', '--abort'], top);
311
- throw new Error(oneLine(failed));
282
+ const args = ['rebase', '--interactive', ...rebaseRange(top, sha)];
283
+ await runRebaseAsync(top, args, { env: prepared.env });
312
284
  } finally {
313
285
  fs.rmSync(prepared.dir, { recursive: true, force: true });
314
286
  }
315
287
  };
316
288
 
317
289
  const rewordCommit = (top, sha, message) => {
318
- const spec = namedCommit(sha);
319
- const commit = resolveCommit(top, spec);
320
- const head = resolveCommit(top, 'HEAD');
321
- if (commit === head) return void rewordHead(top, message);
290
+ const commit = resolveCommit(top, namedCommit(sha));
291
+ if (commit === resolveCommit(top, 'HEAD')) {
292
+ requireOk(runGit(REWORD_HEAD_ARGS, top, messageBody(message)));
293
+ return;
294
+ }
322
295
  rewordOlder(top, commit, message);
323
296
  };
324
297
 
325
298
  const rewordCommitAsync = async (top, sha, message) => {
326
- const spec = namedCommit(sha);
327
- const commit = resolveCommit(top, spec);
328
- const head = resolveCommit(top, 'HEAD');
329
- if (commit === head) return void rewordHead(top, message);
299
+ const commit = resolveCommit(top, namedCommit(sha));
300
+ if (commit === resolveCommit(top, 'HEAD')) {
301
+ const input = messageBody(message);
302
+ requireOk(await runGitAsync(REWORD_HEAD_ARGS, top, { input }));
303
+ return;
304
+ }
330
305
  await rewordOlderAsync(top, commit, message);
331
306
  };
332
307
 
@@ -348,7 +323,7 @@ const parseBranchLine = (line) => {
348
323
  if (!name) return null;
349
324
  const mark = parts[0] ?? '';
350
325
  const sha = (parts[2] ?? '').trim();
351
- const date = (parts[3] ?? '').trim();
326
+ const date = shortAge(parts[3] ?? '');
352
327
  const track = parseTrack(parts[4]);
353
328
  const subject = parts.slice(5).join('\t').trim();
354
329
  return {
@@ -393,13 +368,10 @@ const listBranches = (top) => {
393
368
  const names = new Set();
394
369
  for (const entry of branches) names.add(entry.name);
395
370
  const primary = defaultBranch(top, names);
396
- const { length } = branches;
397
- const listed = new Array(length);
398
- for (let i = 0; i < length; i++) {
399
- const entry = branches[i];
400
- listed[i] = { ...entry, isDefault: entry.name === primary };
401
- }
402
- return listed;
371
+ return branches.map((entry) => ({
372
+ ...entry,
373
+ isDefault: entry.name === primary,
374
+ }));
403
375
  };
404
376
 
405
377
  const namedBranch = (name) => {
@@ -430,23 +402,11 @@ const createBranchAsync = async (top, name) => {
430
402
  requireOk(await runGitAsync(args, top), 'checkout failed');
431
403
  };
432
404
 
433
- const rebaseBranch = (top, onto) => {
434
- const branch = namedBranch(onto);
435
- const result = runGit(['rebase', branch], top);
436
- const failed = rebaseFailed(result);
437
- if (!failed) return;
438
- runGit(['rebase', '--abort'], top);
439
- throw new Error(oneLine(failed));
440
- };
405
+ const rebaseBranch = (top, onto) =>
406
+ runRebase(top, ['rebase', namedBranch(onto)]);
441
407
 
442
- const rebaseBranchAsync = async (top, onto) => {
443
- const branch = namedBranch(onto);
444
- const result = await runGitAsync(['rebase', branch], top);
445
- const failed = rebaseFailed(result);
446
- if (!failed) return;
447
- await runGitAsync(['rebase', '--abort'], top);
448
- throw new Error(oneLine(failed));
449
- };
408
+ const rebaseBranchAsync = async (top, onto) =>
409
+ runRebaseAsync(top, ['rebase', namedBranch(onto)]);
450
410
 
451
411
  const dropBranch = (top, name) => {
452
412
  const branch = namedBranch(name);
@@ -510,36 +470,31 @@ const pushCmd = (force, branch) => {
510
470
  const noUpstream = (msg) =>
511
471
  `${msg ?? ''}`.toLowerCase().includes('no upstream');
512
472
 
513
- const finishPush = (result, force, branch, run) => {
473
+ const requirePushOk = (result) => {
514
474
  if (result.status === 0) return;
515
- const msg = result.stderr || result.stdout || 'push failed';
516
- if (!noUpstream(msg)) throw pushError(msg);
517
- const next = run(pushCmd(force, branch));
518
- if (next.status !== 0) {
519
- throw pushError(next.stderr || next.stdout || 'push failed');
520
- }
475
+ throw pushError(result.stderr || result.stdout || 'push failed');
521
476
  };
522
477
 
523
478
  const pushChanges = (top, force) => {
524
479
  const useForce = force === true;
525
480
  const branch = pushBranch(top);
526
481
  const tracked = hasUpstream(top) ? '' : branch;
527
- const result = runGit(pushCmd(useForce, tracked), top);
528
- finishPush(result, useForce, branch, (args) => runGit(args, top));
482
+ let result = runGit(pushCmd(useForce, tracked), top);
483
+ if (result.status !== 0 && noUpstream(result.stderr || result.stdout)) {
484
+ result = runGit(pushCmd(useForce, branch), top);
485
+ }
486
+ requirePushOk(result);
529
487
  };
530
488
 
531
489
  const pushChangesAsync = async (top, force) => {
532
490
  const useForce = force === true;
533
491
  const branch = pushBranch(top);
534
492
  const tracked = hasUpstream(top) ? '' : branch;
535
- const result = await runGitAsync(pushCmd(useForce, tracked), top);
536
- if (result.status === 0) return;
537
- const msg = result.stderr || result.stdout || 'push failed';
538
- if (!noUpstream(msg)) throw pushError(msg);
539
- const next = await runGitAsync(pushCmd(useForce, branch), top);
540
- if (next.status !== 0) {
541
- throw pushError(next.stderr || next.stdout || 'push failed');
493
+ let result = await runGitAsync(pushCmd(useForce, tracked), top);
494
+ if (result.status !== 0 && noUpstream(result.stderr || result.stdout)) {
495
+ result = await runGitAsync(pushCmd(useForce, branch), top);
542
496
  }
497
+ requirePushOk(result);
543
498
  };
544
499
 
545
500
  const hasStaged = (top) => {
@@ -551,9 +506,7 @@ const commitPlan = (kind, message) => {
551
506
  const args = ['commit'];
552
507
  if (kind === 'amend') args.push('--amend');
553
508
  args.push('-F', '-');
554
- const text = message ?? '';
555
- const body = text.endsWith('\n') ? text : `${text}\n`;
556
- return { args, body };
509
+ return { args, body: messageBody(message) };
557
510
  };
558
511
 
559
512
  const commitChanges = (top, kind, message) => {