reslop 0.2.0 → 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.
- package/CHANGELOG.md +14 -1
- package/README.md +2 -1
- package/lib/ansi.js +144 -3
- package/lib/api.js +2 -0
- package/lib/capabilities.js +6 -0
- package/lib/cli.js +10 -2
- package/lib/git-branches.js +185 -6
- package/lib/git-reword-todo.js +20 -0
- package/lib/git.js +20 -3
- package/lib/render/branches.js +4 -32
- package/lib/render/chrome.js +47 -5
- package/lib/render/commits.js +63 -1
- package/lib/render/diff.js +4 -4
- package/lib/render/frame.js +13 -1
- package/lib/render/notes.js +22 -5
- package/lib/render/primitives.js +54 -11
- package/lib/render/render.js +3 -3
- package/lib/review-merge.js +270 -0
- package/lib/review.js +38 -6
- package/lib/session/actions.js +30 -3
- package/lib/session/commit.js +101 -8
- package/lib/session/compose.js +48 -27
- package/lib/session/dispatch.js +6 -0
- package/lib/session/files-pane.js +17 -3
- package/lib/session/lifecycle.js +113 -16
- package/lib/session/pointer.js +4 -1
- package/lib/session/todos.js +29 -1
- package/lib/session/watch.js +120 -8
- package/lib/session.js +1 -0
- package/package.json +2 -2
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { TODO_FILE } = require('./files.js');
|
|
4
|
+
|
|
5
|
+
const flatText = (text) => `${text ?? ''}`.trim().replace(/\n/g, ' ');
|
|
6
|
+
|
|
7
|
+
const snapTodo = (todo) => ({
|
|
8
|
+
file: todo.file ?? '',
|
|
9
|
+
text: flatText(todo.text),
|
|
10
|
+
done: todo.done === true,
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
const snapList = (todos) => {
|
|
14
|
+
const list = [];
|
|
15
|
+
for (const todo of todos ?? []) {
|
|
16
|
+
const snap = snapTodo(todo);
|
|
17
|
+
if (!snap.text) continue;
|
|
18
|
+
list.push(snap);
|
|
19
|
+
}
|
|
20
|
+
return list;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const copyMap = (map) => {
|
|
24
|
+
const next = new Map();
|
|
25
|
+
if (!map) return next;
|
|
26
|
+
for (const [key, note] of map) next.set(key, { ...note });
|
|
27
|
+
return next;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const snapshotSide = (store) => ({
|
|
31
|
+
status: store.status,
|
|
32
|
+
todos: snapList(store.todos),
|
|
33
|
+
feedback: copyMap(store.feedback),
|
|
34
|
+
code: copyMap(store.code),
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const captureBaseline = (store) => {
|
|
38
|
+
store.baseline = snapshotSide(store);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const claim = (list, used, text, read) => {
|
|
42
|
+
for (let i = 0; i < list.length; i++) {
|
|
43
|
+
if (used[i]) continue;
|
|
44
|
+
if (read(list[i]) !== text) continue;
|
|
45
|
+
used[i] = true;
|
|
46
|
+
return i;
|
|
47
|
+
}
|
|
48
|
+
return -1;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const pickDone = (baseDone, ourDone, theirDone) => {
|
|
52
|
+
if (ourDone === theirDone) return ourDone;
|
|
53
|
+
if (theirDone === baseDone) return ourDone;
|
|
54
|
+
if (ourDone === baseDone) return theirDone;
|
|
55
|
+
return ourDone;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const withDone = (todo, done) => {
|
|
59
|
+
if (todo.done === done) return todo;
|
|
60
|
+
return { id: todo.id, file: todo.file, text: todo.text, done };
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const liveText = (todo) => flatText(todo.text);
|
|
64
|
+
|
|
65
|
+
const snapText = (snap) => snap.text;
|
|
66
|
+
|
|
67
|
+
const flags = (n) => new Array(n).fill(false);
|
|
68
|
+
|
|
69
|
+
const adoptTodo = (store, snap) => {
|
|
70
|
+
const id = store.nextTodoId;
|
|
71
|
+
store.nextTodoId += 1;
|
|
72
|
+
const file = snap.file || TODO_FILE;
|
|
73
|
+
return { id, file, text: snap.text, done: snap.done };
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const findMerged = (merged, alias, text) => {
|
|
77
|
+
const linked = alias.get(text);
|
|
78
|
+
if (linked) {
|
|
79
|
+
const at = merged.indexOf(linked);
|
|
80
|
+
if (at >= 0) return at;
|
|
81
|
+
}
|
|
82
|
+
for (let i = 0; i < merged.length; i++) {
|
|
83
|
+
if (liveText(merged[i]) === text) return i;
|
|
84
|
+
}
|
|
85
|
+
return -1;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const insertTheir = (store, merged, theirs, theirKeep, alias) => {
|
|
89
|
+
for (let i = 0; i < theirs.length; i++) {
|
|
90
|
+
if (!theirKeep[i]) continue;
|
|
91
|
+
let at = merged.length;
|
|
92
|
+
let placed = false;
|
|
93
|
+
for (let j = i - 1; j >= 0 && !placed; j--) {
|
|
94
|
+
const prev = findMerged(merged, alias, theirs[j].text);
|
|
95
|
+
if (prev < 0) continue;
|
|
96
|
+
at = prev + 1;
|
|
97
|
+
placed = true;
|
|
98
|
+
}
|
|
99
|
+
for (let j = i + 1; j < theirs.length && !placed; j++) {
|
|
100
|
+
const next = findMerged(merged, alias, theirs[j].text);
|
|
101
|
+
if (next < 0) continue;
|
|
102
|
+
at = next;
|
|
103
|
+
placed = true;
|
|
104
|
+
}
|
|
105
|
+
const item = adoptTodo(store, theirs[i]);
|
|
106
|
+
merged.splice(at, 0, item);
|
|
107
|
+
alias.set(theirs[i].text, item);
|
|
108
|
+
}
|
|
109
|
+
return merged;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const keepOurs = (ours, kept, alias, index, text) => {
|
|
113
|
+
kept[index] = true;
|
|
114
|
+
alias.set(text, ours[index]);
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const mergeBaseTodo = (
|
|
118
|
+
base,
|
|
119
|
+
ours,
|
|
120
|
+
theirs,
|
|
121
|
+
used,
|
|
122
|
+
kept,
|
|
123
|
+
dropped,
|
|
124
|
+
theirKeep,
|
|
125
|
+
alias,
|
|
126
|
+
) => {
|
|
127
|
+
const text = base.text;
|
|
128
|
+
const oi = claim(ours, used.ours, text, liveText);
|
|
129
|
+
const ti = claim(theirs, used.theirs, text, snapText);
|
|
130
|
+
if (oi >= 0 && ti >= 0) {
|
|
131
|
+
const done = pickDone(base.done, ours[oi].done === true, theirs[ti].done);
|
|
132
|
+
ours[oi] = withDone(ours[oi], done);
|
|
133
|
+
keepOurs(ours, kept, alias, oi, text);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
if (oi >= 0) {
|
|
137
|
+
if (ours[oi].done === base.done) {
|
|
138
|
+
dropped[oi] = true;
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
keepOurs(ours, kept, alias, oi, text);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (ti >= 0 && theirs[ti].done !== base.done) theirKeep[ti] = true;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const replaceTodo = (todo, snap) => ({
|
|
148
|
+
id: todo.id,
|
|
149
|
+
file: todo.file,
|
|
150
|
+
text: snap.text,
|
|
151
|
+
done: snap.done,
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
const fillGaps = (merged, gaps, theirs, theirKeep, alias) => {
|
|
155
|
+
let filled = 0;
|
|
156
|
+
for (let i = 0; i < theirs.length && filled < gaps.length; i++) {
|
|
157
|
+
if (!theirKeep[i]) continue;
|
|
158
|
+
const gap = gaps[filled];
|
|
159
|
+
const item = replaceTodo(gap.todo, theirs[i]);
|
|
160
|
+
merged.splice(gap.at, 0, item);
|
|
161
|
+
alias.set(theirs[i].text, item);
|
|
162
|
+
theirKeep[i] = false;
|
|
163
|
+
filled += 1;
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const mergeFreshTodo = (ours, theirs, used, kept, alias, index) => {
|
|
168
|
+
const text = liveText(ours[index]);
|
|
169
|
+
const ti = claim(theirs, used.theirs, text, snapText);
|
|
170
|
+
if (ti < 0) {
|
|
171
|
+
kept[index] = true;
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const done = ours[index].done === true || theirs[ti].done === true;
|
|
175
|
+
ours[index] = withDone(ours[index], done);
|
|
176
|
+
keepOurs(ours, kept, alias, index, text);
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const mergeTodos = (store, base, theirs) => {
|
|
180
|
+
const ours = [];
|
|
181
|
+
for (const todo of store.todos) {
|
|
182
|
+
if (!liveText(todo)) continue;
|
|
183
|
+
ours.push(todo);
|
|
184
|
+
}
|
|
185
|
+
const used = { ours: flags(ours.length), theirs: flags(theirs.length) };
|
|
186
|
+
const kept = flags(ours.length);
|
|
187
|
+
const dropped = flags(ours.length);
|
|
188
|
+
const theirKeep = flags(theirs.length);
|
|
189
|
+
const alias = new Map();
|
|
190
|
+
for (const item of base) {
|
|
191
|
+
mergeBaseTodo(item, ours, theirs, used, kept, dropped, theirKeep, alias);
|
|
192
|
+
}
|
|
193
|
+
for (let i = 0; i < ours.length; i++) {
|
|
194
|
+
if (used.ours[i]) continue;
|
|
195
|
+
mergeFreshTodo(ours, theirs, used, kept, alias, i);
|
|
196
|
+
}
|
|
197
|
+
for (let i = 0; i < theirs.length; i++) {
|
|
198
|
+
if (!used.theirs[i]) theirKeep[i] = true;
|
|
199
|
+
}
|
|
200
|
+
const merged = [];
|
|
201
|
+
const gaps = [];
|
|
202
|
+
let cursor = 0;
|
|
203
|
+
for (let i = 0; i < ours.length; i++) {
|
|
204
|
+
if (kept[i]) {
|
|
205
|
+
merged.push(ours[i]);
|
|
206
|
+
cursor += 1;
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
if (!dropped[i]) continue;
|
|
210
|
+
gaps.push({ at: cursor, todo: ours[i] });
|
|
211
|
+
cursor += 1;
|
|
212
|
+
}
|
|
213
|
+
fillGaps(merged, gaps, theirs, theirKeep, alias);
|
|
214
|
+
store.todos = insertTheir(store, merged, theirs, theirKeep, alias);
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const sameNote = (left, right) =>
|
|
218
|
+
left.text === right.text && !!left.done === !!right.done;
|
|
219
|
+
|
|
220
|
+
const cloneNote = (note, done) => {
|
|
221
|
+
if (!!note.done === done) return note;
|
|
222
|
+
return { ...note, done };
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
const pickNote = (ours, base, theirs) => {
|
|
226
|
+
if (ours && theirs && ours.text === theirs.text) {
|
|
227
|
+
const baseDone = base ? base.done === true : ours.done === true;
|
|
228
|
+
const done = pickDone(baseDone, ours.done === true, theirs.done === true);
|
|
229
|
+
return cloneNote(ours, done);
|
|
230
|
+
}
|
|
231
|
+
if (!base) return ours || theirs || null;
|
|
232
|
+
if (!ours) {
|
|
233
|
+
if (!theirs || sameNote(theirs, base)) return null;
|
|
234
|
+
return theirs;
|
|
235
|
+
}
|
|
236
|
+
if (!theirs) {
|
|
237
|
+
if (sameNote(ours, base)) return null;
|
|
238
|
+
return ours;
|
|
239
|
+
}
|
|
240
|
+
if (sameNote(theirs, base)) return ours;
|
|
241
|
+
if (sameNote(ours, base)) return theirs;
|
|
242
|
+
return ours;
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
const mergeMap = (ours, base, theirs) => {
|
|
246
|
+
const keys = new Set();
|
|
247
|
+
for (const key of ours.keys()) keys.add(key);
|
|
248
|
+
for (const key of base.keys()) keys.add(key);
|
|
249
|
+
for (const key of theirs.keys()) keys.add(key);
|
|
250
|
+
for (const key of keys) {
|
|
251
|
+
const next = pickNote(ours.get(key), base.get(key), theirs.get(key));
|
|
252
|
+
if (!next) ours.delete(key);
|
|
253
|
+
else ours.set(key, next);
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
const mergeReview = (store, disk) => {
|
|
258
|
+
const base = store.baseline;
|
|
259
|
+
const baseTodos = base ? base.todos : [];
|
|
260
|
+
const baseStatus = base ? base.status : store.status;
|
|
261
|
+
mergeTodos(store, baseTodos, snapList(disk.todos));
|
|
262
|
+
const feedback = base ? base.feedback : new Map();
|
|
263
|
+
const code = base ? base.code : new Map();
|
|
264
|
+
mergeMap(store.feedback, feedback, disk.feedback);
|
|
265
|
+
mergeMap(store.code, code, disk.code);
|
|
266
|
+
if (store.status === baseStatus && disk.status) store.status = disk.status;
|
|
267
|
+
store.baseline = snapshotSide(disk);
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
module.exports = { captureBaseline, mergeReview };
|
package/lib/review.js
CHANGED
|
@@ -4,6 +4,7 @@ const fs = require('node:fs');
|
|
|
4
4
|
const path = require('node:path');
|
|
5
5
|
|
|
6
6
|
const { TODO_FILE, REVIEW_DIR } = require('./files.js');
|
|
7
|
+
const { captureBaseline, mergeReview } = require('./review-merge.js');
|
|
7
8
|
|
|
8
9
|
const createStore = (reviewPath, templates = []) => ({
|
|
9
10
|
reviewPath,
|
|
@@ -14,6 +15,7 @@ const createStore = (reviewPath, templates = []) => ({
|
|
|
14
15
|
todos: [],
|
|
15
16
|
templates: [...templates],
|
|
16
17
|
status: 'editing',
|
|
18
|
+
baseline: null,
|
|
17
19
|
});
|
|
18
20
|
|
|
19
21
|
const noteCounts = (store) => {
|
|
@@ -119,6 +121,15 @@ const setTodoText = (store, id, text) => {
|
|
|
119
121
|
store.dirty = true;
|
|
120
122
|
};
|
|
121
123
|
|
|
124
|
+
const setTodoDone = (store, id, done) => {
|
|
125
|
+
const i = store.todos.findIndex((todo) => todo.id === id);
|
|
126
|
+
if (i < 0) return;
|
|
127
|
+
const prev = store.todos[i];
|
|
128
|
+
if (prev.done === done) return;
|
|
129
|
+
store.todos[i] = { id: prev.id, file: prev.file, text: prev.text, done };
|
|
130
|
+
store.dirty = true;
|
|
131
|
+
};
|
|
132
|
+
|
|
122
133
|
const checkLabel = (text, done) => {
|
|
123
134
|
const mark = done ? 'x' : ' ';
|
|
124
135
|
return `[${mark}] ${text}`;
|
|
@@ -222,12 +233,13 @@ const SECTION_LINE = {
|
|
|
222
233
|
};
|
|
223
234
|
|
|
224
235
|
const AGENT_INSTRUCTIONS = [
|
|
225
|
-
'Execute reviews with `status
|
|
226
|
-
'Do not start
|
|
227
|
-
'Work through unchecked items. After finishing one, mark it `[x]
|
|
228
|
-
'For `code` items, replace added lines at that location
|
|
229
|
-
'
|
|
230
|
-
|
|
236
|
+
'- Execute reviews with `status` `ready, partial, editing`',
|
|
237
|
+
'- Do not start or change `done` review files',
|
|
238
|
+
'- Work through unchecked items. After finishing one, mark it `[x]`',
|
|
239
|
+
'- For `code` items, replace added lines at that location',
|
|
240
|
+
'- If not `editing`, set `status` to `partial` if some remain, ' +
|
|
241
|
+
'or `done` if all are `[x]`',
|
|
242
|
+
'- Do not redo items already marked `[x]`',
|
|
231
243
|
].join('\n');
|
|
232
244
|
|
|
233
245
|
const toInt = (text) => parseInt(text, 10);
|
|
@@ -622,6 +634,7 @@ const parseReview = (text, reviewPath, templates = []) => {
|
|
|
622
634
|
flushPendingCode(builder, pendingCode);
|
|
623
635
|
flushPendingFeedback(builder, pending);
|
|
624
636
|
builder.dirty = false;
|
|
637
|
+
captureBaseline(builder);
|
|
625
638
|
return builder;
|
|
626
639
|
};
|
|
627
640
|
|
|
@@ -733,8 +746,25 @@ const loadTemplates = (dir) => {
|
|
|
733
746
|
}
|
|
734
747
|
};
|
|
735
748
|
|
|
749
|
+
const readReviewText = (reviewPath) => {
|
|
750
|
+
try {
|
|
751
|
+
return fs.readFileSync(reviewPath, 'utf8');
|
|
752
|
+
} catch (error) {
|
|
753
|
+
if (error.code === 'ENOENT') return '';
|
|
754
|
+
throw error;
|
|
755
|
+
}
|
|
756
|
+
};
|
|
757
|
+
|
|
758
|
+
const mergeDiskReview = (notes) => {
|
|
759
|
+
const text = readReviewText(notes.reviewPath);
|
|
760
|
+
if (!text.startsWith('---')) return;
|
|
761
|
+
const disk = parseReview(text, notes.reviewPath, notes.templates);
|
|
762
|
+
mergeReview(notes, disk);
|
|
763
|
+
};
|
|
764
|
+
|
|
736
765
|
const flushReview = (notes, force = false) => {
|
|
737
766
|
if (!notes || !notes.dirty) return false;
|
|
767
|
+
mergeDiskReview(notes);
|
|
738
768
|
if (!hasNotes(notes) && !force) {
|
|
739
769
|
notes.dirty = false;
|
|
740
770
|
return false;
|
|
@@ -746,6 +776,7 @@ const flushReview = (notes, force = false) => {
|
|
|
746
776
|
const templatesFile = path.join(top, REVIEW_DIR, TEMPLATES_FILE);
|
|
747
777
|
const body = `${JSON.stringify(notes.templates, null, 2)}\n`;
|
|
748
778
|
fs.writeFileSync(templatesFile, body, 'utf8');
|
|
779
|
+
captureBaseline(notes);
|
|
749
780
|
notes.dirty = false;
|
|
750
781
|
return true;
|
|
751
782
|
};
|
|
@@ -770,6 +801,7 @@ module.exports = {
|
|
|
770
801
|
addTodo,
|
|
771
802
|
removeTodo,
|
|
772
803
|
setTodoText,
|
|
804
|
+
setTodoDone,
|
|
773
805
|
checkLabel,
|
|
774
806
|
feedbackKey,
|
|
775
807
|
applyImportedNotes,
|
package/lib/session/actions.js
CHANGED
|
@@ -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,6 +29,8 @@ 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'],
|
|
33
36
|
pull: ['p'],
|
|
@@ -41,6 +44,7 @@ const ACTION_HOT = {
|
|
|
41
44
|
|
|
42
45
|
const ACTION_LABEL = {
|
|
43
46
|
revert: 'drop',
|
|
47
|
+
check: 'x',
|
|
44
48
|
layout: 'mode',
|
|
45
49
|
code: 'edit',
|
|
46
50
|
newBranch: 'new',
|
|
@@ -49,11 +53,20 @@ const ACTION_LABEL = {
|
|
|
49
53
|
next: '→',
|
|
50
54
|
};
|
|
51
55
|
|
|
52
|
-
const DIFF_DISABLED = [
|
|
56
|
+
const DIFF_DISABLED = [
|
|
57
|
+
'check',
|
|
58
|
+
'commit',
|
|
59
|
+
'branch',
|
|
60
|
+
'pull',
|
|
61
|
+
'push',
|
|
62
|
+
'file',
|
|
63
|
+
'diff',
|
|
64
|
+
];
|
|
53
65
|
const UNIT_DISABLED = [...DIFF_DISABLED, 'layout'];
|
|
54
66
|
const FILES_DISABLED = ['layout', 'feedback', 'code'];
|
|
55
67
|
const FILES_HIDDEN = [
|
|
56
68
|
...FILES_DISABLED,
|
|
69
|
+
'check',
|
|
57
70
|
'prev',
|
|
58
71
|
'next',
|
|
59
72
|
'commit',
|
|
@@ -86,6 +99,7 @@ const BRANCHES_DISABLED = [
|
|
|
86
99
|
'feedback',
|
|
87
100
|
'todo',
|
|
88
101
|
'code',
|
|
102
|
+
'check',
|
|
89
103
|
'branch',
|
|
90
104
|
'file',
|
|
91
105
|
'diff',
|
|
@@ -100,6 +114,7 @@ const COMMITS_DISABLED = [
|
|
|
100
114
|
'feedback',
|
|
101
115
|
'todo',
|
|
102
116
|
'code',
|
|
117
|
+
'check',
|
|
103
118
|
'branch',
|
|
104
119
|
'file',
|
|
105
120
|
'diff',
|
|
@@ -124,13 +139,17 @@ const branchGitDisabled = (item) => {
|
|
|
124
139
|
return off;
|
|
125
140
|
};
|
|
126
141
|
|
|
142
|
+
const isFixupCommit = (item) =>
|
|
143
|
+
`${item && item.subject ? item.subject : ''}`.startsWith('fixup!');
|
|
144
|
+
|
|
127
145
|
const commitWriteDisabled = (item) => {
|
|
128
|
-
if (!item || !item.sha) return ['amend', 'fixup', 'drop'];
|
|
146
|
+
if (!item || !item.sha) return ['amend', 'apply', 'reword', 'fixup', 'drop'];
|
|
129
147
|
return [];
|
|
130
148
|
};
|
|
131
149
|
|
|
132
150
|
const commitGitDisabled = (item) => {
|
|
133
|
-
const off = commitWriteDisabled(item);
|
|
151
|
+
const off = [...commitWriteDisabled(item)];
|
|
152
|
+
if (!isFixupCommit(item)) off.push('apply');
|
|
134
153
|
if (item && item.canCommit === true) return off;
|
|
135
154
|
return [...off, 'commit', 'fixup'];
|
|
136
155
|
};
|
|
@@ -149,6 +168,10 @@ const disabledActions = (pane, item) => {
|
|
|
149
168
|
return [...DIFF_DISABLED, ...diffGitDisabled(item)];
|
|
150
169
|
};
|
|
151
170
|
|
|
171
|
+
const HIDDEN_KEYS = {
|
|
172
|
+
l: 'theme',
|
|
173
|
+
};
|
|
174
|
+
|
|
152
175
|
const SCROLL = {
|
|
153
176
|
up: 'scrollUp',
|
|
154
177
|
down: 'scrollDown',
|
|
@@ -194,6 +217,9 @@ for (const id of ACTION_IDS) {
|
|
|
194
217
|
}
|
|
195
218
|
|
|
196
219
|
for (const key of Object.keys(SCROLL)) KEY_TO_ACTION[key] = SCROLL[key];
|
|
220
|
+
for (const key of Object.keys(HIDDEN_KEYS)) {
|
|
221
|
+
KEY_TO_ACTION[key] = HIDDEN_KEYS[key];
|
|
222
|
+
}
|
|
197
223
|
|
|
198
224
|
const actionFromKey = (key, pane, fileScope) => {
|
|
199
225
|
const listPane =
|
|
@@ -318,6 +344,7 @@ module.exports = {
|
|
|
318
344
|
diffGitDisabled,
|
|
319
345
|
branchGitDisabled,
|
|
320
346
|
commitGitDisabled,
|
|
347
|
+
isFixupCommit,
|
|
321
348
|
actionFromKey,
|
|
322
349
|
actionKeys,
|
|
323
350
|
actionHot,
|
package/lib/session/commit.js
CHANGED
|
@@ -3,12 +3,22 @@
|
|
|
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');
|
|
7
8
|
|
|
8
9
|
const COMMIT_STATUS = {
|
|
9
10
|
commit: 'committed',
|
|
10
11
|
amend: 'amended',
|
|
11
12
|
fixup: 'fixup',
|
|
13
|
+
reword: 'reworded',
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const isInPlaceCommit = (kind) => kind === 'amend' || kind === 'reword';
|
|
17
|
+
|
|
18
|
+
const fixupMessage = (entry) => {
|
|
19
|
+
const subject = `${entry && entry.subject ? entry.subject : ''}`.trim();
|
|
20
|
+
if (!subject) return 'fixup!';
|
|
21
|
+
return `fixup! ${subject}`;
|
|
12
22
|
};
|
|
13
23
|
|
|
14
24
|
const canCommit = (ui) => {
|
|
@@ -100,21 +110,33 @@ const onCommitMove = (api, delta) => {
|
|
|
100
110
|
|
|
101
111
|
const startGitCommit = (api, kind) => {
|
|
102
112
|
const ui = api.ui;
|
|
103
|
-
if (kind
|
|
113
|
+
if (!isInPlaceCommit(kind) && !hasStagedChanges(ui)) {
|
|
104
114
|
ui.mode = 'review';
|
|
105
115
|
ui.composer.state.commitKind = null;
|
|
106
116
|
ui.status = 'nothing to commit';
|
|
107
117
|
return;
|
|
108
118
|
}
|
|
109
|
-
ui.composer.state.commitKind = kind;
|
|
110
119
|
let text = '';
|
|
111
120
|
if (kind === 'fixup') {
|
|
112
|
-
|
|
113
|
-
|
|
121
|
+
text = fixupMessage(selectedCommit(api));
|
|
122
|
+
}
|
|
123
|
+
if (kind === 'amend') {
|
|
124
|
+
const headAt = api.state.commits.findIndex((entry) => entry.head);
|
|
125
|
+
api.ui.nav.commitCursor = headAt < 0 ? 0 : headAt;
|
|
126
|
+
if (ui.repo.lastMessage) {
|
|
127
|
+
text = ui.repo.lastMessage(ui.top) ?? '';
|
|
128
|
+
}
|
|
114
129
|
}
|
|
115
|
-
if (kind === '
|
|
116
|
-
|
|
130
|
+
if (kind === 'reword') {
|
|
131
|
+
const entry = selectedCommit(api);
|
|
132
|
+
if (!entry || !entry.sha) return;
|
|
133
|
+
if (typeof ui.repo.commitMessage === 'function') {
|
|
134
|
+
text = ui.repo.commitMessage(ui.top, entry.sha) ?? '';
|
|
135
|
+
} else {
|
|
136
|
+
text = `${entry.subject ?? ''}`;
|
|
137
|
+
}
|
|
117
138
|
}
|
|
139
|
+
ui.composer.state.commitKind = kind;
|
|
118
140
|
ui.composer.openCompose('commit', text);
|
|
119
141
|
};
|
|
120
142
|
|
|
@@ -130,6 +152,43 @@ const onAmend = (api) => {
|
|
|
130
152
|
startGitCommit(api, 'amend');
|
|
131
153
|
};
|
|
132
154
|
|
|
155
|
+
const onReword = (api) => {
|
|
156
|
+
if (api.ui.nav.pane !== 'commits') return;
|
|
157
|
+
if (!canCommit(api.ui)) return;
|
|
158
|
+
startGitCommit(api, 'reword');
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const applyFixupNow = (api) => {
|
|
162
|
+
const entry = selectedCommit(api);
|
|
163
|
+
if (!entry || !entry.sha) return;
|
|
164
|
+
const done = 'applied';
|
|
165
|
+
const repo = api.ui.repo;
|
|
166
|
+
if (!repo.applyFixup) {
|
|
167
|
+
api.ui.status = 'read only';
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
const picked = pickRepoWrite(repo, 'applyFixup', api.ui.uiOpen);
|
|
171
|
+
if (picked.async) {
|
|
172
|
+
const run = (top) => picked.write(top, entry.sha);
|
|
173
|
+
const after = () => refreshAfterWrite(api, { doneStatus: done });
|
|
174
|
+
return void api.ui.ops.runBusy('applying', done, run, after);
|
|
175
|
+
}
|
|
176
|
+
const ok = api.ui.ops.runRepo(done, (top) => {
|
|
177
|
+
repo.applyFixup(top, entry.sha);
|
|
178
|
+
});
|
|
179
|
+
if (!ok) return;
|
|
180
|
+
refreshAfterWrite(api);
|
|
181
|
+
if (!api.ui.status) api.ui.status = done;
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const onApply = (api) => {
|
|
185
|
+
if (api.ui.nav.pane !== 'commits') return;
|
|
186
|
+
if (!canCommit(api.ui)) return;
|
|
187
|
+
const entry = selectedCommit(api);
|
|
188
|
+
if (isFixupCommit(entry)) return void applyFixupNow(api);
|
|
189
|
+
startGitCommit(api, 'amend');
|
|
190
|
+
};
|
|
191
|
+
|
|
133
192
|
const onFixup = (api) => {
|
|
134
193
|
if (api.ui.nav.pane !== 'commits') return;
|
|
135
194
|
if (!canCommit(api.ui)) return;
|
|
@@ -166,20 +225,52 @@ const finishSyncCommit = (api, kind, message, done) => {
|
|
|
166
225
|
}
|
|
167
226
|
};
|
|
168
227
|
|
|
228
|
+
const finishReword = (api, message) => {
|
|
229
|
+
const ui = api.ui;
|
|
230
|
+
const entry = selectedCommit(api);
|
|
231
|
+
if (!entry || !entry.sha) {
|
|
232
|
+
ui.status = 'read only';
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
const done = COMMIT_STATUS.reword;
|
|
236
|
+
const repo = ui.repo;
|
|
237
|
+
if (!repo.reword) {
|
|
238
|
+
ui.status = 'read only';
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
const picked = pickRepoWrite(repo, 'reword', ui.uiOpen);
|
|
242
|
+
if (picked.async) {
|
|
243
|
+
ui.composer.closeCompose();
|
|
244
|
+
const run = (top) => picked.write(top, entry.sha, message);
|
|
245
|
+
const after = () => refreshAfterWrite(api, { doneStatus: done });
|
|
246
|
+
return void ui.ops.runBusy('rewording', done, run, after);
|
|
247
|
+
}
|
|
248
|
+
try {
|
|
249
|
+
repo.reword(ui.top, entry.sha, message);
|
|
250
|
+
} catch (error) {
|
|
251
|
+
ui.status = error.message;
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
ui.composer.closeCompose();
|
|
255
|
+
refreshAfterWrite(api);
|
|
256
|
+
if (!ui.status) ui.status = done;
|
|
257
|
+
};
|
|
258
|
+
|
|
169
259
|
const finishGitCommit = (api) => {
|
|
170
260
|
const ui = api.ui;
|
|
171
261
|
const kind = ui.composer.state.commitKind;
|
|
172
262
|
const editor = ui.composer.state.editor;
|
|
173
263
|
const message = editor ? editor.text : '';
|
|
174
|
-
if (
|
|
264
|
+
if (!message.trim()) {
|
|
175
265
|
ui.status = 'empty commit message';
|
|
176
266
|
return;
|
|
177
267
|
}
|
|
178
|
-
if (kind
|
|
268
|
+
if (!isInPlaceCommit(kind) && !hasStagedChanges(ui)) {
|
|
179
269
|
ui.composer.closeCompose();
|
|
180
270
|
ui.status = 'nothing to commit';
|
|
181
271
|
return;
|
|
182
272
|
}
|
|
273
|
+
if (kind === 'reword') return void finishReword(api, message);
|
|
183
274
|
const done = COMMIT_STATUS[kind] ?? 'committed';
|
|
184
275
|
const uiOpen = ui.uiOpen;
|
|
185
276
|
const picked = pickRepoWrite(ui.repo, 'commit', uiOpen);
|
|
@@ -259,6 +350,8 @@ const createCommitController = (ui) => {
|
|
|
259
350
|
canCommit: () => canCommit(ui),
|
|
260
351
|
onCommit: () => onCommit(api),
|
|
261
352
|
onAmend: () => onAmend(api),
|
|
353
|
+
onReword: () => onReword(api),
|
|
354
|
+
onApply: () => onApply(api),
|
|
262
355
|
onFixup: () => onFixup(api),
|
|
263
356
|
onCommitMove: (delta) => onCommitMove(api, delta),
|
|
264
357
|
cancelCommit: () => cancelCommit(api),
|