reslop 0.1.9 → 0.2.1

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 (96) hide show
  1. package/CHANGELOG.md +25 -1
  2. package/README.md +3 -1
  3. package/bin/reslop.js +2 -2
  4. package/lib/ansi.js +148 -5
  5. package/lib/api.js +4 -2
  6. package/lib/capabilities.js +13 -14
  7. package/lib/cli.js +38 -49
  8. package/lib/clipboard.js +1 -1
  9. package/lib/{deps/changes.js → deps-diff.js} +309 -19
  10. package/lib/deps.js +498 -29
  11. package/lib/detect.js +1 -7
  12. package/lib/diff/align.js +7 -10
  13. package/lib/diff/diff.js +53 -0
  14. package/lib/diff/display.js +5 -4
  15. package/lib/diff/inline.js +26 -21
  16. package/lib/diff/items.js +6 -4
  17. package/lib/diff/patch.js +1 -3
  18. package/lib/diff/unit.js +392 -0
  19. package/lib/files.js +5 -8
  20. package/lib/{git/branches.js → git-branches.js} +191 -11
  21. package/lib/{git/dependencies.js → git-deps.js} +111 -38
  22. package/lib/git-reword-todo.js +20 -0
  23. package/lib/git.js +606 -68
  24. package/lib/github.js +28 -197
  25. package/lib/gitlab.js +24 -195
  26. package/lib/highlight/bash.js +1 -3
  27. package/lib/highlight/css.js +1 -3
  28. package/lib/highlight/js.js +11 -14
  29. package/lib/highlight.js +6 -9
  30. package/lib/keys.js +14 -8
  31. package/lib/manifest.js +330 -0
  32. package/lib/remote.js +374 -0
  33. package/lib/render/branches.js +18 -100
  34. package/lib/render/chrome.js +71 -14
  35. package/lib/render/commits.js +69 -60
  36. package/lib/render/diff.js +20 -20
  37. package/lib/render/files.js +6 -34
  38. package/lib/render/frame.js +36 -13
  39. package/lib/render/notes.js +26 -8
  40. package/lib/render/primitives.js +99 -11
  41. package/lib/{render.js → render/render.js} +8 -8
  42. package/lib/render/unit.js +41 -0
  43. package/lib/review-merge.js +270 -0
  44. package/lib/review.js +782 -14
  45. package/lib/select.js +5 -10
  46. package/lib/session/accessors.js +9 -1
  47. package/lib/session/actions.js +61 -18
  48. package/lib/session/branches.js +116 -128
  49. package/lib/session/change-actions.js +202 -123
  50. package/lib/session/commit.js +201 -115
  51. package/lib/session/compose.js +181 -15
  52. package/lib/session/dispatch.js +110 -60
  53. package/lib/session/files-pane.js +156 -148
  54. package/lib/session/items.js +22 -30
  55. package/lib/session/lifecycle.js +151 -54
  56. package/lib/session/load.js +19 -33
  57. package/lib/session/navigation.js +12 -1
  58. package/lib/session/ops.js +28 -41
  59. package/lib/session/pointer.js +57 -57
  60. package/lib/session/review.js +23 -37
  61. package/lib/session/terminal.js +6 -12
  62. package/lib/session/todos.js +37 -13
  63. package/lib/session/unit.js +208 -0
  64. package/lib/session/update.js +33 -46
  65. package/lib/session/view.js +78 -9
  66. package/lib/session/watch.js +128 -24
  67. package/lib/session.js +22 -246
  68. package/lib/source.js +3 -3
  69. package/lib/update.js +40 -75
  70. package/lib/{git/proc.js → utilities.js} +75 -11
  71. package/lib/wrap.js +1 -3
  72. package/package.json +2 -2
  73. package/lib/deps/items.js +0 -134
  74. package/lib/deps/manifest.js +0 -152
  75. package/lib/deps/merge.js +0 -120
  76. package/lib/deps/proposals.js +0 -164
  77. package/lib/deps/reports.js +0 -180
  78. package/lib/deps/rows.js +0 -176
  79. package/lib/deps/scan.js +0 -138
  80. package/lib/deps/usage.js +0 -68
  81. package/lib/deps/versions.js +0 -119
  82. package/lib/diff.js +0 -33
  83. package/lib/git/changes.js +0 -164
  84. package/lib/git/read.js +0 -253
  85. package/lib/git/run.js +0 -100
  86. package/lib/npm/commands.js +0 -100
  87. package/lib/remote/changes.js +0 -37
  88. package/lib/remote/headers.js +0 -36
  89. package/lib/remote/http.js +0 -122
  90. package/lib/remote/notes.js +0 -94
  91. package/lib/review/format.js +0 -434
  92. package/lib/review/storage.js +0 -150
  93. package/lib/review/store.js +0 -178
  94. package/lib/review/templates.js +0 -49
  95. package/lib/session/origins.js +0 -7
  96. package/lib/sys.js +0 -84
@@ -4,12 +4,13 @@ const fs = require('node:fs');
4
4
  const path = require('node:path');
5
5
 
6
6
  const { isAbortError } = require('./load.js');
7
- const { createDiskWatcher, POLL_MS } = require('./watch.js');
7
+ const { createDiskWatcher, POLL_MS, DEBOUNCE_MS } = require('./watch.js');
8
8
  const files = require('../files.js');
9
- const { itemPath, relativeAge } = files;
10
- const { MANIFEST, LOCKFILE } = require('../deps/manifest.js');
9
+ const { itemPath, relativeAge, TODO_FILE } = files;
10
+ const { MANIFEST, LOCKFILE, depFileMeta } = require('../deps.js');
11
11
  const review = require('../review.js');
12
- const { hasNotes } = review;
12
+ const { hasNotes, loadReview } = review;
13
+ const { mergeReview } = require('../review-merge.js');
13
14
 
14
15
  const loadOptions = (api, extra = {}) => {
15
16
  const { loadConfig, ui } = api;
@@ -30,8 +31,6 @@ const shouldDeferExtras = (api) => {
30
31
  return audit && (waitingOutdated || waitingAudit);
31
32
  };
32
33
 
33
- const watchOpts = (api) => api.watch ?? {};
34
-
35
34
  const endReadLoop = (api) => {
36
35
  const finish = api.ui.finishReadLoop;
37
36
  if (finish) finish();
@@ -50,8 +49,6 @@ const kindFromExtra = (extra = {}) => {
50
49
  return 'reload';
51
50
  };
52
51
 
53
- const shouldResetNav = (kind) => kind !== 'open' && kind !== 'watch';
54
-
55
52
  const finishIfEmpty = (api, kind) => {
56
53
  const { collection, review: notes, ui, nav } = api;
57
54
  if (collection.items.length) return;
@@ -125,9 +122,25 @@ const stampItemDates = (items, dates) => {
125
122
  }
126
123
  };
127
124
 
125
+ const syncUnitEditor = (api) => {
126
+ const composer = api.ui.composer;
127
+ if (!composer || typeof composer.applyDiskText !== 'function') return;
128
+ if (api.nav.pane !== 'unit') return;
129
+ const rel = api.nav.reviewPath;
130
+ if (!rel) return;
131
+ const repo = api.ui.repo;
132
+ if (!repo || typeof repo.fileText !== 'function') return;
133
+ const text = repo.fileText(api.ui.top || api.loadConfig.cwd, rel, api.ui.rev);
134
+ composer.applyDiskText(text);
135
+ };
136
+
128
137
  const syncItemWatches = (api) => {
129
138
  if (!api.diskWatcher) return;
130
- api.diskWatcher.watchFiles(itemRels(api.collection));
139
+ const rels = itemRels(api.collection);
140
+ if (api.nav.pane === 'unit' && api.nav.reviewPath) {
141
+ if (!rels.includes(api.nav.reviewPath)) rels.push(api.nav.reviewPath);
142
+ }
143
+ api.diskWatcher.watchFiles(rels);
131
144
  };
132
145
 
133
146
  const applyLoaded = (api, loaded) => {
@@ -160,6 +173,7 @@ const applyLoaded = (api, loaded) => {
160
173
  markLoaded(api);
161
174
  loader.pendingExtras = loaded.pending === true;
162
175
  syncItemWatches(api);
176
+ syncUnitEditor(api);
163
177
  return true;
164
178
  };
165
179
 
@@ -173,23 +187,42 @@ const load = (api) => {
173
187
  applyLoaded(api, loaded);
174
188
  };
175
189
 
176
- const fileStamp = (top, rel, statSync) => {
190
+ const fileStamp = (top, rel) => {
177
191
  const full = path.join(top, rel);
178
192
  try {
179
- const stat = statSync(full);
193
+ const stat = fs.statSync(full);
180
194
  return `${stat.mtimeMs}:${stat.size}`;
181
195
  } catch {
182
196
  return 'missing';
183
197
  }
184
198
  };
185
199
 
200
+ const npmRels = (items) => {
201
+ const rels = [MANIFEST, LOCKFILE];
202
+ for (const item of items) {
203
+ const rel = itemPath(item);
204
+ if (!depFileMeta(rel)) continue;
205
+ if (rels.includes(rel)) continue;
206
+ rels.push(rel);
207
+ }
208
+ rels.sort();
209
+ return rels;
210
+ };
211
+
186
212
  const npmStamp = (api) => {
187
- const extra = watchOpts(api);
188
- const statSync = extra.statSync ?? fs.statSync;
189
213
  const top = api.ui.top || api.loadConfig.cwd;
190
- const pkg = fileStamp(top, MANIFEST, statSync);
191
- const lock = fileStamp(top, LOCKFILE, statSync);
192
- return `${pkg}|${lock}`;
214
+ const rels = npmRels(api.collection.items);
215
+ const parts = new Array(rels.length);
216
+ for (let i = 0; i < rels.length; i++) {
217
+ parts[i] = `${rels[i]}:${fileStamp(top, rels[i])}`;
218
+ }
219
+ return parts.join('|');
220
+ };
221
+
222
+ const npmExtrasStale = (api) => {
223
+ const cached = api.npmExtras;
224
+ if (!cached || !api.loadConfig.audit) return false;
225
+ return cached.stamp !== npmStamp(api);
193
226
  };
194
227
 
195
228
  const cacheNpmExtras = (api, loaded) => {
@@ -216,8 +249,7 @@ const followExtras = async (api, loaded, gen, kind) => {
216
249
  next = await api.loader.fetchExtras(api.loadConfig.repo, loaded, extra);
217
250
  } catch (error) {
218
251
  if (!api.loader.isCurrent(gen)) return;
219
- failOpen(api, error);
220
- return;
252
+ return void failOpen(api, error);
221
253
  }
222
254
  if (!api.loader.isCurrent(gen)) return;
223
255
  if (api.ui.done) {
@@ -241,7 +273,6 @@ const followExtras = async (api, loaded, gen, kind) => {
241
273
  const npmReuseOpts = (api, extra = {}) => {
242
274
  const opts = {};
243
275
  if (extra.signal) opts.signal = extra.signal;
244
- if (extra.keepView !== true) return opts;
245
276
  const cached = api.npmExtras;
246
277
  if (!cached) return opts;
247
278
  if (cached.stamp !== npmStamp(api)) return opts;
@@ -267,9 +298,8 @@ const resetNavAfterLoad = (api, extra = {}) => {
267
298
  };
268
299
 
269
300
  const afterSnapshot = (api, kind, extra = {}) => {
270
- if (shouldResetNav(kind)) {
271
- resetNavAfterLoad(api, extra);
272
- return;
301
+ if (kind !== 'open' && kind !== 'watch') {
302
+ return void resetNavAfterLoad(api, extra);
273
303
  }
274
304
  if (extra.afterLoad) extra.afterLoad();
275
305
  };
@@ -296,8 +326,7 @@ const runSnapshot = async (api, gen, kind, extra = {}) => {
296
326
  );
297
327
  } catch (error) {
298
328
  if (!api.loader.isCurrent(gen)) return;
299
- failOpen(api, error);
300
- return;
329
+ return void failOpen(api, error);
301
330
  }
302
331
  if (!api.loader.isCurrent(gen)) return;
303
332
  if (api.ui.done) {
@@ -306,7 +335,7 @@ const runSnapshot = async (api, gen, kind, extra = {}) => {
306
335
  }
307
336
  applyLoaded(api, api.loader.tag(loaded, gen));
308
337
  afterSnapshot(api, kind, extra);
309
- if (loaded.pending) {
338
+ if (loaded.pending === true || npmExtrasStale(api)) {
310
339
  api.ops.setBusy('checking npm');
311
340
  if (kind === 'open' || extra.quiet === true) api.ui.paint();
312
341
  await followExtras(api, loaded, gen, kind);
@@ -352,10 +381,9 @@ const refreshFromRepo = (api, extra = {}) => {
352
381
  );
353
382
  applyLoaded(api, api.loader.tag(loaded, gen));
354
383
  afterSnapshot(api, kind, extra);
355
- if (loaded.pending) {
384
+ if (loaded.pending === true || npmExtrasStale(api)) {
356
385
  api.ops.setBusy('checking npm');
357
- followExtras(api, loaded, gen, kind);
358
- return;
386
+ return void followExtras(api, loaded, gen, kind);
359
387
  }
360
388
  if (kind === 'keep') return;
361
389
  finishIfEmpty(api, kind);
@@ -377,8 +405,7 @@ const reloadAfterChange = (api, afterLoad) => {
377
405
  if (afterLoad) afterLoad();
378
406
  };
379
407
  if (api.ui.uiOpen && api.loadConfig.repo.loadAsync) {
380
- refreshFromRepo(api, { afterLoad: restore });
381
- return;
408
+ return void refreshFromRepo(api, { afterLoad: restore });
382
409
  }
383
410
  refreshFromRepo(api);
384
411
  restore();
@@ -392,9 +419,7 @@ const onReload = (api) => {
392
419
 
393
420
  const clearWatchRetry = (api) => {
394
421
  if (api.watchRetry === null) return;
395
- const extra = watchOpts(api);
396
- const clearTimeoutFn = extra.clearTimeout ?? clearTimeout;
397
- clearTimeoutFn(api.watchRetry);
422
+ clearTimeout(api.watchRetry);
398
423
  api.watchRetry = null;
399
424
  };
400
425
 
@@ -404,6 +429,10 @@ const shouldDeferWatch = (api) => {
404
429
  if (!ui.uiOpen) return true;
405
430
  if (!api.loader.didLoad) return true;
406
431
  if (ops.gitBusy) return true;
432
+ if (ui.mode === 'compose') {
433
+ const kind = ui.composer && ui.composer.state.composeKind;
434
+ if (kind === 'file') return false;
435
+ }
407
436
  if (ui.mode !== 'review') return true;
408
437
  return false;
409
438
  };
@@ -411,44 +440,64 @@ const shouldDeferWatch = (api) => {
411
440
  const WATCH_IGNORE_MS = 500;
412
441
 
413
442
  const ignoreWatch = (api, ms = WATCH_IGNORE_MS) => {
414
- const extra = watchOpts(api);
415
- const now = extra.now ?? Date.now;
416
- api.ignoreWatchUntil = now() + ms;
443
+ api.ignoreWatchUntil = Date.now() + ms;
417
444
  };
418
445
 
419
446
  const onDiskChange = (api) => {
420
- const extra = watchOpts(api);
421
- const now = extra.now ?? Date.now;
422
- if (now() < (api.ignoreWatchUntil ?? 0)) return;
447
+ if (Date.now() < (api.ignoreWatchUntil ?? 0)) return;
423
448
  if (shouldDeferWatch(api)) {
424
449
  if (api.watchRetry !== null) return;
425
- const setTimeoutFn = extra.setTimeout ?? setTimeout;
426
- const delay = extra.debounceMs ?? 200;
427
- api.watchRetry = setTimeoutFn(() => {
450
+ api.watchRetry = setTimeout(() => {
428
451
  api.watchRetry = null;
429
452
  onDiskChange(api);
430
- }, delay);
453
+ }, DEBOUNCE_MS);
454
+ if (typeof api.watchRetry.unref === 'function') api.watchRetry.unref();
431
455
  return;
432
456
  }
433
457
  refreshFromRepo(api, { keepView: true, quiet: true });
434
458
  };
435
459
 
436
- const currentFileStamp = (api) => {
437
- if (api.nav.pane !== 'diff') return '';
438
- const rel = itemPath(api.ui.current());
439
- if (!rel) return '';
440
- const extra = watchOpts(api);
441
- const statSync = extra.statSync ?? fs.statSync;
442
- const full = path.join(api.ui.top || api.loadConfig.cwd, rel);
460
+ const catchUpWatch = (api) => {
461
+ clearWatchRetry(api);
462
+ if (shouldDeferWatch(api)) return void onDiskChange(api);
463
+ refreshFromRepo(api, { keepView: true, quiet: true });
464
+ };
465
+
466
+ const stampFile = (root, rel) => {
467
+ const full = path.join(root, rel);
443
468
  let stat;
444
469
  try {
445
- stat = statSync(full);
470
+ stat = fs.statSync(full);
446
471
  } catch {
447
472
  return `${rel}:missing`;
448
473
  }
449
474
  return `${rel}:${stat.mtimeMs}:${stat.size}`;
450
475
  };
451
476
 
477
+ const worktreeStamp = (api) => {
478
+ const root = api.ui.top || api.loadConfig.cwd;
479
+ const parts = [];
480
+ for (const rel of itemRels(api.collection)) {
481
+ if (!rel || rel === TODO_FILE) continue;
482
+ parts.push(stampFile(root, rel));
483
+ }
484
+ return parts.join('\n');
485
+ };
486
+
487
+ const watchesWorktree = (nav) =>
488
+ nav.todoOpen === true || nav.pane === 'branches' || nav.pane === 'commits';
489
+
490
+ const currentFileStamp = (api) => {
491
+ const { nav } = api;
492
+ const root = api.ui.top || api.loadConfig.cwd;
493
+ if (watchesWorktree(nav)) return worktreeStamp(api);
494
+ const pane = nav.pane;
495
+ if (pane !== 'diff' && pane !== 'unit') return '';
496
+ const rel = pane === 'unit' ? nav.reviewPath : itemPath(api.ui.current());
497
+ if (!rel) return '';
498
+ return stampFile(root, rel);
499
+ };
500
+
452
501
  const pollCurrentFile = (api) => {
453
502
  const stamp = currentFileStamp(api);
454
503
  if (!stamp) {
@@ -464,9 +513,46 @@ const pollCurrentFile = (api) => {
464
513
  onDiskChange(api);
465
514
  };
466
515
 
516
+ const reviewFileStamp = (api) => {
517
+ const store = api.review && api.review.store;
518
+ if (!store || !store.reviewPath) return '';
519
+ try {
520
+ const stat = fs.statSync(store.reviewPath);
521
+ return `${stat.mtimeMs}:${stat.size}`;
522
+ } catch {
523
+ return 'missing';
524
+ }
525
+ };
526
+
527
+ const noteReviewStamp = (api) => {
528
+ api.reviewStamp = reviewFileStamp(api);
529
+ };
530
+
531
+ const applyReviewChange = (api) => {
532
+ if (!api.ui || api.ui.done) return;
533
+ const stamp = reviewFileStamp(api);
534
+ if (!stamp || stamp === api.reviewStamp) return;
535
+ if (stamp === 'missing') {
536
+ api.reviewStamp = stamp;
537
+ return;
538
+ }
539
+ const store = api.review && api.review.store;
540
+ if (!store) return;
541
+ let disk;
542
+ try {
543
+ disk = loadReview(store.reviewPath, store.templates);
544
+ } catch {
545
+ return;
546
+ }
547
+ mergeReview(store, disk);
548
+ api.reviewStamp = stamp;
549
+ if (typeof api.ui.paint === 'function') api.ui.paint();
550
+ };
551
+
467
552
  const stopWatch = (api) => {
468
553
  clearWatchRetry(api);
469
554
  api.watchStamp = '';
555
+ api.reviewStamp = '';
470
556
  if (api.ui.term) api.ui.term.stopTimer('watch');
471
557
  if (!api.diskWatcher) return;
472
558
  api.diskWatcher.close();
@@ -478,22 +564,30 @@ const startWatch = (api) => {
478
564
  if (!isWorktree(api.ui)) return;
479
565
  const root = api.ui.top || api.loadConfig.cwd;
480
566
  if (!root) return;
481
- const extra = watchOpts(api);
482
567
  api.diskWatcher = createDiskWatcher({
483
568
  root,
484
569
  paths: api.loadConfig.paths,
485
570
  onChange: () => onDiskChange(api),
486
- ...extra,
571
+ onReview: () => applyReviewChange(api),
487
572
  });
488
573
  syncItemWatches(api);
574
+ noteReviewStamp(api);
489
575
  if (!api.ui.term) return;
490
- api.ui.term.startTimer('watch', () => pollCurrentFile(api), POLL_MS);
576
+ api.ui.term.startTimer(
577
+ 'watch',
578
+ () => {
579
+ pollCurrentFile(api);
580
+ applyReviewChange(api);
581
+ },
582
+ POLL_MS,
583
+ );
491
584
  };
492
585
 
493
586
  const createLifecycle = (api) => {
494
587
  api.diskWatcher = null;
495
588
  api.watchRetry = null;
496
589
  api.watchStamp = '';
590
+ api.reviewStamp = '';
497
591
  api.ignoreWatchUntil = 0;
498
592
  api.npmExtras = null;
499
593
  return {
@@ -505,6 +599,9 @@ const createLifecycle = (api) => {
505
599
  reloadAfterChange: (afterLoad) => reloadAfterChange(api, afterLoad),
506
600
  onReload: () => onReload(api),
507
601
  onDiskChange: () => onDiskChange(api),
602
+ applyReviewChange: () => applyReviewChange(api),
603
+ noteReviewStamp: () => noteReviewStamp(api),
604
+ catchUpWatch: () => catchUpWatch(api),
508
605
  pollCurrentFile: () => pollCurrentFile(api),
509
606
  startWatch: () => startWatch(api),
510
607
  stopWatch: () => stopWatch(api),
@@ -1,5 +1,7 @@
1
1
  'use strict';
2
2
 
3
+ const { stateView } = require('./accessors.js');
4
+
3
5
  const isAbortError = (error) => !!(error && error.code === 'ABORT');
4
6
 
5
7
  const createLoadCoordinator = () => {
@@ -46,40 +48,24 @@ const createLoadCoordinator = () => {
46
48
  return repo.loadExtras(loaded, extra);
47
49
  };
48
50
 
49
- return {
50
- get gen() {
51
- return state.gen;
52
- },
53
- get promise() {
54
- return state.promise;
55
- },
56
- set promise(value) {
57
- state.promise = value;
58
- },
59
- get pendingExtras() {
60
- return state.pendingExtras;
61
- },
62
- set pendingExtras(value) {
63
- state.pendingExtras = value;
51
+ return stateView(
52
+ state,
53
+ {
54
+ get signal() {
55
+ return state.abort?.signal;
56
+ },
57
+ bump,
58
+ isCurrent,
59
+ accept,
60
+ abort,
61
+ reset,
62
+ tag,
63
+ fetchSnapshot,
64
+ fetchExtras,
64
65
  },
65
- get didLoad() {
66
- return state.didLoad;
67
- },
68
- set didLoad(value) {
69
- state.didLoad = value;
70
- },
71
- get signal() {
72
- return state.abort?.signal;
73
- },
74
- bump,
75
- isCurrent,
76
- accept,
77
- abort,
78
- reset,
79
- tag,
80
- fetchSnapshot,
81
- fetchExtras,
82
- };
66
+ ['promise', 'pendingExtras', 'didLoad'],
67
+ ['gen'],
68
+ );
83
69
  };
84
70
 
85
71
  module.exports = { isAbortError, createLoadCoordinator };
@@ -12,6 +12,8 @@ const initialState = (options = {}) => ({
12
12
  scroll: 0,
13
13
  listScroll: 0,
14
14
  reviewPath: null,
15
+ fileScope: 'diff',
16
+ unitLine: 0,
15
17
  selection: null,
16
18
  mouseAnchor: null,
17
19
  pendingClick: null,
@@ -30,11 +32,20 @@ const clampIndex = (nav, length) => {
30
32
 
31
33
  const current = (nav, items, todoItem) => {
32
34
  if (nav.todoOpen) return todoItem;
35
+ if (nav.pane === 'unit' && nav.reviewPath) {
36
+ const item = items[nav.index];
37
+ if (item && listPath(item) === nav.reviewPath) return item;
38
+ for (const entry of items) {
39
+ if (listPath(entry) === nav.reviewPath) return entry;
40
+ }
41
+ return null;
42
+ }
33
43
  return items[nav.index] ?? null;
34
44
  };
35
45
 
36
46
  const syncReviewPath = (nav, item) => {
37
- if (nav.pane !== 'diff') return;
47
+ if (nav.pane !== 'diff' && nav.pane !== 'unit') return;
48
+ if (nav.pane === 'unit' && nav.reviewPath) return;
38
49
  nav.reviewPath = item ? listPath(item) : null;
39
50
  };
40
51
 
@@ -1,5 +1,7 @@
1
1
  'use strict';
2
2
 
3
+ const { stateView } = require('./accessors.js');
4
+
3
5
  const ASYNC_WITHOUT_UI = ['pull', 'push'];
4
6
 
5
7
  const pickRepoWrite = (repo, name, uiOpen) => {
@@ -18,7 +20,7 @@ const pickRepoWrite = (repo, name, uiOpen) => {
18
20
  };
19
21
  };
20
22
 
21
- const createOpsRunner = (options) => {
23
+ const createOpsRunner = (ui) => {
22
24
  const state = {
23
25
  gitBusy: false,
24
26
  busy: '',
@@ -31,53 +33,46 @@ const createOpsRunner = (options) => {
31
33
 
32
34
  const setBusy = (kind) => {
33
35
  state.busy = kind;
34
- options.startProgress();
36
+ ui.startProgress();
35
37
  };
36
38
 
37
39
  const clearBusy = () => {
38
40
  state.busy = '';
39
- options.stopProgress();
41
+ ui.stopProgress();
40
42
  };
41
43
 
42
44
  const runRepo = (label, write) => {
43
45
  try {
44
- write(options.top());
45
- options.setStatus(label);
46
+ write(ui.top);
47
+ ui.status = label;
46
48
  return true;
47
49
  } catch (error) {
48
- options.setStatus(error.message);
50
+ ui.status = error.message;
49
51
  return false;
50
52
  }
51
53
  };
52
54
 
53
55
  const finishBusy = (done, after) => {
54
- if (options.isDone()) {
55
- options.paint();
56
- return;
57
- }
58
- if (options.getMode() === 'confirmPush') {
59
- options.paint();
60
- return;
61
- }
62
- if (!options.getStatus() && after) after();
63
- if (!options.getStatus() && !state.busy) options.setStatus(done);
64
- options.paint();
56
+ if (ui.done || ui.mode === 'confirmPush') return void ui.paint();
57
+ if (!ui.status && after) after();
58
+ if (!ui.status && !state.busy) ui.status = done;
59
+ ui.paint();
65
60
  };
66
61
 
67
62
  const runBusy = async (kind, done, write, after) => {
68
63
  if (state.gitBusy) return;
69
64
  state.gitBusy = true;
70
- options.setStatus('');
71
- options.resetProgressFrame();
65
+ ui.status = '';
66
+ ui.progressFrame = 0;
72
67
  setBusy(kind);
73
- options.paint();
68
+ ui.paint();
74
69
  try {
75
- await write(options.top());
70
+ await write(ui.top);
76
71
  } catch (error) {
77
72
  if (error.rejected && kind !== 'force pushing') {
78
- options.setMode('confirmPush');
73
+ ui.mode = 'confirmPush';
79
74
  } else {
80
- options.setStatus(error.message);
75
+ ui.status = error.message;
81
76
  }
82
77
  } finally {
83
78
  clearBusy();
@@ -86,25 +81,17 @@ const createOpsRunner = (options) => {
86
81
  finishBusy(done, after);
87
82
  };
88
83
 
89
- return {
90
- get gitBusy() {
91
- return state.gitBusy;
84
+ return stateView(
85
+ state,
86
+ {
87
+ reset,
88
+ setBusy,
89
+ clearBusy,
90
+ runRepo,
91
+ runBusy,
92
92
  },
93
- set gitBusy(value) {
94
- state.gitBusy = value;
95
- },
96
- get busy() {
97
- return state.busy;
98
- },
99
- set busy(value) {
100
- state.busy = value;
101
- },
102
- reset,
103
- setBusy,
104
- clearBusy,
105
- runRepo,
106
- runBusy,
107
- };
93
+ ['gitBusy', 'busy'],
94
+ );
108
95
  };
109
96
 
110
97
  module.exports = { ASYNC_WITHOUT_UI, pickRepoWrite, createOpsRunner };