reslop 0.1.2 → 0.1.4
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 +18 -1
- package/README.md +38 -36
- package/lib/api.js +3 -0
- package/lib/cli.js +53 -16
- package/lib/clipboard.js +11 -11
- package/lib/detect.js +2 -0
- package/lib/files.js +36 -12
- package/lib/git.js +335 -65
- package/lib/gitlab.js +520 -0
- package/lib/highlight.js +160 -5
- package/lib/keys.js +8 -2
- package/lib/render.js +87 -30
- package/lib/session.js +227 -44
- package/lib/source.js +5 -2
- package/lib/sys.js +59 -0
- package/package.json +2 -2
package/lib/keys.js
CHANGED
|
@@ -4,8 +4,8 @@ const ACTIONS = {
|
|
|
4
4
|
add: { keys: ['a'], label: 'Add' },
|
|
5
5
|
unstage: { keys: ['u'], label: 'Unstage' },
|
|
6
6
|
revert: { keys: ['r'], label: 'Revert' },
|
|
7
|
-
prev: { keys: ['left', 'p'], hot: '←', label: 'Prev' },
|
|
8
|
-
next: { keys: ['right', 'n'], hot: '→', label: 'Next' },
|
|
7
|
+
prev: { keys: ['left', 'p', 'k'], hot: '←', label: 'Prev' },
|
|
8
|
+
next: { keys: ['right', 'n', 'j'], hot: '→', label: 'Next' },
|
|
9
9
|
layout: { keys: ['m'], label: 'Mode' },
|
|
10
10
|
files: { keys: ['l'], label: 'Files' },
|
|
11
11
|
feedback: { keys: ['f'], label: 'Feedback' },
|
|
@@ -19,6 +19,12 @@ const FILES_DISABLED = ['layout', 'feedback'];
|
|
|
19
19
|
const SCROLL = {
|
|
20
20
|
up: 'scrollUp',
|
|
21
21
|
down: 'scrollDown',
|
|
22
|
+
'ctrl-y': 'scrollUp',
|
|
23
|
+
'ctrl-e': 'scrollDown',
|
|
24
|
+
'ctrl-b': 'pageUp',
|
|
25
|
+
'ctrl-f': 'pageDown',
|
|
26
|
+
'ctrl-u': 'halfUp',
|
|
27
|
+
'ctrl-d': 'halfDown',
|
|
22
28
|
enter: 'open',
|
|
23
29
|
backspace: 'removeTodo',
|
|
24
30
|
delete: 'removeTodo',
|
package/lib/render.js
CHANGED
|
@@ -103,6 +103,16 @@ const FILE_STATUS_FG = {
|
|
|
103
103
|
partial: THEME.warnFg,
|
|
104
104
|
};
|
|
105
105
|
|
|
106
|
+
const FILE_COL_KEYS = ['add', 'del', 'status', 'n', 'm'];
|
|
107
|
+
|
|
108
|
+
const FILE_COL_ALIGN = {
|
|
109
|
+
add: 'start',
|
|
110
|
+
del: 'start',
|
|
111
|
+
status: 'start',
|
|
112
|
+
n: 'start',
|
|
113
|
+
m: 'end',
|
|
114
|
+
};
|
|
115
|
+
|
|
106
116
|
const INFO_STATUS = [
|
|
107
117
|
'copied',
|
|
108
118
|
'staged',
|
|
@@ -504,12 +514,15 @@ const paintButtons = (layout, width, color) => {
|
|
|
504
514
|
return out;
|
|
505
515
|
};
|
|
506
516
|
|
|
517
|
+
const remoteCounts = (kind, { counts, notes, sourceLabel }) => {
|
|
518
|
+
const n = counts.pr ?? 0;
|
|
519
|
+
const label = sourceLabel || kind;
|
|
520
|
+
return ` ${kind} ${label} ${n} ${notes}`;
|
|
521
|
+
};
|
|
522
|
+
|
|
507
523
|
const COUNTS_KIND = {
|
|
508
|
-
pr: (
|
|
509
|
-
|
|
510
|
-
const label = sourceLabel || 'pr';
|
|
511
|
-
return ` pr ${label} ${n} ${notes}`;
|
|
512
|
-
},
|
|
524
|
+
pr: (args) => remoteCounts('pr', args),
|
|
525
|
+
mr: (args) => remoteCounts('mr', args),
|
|
513
526
|
commit: ({ counts, notes, revShort }) => {
|
|
514
527
|
const n = counts.commit ?? 0;
|
|
515
528
|
return ` commit ${revShort} ${n} ${notes}`;
|
|
@@ -525,7 +538,7 @@ const COUNTS_KIND = {
|
|
|
525
538
|
};
|
|
526
539
|
|
|
527
540
|
const countsKind = (sourceKind, revShort) => {
|
|
528
|
-
if (sourceKind === 'pr') return
|
|
541
|
+
if (sourceKind === 'pr' || sourceKind === 'mr') return sourceKind;
|
|
529
542
|
if (revShort) return 'commit';
|
|
530
543
|
return 'worktree';
|
|
531
544
|
};
|
|
@@ -575,23 +588,36 @@ const paintStatusLine = (view, status, width, color) => {
|
|
|
575
588
|
);
|
|
576
589
|
};
|
|
577
590
|
|
|
591
|
+
const signedCount = (sign, n) => `${sign}${n ?? 0}`;
|
|
592
|
+
|
|
593
|
+
const fileRowFields = (entry) => {
|
|
594
|
+
const { added, removed, staged, remaining } = entry;
|
|
595
|
+
const add = signedCount('+', added);
|
|
596
|
+
const del = signedCount('-', removed);
|
|
597
|
+
const status = entry.status ?? '';
|
|
598
|
+
const n = `${staged ?? 0}`;
|
|
599
|
+
const m = `${remaining ?? 0}`;
|
|
600
|
+
return { add, del, status, n, m };
|
|
601
|
+
};
|
|
602
|
+
|
|
578
603
|
const fileColumns = (files) => {
|
|
579
|
-
|
|
580
|
-
|
|
604
|
+
const widths = {};
|
|
605
|
+
for (const key of FILE_COL_KEYS) widths[key] = 0;
|
|
581
606
|
for (const entry of files) {
|
|
582
|
-
const
|
|
583
|
-
const
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
if (cw > countW) countW = cw;
|
|
607
|
+
const fields = fileRowFields(entry);
|
|
608
|
+
for (const key of FILE_COL_KEYS) {
|
|
609
|
+
const w = visibleWidth(fields[key]);
|
|
610
|
+
if (w > widths[key]) widths[key] = w;
|
|
611
|
+
}
|
|
588
612
|
}
|
|
589
|
-
return
|
|
613
|
+
return widths;
|
|
590
614
|
};
|
|
591
615
|
|
|
592
|
-
const
|
|
616
|
+
const padVisible = (text, width, align) => {
|
|
593
617
|
const n = Math.max(0, width - visibleWidth(text));
|
|
594
|
-
|
|
618
|
+
const gap = ' '.repeat(n);
|
|
619
|
+
if (align === 'end') return `${text}${gap}`;
|
|
620
|
+
return `${gap}${text}`;
|
|
595
621
|
};
|
|
596
622
|
|
|
597
623
|
const fileRowFg = (status, selected) => {
|
|
@@ -601,25 +627,55 @@ const fileRowFg = (status, selected) => {
|
|
|
601
627
|
return THEME.mutedFg;
|
|
602
628
|
};
|
|
603
629
|
|
|
630
|
+
const fileRowParts = (entry, cols) => {
|
|
631
|
+
const fields = fileRowFields(entry);
|
|
632
|
+
const parts = {};
|
|
633
|
+
for (const key of FILE_COL_KEYS) {
|
|
634
|
+
const width = cols?.[key] ?? visibleWidth(fields[key]);
|
|
635
|
+
const align = FILE_COL_ALIGN[key];
|
|
636
|
+
parts[key] = padVisible(fields[key], width, align);
|
|
637
|
+
}
|
|
638
|
+
return parts;
|
|
639
|
+
};
|
|
640
|
+
|
|
641
|
+
const joinFileSuffix = (parts) => {
|
|
642
|
+
const { add, del, status, n, m } = parts;
|
|
643
|
+
return ` ${add} ${del} ${status} ${n}/${m}`;
|
|
644
|
+
};
|
|
645
|
+
|
|
646
|
+
const paintFileSuffix = (parts, fgRgb, bgRgb, color) => {
|
|
647
|
+
const { add, del, status, n, m } = parts;
|
|
648
|
+
const gap = paint(' ', fgRgb, bgRgb, color);
|
|
649
|
+
const addText = paint(add, THEME.addLineFg, bgRgb, color);
|
|
650
|
+
const delText = paint(del, THEME.delLineFg, bgRgb, color);
|
|
651
|
+
const statusText = paint(status, fgRgb, bgRgb, color);
|
|
652
|
+
const nText = paint(n, fgRgb, bgRgb, color);
|
|
653
|
+
const slash = paint('/', fgRgb, bgRgb, color);
|
|
654
|
+
const mText = paint(m, fgRgb, bgRgb, color);
|
|
655
|
+
const counts = addText + gap + delText;
|
|
656
|
+
const meta = statusText + gap + nText + slash + mText;
|
|
657
|
+
return gap + counts + gap + meta;
|
|
658
|
+
};
|
|
659
|
+
|
|
604
660
|
const paintFileRow = (entry, width, color, selected, cols) => {
|
|
605
661
|
const inner = Math.max(1, width - 1);
|
|
606
662
|
const mark = selected ? FILE_MARK : ' ';
|
|
607
663
|
const prefix = ` ${mark} `;
|
|
608
|
-
const
|
|
609
|
-
const
|
|
610
|
-
const statusW = cols?.statusW ?? visibleWidth(status);
|
|
611
|
-
const countW = cols?.countW ?? visibleWidth(count);
|
|
612
|
-
const statusCol = padStartVisible(status, statusW);
|
|
613
|
-
const countCol = padStartVisible(count, countW);
|
|
614
|
-
const suffix = ` ${statusCol} ${countCol}`;
|
|
664
|
+
const parts = fileRowParts(entry, cols);
|
|
665
|
+
const suffix = joinFileSuffix(parts);
|
|
615
666
|
const pathW = Math.max(1, inner - visibleWidth(prefix + suffix));
|
|
616
|
-
const
|
|
667
|
+
const { path, status } = entry;
|
|
668
|
+
const clipped = truncateVisible(path, pathW);
|
|
617
669
|
const pad = Math.max(0, pathW - visibleWidth(clipped));
|
|
618
|
-
const
|
|
670
|
+
const left = `${prefix}${clipped}${' '.repeat(pad)}`;
|
|
619
671
|
const fgRgb = fileRowFg(status, selected);
|
|
620
672
|
const bgRgb = selected ? THEME.buttonBg : THEME.ctxBg;
|
|
621
|
-
const
|
|
622
|
-
|
|
673
|
+
const edge = fill(1, fgRgb, bgRgb, color);
|
|
674
|
+
if (!color) return `${left}${suffix}${edge}`;
|
|
675
|
+
let out = `${seq(fgRgb, bgRgb)}${EL}`;
|
|
676
|
+
out += paint(left, fgRgb, bgRgb, color);
|
|
677
|
+
out += paintFileSuffix(parts, fgRgb, bgRgb, color);
|
|
678
|
+
return out + edge;
|
|
623
679
|
};
|
|
624
680
|
|
|
625
681
|
const todoInnerWidth = (width) => Math.max(1, width - 2);
|
|
@@ -950,7 +1006,7 @@ const stackFrame = (view, width, color, rows, layout, windowed, notes) => {
|
|
|
950
1006
|
return overlayRows(rows, view.selection);
|
|
951
1007
|
};
|
|
952
1008
|
|
|
953
|
-
const frameResult = (text, painted, layout, windowed, hits) => {
|
|
1009
|
+
const frameResult = (text, painted, layout, windowed, hits, bodyH) => {
|
|
954
1010
|
const fileHits = windowed.fileHits;
|
|
955
1011
|
const todoHits = windowed.todoHits;
|
|
956
1012
|
const templateHits = hits.templateHits;
|
|
@@ -968,6 +1024,7 @@ const frameResult = (text, painted, layout, windowed, hits) => {
|
|
|
968
1024
|
height: painted.length,
|
|
969
1025
|
scroll,
|
|
970
1026
|
cursor,
|
|
1027
|
+
bodyH,
|
|
971
1028
|
};
|
|
972
1029
|
};
|
|
973
1030
|
|
|
@@ -999,7 +1056,7 @@ const renderFrame = (view, options = {}) => {
|
|
|
999
1056
|
notes.noteCursor,
|
|
1000
1057
|
bodyH,
|
|
1001
1058
|
);
|
|
1002
|
-
return frameResult(text, painted, layout, windowed, hits);
|
|
1059
|
+
return frameResult(text, painted, layout, windowed, hits, bodyH);
|
|
1003
1060
|
};
|
|
1004
1061
|
|
|
1005
1062
|
const presentCursor = (cursor) => {
|
package/lib/session.js
CHANGED
|
@@ -9,6 +9,7 @@ const review = require('./review.js');
|
|
|
9
9
|
const wrap = require('./wrap.js');
|
|
10
10
|
const { extractText, isRange } = require('./select.js');
|
|
11
11
|
const { copyText } = require('./clipboard.js');
|
|
12
|
+
const { watchResize } = require('./sys.js');
|
|
12
13
|
const { fileEntries, itemPath } = require('./files.js');
|
|
13
14
|
const { Editor } = require('./editor.js');
|
|
14
15
|
const { refreshIndexPatches } = require('./diff.js');
|
|
@@ -29,6 +30,10 @@ const { prefixTemplates, TEMPLATE_SHOW, rememberTemplate } = review;
|
|
|
29
30
|
|
|
30
31
|
const LAYOUTS = ['unified', 'mixed', 'side'];
|
|
31
32
|
const READ_ONLY_ORIGINS = ['commit', 'pr'];
|
|
33
|
+
const SOURCE_KIND = {
|
|
34
|
+
'github-pr': 'pr',
|
|
35
|
+
'gitlab-mr': 'mr',
|
|
36
|
+
};
|
|
32
37
|
const ENTER_TERM = '\x1b[?1049h\x1b[?25l\x1b[?7l\x1b[?1002h\x1b[?1006h';
|
|
33
38
|
const LEAVE_TERM =
|
|
34
39
|
'\x1b[?1006l\x1b[?1002l\x1b[?7h\x1b[0 q\x1b[?12l\x1b[?25h\x1b[?1049l';
|
|
@@ -82,6 +87,8 @@ const itemFeedKey = (item) => {
|
|
|
82
87
|
return feedbackKey({ file, oldStart, newStart, blockId });
|
|
83
88
|
};
|
|
84
89
|
|
|
90
|
+
const isAbortError = (error) => !!(error && error.code === 'ABORT');
|
|
91
|
+
|
|
85
92
|
const sameCursor = (a, b) => {
|
|
86
93
|
if (!a && !b) return true;
|
|
87
94
|
if (!a || !b) return false;
|
|
@@ -128,6 +135,10 @@ const HANDLERS = {
|
|
|
128
135
|
quit: (session) => session.onQuit(),
|
|
129
136
|
scrollUp: (session) => session.onScroll(-1),
|
|
130
137
|
scrollDown: (session) => session.onScroll(1),
|
|
138
|
+
pageUp: (session) => session.onScroll(session.scrollStep(-1)),
|
|
139
|
+
pageDown: (session) => session.onScroll(session.scrollStep(1)),
|
|
140
|
+
halfUp: (session) => session.onScroll(session.scrollStep(-0.5)),
|
|
141
|
+
halfDown: (session) => session.onScroll(session.scrollStep(0.5)),
|
|
131
142
|
};
|
|
132
143
|
|
|
133
144
|
const KEY_MODE = {
|
|
@@ -191,6 +202,7 @@ class Session {
|
|
|
191
202
|
this.mkdirSync = options.mkdirSync ?? fs.mkdirSync;
|
|
192
203
|
this.now = options.now ?? systemNow;
|
|
193
204
|
this.forceNewReview = options.newReview === true;
|
|
205
|
+
this.readOnly = options.readOnly === true;
|
|
194
206
|
this.audit = options.audit === true;
|
|
195
207
|
this.outdatedMap = options.outdatedMap;
|
|
196
208
|
this.auditMap = options.auditMap;
|
|
@@ -233,6 +245,16 @@ class Session {
|
|
|
233
245
|
this.saveTimer = null;
|
|
234
246
|
this.blinkTimer = null;
|
|
235
247
|
this.blinkOn = true;
|
|
248
|
+
this.loadGen = 0;
|
|
249
|
+
this.loadAbort = null;
|
|
250
|
+
this.loadPromise = null;
|
|
251
|
+
this.busy = '';
|
|
252
|
+
this.emptyReview = false;
|
|
253
|
+
this.loadError = null;
|
|
254
|
+
this.didLoad = false;
|
|
255
|
+
this.pendingExtras = false;
|
|
256
|
+
this.uiOpen = false;
|
|
257
|
+
this.finishReadLoop = null;
|
|
236
258
|
}
|
|
237
259
|
|
|
238
260
|
constructor(options) {
|
|
@@ -240,14 +262,35 @@ class Session {
|
|
|
240
262
|
this.resetState();
|
|
241
263
|
}
|
|
242
264
|
|
|
243
|
-
|
|
244
|
-
const here = itemKey(this.items[this.index]);
|
|
265
|
+
loadOptions(extra = {}) {
|
|
245
266
|
const commit = this.rev;
|
|
246
267
|
const audit = this.audit;
|
|
247
268
|
const outdatedMap = this.outdatedMap;
|
|
248
269
|
const auditMap = this.auditMap;
|
|
249
|
-
|
|
250
|
-
|
|
270
|
+
return { commit, audit, outdatedMap, auditMap, ...extra };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
shouldDeferExtras() {
|
|
274
|
+
if (!this.audit) return false;
|
|
275
|
+
if (this.outdatedMap !== undefined && this.auditMap !== undefined) {
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
return true;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
bumpLoad() {
|
|
282
|
+
this.loadGen += 1;
|
|
283
|
+
if (this.loadAbort) this.loadAbort.abort();
|
|
284
|
+
this.loadAbort = new AbortController();
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
ensureRepo() {
|
|
288
|
+
if (!this.repo.toplevel) return;
|
|
289
|
+
this.top = this.repo.toplevel(this.cwd);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
applyLoaded(loaded) {
|
|
293
|
+
const here = itemKey(this.items[this.index]);
|
|
251
294
|
this.top = loaded.top ?? this.cwd;
|
|
252
295
|
const items = loaded.items;
|
|
253
296
|
this.items = items.filter((item) => !this.dismissed.has(itemKey(item)));
|
|
@@ -277,6 +320,121 @@ class Session {
|
|
|
277
320
|
this.clampIndex();
|
|
278
321
|
this.syncReviewPath();
|
|
279
322
|
this.syncFileCursor();
|
|
323
|
+
this.didLoad = true;
|
|
324
|
+
this.pendingExtras = loaded.pending === true;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
load() {
|
|
328
|
+
const loaded = this.repo.load(this.cwd, this.paths, this.loadOptions());
|
|
329
|
+
this.applyLoaded(loaded);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
paint() {
|
|
333
|
+
if (!this.uiOpen) return;
|
|
334
|
+
this.draw();
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
viewStatus() {
|
|
338
|
+
if (this.status) return this.status;
|
|
339
|
+
return this.busy;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
finishIfEmpty(kind) {
|
|
343
|
+
if (this.items.length) return;
|
|
344
|
+
if (this.notes && hasNotes(this.notes)) this.flushReview();
|
|
345
|
+
this.done = true;
|
|
346
|
+
this.exitCode = 0;
|
|
347
|
+
this.status = 'nothing to review';
|
|
348
|
+
this.emptyReview = kind === 'open';
|
|
349
|
+
if (this.finishReadLoop) this.finishReadLoop();
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
failOpen(error) {
|
|
353
|
+
if (isAbortError(error)) return;
|
|
354
|
+
this.loadError = error;
|
|
355
|
+
this.done = true;
|
|
356
|
+
this.exitCode = 1;
|
|
357
|
+
this.busy = '';
|
|
358
|
+
if (this.finishReadLoop) this.finishReadLoop();
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
async followExtras(loaded, gen, kind) {
|
|
362
|
+
if (!this.repo.loadExtras) {
|
|
363
|
+
this.busy = '';
|
|
364
|
+
this.pendingExtras = false;
|
|
365
|
+
this.paint();
|
|
366
|
+
this.finishIfEmpty(kind);
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
const signal = this.loadAbort ? this.loadAbort.signal : undefined;
|
|
370
|
+
const extra = this.loadOptions({
|
|
371
|
+
paths: this.paths,
|
|
372
|
+
signal,
|
|
373
|
+
});
|
|
374
|
+
let next;
|
|
375
|
+
try {
|
|
376
|
+
next = await this.repo.loadExtras(loaded, extra);
|
|
377
|
+
} catch (error) {
|
|
378
|
+
if (gen !== this.loadGen) return;
|
|
379
|
+
this.failOpen(error);
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
if (gen !== this.loadGen || this.done) return;
|
|
383
|
+
this.applyLoaded(next);
|
|
384
|
+
this.busy = '';
|
|
385
|
+
this.paint();
|
|
386
|
+
this.finishIfEmpty(kind);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
openLoad() {
|
|
390
|
+
if (this.loadPromise) return this.loadPromise;
|
|
391
|
+
if (this.didLoad && !this.pendingExtras) {
|
|
392
|
+
this.loadPromise = Promise.resolve();
|
|
393
|
+
return this.loadPromise;
|
|
394
|
+
}
|
|
395
|
+
this.bumpLoad();
|
|
396
|
+
this.loadPromise = this.runOpenLoad();
|
|
397
|
+
return this.loadPromise;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
async runOpenLoad() {
|
|
401
|
+
const gen = this.loadGen;
|
|
402
|
+
const signal = this.loadAbort.signal;
|
|
403
|
+
const options = this.loadOptions({
|
|
404
|
+
deferExtras: this.shouldDeferExtras(),
|
|
405
|
+
signal,
|
|
406
|
+
});
|
|
407
|
+
this.busy = 'loading';
|
|
408
|
+
this.paint();
|
|
409
|
+
let loaded;
|
|
410
|
+
try {
|
|
411
|
+
if (this.repo.loadAsync) {
|
|
412
|
+
loaded = await this.repo.loadAsync(this.cwd, this.paths, options);
|
|
413
|
+
} else {
|
|
414
|
+
loaded = this.repo.load(this.cwd, this.paths, options);
|
|
415
|
+
}
|
|
416
|
+
} catch (error) {
|
|
417
|
+
if (gen !== this.loadGen) return;
|
|
418
|
+
this.failOpen(error);
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
if (gen !== this.loadGen || this.done) return;
|
|
422
|
+
this.applyLoaded(loaded);
|
|
423
|
+
if (loaded.pending) {
|
|
424
|
+
this.busy = 'checking npm';
|
|
425
|
+
this.paint();
|
|
426
|
+
await this.followExtras(loaded, gen, 'open');
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
this.busy = '';
|
|
430
|
+
this.paint();
|
|
431
|
+
this.finishIfEmpty('open');
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
async loadReady() {
|
|
435
|
+
this.ensureRepo();
|
|
436
|
+
await this.openLoad();
|
|
437
|
+
if (this.loadError) throw this.loadError;
|
|
280
438
|
}
|
|
281
439
|
|
|
282
440
|
initReview() {
|
|
@@ -386,14 +544,14 @@ class Session {
|
|
|
386
544
|
view() {
|
|
387
545
|
const templates = this.shownTemplates();
|
|
388
546
|
const change = this.change;
|
|
389
|
-
const
|
|
547
|
+
const sourceKind = (change && SOURCE_KIND[change.source]) || '';
|
|
390
548
|
const repoName = this.repoName || path.basename(this.top || this.cwd || '');
|
|
391
549
|
return {
|
|
392
550
|
item: this.current(),
|
|
393
551
|
index: this.index,
|
|
394
552
|
total: this.items.length,
|
|
395
553
|
scroll: this.scroll,
|
|
396
|
-
status: this.
|
|
554
|
+
status: this.viewStatus(),
|
|
397
555
|
layout: this.layout,
|
|
398
556
|
counts: this.counts(),
|
|
399
557
|
selection: this.selection,
|
|
@@ -404,7 +562,7 @@ class Session {
|
|
|
404
562
|
repoName,
|
|
405
563
|
revShort: this.revShort || '',
|
|
406
564
|
sourceLabel: this.sourceLabel || '',
|
|
407
|
-
sourceKind
|
|
565
|
+
sourceKind,
|
|
408
566
|
mode: this.mode,
|
|
409
567
|
compose: this.composeView(),
|
|
410
568
|
noteText: this.idleNoteText(),
|
|
@@ -466,20 +624,25 @@ class Session {
|
|
|
466
624
|
|
|
467
625
|
reloadAfterChange() {
|
|
468
626
|
const here = this.index;
|
|
469
|
-
this.
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
this.
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
}
|
|
627
|
+
this.loadPromise = null;
|
|
628
|
+
this.bumpLoad();
|
|
629
|
+
const options = this.loadOptions({
|
|
630
|
+
deferExtras: this.shouldDeferExtras(),
|
|
631
|
+
});
|
|
632
|
+
const loaded = this.repo.load(this.cwd, this.paths, options);
|
|
633
|
+
this.applyLoaded(loaded);
|
|
477
634
|
this.index = here;
|
|
478
635
|
this.clampIndex();
|
|
479
636
|
this.syncReviewPath();
|
|
480
637
|
this.syncFileCursor();
|
|
481
638
|
this.scroll = 0;
|
|
482
639
|
this.clearSelection();
|
|
640
|
+
if (loaded.pending) {
|
|
641
|
+
this.busy = 'checking npm';
|
|
642
|
+
this.followExtras(loaded, this.loadGen, 'reload');
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
this.finishIfEmpty('reload');
|
|
483
646
|
}
|
|
484
647
|
|
|
485
648
|
clearSelection() {
|
|
@@ -535,16 +698,20 @@ class Session {
|
|
|
535
698
|
return items;
|
|
536
699
|
}
|
|
537
700
|
|
|
538
|
-
|
|
539
|
-
if (!
|
|
540
|
-
|
|
541
|
-
if (isReadOnlyOrigin(first.origin)) {
|
|
701
|
+
guardWritable(item) {
|
|
702
|
+
if (!item) return false;
|
|
703
|
+
if (this.readOnly || isReadOnlyOrigin(item.origin)) {
|
|
542
704
|
this.status = 'read only';
|
|
543
705
|
return false;
|
|
544
706
|
}
|
|
545
707
|
return true;
|
|
546
708
|
}
|
|
547
709
|
|
|
710
|
+
guardFileGit(items) {
|
|
711
|
+
if (!items.length) return false;
|
|
712
|
+
return this.guardWritable(items[0]);
|
|
713
|
+
}
|
|
714
|
+
|
|
548
715
|
applyFileGit(items, label, write, afterItem) {
|
|
549
716
|
try {
|
|
550
717
|
for (const item of items) {
|
|
@@ -578,12 +745,13 @@ class Session {
|
|
|
578
745
|
onFileAdd() {
|
|
579
746
|
const items = this.fileActionItems();
|
|
580
747
|
if (!this.guardFileGit(items)) return;
|
|
748
|
+
const rel = itemPath(items[0]);
|
|
581
749
|
const targets = items.filter((item) => item.origin !== 'staged');
|
|
582
750
|
if (!targets.length) {
|
|
583
751
|
this.status = 'already staged';
|
|
752
|
+
this.selectNextFileAfter(rel);
|
|
584
753
|
return;
|
|
585
754
|
}
|
|
586
|
-
const rel = itemPath(targets[0]);
|
|
587
755
|
const proposed = targets.some((item) => needsReload(item));
|
|
588
756
|
const ok = this.applyFileGit(
|
|
589
757
|
targets,
|
|
@@ -598,12 +766,13 @@ class Session {
|
|
|
598
766
|
onFileUnstage() {
|
|
599
767
|
const items = this.fileActionItems();
|
|
600
768
|
if (!this.guardFileGit(items)) return;
|
|
769
|
+
const rel = itemPath(items[0]);
|
|
601
770
|
const targets = items.filter((item) => item.origin === 'staged');
|
|
602
771
|
if (!targets.length) {
|
|
603
772
|
this.status = 'not staged';
|
|
773
|
+
this.selectNextFileAfter(rel);
|
|
604
774
|
return;
|
|
605
775
|
}
|
|
606
|
-
const rel = itemPath(targets[0]);
|
|
607
776
|
const ok = this.applyFileGit(
|
|
608
777
|
targets,
|
|
609
778
|
'unstaged',
|
|
@@ -638,11 +807,7 @@ class Session {
|
|
|
638
807
|
onAdd() {
|
|
639
808
|
if (this.pane === 'files') return void this.onFileAdd();
|
|
640
809
|
const item = this.activeDiffItem();
|
|
641
|
-
if (!item) return;
|
|
642
|
-
if (isReadOnlyOrigin(item.origin)) {
|
|
643
|
-
this.status = 'read only';
|
|
644
|
-
return;
|
|
645
|
-
}
|
|
810
|
+
if (!this.guardWritable(item)) return;
|
|
646
811
|
if (item.origin === 'staged') {
|
|
647
812
|
this.status = 'already staged';
|
|
648
813
|
return;
|
|
@@ -664,11 +829,7 @@ class Session {
|
|
|
664
829
|
onUnstage() {
|
|
665
830
|
if (this.pane === 'files') return void this.onFileUnstage();
|
|
666
831
|
const item = this.activeDiffItem();
|
|
667
|
-
if (!item) return;
|
|
668
|
-
if (isReadOnlyOrigin(item.origin)) {
|
|
669
|
-
this.status = 'read only';
|
|
670
|
-
return;
|
|
671
|
-
}
|
|
832
|
+
if (!this.guardWritable(item)) return;
|
|
672
833
|
if (item.origin !== 'staged') {
|
|
673
834
|
this.status = 'not staged';
|
|
674
835
|
return;
|
|
@@ -685,11 +846,7 @@ class Session {
|
|
|
685
846
|
onRevert() {
|
|
686
847
|
if (this.pane === 'files') return void this.onFileRevert();
|
|
687
848
|
const item = this.activeDiffItem();
|
|
688
|
-
if (!item) return;
|
|
689
|
-
if (isReadOnlyOrigin(item.origin)) {
|
|
690
|
-
this.status = 'read only';
|
|
691
|
-
return;
|
|
692
|
-
}
|
|
849
|
+
if (!this.guardWritable(item)) return;
|
|
693
850
|
this.applyGit(
|
|
694
851
|
item,
|
|
695
852
|
'reverted',
|
|
@@ -1267,13 +1424,12 @@ class Session {
|
|
|
1267
1424
|
this.status = 'nothing to review';
|
|
1268
1425
|
return;
|
|
1269
1426
|
}
|
|
1270
|
-
const next =
|
|
1271
|
-
|
|
1272
|
-
this.
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
this.status = 'last file';
|
|
1427
|
+
const next = Math.min(
|
|
1428
|
+
files.length - 1,
|
|
1429
|
+
Math.max(0, this.fileCursor + delta),
|
|
1430
|
+
);
|
|
1431
|
+
if (next === this.fileCursor) {
|
|
1432
|
+
this.status = next <= 0 ? 'first file' : 'last file';
|
|
1277
1433
|
return;
|
|
1278
1434
|
}
|
|
1279
1435
|
this.fileCursor = next;
|
|
@@ -1283,6 +1439,16 @@ class Session {
|
|
|
1283
1439
|
this.status = '';
|
|
1284
1440
|
}
|
|
1285
1441
|
|
|
1442
|
+
scrollStep(fraction) {
|
|
1443
|
+
const bodyH = this.lastFrame?.bodyH;
|
|
1444
|
+
const size = this.lastSize ?? this.getSize();
|
|
1445
|
+
const fallback = Math.max(1, (size.height ?? 24) - 4);
|
|
1446
|
+
const height = Math.max(1, bodyH ?? fallback);
|
|
1447
|
+
const n = Math.max(1, Math.floor(height * Math.abs(fraction)));
|
|
1448
|
+
if (fraction < 0) return -n;
|
|
1449
|
+
return n;
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1286
1452
|
onScroll(delta) {
|
|
1287
1453
|
if (this.pane === 'files') {
|
|
1288
1454
|
this.onFileMove(delta);
|
|
@@ -1428,12 +1594,20 @@ class Session {
|
|
|
1428
1594
|
}
|
|
1429
1595
|
|
|
1430
1596
|
async startUi() {
|
|
1597
|
+
if (this.didLoad && !this.items.length) {
|
|
1598
|
+
this.emptyReview = true;
|
|
1599
|
+
return 0;
|
|
1600
|
+
}
|
|
1601
|
+
this.uiOpen = true;
|
|
1431
1602
|
this.enterTerm();
|
|
1432
1603
|
try {
|
|
1604
|
+
this.openLoad();
|
|
1433
1605
|
this.draw();
|
|
1434
1606
|
await this.readLoop();
|
|
1435
1607
|
} finally {
|
|
1608
|
+
this.uiOpen = false;
|
|
1436
1609
|
this.leaveTerm();
|
|
1610
|
+
if (this.loadAbort) this.loadAbort.abort();
|
|
1437
1611
|
}
|
|
1438
1612
|
return this.exitCode;
|
|
1439
1613
|
}
|
|
@@ -1442,7 +1616,10 @@ class Session {
|
|
|
1442
1616
|
return new Promise((resolve) => {
|
|
1443
1617
|
this.abortController = new AbortController();
|
|
1444
1618
|
const { signal } = this.abortController;
|
|
1619
|
+
let settled = false;
|
|
1445
1620
|
const finish = () => {
|
|
1621
|
+
if (settled) return;
|
|
1622
|
+
settled = true;
|
|
1446
1623
|
if (this.saveTimer !== null) {
|
|
1447
1624
|
this.clearInterval(this.saveTimer);
|
|
1448
1625
|
this.saveTimer = null;
|
|
@@ -1452,8 +1629,14 @@ class Session {
|
|
|
1452
1629
|
this.blinkTimer = null;
|
|
1453
1630
|
}
|
|
1454
1631
|
this.abortController.abort();
|
|
1632
|
+
this.finishReadLoop = null;
|
|
1455
1633
|
resolve();
|
|
1456
1634
|
};
|
|
1635
|
+
this.finishReadLoop = finish;
|
|
1636
|
+
if (this.done) {
|
|
1637
|
+
finish();
|
|
1638
|
+
return;
|
|
1639
|
+
}
|
|
1457
1640
|
const onData = (chunk) => {
|
|
1458
1641
|
this.pushInput(chunk.toString('utf8'));
|
|
1459
1642
|
if (this.done) {
|
|
@@ -1468,7 +1651,7 @@ class Session {
|
|
|
1468
1651
|
CURSOR_BLINK_MS,
|
|
1469
1652
|
);
|
|
1470
1653
|
this.stdin.on('data', (chunk) => onData(chunk), { signal });
|
|
1471
|
-
|
|
1654
|
+
watchResize(this.stdout, process, () => this.draw(), signal);
|
|
1472
1655
|
});
|
|
1473
1656
|
}
|
|
1474
1657
|
}
|
package/lib/source.js
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const { parseGithubPrUrl } = require('./github.js');
|
|
4
|
+
const { parseGitlabMrUrl } = require('./gitlab.js');
|
|
4
5
|
|
|
5
6
|
const selectChangeSource = (paths) => {
|
|
6
7
|
if (!paths.length) return { kind: 'local', paths };
|
|
7
8
|
const first = paths[0];
|
|
8
9
|
const pr = parseGithubPrUrl(first);
|
|
9
|
-
if (
|
|
10
|
-
|
|
10
|
+
if (pr) return { kind: 'github-pr', pr, paths: paths.slice(1) };
|
|
11
|
+
const mr = parseGitlabMrUrl(first);
|
|
12
|
+
if (mr) return { kind: 'gitlab-mr', mr, paths: paths.slice(1) };
|
|
13
|
+
return { kind: 'local', paths };
|
|
11
14
|
};
|
|
12
15
|
|
|
13
16
|
const nop = () => {};
|