reslop 0.2.0 → 0.2.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.
Files changed (43) hide show
  1. package/CHANGELOG.md +27 -1
  2. package/README.md +13 -2
  3. package/lib/ansi.js +263 -6
  4. package/lib/api.js +2 -0
  5. package/lib/capabilities.js +6 -0
  6. package/lib/cli.js +13 -2
  7. package/lib/deps.js +43 -3
  8. package/lib/git-branches.js +228 -17
  9. package/lib/git-reword-todo.js +20 -0
  10. package/lib/git.js +20 -3
  11. package/lib/keys.js +10 -4
  12. package/lib/npm-commands.js +513 -0
  13. package/lib/render/branches.js +23 -58
  14. package/lib/render/chrome.js +156 -99
  15. package/lib/render/commits.js +643 -31
  16. package/lib/render/diff.js +4 -4
  17. package/lib/render/frame.js +51 -5
  18. package/lib/render/notes.js +22 -5
  19. package/lib/render/npm.js +228 -0
  20. package/lib/render/primitives.js +48 -34
  21. package/lib/render/render.js +3 -3
  22. package/lib/report-model.js +127 -0
  23. package/lib/report-parse.js +94 -0
  24. package/lib/report-render.js +87 -0
  25. package/lib/report-run.js +164 -0
  26. package/lib/report-test.js +506 -0
  27. package/lib/report-tools.js +311 -0
  28. package/lib/review-merge.js +270 -0
  29. package/lib/review.js +38 -6
  30. package/lib/session/actions.js +81 -5
  31. package/lib/session/commit.js +187 -19
  32. package/lib/session/compose.js +96 -32
  33. package/lib/session/dispatch.js +36 -2
  34. package/lib/session/files-pane.js +36 -6
  35. package/lib/session/lifecycle.js +116 -16
  36. package/lib/session/navigation.js +1 -0
  37. package/lib/session/npm.js +335 -0
  38. package/lib/session/pointer.js +12 -1
  39. package/lib/session/todos.js +29 -1
  40. package/lib/session/view.js +14 -3
  41. package/lib/session/watch.js +120 -8
  42. package/lib/session.js +11 -2
  43. package/package.json +7 -7
@@ -7,6 +7,7 @@ const ACTIONS = {
7
7
  commit: ['c'],
8
8
  prev: ['left', 'p', 'k'],
9
9
  next: ['right', 'n', 'j'],
10
+ check: ['x', ' '],
10
11
  layout: ['m'],
11
12
  feedback: ['f'],
12
13
  todo: ['t'],
@@ -28,32 +29,59 @@ const BRANCH_ACTIONS = {
28
29
 
29
30
  const COMMIT_ACTIONS = {
30
31
  amend: ['a'],
32
+ apply: ['a'],
33
+ reword: ['r'],
31
34
  fixup: ['f'],
32
35
  drop: ['d'],
36
+ view: ['v'],
33
37
  pull: ['p'],
34
38
  push: ['s'],
35
39
  };
36
40
 
41
+ const NPM_ACTIONS = {
42
+ npm: ['n'],
43
+ npmEdit: ['e'],
44
+ npmNew: ['n'],
45
+ npmStop: ['escape'],
46
+ npmRerun: ['r'],
47
+ };
48
+
37
49
  const ACTION_HOT = {
38
50
  prev: '←',
39
51
  next: '→',
52
+ npmStop: 'esc',
40
53
  };
41
54
 
42
55
  const ACTION_LABEL = {
43
56
  revert: 'drop',
57
+ check: 'x',
44
58
  layout: 'mode',
59
+ view: 'view',
45
60
  code: 'edit',
46
61
  newBranch: 'new',
62
+ npmEdit: 'edit',
63
+ npmNew: 'new',
64
+ npmStop: 'esc',
65
+ npmRerun: 're-run',
47
66
  quit: 'q',
48
67
  prev: '←',
49
68
  next: '→',
50
69
  };
51
70
 
52
- const DIFF_DISABLED = ['commit', 'branch', 'pull', 'push', 'file', 'diff'];
71
+ const DIFF_DISABLED = [
72
+ 'check',
73
+ 'commit',
74
+ 'branch',
75
+ 'pull',
76
+ 'push',
77
+ 'file',
78
+ 'diff',
79
+ ];
53
80
  const UNIT_DISABLED = [...DIFF_DISABLED, 'layout'];
54
81
  const FILES_DISABLED = ['layout', 'feedback', 'code'];
55
82
  const FILES_HIDDEN = [
56
83
  ...FILES_DISABLED,
84
+ 'check',
57
85
  'prev',
58
86
  'next',
59
87
  'commit',
@@ -86,6 +114,7 @@ const BRANCHES_DISABLED = [
86
114
  'feedback',
87
115
  'todo',
88
116
  'code',
117
+ 'check',
89
118
  'branch',
90
119
  'file',
91
120
  'diff',
@@ -100,11 +129,30 @@ const COMMITS_DISABLED = [
100
129
  'feedback',
101
130
  'todo',
102
131
  'code',
132
+ 'check',
133
+ 'branch',
134
+ 'file',
135
+ 'diff',
136
+ 'prev',
137
+ 'next',
138
+ ];
139
+ const NPM_DISABLED = [
140
+ 'add',
141
+ 'unstage',
142
+ 'revert',
143
+ 'commit',
144
+ 'layout',
145
+ 'feedback',
146
+ 'todo',
147
+ 'code',
148
+ 'check',
103
149
  'branch',
104
150
  'file',
105
151
  'diff',
106
152
  'prev',
107
153
  'next',
154
+ 'pull',
155
+ 'push',
108
156
  ];
109
157
 
110
158
  const isTodosTarget = (item) =>
@@ -124,13 +172,17 @@ const branchGitDisabled = (item) => {
124
172
  return off;
125
173
  };
126
174
 
175
+ const isFixupCommit = (item) =>
176
+ `${item && item.subject ? item.subject : ''}`.startsWith('fixup!');
177
+
127
178
  const commitWriteDisabled = (item) => {
128
- if (!item || !item.sha) return ['amend', 'fixup', 'drop'];
179
+ if (!item || !item.sha) return ['amend', 'apply', 'reword', 'fixup', 'drop'];
129
180
  return [];
130
181
  };
131
182
 
132
183
  const commitGitDisabled = (item) => {
133
- const off = commitWriteDisabled(item);
184
+ const off = [...commitWriteDisabled(item)];
185
+ if (!isFixupCommit(item)) off.push('apply');
134
186
  if (item && item.canCommit === true) return off;
135
187
  return [...off, 'commit', 'fixup'];
136
188
  };
@@ -145,10 +197,15 @@ const disabledActions = (pane, item) => {
145
197
  if (pane === 'commits') {
146
198
  return [...COMMITS_DISABLED, ...commitWriteDisabled(item)];
147
199
  }
200
+ if (pane === 'npm') return NPM_DISABLED;
148
201
  if (onTodos) return TODO_DISABLED;
149
202
  return [...DIFF_DISABLED, ...diffGitDisabled(item)];
150
203
  };
151
204
 
205
+ const HIDDEN_KEYS = {
206
+ l: 'theme',
207
+ };
208
+
152
209
  const SCROLL = {
153
210
  up: 'scrollUp',
154
211
  down: 'scrollDown',
@@ -182,10 +239,16 @@ const COMMIT_KEYS = {};
182
239
  for (const id of Object.keys(COMMIT_ACTIONS)) {
183
240
  for (const key of COMMIT_ACTIONS[id]) COMMIT_KEYS[key] = id;
184
241
  }
242
+ const NPM_KEYS = {
243
+ e: 'npmEdit',
244
+ n: 'npmNew',
245
+ };
246
+ FILES_KEYS.n = 'npm';
185
247
  const PANE_KEYS = {
186
248
  files: FILES_KEYS,
187
249
  branches: BRANCH_KEYS,
188
250
  commits: COMMIT_KEYS,
251
+ npm: NPM_KEYS,
189
252
  };
190
253
 
191
254
  for (const id of ACTION_IDS) {
@@ -194,10 +257,16 @@ for (const id of ACTION_IDS) {
194
257
  }
195
258
 
196
259
  for (const key of Object.keys(SCROLL)) KEY_TO_ACTION[key] = SCROLL[key];
260
+ for (const key of Object.keys(HIDDEN_KEYS)) {
261
+ KEY_TO_ACTION[key] = HIDDEN_KEYS[key];
262
+ }
197
263
 
198
264
  const actionFromKey = (key, pane, fileScope) => {
199
265
  const listPane =
200
- pane === 'files' || pane === 'branches' || pane === 'commits';
266
+ pane === 'files' ||
267
+ pane === 'branches' ||
268
+ pane === 'commits' ||
269
+ pane === 'npm';
201
270
  if (listPane && (key === 'left' || key === 'right')) return null;
202
271
  const paneKeys = PANE_KEYS[pane];
203
272
  if (paneKeys && paneKeys[key]) return paneKeys[key];
@@ -209,7 +278,11 @@ const actionFromKey = (key, pane, fileScope) => {
209
278
  };
210
279
 
211
280
  const actionKeys = (id) =>
212
- ACTIONS[id] ?? BRANCH_ACTIONS[id] ?? COMMIT_ACTIONS[id] ?? [];
281
+ ACTIONS[id] ??
282
+ BRANCH_ACTIONS[id] ??
283
+ COMMIT_ACTIONS[id] ??
284
+ NPM_ACTIONS[id] ??
285
+ [];
213
286
 
214
287
  const actionHot = (id) => ACTION_HOT[id] ?? actionKeys(id)[0];
215
288
 
@@ -301,6 +374,7 @@ module.exports = {
301
374
  ACTION_IDS,
302
375
  BRANCH_ACTIONS,
303
376
  COMMIT_ACTIONS,
377
+ NPM_ACTIONS,
304
378
  ACTION_HOT,
305
379
  ACTION_LABEL,
306
380
  DIFF_DISABLED,
@@ -312,12 +386,14 @@ module.exports = {
312
386
  TODO_DISABLED,
313
387
  BRANCHES_DISABLED,
314
388
  COMMITS_DISABLED,
389
+ NPM_DISABLED,
315
390
  SCROLL,
316
391
  PANE_ONLY,
317
392
  disabledActions,
318
393
  diffGitDisabled,
319
394
  branchGitDisabled,
320
395
  commitGitDisabled,
396
+ isFixupCommit,
321
397
  actionFromKey,
322
398
  actionKeys,
323
399
  actionHot,
@@ -3,12 +3,56 @@
3
3
  const { stateView } = require('./accessors.js');
4
4
 
5
5
  const { isReadOnlyOrigin } = require('../files.js');
6
+ const { isFixupCommit } = require('./actions.js');
6
7
  const { pickRepoWrite } = require('./ops.js');
8
+ const {
9
+ commitLine,
10
+ commitText,
11
+ splitMessage,
12
+ } = require('../render/commits.js');
7
13
 
8
14
  const COMMIT_STATUS = {
9
15
  commit: 'committed',
10
16
  amend: 'amended',
11
17
  fixup: 'fixup',
18
+ reword: 'reworded',
19
+ };
20
+
21
+ const isInPlaceCommit = (kind) => kind === 'amend' || kind === 'reword';
22
+
23
+ const PENDING_COMMIT = {
24
+ pending: true,
25
+ subject: 'uncommitted changes',
26
+ sha: '',
27
+ shortSha: '',
28
+ author: '',
29
+ email: '',
30
+ date: '',
31
+ when: '',
32
+ refs: '',
33
+ body: '',
34
+ head: false,
35
+ };
36
+
37
+ const listedCommits = (commits) => [PENDING_COMMIT].concat(commits ?? []);
38
+
39
+ const isPending = (entry) => Boolean(entry && entry.pending);
40
+
41
+ const briefCommit = (ui) => ui.commits.commitView !== 'full';
42
+
43
+ const joinMessage = (line, rest) => (rest ? `${line}\n${rest}` : line);
44
+
45
+ const keepBriefBody = (ui, text) => {
46
+ if (!briefCommit(ui)) return text;
47
+ const parts = splitMessage(text);
48
+ ui.composer.state.commitBody = parts.rest;
49
+ return parts.line;
50
+ };
51
+
52
+ const fixupMessage = (entry) => {
53
+ const subject = commitLine(entry).trim();
54
+ if (!subject) return 'fixup!';
55
+ return `fixup! ${subject}`;
12
56
  };
13
57
 
14
58
  const canCommit = (ui) => {
@@ -37,11 +81,11 @@ const hasStagedChanges = (ui) => {
37
81
 
38
82
  const selectedCommit = (api) => {
39
83
  const cursor = api.ui.nav.commitCursor;
40
- return api.state.commits[cursor] ?? null;
84
+ return listedCommits(api.state.commits)[cursor] ?? null;
41
85
  };
42
86
 
43
87
  const clampCommitCursor = (api) => {
44
- const last = api.state.commits.length - 1;
88
+ const last = listedCommits(api.state.commits).length - 1;
45
89
  const nav = api.ui.nav;
46
90
  if (nav.commitCursor > last) nav.commitCursor = Math.max(0, last);
47
91
  };
@@ -65,7 +109,6 @@ const refreshAfterWrite = (api, extra = {}) => {
65
109
 
66
110
  const showCommits = (api) => {
67
111
  if (api.ui.mode === 'compose') return;
68
- if (!canCommit(api.ui)) return;
69
112
  const repo = api.ui.repo;
70
113
  if (!repo.listCommits) {
71
114
  api.ui.status = 'read only';
@@ -84,11 +127,8 @@ const showCommits = (api) => {
84
127
  };
85
128
 
86
129
  const onCommitMove = (api, delta) => {
87
- const commits = api.state.commits;
88
- if (!commits.length) {
89
- api.ui.status = 'nothing to review';
90
- return;
91
- }
130
+ if (api.ui.mode === 'compose') return;
131
+ const commits = listedCommits(api.state.commits);
92
132
  const next = Math.min(
93
133
  commits.length - 1,
94
134
  Math.max(0, api.ui.nav.commitCursor + delta),
@@ -100,36 +140,114 @@ const onCommitMove = (api, delta) => {
100
140
 
101
141
  const startGitCommit = (api, kind) => {
102
142
  const ui = api.ui;
103
- if (kind !== 'amend' && !hasStagedChanges(ui)) {
143
+ if (!isInPlaceCommit(kind) && !hasStagedChanges(ui)) {
104
144
  ui.mode = 'review';
105
145
  ui.composer.state.commitKind = null;
106
146
  ui.status = 'nothing to commit';
107
147
  return;
108
148
  }
109
- ui.composer.state.commitKind = kind;
110
149
  let text = '';
111
- if (kind === 'fixup') {
112
- const entry = selectedCommit(api);
113
- text = entry ? entry.shortSha || entry.sha : 'HEAD';
150
+ if (kind === 'fixup') text = fixupMessage(selectedCommit(api));
151
+ if (kind === 'commit') api.ui.nav.commitCursor = 0;
152
+ if (kind === 'amend') {
153
+ const headAt = api.state.commits.findIndex((entry) => entry.head);
154
+ const at = headAt < 0 ? 0 : headAt;
155
+ api.ui.nav.commitCursor = api.state.commits.length ? at + 1 : 0;
156
+ if (ui.repo.lastMessage) text = ui.repo.lastMessage(ui.top) ?? '';
114
157
  }
115
- if (kind === 'amend' && ui.repo.lastMessage) {
116
- text = ui.repo.lastMessage(ui.top) ?? '';
158
+ if (kind === 'reword') {
159
+ const entry = selectedCommit(api);
160
+ if (!entry || !entry.sha) return;
161
+ if (typeof ui.repo.commitMessage === 'function') {
162
+ text = ui.repo.commitMessage(ui.top, entry.sha) ?? '';
163
+ } else {
164
+ text = commitText(entry);
165
+ }
117
166
  }
167
+ ui.composer.state.commitBody = '';
168
+ if (isInPlaceCommit(kind)) text = keepBriefBody(ui, text);
169
+ ui.composer.state.commitKind = kind;
118
170
  ui.composer.openCompose('commit', text);
171
+ const editor = ui.composer.state.editor;
172
+ if (!editor) return;
173
+ const lineEnd = editor.text.indexOf('\n');
174
+ editor.cursor = lineEnd < 0 ? editor.text.length : lineEnd;
119
175
  };
120
176
 
121
177
  const onCommit = (api) => {
122
178
  if (api.ui.nav.pane !== 'commits') return void showCommits(api);
123
179
  if (!canCommit(api.ui)) return;
180
+ api.ui.nav.commitCursor = 0;
124
181
  startGitCommit(api, 'commit');
125
182
  };
126
183
 
184
+ const showSelectedDiff = (api) => {
185
+ if (api.ui.mode === 'compose') return;
186
+ const entry = selectedCommit(api);
187
+ if (!entry) return;
188
+ const ui = api.ui;
189
+ if (isPending(entry) || !entry.sha) {
190
+ ui.rev = '';
191
+ ui.revShort = '';
192
+ } else {
193
+ ui.rev = entry.sha;
194
+ ui.revShort = entry.shortSha || '';
195
+ }
196
+ const openDiff = () => {
197
+ ui.nav.todoOpen = false;
198
+ ui.nav.scroll = 0;
199
+ ui.nav.index = 0;
200
+ ui.nav.pane = ui.items.length ? 'diff' : 'files';
201
+ ui.syncReviewPath();
202
+ ui.syncFileCursor();
203
+ ui.status = '';
204
+ };
205
+ ui.refreshFromRepo({ keepEmpty: true, afterLoad: openDiff });
206
+ };
207
+
127
208
  const onAmend = (api) => {
128
209
  if (api.ui.nav.pane !== 'commits') return;
129
210
  if (!canCommit(api.ui)) return;
130
211
  startGitCommit(api, 'amend');
131
212
  };
132
213
 
214
+ const onReword = (api) => {
215
+ if (api.ui.nav.pane !== 'commits') return;
216
+ if (!canCommit(api.ui)) return;
217
+ startGitCommit(api, 'reword');
218
+ };
219
+
220
+ const applyFixupNow = (api) => {
221
+ const entry = selectedCommit(api);
222
+ if (!entry || !entry.sha) return;
223
+ const done = 'applied';
224
+ const repo = api.ui.repo;
225
+ if (!repo.applyFixup) {
226
+ api.ui.status = 'read only';
227
+ return;
228
+ }
229
+ const picked = pickRepoWrite(repo, 'applyFixup', api.ui.uiOpen);
230
+ if (picked.async) {
231
+ const run = (top) => picked.write(top, entry.sha);
232
+ const after = () => refreshAfterWrite(api, { doneStatus: done });
233
+ return void api.ui.ops.runBusy('applying', done, run, after);
234
+ }
235
+ const ok = api.ui.ops.runRepo(done, (top) => {
236
+ repo.applyFixup(top, entry.sha);
237
+ });
238
+ if (!ok) return;
239
+ refreshAfterWrite(api);
240
+ if (!api.ui.status) api.ui.status = done;
241
+ };
242
+
243
+ const onApply = (api) => {
244
+ if (api.ui.nav.pane !== 'commits') return;
245
+ if (!canCommit(api.ui)) return;
246
+ const entry = selectedCommit(api);
247
+ if (isFixupCommit(entry)) return void applyFixupNow(api);
248
+ startGitCommit(api, 'amend');
249
+ };
250
+
133
251
  const onFixup = (api) => {
134
252
  if (api.ui.nav.pane !== 'commits') return;
135
253
  if (!canCommit(api.ui)) return;
@@ -166,20 +284,55 @@ const finishSyncCommit = (api, kind, message, done) => {
166
284
  }
167
285
  };
168
286
 
287
+ const finishReword = (api, message) => {
288
+ const ui = api.ui;
289
+ const entry = selectedCommit(api);
290
+ if (!entry || !entry.sha) {
291
+ ui.status = 'read only';
292
+ return;
293
+ }
294
+ const done = COMMIT_STATUS.reword;
295
+ const repo = ui.repo;
296
+ if (!repo.reword) {
297
+ ui.status = 'read only';
298
+ return;
299
+ }
300
+ const picked = pickRepoWrite(repo, 'reword', ui.uiOpen);
301
+ if (picked.async) {
302
+ ui.composer.closeCompose();
303
+ const run = (top) => picked.write(top, entry.sha, message);
304
+ const after = () => refreshAfterWrite(api, { doneStatus: done });
305
+ return void ui.ops.runBusy('rewording', done, run, after);
306
+ }
307
+ try {
308
+ repo.reword(ui.top, entry.sha, message);
309
+ } catch (error) {
310
+ ui.status = error.message;
311
+ return;
312
+ }
313
+ ui.composer.closeCompose();
314
+ refreshAfterWrite(api);
315
+ if (!ui.status) ui.status = done;
316
+ };
317
+
169
318
  const finishGitCommit = (api) => {
170
319
  const ui = api.ui;
171
320
  const kind = ui.composer.state.commitKind;
172
321
  const editor = ui.composer.state.editor;
173
- const message = editor ? editor.text : '';
174
- if (kind !== 'fixup' && !message.trim()) {
322
+ let message = editor ? editor.text : '';
323
+ if (isInPlaceCommit(kind) && briefCommit(ui)) {
324
+ message = joinMessage(message, ui.composer.state.commitBody);
325
+ }
326
+ if (!message.trim()) {
175
327
  ui.status = 'empty commit message';
176
328
  return;
177
329
  }
178
- if (kind !== 'amend' && !hasStagedChanges(ui)) {
330
+ if (!isInPlaceCommit(kind) && !hasStagedChanges(ui)) {
179
331
  ui.composer.closeCompose();
180
332
  ui.status = 'nothing to commit';
181
333
  return;
182
334
  }
335
+ if (kind === 'reword') return void finishReword(api, message);
183
336
  const done = COMMIT_STATUS[kind] ?? 'committed';
184
337
  const uiOpen = ui.uiOpen;
185
338
  const picked = pickRepoWrite(ui.repo, 'commit', uiOpen);
@@ -239,10 +392,19 @@ const confirmDropCommit = (api) => {
239
392
  if (!api.ui.status) api.ui.status = done;
240
393
  };
241
394
 
395
+ const onView = (api) => {
396
+ if (api.ui.nav.pane !== 'commits') return;
397
+ const next = api.state.commitView === 'full' ? 'brief' : 'full';
398
+ api.state.commitView = next;
399
+ api.ui.nav.listScroll = 0;
400
+ api.ui.status = next;
401
+ };
402
+
242
403
  const reset = (state) => {
243
404
  state.commits = [];
244
405
  state.dropName = '';
245
406
  state.dropSha = '';
407
+ state.commitView = 'brief';
246
408
  };
247
409
 
248
410
  const createCommitController = (ui) => {
@@ -250,6 +412,7 @@ const createCommitController = (ui) => {
250
412
  commits: [],
251
413
  dropName: '',
252
414
  dropSha: '',
415
+ commitView: 'brief',
253
416
  };
254
417
  const api = { ui, state };
255
418
  return stateView(
@@ -259,8 +422,12 @@ const createCommitController = (ui) => {
259
422
  canCommit: () => canCommit(ui),
260
423
  onCommit: () => onCommit(api),
261
424
  onAmend: () => onAmend(api),
425
+ onReword: () => onReword(api),
426
+ onApply: () => onApply(api),
262
427
  onFixup: () => onFixup(api),
263
428
  onCommitMove: (delta) => onCommitMove(api, delta),
429
+ onShowDiff: () => showSelectedDiff(api),
430
+ onView: () => onView(api),
264
431
  cancelCommit: () => cancelCommit(api),
265
432
  startGitCommit: (kind) => startGitCommit(api, kind),
266
433
  finishGitCommit: () => finishGitCommit(api),
@@ -269,11 +436,12 @@ const createCommitController = (ui) => {
269
436
  cancelDropCommit: () => cancelDropCommit(api),
270
437
  confirmDropCommit: () => confirmDropCommit(api),
271
438
  },
272
- ['commits', 'dropName'],
439
+ ['commits', 'dropName', 'commitView'],
273
440
  );
274
441
  };
275
442
 
276
443
  module.exports = {
277
444
  COMMIT_STATUS,
278
445
  createCommitController,
446
+ listedCommits,
279
447
  };