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 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.0...HEAD
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 THEME = {
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 CODE_FG = {
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 LOG_COL_FG = [
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
@@ -16,6 +16,8 @@ const cli = require('./cli.js');
16
16
  module.exports = {
17
17
  THEME: ansi.THEME,
18
18
  CODE_FG: ansi.CODE_FG,
19
+ THEME_NAMES: ansi.THEME_NAMES,
20
+ setTheme: ansi.setTheme,
19
21
  ACTIONS: keys.ACTIONS,
20
22
  diffChars: diff.diffChars,
21
23
  parseDiff: diff.parseDiff,
@@ -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 = 'Usage: reslop [-n] [-r] [path | commit | pr-url | mr-url]';
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);
@@ -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 lastMessage = (top) => {
24
- const result = runGit(['log', '-1', '--format=%B'], top);
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;
@@ -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, branchLabel, paintBracketName } = primitives;
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
- const inner = Math.max(1, width - BRANCH_EDGE);
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 ?? [];