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
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased][unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.2.1][] - 2026-09-23
|
|
6
|
+
|
|
7
|
+
- Type a new commit message at the top of the list
|
|
8
|
+
- Improve `fixup!` functions: create and apply
|
|
9
|
+
- Reword the selected commit message with `r`
|
|
10
|
+
- Add light theme: `-light` option and `l` hotkey to switch
|
|
11
|
+
- Support checkbox state in TODOs, UI for state change
|
|
12
|
+
- Reload the file list when leaving todos, branches, or commits
|
|
13
|
+
- Let agents run `editing` reviews and leave `done` files unchanged
|
|
14
|
+
- Skip npm audit and outdated unless package files changed
|
|
15
|
+
- Sync and merge TODOs with file instead of overwriting it
|
|
16
|
+
|
|
5
17
|
## [0.2.0][] - 2026-09-22
|
|
6
18
|
|
|
7
19
|
- Add a full-size file editor
|
|
@@ -105,7 +117,8 @@
|
|
|
105
117
|
- Feedback and todos that produce a repair plan for an agent
|
|
106
118
|
- Intra-line highlighting in unified, mixed, and side-by-side layouts
|
|
107
119
|
|
|
108
|
-
[unreleased]: https://github.com/tshemsedinov/reslop/compare/v0.2.
|
|
120
|
+
[unreleased]: https://github.com/tshemsedinov/reslop/compare/v0.2.1...HEAD
|
|
121
|
+
[0.2.1]: https://github.com/tshemsedinov/reslop/compare/v0.2.0...v0.2.1
|
|
109
122
|
[0.2.0]: https://github.com/tshemsedinov/reslop/compare/v0.1.9...v0.2.0
|
|
110
123
|
[0.1.9]: https://github.com/tshemsedinov/reslop/compare/v0.1.8...v0.1.9
|
|
111
124
|
[0.1.8]: https://github.com/tshemsedinov/reslop/compare/v0.1.7...v0.1.8
|
package/README.md
CHANGED
|
@@ -24,7 +24,7 @@ Review → Plan → Repair → Verify
|
|
|
24
24
|
- Stage, unstage, or revert each contiguous block of diff lines
|
|
25
25
|
- Auto-reload local diffs when files change, keeping the current screen
|
|
26
26
|
- Import GitHub PR and GitLab MR review comments into the local plan for AI
|
|
27
|
-
- Commits list: commit, amend, fixup, drop, pull, and push
|
|
27
|
+
- Commits list: commit, amend, fixup, reword, drop, pull, and push
|
|
28
28
|
- Branches: checkout, create, rebase, drop, pull, and push
|
|
29
29
|
- Review each npm dependency once across `package.json` and the lockfile
|
|
30
30
|
- Propose unused removals, npm audit fixes, and outdated updates as diffs
|
|
@@ -48,6 +48,7 @@ Works on Linux, macOS, and Windows. On Windows use Windows Terminal.
|
|
|
48
48
|
- `reslop https://github.com/metarhia/metacom/pull/555` GitHub/GitLab PR/MR
|
|
49
49
|
- `reslop -n` start a new review even if the latest is still editing
|
|
50
50
|
- `reslop -r` read-only mode
|
|
51
|
+
- `reslop -light` light color theme (default dark)
|
|
51
52
|
|
|
52
53
|
Reviews go in `.review/YYYY-MM-DD-NN.md` with frontmatter `status`:
|
|
53
54
|
|
package/lib/ansi.js
CHANGED
|
@@ -40,7 +40,7 @@ const WIDE_CP = [
|
|
|
40
40
|
0x1fae9, 0x1faf0, 0x1faf8, 0x20000, 0x2fffd, 0x30000, 0x3fffd,
|
|
41
41
|
];
|
|
42
42
|
|
|
43
|
-
const
|
|
43
|
+
const DARK_UI = {
|
|
44
44
|
delLineFg: [0xcc, 0x7a, 0x7a],
|
|
45
45
|
delLineBg: [0x3a, 0x1a, 0x1a],
|
|
46
46
|
delCharFg: [0xff, 0xff, 0xff],
|
|
@@ -87,7 +87,7 @@ const THEME = {
|
|
|
87
87
|
stagedAddCharBg: [0x4e, 0x4e, 0x58],
|
|
88
88
|
};
|
|
89
89
|
|
|
90
|
-
const
|
|
90
|
+
const DARK_CODE = {
|
|
91
91
|
keyword: [197, 134, 192],
|
|
92
92
|
control: [197, 134, 192],
|
|
93
93
|
storage: [86, 156, 214],
|
|
@@ -122,7 +122,7 @@ const CODE_FG = {
|
|
|
122
122
|
logDebug: [140, 160, 180],
|
|
123
123
|
};
|
|
124
124
|
|
|
125
|
-
const
|
|
125
|
+
const DARK_LOG_COLS = [
|
|
126
126
|
[210, 210, 220],
|
|
127
127
|
[120, 180, 220],
|
|
128
128
|
[180, 160, 220],
|
|
@@ -133,6 +133,143 @@ const LOG_COL_FG = [
|
|
|
133
133
|
[200, 170, 140],
|
|
134
134
|
];
|
|
135
135
|
|
|
136
|
+
const LIGHT_UI = {
|
|
137
|
+
delLineFg: [0xb3, 0x1d, 0x28],
|
|
138
|
+
delLineBg: [0xff, 0xeb, 0xe9],
|
|
139
|
+
delCharFg: [0x1f, 0x23, 0x28],
|
|
140
|
+
delCharBg: [0xff, 0xc1, 0xc0],
|
|
141
|
+
addLineFg: [0x1a, 0x7f, 0x37],
|
|
142
|
+
addLineBg: [0xe6, 0xff, 0xec],
|
|
143
|
+
addCharFg: [0x1f, 0x23, 0x28],
|
|
144
|
+
addCharBg: [0xab, 0xf2, 0xbc],
|
|
145
|
+
ctxFg: [0x57, 0x60, 0x6a],
|
|
146
|
+
ctxBg: [0xff, 0xff, 0xff],
|
|
147
|
+
splitGutterFg: [0xea, 0xee, 0xf2],
|
|
148
|
+
splitGutterBg: [0xea, 0xee, 0xf2],
|
|
149
|
+
headerFg: [0x24, 0x29, 0x2f],
|
|
150
|
+
headerBg: [0xd8, 0xde, 0xe4],
|
|
151
|
+
headerChromeFg: [0x42, 0x4a, 0x53],
|
|
152
|
+
headerRepoFg: [0x24, 0x29, 0x2f],
|
|
153
|
+
headerDirFg: [0x57, 0x60, 0x6a],
|
|
154
|
+
headerSlashFg: [0x8c, 0x95, 0x9f],
|
|
155
|
+
headerFileFg: [0x1f, 0x23, 0x28],
|
|
156
|
+
headerMetaFg: [0x42, 0x4a, 0x53],
|
|
157
|
+
chromeFg: [0x24, 0x29, 0x2f],
|
|
158
|
+
chromeBg: [0xf0, 0xf2, 0xf5],
|
|
159
|
+
noteBg: [0xe8, 0xf0, 0xfe],
|
|
160
|
+
buttonFg: [0x57, 0x60, 0x6a],
|
|
161
|
+
buttonBg: [0xe1, 0xe5, 0xea],
|
|
162
|
+
buttonHotFg: [0x0f, 0x14, 0x19],
|
|
163
|
+
currentBg: [0xac, 0xe1, 0xc2],
|
|
164
|
+
checkFg: [0x1f, 0x23, 0x28],
|
|
165
|
+
checkBg: [0xc8, 0xcf, 0xd8],
|
|
166
|
+
checkDoneFg: [0x0f, 0x3d, 0x1f],
|
|
167
|
+
checkDoneBg: [0xab, 0xf2, 0xbc],
|
|
168
|
+
errorFg: [0xcf, 0x22, 0x2e],
|
|
169
|
+
warnFg: [0x9a, 0x67, 0x00],
|
|
170
|
+
mutedFg: [0x6e, 0x77, 0x81],
|
|
171
|
+
shaFg: [0x05, 0x50, 0xae],
|
|
172
|
+
shaDarkFg: [0x0b, 0x3d, 0x70],
|
|
173
|
+
stagedDelLineFg: [0x57, 0x60, 0x6a],
|
|
174
|
+
stagedDelLineBg: [0xf3, 0xf4, 0xf6],
|
|
175
|
+
stagedDelCharFg: [0x1f, 0x23, 0x28],
|
|
176
|
+
stagedDelCharBg: [0xd8, 0xdc, 0xe1],
|
|
177
|
+
stagedAddLineFg: [0x42, 0x4a, 0x53],
|
|
178
|
+
stagedAddLineBg: [0xea, 0xec, 0xef],
|
|
179
|
+
stagedAddCharFg: [0x0f, 0x14, 0x19],
|
|
180
|
+
stagedAddCharBg: [0xcc, 0xd1, 0xd7],
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const LIGHT_CODE = {
|
|
184
|
+
keyword: [175, 0, 219],
|
|
185
|
+
control: [175, 0, 219],
|
|
186
|
+
storage: [0, 0, 255],
|
|
187
|
+
literal: [0, 0, 255],
|
|
188
|
+
string: [163, 21, 21],
|
|
189
|
+
template: [163, 21, 21],
|
|
190
|
+
escape: [238, 0, 0],
|
|
191
|
+
interpolation: [0, 16, 128],
|
|
192
|
+
number: [9, 134, 88],
|
|
193
|
+
comment: [0, 128, 0],
|
|
194
|
+
function: [121, 94, 38],
|
|
195
|
+
method: [121, 94, 38],
|
|
196
|
+
className: [38, 127, 153],
|
|
197
|
+
type: [38, 127, 153],
|
|
198
|
+
interface: [38, 127, 153],
|
|
199
|
+
property: [0, 16, 128],
|
|
200
|
+
variable: [0, 16, 128],
|
|
201
|
+
parameter: [0, 16, 128],
|
|
202
|
+
constant: [0, 112, 193],
|
|
203
|
+
operator: [60, 60, 60],
|
|
204
|
+
punct: [60, 60, 60],
|
|
205
|
+
regex: [129, 31, 63],
|
|
206
|
+
decorator: [121, 94, 38],
|
|
207
|
+
tag: [128, 0, 0],
|
|
208
|
+
attr: [229, 0, 0],
|
|
209
|
+
plain: [31, 31, 31],
|
|
210
|
+
punctDim: [140, 140, 140],
|
|
211
|
+
logDate: [60, 60, 70],
|
|
212
|
+
logError: [205, 49, 49],
|
|
213
|
+
logWarn: [160, 110, 0],
|
|
214
|
+
logInfo: [0, 90, 180],
|
|
215
|
+
logDebug: [100, 110, 125],
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const LIGHT_LOG_COLS = [
|
|
219
|
+
[60, 60, 70],
|
|
220
|
+
[0, 95, 150],
|
|
221
|
+
[110, 70, 170],
|
|
222
|
+
[30, 120, 60],
|
|
223
|
+
[150, 95, 20],
|
|
224
|
+
[170, 50, 90],
|
|
225
|
+
[20, 120, 120],
|
|
226
|
+
[130, 90, 50],
|
|
227
|
+
];
|
|
228
|
+
|
|
229
|
+
const PALETTES = {
|
|
230
|
+
dark: { ui: DARK_UI, code: DARK_CODE, logCols: DARK_LOG_COLS },
|
|
231
|
+
light: { ui: LIGHT_UI, code: LIGHT_CODE, logCols: LIGHT_LOG_COLS },
|
|
232
|
+
};
|
|
233
|
+
const THEME_NAMES = Object.keys(PALETTES);
|
|
234
|
+
|
|
235
|
+
const applyColors = (into, from) => {
|
|
236
|
+
for (const key of Object.keys(from)) {
|
|
237
|
+
const src = from[key];
|
|
238
|
+
let dst = into[key];
|
|
239
|
+
if (!dst) {
|
|
240
|
+
dst = [0, 0, 0];
|
|
241
|
+
into[key] = dst;
|
|
242
|
+
}
|
|
243
|
+
dst[0] = src[0];
|
|
244
|
+
dst[1] = src[1];
|
|
245
|
+
dst[2] = src[2];
|
|
246
|
+
}
|
|
247
|
+
return into;
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
const cloneColors = (colors) => {
|
|
251
|
+
const into = Array.isArray(colors) ? [] : {};
|
|
252
|
+
return applyColors(into, colors);
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
const THEME = cloneColors(DARK_UI);
|
|
256
|
+
const CODE_FG = cloneColors(DARK_CODE);
|
|
257
|
+
const LOG_COL_FG = cloneColors(DARK_LOG_COLS);
|
|
258
|
+
let currentTheme = 'dark';
|
|
259
|
+
|
|
260
|
+
const setTheme = (name) => {
|
|
261
|
+
if (!Object.hasOwn(PALETTES, name)) {
|
|
262
|
+
throw new Error(`unknown theme ${name}`);
|
|
263
|
+
}
|
|
264
|
+
const palette = PALETTES[name];
|
|
265
|
+
applyColors(THEME, palette.ui);
|
|
266
|
+
applyColors(CODE_FG, palette.code);
|
|
267
|
+
applyColors(LOG_COL_FG, palette.logCols);
|
|
268
|
+
currentTheme = name;
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
const themeName = () => currentTheme;
|
|
272
|
+
|
|
136
273
|
const codeFg = (style) => {
|
|
137
274
|
if (typeof style !== 'string') return CODE_FG.plain;
|
|
138
275
|
if (Object.hasOwn(CODE_FG, style)) return CODE_FG[style];
|
|
@@ -222,6 +359,10 @@ module.exports = {
|
|
|
222
359
|
EL,
|
|
223
360
|
THEME,
|
|
224
361
|
CODE_FG,
|
|
362
|
+
PALETTES,
|
|
363
|
+
THEME_NAMES,
|
|
364
|
+
setTheme,
|
|
365
|
+
themeName,
|
|
225
366
|
codeFg,
|
|
226
367
|
fg,
|
|
227
368
|
bg,
|
package/lib/api.js
CHANGED
package/lib/capabilities.js
CHANGED
|
@@ -18,6 +18,7 @@ const CHANGE_METHODS = [
|
|
|
18
18
|
'revertFile',
|
|
19
19
|
'edit',
|
|
20
20
|
'writeFile',
|
|
21
|
+
'stagePath',
|
|
21
22
|
];
|
|
22
23
|
|
|
23
24
|
const BRANCH_METHODS = [
|
|
@@ -25,6 +26,7 @@ const BRANCH_METHODS = [
|
|
|
25
26
|
'commitAsync',
|
|
26
27
|
'hasStaged',
|
|
27
28
|
'lastMessage',
|
|
29
|
+
'commitMessage',
|
|
28
30
|
'listBranches',
|
|
29
31
|
'listCommits',
|
|
30
32
|
'currentBranch',
|
|
@@ -38,6 +40,10 @@ const BRANCH_METHODS = [
|
|
|
38
40
|
'dropAsync',
|
|
39
41
|
'dropCommit',
|
|
40
42
|
'dropCommitAsync',
|
|
43
|
+
'applyFixup',
|
|
44
|
+
'applyFixupAsync',
|
|
45
|
+
'reword',
|
|
46
|
+
'rewordAsync',
|
|
41
47
|
'pull',
|
|
42
48
|
'pullAsync',
|
|
43
49
|
'push',
|
package/lib/cli.js
CHANGED
|
@@ -10,12 +10,14 @@ const { gitlabToken, loadMergeRequest } = require('./gitlab.js');
|
|
|
10
10
|
const { selectChangeSource, createLoadedSource } = require('./source.js');
|
|
11
11
|
const { Session } = require('./session.js');
|
|
12
12
|
const { capabilitiesFor, attachCapabilities } = require('./capabilities.js');
|
|
13
|
+
const { setTheme } = require('./ansi.js');
|
|
13
14
|
|
|
14
15
|
const HEX_REV = /^[0-9a-f]{4,40}$/i;
|
|
15
16
|
const HEAD_REV = /^HEAD([~^].*)?$/i;
|
|
16
17
|
const RANGE_REV = /[~^]/;
|
|
17
18
|
|
|
18
|
-
const USAGE =
|
|
19
|
+
const USAGE =
|
|
20
|
+
'Usage: reslop [-n] [-r] [-light] [path | commit | pr-url | mr-url]';
|
|
19
21
|
|
|
20
22
|
const REMOTE_CHANGE = {
|
|
21
23
|
pr: {
|
|
@@ -48,6 +50,7 @@ const parseArgv = (argv) => {
|
|
|
48
50
|
const paths = [];
|
|
49
51
|
let newReview = false;
|
|
50
52
|
let readOnly = false;
|
|
53
|
+
let theme = 'dark';
|
|
51
54
|
for (const arg of argv) {
|
|
52
55
|
if (arg === '-n') {
|
|
53
56
|
newReview = true;
|
|
@@ -57,12 +60,16 @@ const parseArgv = (argv) => {
|
|
|
57
60
|
readOnly = true;
|
|
58
61
|
continue;
|
|
59
62
|
}
|
|
63
|
+
if (arg === '-light') {
|
|
64
|
+
theme = 'light';
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
60
67
|
if (arg.startsWith('-')) {
|
|
61
68
|
throw new Error(`unknown option ${arg}`);
|
|
62
69
|
}
|
|
63
70
|
paths.push(arg);
|
|
64
71
|
}
|
|
65
|
-
return { paths, newReview, readOnly };
|
|
72
|
+
return { paths, newReview, readOnly, theme };
|
|
66
73
|
};
|
|
67
74
|
|
|
68
75
|
const looksLikeRev = (spec) => {
|
|
@@ -183,6 +190,7 @@ const run = async (proc, deps = {}) => {
|
|
|
183
190
|
stderr.write(`${USAGE}\n`);
|
|
184
191
|
return 1;
|
|
185
192
|
}
|
|
193
|
+
setTheme(args.theme);
|
|
186
194
|
let session;
|
|
187
195
|
try {
|
|
188
196
|
session = await loadSession(proc, deps, args);
|
package/lib/git-branches.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const os = require('node:os');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
|
|
3
7
|
const { oneLine, requireOk, runGit, runGitAsync } = require('./git.js');
|
|
4
8
|
|
|
5
9
|
const BRANCH_FORMAT = [
|
|
@@ -20,12 +24,15 @@ const PUSH_REJECTED = [
|
|
|
20
24
|
'updates were rejected',
|
|
21
25
|
];
|
|
22
26
|
|
|
23
|
-
const
|
|
24
|
-
const
|
|
27
|
+
const commitMessage = (top, sha) => {
|
|
28
|
+
const spec = `${sha ?? ''}`.trim() || 'HEAD';
|
|
29
|
+
const result = runGit(['log', '-1', '--format=%B', spec], top);
|
|
25
30
|
if (result.status !== 0) return '';
|
|
26
31
|
return (result.stdout ?? '').replace(/\s+$/, '');
|
|
27
32
|
};
|
|
28
33
|
|
|
34
|
+
const lastMessage = (top) => commitMessage(top, 'HEAD');
|
|
35
|
+
|
|
29
36
|
const COMMIT_FORMAT = ['%H', '%h', '%an', '%ar', '%s'].join('%x1f');
|
|
30
37
|
const COMMIT_LIMIT = 256;
|
|
31
38
|
const UNIT = '\x1f';
|
|
@@ -120,6 +127,177 @@ const dropCommitAsync = async (top, sha) => {
|
|
|
120
127
|
await dropOlderCommitAsync(top, target);
|
|
121
128
|
};
|
|
122
129
|
|
|
130
|
+
const FIXUP_MARK = 'fixup!';
|
|
131
|
+
const AUTOSQUASH_ARGS = [
|
|
132
|
+
'-c',
|
|
133
|
+
'sequence.editor=:',
|
|
134
|
+
'rebase',
|
|
135
|
+
'--interactive',
|
|
136
|
+
'--autosquash',
|
|
137
|
+
];
|
|
138
|
+
|
|
139
|
+
const isFixupSubject = (subject) =>
|
|
140
|
+
`${subject ?? ''}`.trim().startsWith(FIXUP_MARK);
|
|
141
|
+
|
|
142
|
+
const fixupRest = (subject) => {
|
|
143
|
+
let text = `${subject ?? ''}`.trim();
|
|
144
|
+
while (text.startsWith(FIXUP_MARK)) {
|
|
145
|
+
text = text.slice(FIXUP_MARK.length).trim();
|
|
146
|
+
}
|
|
147
|
+
return text;
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const commitSubject = (top, sha) => {
|
|
151
|
+
const args = ['log', '-1', '--format=%s', sha];
|
|
152
|
+
const result = requireOk(runGit(args, top), 'log failed');
|
|
153
|
+
return (result.stdout ?? '').trim();
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const ancestorWithSubject = (top, sha, want) => {
|
|
157
|
+
const args = ['log', `--format=${COMMIT_FORMAT}`, `${sha}^`];
|
|
158
|
+
const result = runGit(args, top);
|
|
159
|
+
if (result.status !== 0) return '';
|
|
160
|
+
for (const line of result.stdout.split('\n')) {
|
|
161
|
+
if (!line) continue;
|
|
162
|
+
const entry = parseCommitLine(line, 1);
|
|
163
|
+
if (entry && entry.subject === want) return entry.sha;
|
|
164
|
+
}
|
|
165
|
+
return '';
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const applyAutosquash = (top, ontoArgs) => {
|
|
169
|
+
const result = runGit([...AUTOSQUASH_ARGS, ...ontoArgs], top);
|
|
170
|
+
const failed = rebaseFailed(result);
|
|
171
|
+
if (!failed) return;
|
|
172
|
+
runGit(['rebase', '--abort'], top);
|
|
173
|
+
throw new Error(oneLine(failed));
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const applyAutosquashAsync = async (top, ontoArgs) => {
|
|
177
|
+
const result = await runGitAsync([...AUTOSQUASH_ARGS, ...ontoArgs], top);
|
|
178
|
+
const failed = rebaseFailed(result);
|
|
179
|
+
if (!failed) return;
|
|
180
|
+
await runGitAsync(['rebase', '--abort'], top);
|
|
181
|
+
throw new Error(oneLine(failed));
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const applyFixupOnto = (top, original, applyRange) => {
|
|
185
|
+
const parent = runGit(['rev-parse', '--verify', `${original}^`], top);
|
|
186
|
+
if (parent.status !== 0) return applyRange(top, ['--root']);
|
|
187
|
+
return applyRange(top, [(parent.stdout ?? '').trim()]);
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const applyFixup = (top, sha) => {
|
|
191
|
+
const spec = namedCommit(sha);
|
|
192
|
+
const commit = resolveCommit(top, spec);
|
|
193
|
+
const subject = commitSubject(top, commit);
|
|
194
|
+
if (!isFixupSubject(subject)) throw new Error('not a fixup commit');
|
|
195
|
+
const want = fixupRest(subject);
|
|
196
|
+
if (!want) throw new Error('no fixup target');
|
|
197
|
+
const original = ancestorWithSubject(top, commit, want);
|
|
198
|
+
if (!original) throw new Error('no fixup target');
|
|
199
|
+
applyFixupOnto(top, original, applyAutosquash);
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
const applyFixupAsync = async (top, sha) => {
|
|
203
|
+
const spec = namedCommit(sha);
|
|
204
|
+
const commit = resolveCommit(top, spec);
|
|
205
|
+
const subject = commitSubject(top, commit);
|
|
206
|
+
if (!isFixupSubject(subject)) throw new Error('not a fixup commit');
|
|
207
|
+
const want = fixupRest(subject);
|
|
208
|
+
if (!want) throw new Error('no fixup target');
|
|
209
|
+
const original = ancestorWithSubject(top, commit, want);
|
|
210
|
+
if (!original) throw new Error('no fixup target');
|
|
211
|
+
await applyFixupOnto(top, original, applyAutosquashAsync);
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const messageBody = (message) => {
|
|
215
|
+
const text = `${message ?? ''}`;
|
|
216
|
+
return text.endsWith('\n') ? text : `${text}\n`;
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
const rewordHead = (top, message) => {
|
|
220
|
+
const args = ['commit', '--amend', '--only', '-F', '-'];
|
|
221
|
+
requireOk(runGit(args, top, messageBody(message)));
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const REWORD_TODO = path.join(__dirname, 'git-reword-todo.js');
|
|
225
|
+
|
|
226
|
+
const prepareRewordEditor = (message) => {
|
|
227
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'reslop-reword-'));
|
|
228
|
+
const msgFile = path.join(dir, 'MSG');
|
|
229
|
+
fs.writeFileSync(msgFile, messageBody(message));
|
|
230
|
+
const node = JSON.stringify(process.execPath);
|
|
231
|
+
const script = JSON.stringify(REWORD_TODO);
|
|
232
|
+
return {
|
|
233
|
+
dir,
|
|
234
|
+
env: {
|
|
235
|
+
GIT_SEQUENCE_EDITOR: `${node} ${script}`,
|
|
236
|
+
RESLOP_REWORD_FILE: msgFile,
|
|
237
|
+
},
|
|
238
|
+
};
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
const rewordRange = (top, sha, env) => {
|
|
242
|
+
const parent = runGit(['rev-parse', '--verify', `${sha}^`], top);
|
|
243
|
+
const onto =
|
|
244
|
+
parent.status !== 0 ? ['--root'] : [(parent.stdout ?? '').trim()];
|
|
245
|
+
return runGit(['rebase', '--interactive', ...onto], top, undefined, { env });
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
const rewordRangeAsync = async (top, sha, env) => {
|
|
249
|
+
const parent = runGit(['rev-parse', '--verify', `${sha}^`], top);
|
|
250
|
+
const onto =
|
|
251
|
+
parent.status !== 0 ? ['--root'] : [(parent.stdout ?? '').trim()];
|
|
252
|
+
return runGitAsync(['rebase', '--interactive', ...onto], top, { env });
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
const finishRewordRebase = (top, result) => {
|
|
256
|
+
const failed = rebaseFailed(result);
|
|
257
|
+
if (!failed) return;
|
|
258
|
+
runGit(['rebase', '--abort'], top);
|
|
259
|
+
throw new Error(oneLine(failed));
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
const rewordOlder = (top, sha, message) => {
|
|
263
|
+
const prepared = prepareRewordEditor(message);
|
|
264
|
+
try {
|
|
265
|
+
const result = rewordRange(top, sha, prepared.env);
|
|
266
|
+
finishRewordRebase(top, result);
|
|
267
|
+
} finally {
|
|
268
|
+
fs.rmSync(prepared.dir, { recursive: true, force: true });
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
const rewordOlderAsync = async (top, sha, message) => {
|
|
273
|
+
const prepared = prepareRewordEditor(message);
|
|
274
|
+
try {
|
|
275
|
+
const result = await rewordRangeAsync(top, sha, prepared.env);
|
|
276
|
+
const failed = rebaseFailed(result);
|
|
277
|
+
if (!failed) return;
|
|
278
|
+
await runGitAsync(['rebase', '--abort'], top);
|
|
279
|
+
throw new Error(oneLine(failed));
|
|
280
|
+
} finally {
|
|
281
|
+
fs.rmSync(prepared.dir, { recursive: true, force: true });
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
const rewordCommit = (top, sha, message) => {
|
|
286
|
+
const spec = namedCommit(sha);
|
|
287
|
+
const commit = resolveCommit(top, spec);
|
|
288
|
+
const head = resolveCommit(top, 'HEAD');
|
|
289
|
+
if (commit === head) return void rewordHead(top, message);
|
|
290
|
+
rewordOlder(top, commit, message);
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
const rewordCommitAsync = async (top, sha, message) => {
|
|
294
|
+
const spec = namedCommit(sha);
|
|
295
|
+
const commit = resolveCommit(top, spec);
|
|
296
|
+
const head = resolveCommit(top, 'HEAD');
|
|
297
|
+
if (commit === head) return void rewordHead(top, message);
|
|
298
|
+
await rewordOlderAsync(top, commit, message);
|
|
299
|
+
};
|
|
300
|
+
|
|
123
301
|
const parseTrack = (text) => {
|
|
124
302
|
const raw = `${text ?? ''}`.trim().replace(/^\[|\]$/g, '');
|
|
125
303
|
if (!raw) return { ahead: 0, behind: 0, gone: false };
|
|
@@ -338,10 +516,6 @@ const hasStaged = (top) => {
|
|
|
338
516
|
};
|
|
339
517
|
|
|
340
518
|
const commitPlan = (kind, message) => {
|
|
341
|
-
if (kind === 'fixup') {
|
|
342
|
-
const target = (message ?? '').trim() || 'HEAD';
|
|
343
|
-
return { args: ['commit', `--fixup=${target}`], body: undefined };
|
|
344
|
-
}
|
|
345
519
|
const args = ['commit'];
|
|
346
520
|
if (kind === 'amend') args.push('--amend');
|
|
347
521
|
args.push('-F', '-');
|
|
@@ -362,6 +536,7 @@ const commitChangesAsync = async (top, kind, message) => {
|
|
|
362
536
|
|
|
363
537
|
module.exports = {
|
|
364
538
|
lastMessage,
|
|
539
|
+
commitMessage,
|
|
365
540
|
listCommits,
|
|
366
541
|
listBranches,
|
|
367
542
|
checkoutBranch,
|
|
@@ -372,6 +547,10 @@ module.exports = {
|
|
|
372
547
|
rebaseBranchAsync,
|
|
373
548
|
dropCommit,
|
|
374
549
|
dropCommitAsync,
|
|
550
|
+
applyFixup,
|
|
551
|
+
applyFixupAsync,
|
|
552
|
+
rewordCommit,
|
|
553
|
+
rewordCommitAsync,
|
|
375
554
|
dropBranch,
|
|
376
555
|
dropBranchAsync,
|
|
377
556
|
pullChanges,
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
|
|
5
|
+
const file = process.argv[process.argv.length - 1];
|
|
6
|
+
const msg = process.env.RESLOP_REWORD_FILE;
|
|
7
|
+
if (!file || !msg) process.exit(1);
|
|
8
|
+
const text = fs.readFileSync(file, 'utf8');
|
|
9
|
+
const rows = text.split('\n');
|
|
10
|
+
const out = [];
|
|
11
|
+
let seen = false;
|
|
12
|
+
for (const row of rows) {
|
|
13
|
+
out.push(row);
|
|
14
|
+
if (seen) continue;
|
|
15
|
+
if (!/^(pick|p)\s+/.test(row)) continue;
|
|
16
|
+
const spec = JSON.stringify(msg);
|
|
17
|
+
out.push(`exec git commit --amend --only -F ${spec}`);
|
|
18
|
+
seen = true;
|
|
19
|
+
}
|
|
20
|
+
fs.writeFileSync(file, out.join('\n'));
|
package/lib/git.js
CHANGED
|
@@ -25,7 +25,7 @@ const GIT_MAX_BUFFER = proc.DEFAULT_MAX_BUFFER;
|
|
|
25
25
|
|
|
26
26
|
const gitProcOptions = (cwd, extra = {}) => ({
|
|
27
27
|
cwd,
|
|
28
|
-
env: { ...process.env, GIT_OPTIONAL_LOCKS: '0' },
|
|
28
|
+
env: { ...process.env, GIT_OPTIONAL_LOCKS: '0', ...extra.env },
|
|
29
29
|
encoding: GIT_ENCODING,
|
|
30
30
|
maxBuffer: GIT_MAX_BUFFER,
|
|
31
31
|
input: extra.input,
|
|
@@ -45,8 +45,8 @@ const requireOk = (result, fallback = 'git failed') => {
|
|
|
45
45
|
throw new Error(oneLine(msg, fallback));
|
|
46
46
|
};
|
|
47
47
|
|
|
48
|
-
const runGit = (args, cwd, input) => {
|
|
49
|
-
const options = gitProcOptions(cwd, { input });
|
|
48
|
+
const runGit = (args, cwd, input, extra = {}) => {
|
|
49
|
+
const options = gitProcOptions(cwd, { input, env: extra.env });
|
|
50
50
|
return gitResult(runProcSync('git', args, options));
|
|
51
51
|
};
|
|
52
52
|
|
|
@@ -503,6 +503,11 @@ const writeFile = (top, rel, text) => {
|
|
|
503
503
|
writeWorktree(abs, text);
|
|
504
504
|
};
|
|
505
505
|
|
|
506
|
+
const stagePath = (top, rel) => {
|
|
507
|
+
if (!rel) return;
|
|
508
|
+
requireOk(runGit(['add', '--', rel], top));
|
|
509
|
+
};
|
|
510
|
+
|
|
506
511
|
module.exports = {
|
|
507
512
|
oneLine,
|
|
508
513
|
requireOk,
|
|
@@ -521,6 +526,7 @@ module.exports = {
|
|
|
521
526
|
readSnapshotAsync,
|
|
522
527
|
editItem,
|
|
523
528
|
writeFile,
|
|
529
|
+
stagePath,
|
|
524
530
|
};
|
|
525
531
|
|
|
526
532
|
const branches = require('./git-branches.js');
|
|
@@ -590,10 +596,12 @@ const createGitRepo = () =>
|
|
|
590
596
|
revertFile: revertGitFile,
|
|
591
597
|
edit: editItem,
|
|
592
598
|
writeFile,
|
|
599
|
+
stagePath,
|
|
593
600
|
commit: branches.commitChanges,
|
|
594
601
|
commitAsync: branches.commitChangesAsync,
|
|
595
602
|
hasStaged: branches.hasStaged,
|
|
596
603
|
lastMessage: branches.lastMessage,
|
|
604
|
+
commitMessage: branches.commitMessage,
|
|
597
605
|
listBranches: branches.listBranches,
|
|
598
606
|
listCommits: branches.listCommits,
|
|
599
607
|
currentBranch,
|
|
@@ -607,6 +615,10 @@ const createGitRepo = () =>
|
|
|
607
615
|
dropAsync: branches.dropBranchAsync,
|
|
608
616
|
dropCommit: branches.dropCommit,
|
|
609
617
|
dropCommitAsync: branches.dropCommitAsync,
|
|
618
|
+
applyFixup: branches.applyFixup,
|
|
619
|
+
applyFixupAsync: branches.applyFixupAsync,
|
|
620
|
+
reword: branches.rewordCommit,
|
|
621
|
+
rewordAsync: branches.rewordCommitAsync,
|
|
610
622
|
pull: branches.pullChanges,
|
|
611
623
|
pullAsync: branches.pullChangesAsync,
|
|
612
624
|
push: branches.pushChanges,
|
|
@@ -628,6 +640,7 @@ module.exports.commitChangesAsync = branches.commitChangesAsync;
|
|
|
628
640
|
module.exports.hasStaged = branches.hasStaged;
|
|
629
641
|
module.exports.currentBranch = currentBranch;
|
|
630
642
|
module.exports.lastMessage = branches.lastMessage;
|
|
643
|
+
module.exports.commitMessage = branches.commitMessage;
|
|
631
644
|
module.exports.listBranches = branches.listBranches;
|
|
632
645
|
module.exports.listCommits = branches.listCommits;
|
|
633
646
|
module.exports.checkoutBranch = branches.checkoutBranch;
|
|
@@ -640,6 +653,10 @@ module.exports.dropBranch = branches.dropBranch;
|
|
|
640
653
|
module.exports.dropBranchAsync = branches.dropBranchAsync;
|
|
641
654
|
module.exports.dropCommit = branches.dropCommit;
|
|
642
655
|
module.exports.dropCommitAsync = branches.dropCommitAsync;
|
|
656
|
+
module.exports.applyFixup = branches.applyFixup;
|
|
657
|
+
module.exports.applyFixupAsync = branches.applyFixupAsync;
|
|
658
|
+
module.exports.rewordCommit = branches.rewordCommit;
|
|
659
|
+
module.exports.rewordCommitAsync = branches.rewordCommitAsync;
|
|
643
660
|
module.exports.pullChanges = branches.pullChanges;
|
|
644
661
|
module.exports.pullChangesAsync = branches.pullChangesAsync;
|
|
645
662
|
module.exports.pushChanges = branches.pushChanges;
|
package/lib/render/branches.js
CHANGED
|
@@ -6,7 +6,8 @@ const { measureColumns, padColumns, joinColumns, paintColumns } = primitives;
|
|
|
6
6
|
|
|
7
7
|
const { THEME, paint, visibleWidth, truncateVisible, seq, EL } = ansi;
|
|
8
8
|
const { fill, FILE_MARK, paneResult } = primitives;
|
|
9
|
-
const { paintCursorList,
|
|
9
|
+
const { paintCursorList, paintListEdit } = primitives;
|
|
10
|
+
const { branchLabel, paintBracketName } = primitives;
|
|
10
11
|
|
|
11
12
|
const BRANCH_COL_ALIGN = {
|
|
12
13
|
sha: 'end',
|
|
@@ -116,37 +117,8 @@ const paintBranchRow = (entry, width, color, selected, cols) => {
|
|
|
116
117
|
return out + edge;
|
|
117
118
|
};
|
|
118
119
|
|
|
119
|
-
const paintBranchEdit = (compose, width, color) =>
|
|
120
|
-
|
|
121
|
-
const prefix = ` ${FILE_MARK} `;
|
|
122
|
-
const prefixW = visibleWidth(prefix);
|
|
123
|
-
const rest = Math.max(1, inner - prefixW);
|
|
124
|
-
const text = compose.text ?? '';
|
|
125
|
-
const label = `[${text}]`;
|
|
126
|
-
const clipped = truncateVisible(label, rest);
|
|
127
|
-
const pad = Math.max(0, rest - visibleWidth(clipped));
|
|
128
|
-
const left = `${prefix}${clipped}${' '.repeat(pad)}`;
|
|
129
|
-
const fgRgb = THEME.chromeFg;
|
|
130
|
-
const bgRgb = THEME.buttonBg;
|
|
131
|
-
const edge = fill(BRANCH_EDGE, fgRgb, THEME.ctxBg, color);
|
|
132
|
-
let row = `${left}${edge}`;
|
|
133
|
-
if (color) {
|
|
134
|
-
const gap = ' '.repeat(pad);
|
|
135
|
-
row = `${seq(fgRgb, bgRgb)}${EL}`;
|
|
136
|
-
row += paint(prefix, fgRgb, bgRgb, color);
|
|
137
|
-
row += paintBracketName(
|
|
138
|
-
clipped,
|
|
139
|
-
THEME.buttonHotFg,
|
|
140
|
-
THEME.mutedFg,
|
|
141
|
-
bgRgb,
|
|
142
|
-
color,
|
|
143
|
-
);
|
|
144
|
-
row += paint(gap, fgRgb, bgRgb, color);
|
|
145
|
-
row += edge;
|
|
146
|
-
}
|
|
147
|
-
const col = Math.min((compose.cursor ?? 0) + 1, visibleWidth(clipped));
|
|
148
|
-
return { row, cursor: { x: prefixW + col + 1 } };
|
|
149
|
-
};
|
|
120
|
+
const paintBranchEdit = (compose, width, color) =>
|
|
121
|
+
paintListEdit(compose, width, color, BRANCH_EDGE);
|
|
150
122
|
|
|
151
123
|
const paintBodyBranches = (view, width, color, bodyH, headerLines) => {
|
|
152
124
|
const branches = view.branches ?? [];
|