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
@@ -1,10 +1,14 @@
1
1
  'use strict';
2
2
 
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+
3
6
  const files = require('../files.js');
4
- const { listPath, isTodoItem, isTodosEntry, isReadOnlyOrigin } = files;
7
+ const { listPath, isTaskItem, isTasksEntry, isReadOnlyOrigin } = files;
5
8
  const items = require('./items.js');
6
9
  const { needsReload, npmBusyKind, sameBlock } = items;
7
- const { hunkKey } = require('../diff/diff.js');
10
+ const diff = require('../diff/diff.js');
11
+ const { hunkKey } = diff;
8
12
 
9
13
  const nextUnstageOrigin = (item) => {
10
14
  if (item.file.isNew === true) return 'untracked';
@@ -23,13 +27,63 @@ const COMMANDS = {
23
27
  revert: {
24
28
  match: () => true,
25
29
  },
30
+ ignore: {
31
+ match: () => true,
32
+ },
33
+ };
34
+
35
+ const IGNORE_FILES = ['.gitignore', '.npmignore'];
36
+
37
+ const ignorePatternListed = (text, pattern) => {
38
+ for (const line of `${text ?? ''}`.split('\n')) {
39
+ if (line.trim() === pattern) return true;
40
+ }
41
+ return false;
42
+ };
43
+
44
+ const appendIgnorePattern = (text, pattern) => {
45
+ if (ignorePatternListed(text, pattern)) return text;
46
+ const base = text.length && !text.endsWith('\n') ? `${text}\n` : text;
47
+ return `${base}${pattern}\n`;
48
+ };
49
+
50
+ const appendIgnoreFile = (top, name, pattern) => {
51
+ const abs = path.join(top, name);
52
+ if (name === '.npmignore' && !fs.existsSync(abs)) return false;
53
+ const text = fs.existsSync(abs) ? fs.readFileSync(abs, 'utf8') : '';
54
+ const next = appendIgnorePattern(text, pattern);
55
+ if (next === text) return false;
56
+ fs.writeFileSync(abs, next);
57
+ return true;
58
+ };
59
+
60
+ const ignoreRelPath = (ui, rel) => {
61
+ const pattern = `${rel ?? ''}`.replace(/^\//, '');
62
+ if (!pattern) {
63
+ ui.status = 'not a diff block';
64
+ return;
65
+ }
66
+ let changed = false;
67
+ for (const name of IGNORE_FILES) {
68
+ if (appendIgnoreFile(ui.top, name, pattern)) changed = true;
69
+ }
70
+ if (!changed) {
71
+ ui.status = 'already ignored';
72
+ return;
73
+ }
74
+ ui.ignoreWatch();
75
+ ui.nav.clearSelection();
76
+ ui.reloadAfterChange();
77
+ ui.status = 'ignored';
26
78
  };
27
79
 
80
+ const sameEntry = (entry) => entry;
81
+
28
82
  const keepOrigin = (ui, item, origin) => {
29
83
  ui.collection.keepOrigin(item, origin);
30
84
  const idx = ui.collection.findRestoredIndex({ ...item, origin });
31
85
  if (idx >= 0) ui.nav.index = idx;
32
- if (ui.ignoreWatch) ui.ignoreWatch();
86
+ ui.ignoreWatch();
33
87
  ui.nav.clearSelection();
34
88
  ui.syncReviewPath();
35
89
  ui.syncFileCursor();
@@ -42,11 +96,11 @@ const fileCursorEntry = (ui) => {
42
96
 
43
97
  const fileActionItems = (ui) => {
44
98
  const entry = fileCursorEntry(ui);
45
- if (!entry || isTodosEntry(entry)) return [];
99
+ if (!entry || isTasksEntry(entry)) return [];
46
100
  const rel = entry.path;
47
101
  const selected = [];
48
102
  for (const item of ui.items) {
49
- if (isTodoItem(item)) continue;
103
+ if (isTaskItem(item)) continue;
50
104
  if (listPath(item) !== rel) continue;
51
105
  selected.push(item);
52
106
  }
@@ -56,7 +110,7 @@ const fileActionItems = (ui) => {
56
110
  const activeDiffItem = (ui) => {
57
111
  const item = ui.current();
58
112
  if (!item) return null;
59
- if (isTodoItem(item)) {
113
+ if (isTaskItem(item)) {
60
114
  ui.status = 'not a diff block';
61
115
  return null;
62
116
  }
@@ -139,13 +193,60 @@ const applyBlockAdd = (ui, item) => {
139
193
  keepOrigin(ui, item, 'staged');
140
194
  };
141
195
 
196
+ const enqueueOrigins = (ui, targets, spec) => {
197
+ const writes = new Array(targets.length);
198
+ for (let i = 0; i < targets.length; i++) {
199
+ const current = liveItem(ui, targets[i]);
200
+ writes[i] = current;
201
+ keepOrigin(ui, current, spec.originOf(current));
202
+ }
203
+ if (spec.afterUi) spec.afterUi();
204
+ if (!spec.busy) ui.status = spec.done;
205
+ ui.ops.enqueueLazy({
206
+ kind: spec.busy || spec.done,
207
+ done: spec.done,
208
+ reload: spec.reload === true,
209
+ write: async (top) => {
210
+ for (const item of writes) await spec.write(top, item);
211
+ },
212
+ });
213
+ };
214
+
215
+ const stageLazy = (ui, targets, extra = {}) => {
216
+ const busy = extra.busy || '';
217
+ const repo = ui.repo;
218
+ enqueueOrigins(ui, targets, {
219
+ done: 'staged',
220
+ busy,
221
+ reload: extra.reload === true,
222
+ originOf: () => 'staged',
223
+ afterUi: extra.afterUi,
224
+ write: (top, item) => {
225
+ if (busy && repo.addAsync) return repo.addAsync(top, item);
226
+ return repo.add(top, item);
227
+ },
228
+ });
229
+ };
230
+
231
+ const unstageLazy = (ui, targets, extra = {}) => {
232
+ const repo = ui.repo;
233
+ enqueueOrigins(ui, targets, {
234
+ done: 'unstaged',
235
+ busy: '',
236
+ originOf: nextUnstageOrigin,
237
+ afterUi: extra.afterUi,
238
+ write: (top, item) => repo.unstage(top, item),
239
+ });
240
+ };
241
+
142
242
  const onBlockAdd = (ui, item) => {
143
243
  const busy = npmBusyKind(item);
144
244
  const repo = ui.repo;
145
- if (ui.uiOpen && busy && repo.addAsync) {
146
- const after = () => applyBlockAdd(ui, item);
147
- const write = (top) => repo.addAsync(top, item);
148
- return void ui.ops.runBusy(busy, 'staged', write, after);
245
+ if (ui.uiOpen) {
246
+ return void stageLazy(ui, [item], {
247
+ busy,
248
+ reload: needsReload(item),
249
+ });
149
250
  }
150
251
  applyGit(
151
252
  ui,
@@ -159,6 +260,7 @@ const onBlockAdd = (ui, item) => {
159
260
  };
160
261
 
161
262
  const onBlockUnstage = (ui, item) => {
263
+ if (ui.uiOpen) return void unstageLazy(ui, [item]);
162
264
  const origin = nextUnstageOrigin(item);
163
265
  const repo = ui.repo;
164
266
  applyGit(
@@ -170,6 +272,12 @@ const onBlockUnstage = (ui, item) => {
170
272
  );
171
273
  };
172
274
 
275
+ const dismissReloadable = (ui, targets) => {
276
+ for (const item of targets) {
277
+ if (needsReload(item)) ui.collection.dismiss(item);
278
+ }
279
+ };
280
+
173
281
  const onBlockRevert = (ui, item) => {
174
282
  const repo = ui.repo;
175
283
  applyGit(
@@ -178,7 +286,7 @@ const onBlockRevert = (ui, item) => {
178
286
  'dropped',
179
287
  (top, current) => repo.revert(top, current),
180
288
  () => {
181
- if (needsReload(item)) ui.collection.dismiss(item);
289
+ dismissReloadable(ui, [item]);
182
290
  ui.reloadAfterChange();
183
291
  },
184
292
  );
@@ -194,6 +302,7 @@ const onBlockCommand = (ui, name) => {
194
302
  const command = COMMANDS[name];
195
303
  const item = activeDiffItem(ui);
196
304
  if (!guardWritable(ui, item)) return;
305
+ if (name === 'ignore') return void ignoreRelPath(ui, listPath(item));
197
306
  if (!command.match(item)) {
198
307
  ui.status = command.skipStatus;
199
308
  return;
@@ -201,109 +310,83 @@ const onBlockCommand = (ui, name) => {
201
310
  BLOCK[name](ui, item);
202
311
  };
203
312
 
204
- const writeFileAddAsync = (ui, targets) => async (top) => {
205
- const repo = ui.repo;
206
- for (const item of targets) {
207
- const current = liveItem(ui, item);
208
- await repo.addAsync(top, current);
209
- keepOrigin(ui, current, 'staged');
313
+ const stageTargets = (ui, targets, after = null) => {
314
+ const reload = targets.some(needsReload);
315
+ const busy = targets.map(npmBusyKind).find((kind) => kind) || '';
316
+ if (ui.uiOpen) {
317
+ return void stageLazy(ui, targets, { busy, reload, afterUi: after });
210
318
  }
319
+ const keepStaged = (entry) => keepOrigin(ui, entry, 'staged');
320
+ const result = applyFileGit(ui, targets, 'staged', ui.repo.add, keepStaged);
321
+ if (reconcilePartial(ui, result)) return;
322
+ if (reload) ui.reloadAfterChange();
323
+ if (after) after();
211
324
  };
212
325
 
213
- const afterFileAdd = (ui, rel, proposed) => () => {
214
- if (proposed) {
215
- return void ui.reloadAfterChange(() => selectNextFileAfter(ui, rel));
216
- }
217
- selectNextFileAfter(ui, rel);
218
- };
219
-
220
- const onFileAdd = (ui) => {
221
- const selected = fileActionItems(ui);
222
- if (!selected.length) {
223
- ui.status = 'not a diff block';
224
- return;
225
- }
226
- if (!guardWritable(ui, selected[0])) return;
227
- const rel = listPath(selected[0]);
228
- const targets = selected.filter(COMMANDS.add.match);
229
- if (!targets.length) {
230
- ui.status = 'already staged';
231
- selectNextFileAfter(ui, rel);
232
- return;
233
- }
234
- const proposed = targets.some(needsReload);
235
- const busy = targets.map(npmBusyKind).find((kind) => kind);
236
- const repo = ui.repo;
237
- if (ui.uiOpen && busy && repo.addAsync) {
238
- const write = writeFileAddAsync(ui, targets);
239
- const after = afterFileAdd(ui, rel, proposed);
240
- return void ui.ops.runBusy(busy, 'staged', write, after);
241
- }
326
+ const unstageTargets = (ui, targets, after = null) => {
327
+ if (ui.uiOpen) return void unstageLazy(ui, targets, { afterUi: after });
328
+ const keepUnstaged = (entry) =>
329
+ keepOrigin(ui, entry, nextUnstageOrigin(entry));
242
330
  const result = applyFileGit(
243
331
  ui,
244
332
  targets,
245
- 'staged',
246
- (top, current) => repo.add(top, current),
247
- (item) => keepOrigin(ui, item, 'staged'),
333
+ 'unstaged',
334
+ ui.repo.unstage,
335
+ keepUnstaged,
248
336
  );
249
337
  if (reconcilePartial(ui, result)) return;
250
- if (proposed) ui.reloadAfterChange();
251
- selectNextFileAfter(ui, rel);
338
+ if (after) after();
252
339
  };
253
340
 
254
- const onFileUnstage = (ui) => {
255
- const selected = fileActionItems(ui);
256
- if (!selected.length) {
257
- ui.status = 'not a diff block';
258
- return;
259
- }
260
- if (!guardWritable(ui, selected[0])) return;
261
- const rel = listPath(selected[0]);
262
- const targets = selected.filter(COMMANDS.unstage.match);
263
- if (!targets.length) {
264
- ui.status = 'not staged';
265
- selectNextFileAfter(ui, rel);
266
- return;
267
- }
268
- const repo = ui.repo;
341
+ const revertTargets = (ui, targets) => {
342
+ dismissReloadable(ui, targets);
269
343
  const result = applyFileGit(
270
344
  ui,
271
345
  targets,
272
- 'unstaged',
273
- (top, current) => repo.unstage(top, current),
274
- (item) => keepOrigin(ui, item, nextUnstageOrigin(item)),
346
+ 'dropped',
347
+ ui.repo.revert,
348
+ sameEntry,
275
349
  );
276
- if (reconcilePartial(ui, result)) return;
277
- selectNextFileAfter(ui, rel);
350
+ if (!result.ok && !result.completed.length) return;
351
+ ui.reloadAfterChange();
278
352
  };
279
353
 
280
- const onFileRevert = (ui) => {
281
- const selected = fileActionItems(ui);
282
- if (!selected.length) {
283
- ui.status = 'not a diff block';
284
- return;
285
- }
286
- if (!guardWritable(ui, selected[0])) return;
287
- const rel = listPath(selected[0]);
288
- for (const item of selected) {
289
- if (needsReload(item)) ui.collection.dismiss(item);
290
- }
291
- let ok = false;
354
+ const revertFile = (ui, rel, selected, after) => {
355
+ dismissReloadable(ui, selected);
292
356
  try {
293
357
  ui.repo.revertFile(ui.top, rel, selected);
294
358
  ui.status = 'dropped';
295
- ok = true;
296
359
  } catch (error) {
297
360
  ui.status = error.message;
361
+ return void ui.reloadAfterChange();
298
362
  }
299
- const after = ok ? () => selectNextFileAfter(ui, rel) : null;
300
363
  ui.reloadAfterChange(after);
301
364
  };
302
365
 
303
- const FILE = {
304
- add: onFileAdd,
305
- unstage: onFileUnstage,
306
- revert: onFileRevert,
366
+ const TARGETS = {
367
+ add: stageTargets,
368
+ unstage: unstageTargets,
369
+ revert: revertTargets,
370
+ };
371
+
372
+ const onFileCommand = (ui, name) => {
373
+ const selected = fileActionItems(ui);
374
+ if (!selected.length) {
375
+ ui.status = 'not a diff block';
376
+ return;
377
+ }
378
+ if (!guardWritable(ui, selected[0])) return;
379
+ const rel = listPath(selected[0]);
380
+ const next = () => selectNextFileAfter(ui, rel);
381
+ if (name === 'ignore') return void ignoreRelPath(ui, rel);
382
+ if (name === 'revert') return void revertFile(ui, rel, selected, next);
383
+ const command = COMMANDS[name];
384
+ const targets = selected.filter(command.match);
385
+ if (!targets.length) {
386
+ ui.status = command.skipStatus;
387
+ return void next();
388
+ }
389
+ TARGETS[name](ui, targets, next);
307
390
  };
308
391
 
309
392
  const hunkActionItems = (ui, item) => {
@@ -317,62 +400,6 @@ const hunkActionItems = (ui, item) => {
317
400
  return selected;
318
401
  };
319
402
 
320
- const onUnitAdd = (ui, targets) => {
321
- const proposed = targets.some(needsReload);
322
- const busy = targets.map(npmBusyKind).find((kind) => kind);
323
- const repo = ui.repo;
324
- if (ui.uiOpen && busy && repo.addAsync) {
325
- const write = writeFileAddAsync(ui, targets);
326
- const after = () => {
327
- if (proposed) ui.reloadAfterChange();
328
- };
329
- return void ui.ops.runBusy(busy, 'staged', write, after);
330
- }
331
- const result = applyFileGit(
332
- ui,
333
- targets,
334
- 'staged',
335
- (top, current) => repo.add(top, current),
336
- (entry) => keepOrigin(ui, entry, 'staged'),
337
- );
338
- if (reconcilePartial(ui, result)) return;
339
- if (proposed) ui.reloadAfterChange();
340
- };
341
-
342
- const onUnitUnstage = (ui, targets) => {
343
- const repo = ui.repo;
344
- const result = applyFileGit(
345
- ui,
346
- targets,
347
- 'unstaged',
348
- (top, current) => repo.unstage(top, current),
349
- (entry) => keepOrigin(ui, entry, nextUnstageOrigin(entry)),
350
- );
351
- reconcilePartial(ui, result);
352
- };
353
-
354
- const onUnitRevert = (ui, targets) => {
355
- for (const item of targets) {
356
- if (needsReload(item)) ui.collection.dismiss(item);
357
- }
358
- const repo = ui.repo;
359
- const result = applyFileGit(
360
- ui,
361
- targets,
362
- 'dropped',
363
- (top, current) => repo.revert(top, current),
364
- (entry) => entry,
365
- );
366
- if (!result.ok && !result.completed.length) return;
367
- ui.reloadAfterChange();
368
- };
369
-
370
- const UNIT = {
371
- add: onUnitAdd,
372
- unstage: onUnitUnstage,
373
- revert: onUnitRevert,
374
- };
375
-
376
403
  const onUnitCommand = (ui, name) => {
377
404
  const command = COMMANDS[name];
378
405
  const item = activeDiffItem(ui);
@@ -383,11 +410,11 @@ const onUnitCommand = (ui, name) => {
383
410
  ui.status = command.skipStatus ?? 'not a diff block';
384
411
  return;
385
412
  }
386
- UNIT[name](ui, targets);
413
+ TARGETS[name](ui, targets);
387
414
  };
388
415
 
389
416
  const onCommand = (ui, name) => {
390
- if (ui.nav.pane === 'files') return void FILE[name](ui);
417
+ if (ui.nav.pane === 'files') return void onFileCommand(ui, name);
391
418
  if (ui.nav.pane === 'unit') return void onUnitCommand(ui, name);
392
419
  onBlockCommand(ui, name);
393
420
  };
@@ -397,6 +424,7 @@ const createChangeActions = (ui) => ({
397
424
  onAdd: () => onCommand(ui, 'add'),
398
425
  onUnstage: () => onCommand(ui, 'unstage'),
399
426
  onRevert: () => onCommand(ui, 'revert'),
427
+ onIgnore: () => onCommand(ui, 'ignore'),
400
428
  });
401
429
 
402
430
  module.exports = { createChangeActions };
@@ -1,15 +1,16 @@
1
1
  'use strict';
2
2
 
3
- const { stateView } = require('./accessors.js');
4
-
5
- const { isReadOnlyOrigin } = require('../files.js');
6
- const { isFixupCommit } = require('./actions.js');
7
- const { pickRepoWrite } = require('./ops.js');
8
- const {
9
- commitLine,
10
- commitText,
11
- splitMessage,
12
- } = require('../render/commits.js');
3
+ const accessors = require('./accessors.js');
4
+ const { stateView } = accessors;
5
+
6
+ const files = require('../files.js');
7
+ const { isReadOnlyOrigin } = files;
8
+ const actions = require('./actions.js');
9
+ const { isFixupCommit } = actions;
10
+ const ops = require('./ops.js');
11
+ const { pickRepoWrite } = ops;
12
+ const commits = require('../render/commits.js');
13
+ const { commitLine, commitText, splitMessage } = commits;
13
14
 
14
15
  const COMMIT_STATUS = {
15
16
  commit: 'committed',
@@ -42,6 +43,13 @@ const briefCommit = (ui) => ui.commits.commitView !== 'full';
42
43
 
43
44
  const joinMessage = (line, rest) => (rest ? `${line}\n${rest}` : line);
44
45
 
46
+ const tidyCommitMessage = (message) => {
47
+ const lines = `${message ?? ''}`.split('\n').map((line) => line.trim());
48
+ while (lines.length && lines[0] === '') lines.shift();
49
+ while (lines.length && lines[lines.length - 1] === '') lines.pop();
50
+ return lines.join('\n');
51
+ };
52
+
45
53
  const keepBriefBody = (ui, text) => {
46
54
  if (!briefCommit(ui)) return text;
47
55
  const parts = splitMessage(text);
@@ -90,6 +98,24 @@ const clampCommitCursor = (api) => {
90
98
  if (nav.commitCursor > last) nav.commitCursor = Math.max(0, last);
91
99
  };
92
100
 
101
+ const commitIndexForView = (api) => {
102
+ const ui = api.ui;
103
+ if (hasStagedChanges(ui)) return 0;
104
+ const list = listedCommits(api.state.commits);
105
+ const want = `${ui.rev ?? ''}`;
106
+ const short = `${ui.revShort ?? ''}`;
107
+ if (!want && !short) return 0;
108
+ for (let i = 0; i < list.length; i++) {
109
+ const entry = list[i];
110
+ if (!entry || entry.pending) continue;
111
+ const sha = `${entry.sha ?? ''}`;
112
+ const brief = `${entry.shortSha ?? ''}`;
113
+ if (want && sha === want) return i;
114
+ if (short && (brief === short || sha.startsWith(short))) return i;
115
+ }
116
+ return 0;
117
+ };
118
+
93
119
  const refreshCommitList = (api) => {
94
120
  const repo = api.ui.repo;
95
121
  if (!repo.listCommits) return;
@@ -120,10 +146,12 @@ const showCommits = (api) => {
120
146
  api.ui.status = error.message;
121
147
  return;
122
148
  }
123
- api.ui.nav.commitCursor = 0;
124
- api.ui.nav.pane = 'commits';
149
+ const nav = api.ui.nav;
150
+ nav.commitCursor = commitIndexForView(api);
151
+ nav.pane = 'commits';
152
+ nav.resetListScroll('commits');
125
153
  api.ui.status = '';
126
- api.ui.nav.clearSelection();
154
+ nav.clearSelection();
127
155
  };
128
156
 
129
157
  const onCommitMove = (api, delta) => {
@@ -181,11 +209,23 @@ const onCommit = (api) => {
181
209
  startGitCommit(api, 'commit');
182
210
  };
183
211
 
212
+ const openLoadedDiff = (ui) => {
213
+ ui.nav.tasksOpen = false;
214
+ ui.nav.scroll = 0;
215
+ ui.nav.index = 0;
216
+ ui.nav.pane = ui.items.length ? 'diff' : 'files';
217
+ ui.syncReviewPath();
218
+ ui.syncFileCursor();
219
+ ui.status = '';
220
+ };
221
+
184
222
  const showSelectedDiff = (api) => {
185
223
  if (api.ui.mode === 'compose') return;
186
224
  const entry = selectedCommit(api);
187
225
  if (!entry) return;
188
226
  const ui = api.ui;
227
+ const viewingCommit = Boolean(ui.rev || ui.revShort);
228
+ if (isPending(entry) && !viewingCommit) return void onCommit(api);
189
229
  if (isPending(entry) || !entry.sha) {
190
230
  ui.rev = '';
191
231
  ui.revShort = '';
@@ -193,16 +233,10 @@ const showSelectedDiff = (api) => {
193
233
  ui.rev = entry.sha;
194
234
  ui.revShort = entry.shortSha || '';
195
235
  }
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 });
236
+ ui.refreshFromRepo({
237
+ keepEmpty: true,
238
+ afterLoad: () => openLoadedDiff(ui),
239
+ });
206
240
  };
207
241
 
208
242
  const onAmend = (api) => {
@@ -226,18 +260,9 @@ const applyFixupNow = (api) => {
226
260
  api.ui.status = 'read only';
227
261
  return;
228
262
  }
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;
263
+ api.ui.ops.runWrite('applyFixup', [entry.sha], 'applying', done, (extra) =>
264
+ refreshAfterWrite(api, extra),
265
+ );
241
266
  };
242
267
 
243
268
  const onApply = (api) => {
@@ -254,12 +279,6 @@ const onFixup = (api) => {
254
279
  startGitCommit(api, 'fixup');
255
280
  };
256
281
 
257
- const cancelCommit = (api) => {
258
- api.ui.mode = 'review';
259
- api.ui.composer.state.commitKind = null;
260
- api.ui.status = '';
261
- };
262
-
263
282
  const finishAsyncCommit = (api, kind, message, done, write) => {
264
283
  const ui = api.ui;
265
284
  ui.composer.closeCompose();
@@ -323,6 +342,7 @@ const finishGitCommit = (api) => {
323
342
  if (isInPlaceCommit(kind) && briefCommit(ui)) {
324
343
  message = joinMessage(message, ui.composer.state.commitBody);
325
344
  }
345
+ message = tidyCommitMessage(message);
326
346
  if (!message.trim()) {
327
347
  ui.status = 'empty commit message';
328
348
  return;
@@ -360,12 +380,6 @@ const cancelDropCommit = (api) => {
360
380
  api.ui.status = '';
361
381
  };
362
382
 
363
- const dropAsync = (api, sha, done, write) => {
364
- const run = (top) => write(top, sha);
365
- const after = () => refreshAfterWrite(api, { doneStatus: done });
366
- return void api.ui.ops.runBusy('dropping', done, run, after);
367
- };
368
-
369
383
  const confirmDropCommit = (api) => {
370
384
  const sha = api.state.dropSha;
371
385
  const name = api.state.dropName;
@@ -379,17 +393,9 @@ const confirmDropCommit = (api) => {
379
393
  api.ui.status = 'read only';
380
394
  return;
381
395
  }
382
- const uiOpen = api.ui.uiOpen;
383
- const picked = pickRepoWrite(repo, 'dropCommit', uiOpen);
384
- if (picked.async) {
385
- return void dropAsync(api, sha, done, picked.write);
386
- }
387
- const ok = api.ui.ops.runRepo(done, (top) => {
388
- repo.dropCommit(top, sha);
389
- });
390
- if (!ok) return;
391
- refreshAfterWrite(api);
392
- if (!api.ui.status) api.ui.status = done;
396
+ api.ui.ops.runWrite('dropCommit', [sha], 'dropping', done, (extra) =>
397
+ refreshAfterWrite(api, extra),
398
+ );
393
399
  };
394
400
 
395
401
  const onView = (api) => {
@@ -428,8 +434,6 @@ const createCommitController = (ui) => {
428
434
  onCommitMove: (delta) => onCommitMove(api, delta),
429
435
  onShowDiff: () => showSelectedDiff(api),
430
436
  onView: () => onView(api),
431
- cancelCommit: () => cancelCommit(api),
432
- startGitCommit: (kind) => startGitCommit(api, kind),
433
437
  finishGitCommit: () => finishGitCommit(api),
434
438
  refresh: () => refreshCommitList(api),
435
439
  onDropCommit: () => onDropCommit(api),
@@ -441,7 +445,7 @@ const createCommitController = (ui) => {
441
445
  };
442
446
 
443
447
  module.exports = {
444
- COMMIT_STATUS,
448
+ hasStagedChanges,
445
449
  createCommitController,
446
450
  listedCommits,
447
451
  };