reslop 0.2.3 → 0.2.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.
Files changed (80) hide show
  1. package/CHANGELOG.md +14 -1
  2. package/README.md +8 -3
  3. package/bin/reslop.js +4 -2
  4. package/lib/api.js +2 -1
  5. package/lib/cli.js +19 -10
  6. package/lib/clipboard.js +6 -3
  7. package/lib/deps-diff.js +61 -115
  8. package/lib/deps.js +24 -61
  9. package/lib/detect.js +4 -2
  10. package/lib/diff/align.js +2 -1
  11. package/lib/diff/diff.js +6 -3
  12. package/lib/diff/display.js +2 -1
  13. package/lib/diff/inline.js +2 -1
  14. package/lib/diff/patch.js +2 -1
  15. package/lib/diff/unit.js +4 -2
  16. package/lib/editor.js +22 -4
  17. package/lib/files.js +38 -12
  18. package/lib/git-branches.js +107 -154
  19. package/lib/git-deps.js +20 -60
  20. package/lib/git-worktree.js +508 -0
  21. package/lib/git.js +23 -580
  22. package/lib/github.js +2 -1
  23. package/lib/gitlab.js +2 -1
  24. package/lib/highlight/bash.js +2 -1
  25. package/lib/highlight/css.js +2 -1
  26. package/lib/highlight/csv.js +2 -1
  27. package/lib/highlight/dot.js +2 -1
  28. package/lib/highlight/html.js +2 -1
  29. package/lib/highlight/json.js +2 -1
  30. package/lib/highlight/log.js +2 -1
  31. package/lib/highlight/txt.js +2 -1
  32. package/lib/highlight.js +2 -1
  33. package/lib/keys.js +2 -2
  34. package/lib/manifest.js +4 -2
  35. package/lib/npm-commands.js +83 -4
  36. package/lib/remote.js +20 -13
  37. package/lib/render/branches.js +6 -15
  38. package/lib/render/chrome.js +27 -7
  39. package/lib/render/commits.js +158 -46
  40. package/lib/render/diff.js +130 -47
  41. package/lib/render/files.js +4 -12
  42. package/lib/render/frame.js +3 -9
  43. package/lib/render/notes.js +4 -11
  44. package/lib/render/npm.js +1 -13
  45. package/lib/render/present.js +2 -1
  46. package/lib/render/primitives.js +28 -15
  47. package/lib/render/render.js +2 -1
  48. package/lib/render/unit.js +14 -11
  49. package/lib/report-model.js +6 -4
  50. package/lib/report-parse.js +8 -13
  51. package/lib/report-render.js +38 -21
  52. package/lib/report-run.js +12 -10
  53. package/lib/report-test.js +5 -4
  54. package/lib/report-tools.js +44 -18
  55. package/lib/review-merge.js +2 -1
  56. package/lib/review.js +6 -2
  57. package/lib/session/actions.js +49 -65
  58. package/lib/session/branches.js +13 -49
  59. package/lib/session/change-actions.js +2 -1
  60. package/lib/session/commit.js +34 -49
  61. package/lib/session/compose.js +37 -11
  62. package/lib/session/dispatch.js +17 -25
  63. package/lib/session/files-pane.js +6 -27
  64. package/lib/session/items.js +6 -3
  65. package/lib/session/lifecycle.js +8 -4
  66. package/lib/session/load.js +4 -2
  67. package/lib/session/navigation.js +1 -0
  68. package/lib/session/npm.js +54 -1
  69. package/lib/session/ops.js +15 -1
  70. package/lib/session/pointer.js +22 -3
  71. package/lib/session/review.js +2 -1
  72. package/lib/session/terminal.js +4 -2
  73. package/lib/session/update.js +2 -1
  74. package/lib/session/view.js +29 -3
  75. package/lib/session.js +37 -23
  76. package/lib/source.js +6 -3
  77. package/lib/update.js +5 -9
  78. package/lib/utilities.js +4 -2
  79. package/lib/wrap.js +3 -1
  80. package/package.json +1 -1
package/lib/editor.js CHANGED
@@ -1,7 +1,9 @@
1
1
  'use strict';
2
2
 
3
- const { graphemes } = require('./ansi.js');
4
- const { wrapMove } = require('./wrap.js');
3
+ const ansi = require('./ansi.js');
4
+ const { graphemes, visibleWidth } = ansi;
5
+ const wrap = require('./wrap.js');
6
+ const { wrapMove } = wrap;
5
7
 
6
8
  const UNDO_CAP = 100;
7
9
 
@@ -59,6 +61,7 @@ class Editor {
59
61
  this.undo = [];
60
62
  this.redo = [];
61
63
  this.wantCol = null;
64
+ this.scrollCol = 0;
62
65
  }
63
66
 
64
67
  snapshot() {
@@ -132,6 +135,19 @@ class Editor {
132
135
  return { line, col };
133
136
  }
134
137
 
138
+ visibleLineCol() {
139
+ const { col, line } = this.linePos();
140
+ const row = this.text.split('\n')[line] ?? '';
141
+ return visibleWidth(row.slice(0, col));
142
+ }
143
+
144
+ reveal(width) {
145
+ const col = this.visibleLineCol();
146
+ const room = Math.max(1, width);
147
+ if (col < this.scrollCol) this.scrollCol = col;
148
+ if (col >= this.scrollCol + room) this.scrollCol = col - room + 1;
149
+ }
150
+
135
151
  moveLine(delta, width) {
136
152
  if (width !== undefined) {
137
153
  const col = this.wantCol;
@@ -142,8 +158,10 @@ class Editor {
142
158
  }
143
159
  const rows = this.text.split('\n');
144
160
  const { line, col } = this.linePos();
145
- const next = line + delta;
146
- if (next < 0 || next >= rows.length) return;
161
+ let next = line + delta;
162
+ if (next < 0) next = 0;
163
+ if (next >= rows.length) next = rows.length - 1;
164
+ if (next === line) return;
147
165
  const row = rows[next];
148
166
  const c = Math.min(col, row.length);
149
167
  let cursor = 0;
package/lib/files.js CHANGED
@@ -1,11 +1,12 @@
1
1
  'use strict';
2
2
 
3
- const { listFilePath } = require('./manifest.js');
3
+ const manifest = require('./manifest.js');
4
+ const { listFilePath } = manifest;
4
5
 
5
6
  const REVIEW_DIR = '.review';
6
7
  const ORIGIN_ORDER = ['staged', 'unstaged', 'untracked', 'commit', 'pr'];
7
8
  const TODO_FILE = 'TODOs';
8
- const REPO_TODOS_LABEL = 'Repository TODOs and Issues';
9
+ const REPO_TODOS_LABEL = 'Project Backlog and Issues';
9
10
  const TOTAL_LABEL = 'total';
10
11
 
11
12
  const isPathInScope = (rel, paths) => {
@@ -73,23 +74,47 @@ const lineDelta = ({ hunk, blockId }) => {
73
74
  return delta;
74
75
  };
75
76
 
76
- const countAgo = (n, unit) => {
77
- const label = n === 1 ? unit : `${unit}s`;
78
- return `${n} ${label} ago`;
77
+ const AGE_UNIT = {
78
+ second: 's',
79
+ minute: 'm',
80
+ hour: 'h',
81
+ day: 'd',
82
+ week: 'w',
83
+ month: 'mo',
84
+ year: 'y',
85
+ };
86
+
87
+ const AGE_AGO = /^(\d+)\s+(second|minute|hour|day|week|month|year)s?\s+ago$/i;
88
+ const AGE_SHORT = /^(\d+)(mo|[smhdwy])(?: ago)?$/i;
89
+
90
+ const shortUnit = (n, unit) => `${n}${AGE_UNIT[unit]} ago`;
91
+
92
+ const shortAge = (text) => {
93
+ const raw = `${text ?? ''}`.trim();
94
+ if (!raw) return '';
95
+ if (/^yesterday$/i.test(raw)) return '1d ago';
96
+ const match = AGE_AGO.exec(raw);
97
+ if (match) {
98
+ const unit = AGE_UNIT[match[2].toLowerCase()];
99
+ if (unit) return `${match[1]}${unit} ago`;
100
+ }
101
+ const brief = AGE_SHORT.exec(raw);
102
+ if (brief) return `${brief[1]}${brief[2].toLowerCase()} ago`;
103
+ return raw;
79
104
  };
80
105
 
81
106
  const relativeAge = (thenMs, nowMs = Date.now()) => {
82
107
  const diff = Math.max(0, Math.round((nowMs - thenMs) / 1000));
83
- if (diff < 90) return countAgo(diff, 'second');
108
+ if (diff < 90) return shortUnit(diff, 'second');
84
109
  const minutes = Math.round(diff / 60);
85
- if (minutes < 90) return countAgo(minutes, 'minute');
110
+ if (minutes < 90) return shortUnit(minutes, 'minute');
86
111
  const hours = Math.round(minutes / 60);
87
- if (hours < 36) return countAgo(hours, 'hour');
112
+ if (hours < 36) return shortUnit(hours, 'hour');
88
113
  const days = Math.round(hours / 24);
89
- if (days < 14) return countAgo(days, 'day');
90
- if (days < 70) return countAgo(Math.round(days / 7), 'week');
91
- if (days < 365) return countAgo(Math.round(days / 30), 'month');
92
- return countAgo(Math.round(days / 365), 'year');
114
+ if (days < 14) return shortUnit(days, 'day');
115
+ if (days < 70) return shortUnit(Math.round(days / 7), 'week');
116
+ if (days < 365) return shortUnit(Math.round(days / 30), 'month');
117
+ return shortUnit(Math.round(days / 365), 'year');
93
118
  };
94
119
 
95
120
  const addLineDelta = (group, origin, delta) => {
@@ -231,4 +256,5 @@ module.exports = {
231
256
  fileEntries,
232
257
  fileTotals,
233
258
  relativeAge,
259
+ shortAge,
234
260
  };
@@ -4,7 +4,10 @@ const fs = require('node:fs');
4
4
  const os = require('node:os');
5
5
  const path = require('node:path');
6
6
 
7
- const { oneLine, requireOk, runGit, runGitAsync } = require('./git.js');
7
+ const files = require('./files.js');
8
+ const gitWorktree = require('./git-worktree.js');
9
+ const { shortAge } = files;
10
+ const { oneLine, requireOk, runGit, runGitAsync } = gitWorktree;
8
11
 
9
12
  const BRANCH_FORMAT = [
10
13
  '%(HEAD)',
@@ -72,27 +75,56 @@ const parseCommitRecord = (record, index) => {
72
75
  shortSha: read('%h') || sha.slice(0, 7),
73
76
  author: read('%an'),
74
77
  email: read('%ae'),
75
- date: read('%ar'),
78
+ date: shortAge(read('%ar')),
76
79
  when: read('%ci'),
77
80
  refs: formatCommitRefs(read('%D')),
78
81
  subject,
79
82
  body,
83
+ added: 0,
84
+ removed: 0,
80
85
  head: index === 0,
81
86
  };
82
87
  };
83
88
 
89
+ const STAT_LEAD = /^\n* *\d+ files? changed[^\n]*\n?/;
90
+
91
+ const takeStat = (chunk) => {
92
+ const match = STAT_LEAD.exec(chunk);
93
+ if (!match) return { stat: '', rest: chunk };
94
+ return { stat: match[0], rest: chunk.slice(match[0].length) };
95
+ };
96
+
97
+ const countIn = (line, word) => {
98
+ const match = new RegExp(`(\\d+) ${word}`).exec(line);
99
+ if (!match) return 0;
100
+ return parseInt(match[1], 10);
101
+ };
102
+
103
+ const parseShortstat = (line) => ({
104
+ added: countIn(line, 'insertion'),
105
+ removed: countIn(line, 'deletion'),
106
+ });
107
+
84
108
  const listCommits = (top) => {
85
109
  const args = [
86
110
  'log',
87
111
  '-z',
112
+ '--shortstat',
88
113
  `--format=${COMMIT_FORMAT}`,
89
114
  `--max-count=${COMMIT_LIMIT}`,
90
115
  ];
91
116
  const result = requireOk(runGit(args, top), 'log failed');
92
117
  const commits = [];
93
- for (const record of result.stdout.split('\0')) {
94
- if (!record) continue;
95
- const entry = parseCommitRecord(record, commits.length);
118
+ for (const chunk of result.stdout.split('\0')) {
119
+ const taken = takeStat(chunk);
120
+ if (taken.stat && commits.length) {
121
+ const stat = parseShortstat(taken.stat);
122
+ const last = commits[commits.length - 1];
123
+ last.added = stat.added;
124
+ last.removed = stat.removed;
125
+ }
126
+ if (!taken.rest) continue;
127
+ const entry = parseCommitRecord(taken.rest, commits.length);
96
128
  if (entry) commits.push(entry);
97
129
  }
98
130
  return commits;
@@ -113,48 +145,40 @@ const dropHeadCommit = (top) => {
113
145
  requireOk(runGit(['reset', '--soft', 'HEAD~1'], top), 'drop failed');
114
146
  };
115
147
 
116
- const rebaseFailed = (result) => {
117
- if (result.status === 0) return '';
118
- return result.stderr || result.stdout || 'rebase failed';
119
- };
120
-
121
- const dropOlderCommit = (top, sha) => {
122
- const onto = `${sha}^`;
123
- const result = runGit(['rebase', '--onto', onto, sha], top);
124
- const failed = rebaseFailed(result);
125
- if (!failed) return;
148
+ const runRebase = (top, args, extra = {}) => {
149
+ const result = runGit(args, top, undefined, extra);
150
+ if (result.status === 0) return;
126
151
  runGit(['rebase', '--abort'], top);
127
- throw new Error(oneLine(failed));
152
+ requireOk(result, 'rebase failed');
128
153
  };
129
154
 
130
- const dropOlderCommitAsync = async (top, sha) => {
131
- const onto = `${sha}^`;
132
- const result = await runGitAsync(['rebase', '--onto', onto, sha], top);
133
- const failed = rebaseFailed(result);
134
- if (!failed) return;
155
+ const runRebaseAsync = async (top, args, extra = {}) => {
156
+ const result = await runGitAsync(args, top, extra);
157
+ if (result.status === 0) return;
135
158
  await runGitAsync(['rebase', '--abort'], top);
136
- throw new Error(oneLine(failed));
159
+ requireOk(result, 'rebase failed');
137
160
  };
138
161
 
139
- const dropCommit = (top, sha) => {
140
- const spec = namedCommit(sha);
141
- const target = resolveCommit(top, spec);
162
+ const dropTarget = (top, sha) => {
163
+ const target = resolveCommit(top, namedCommit(sha));
142
164
  const parent = runGit(['rev-parse', '--verify', `${target}^`], top);
143
165
  if (parent.status !== 0) throw new Error('cannot drop root commit');
144
- const head = resolveCommit(top, 'HEAD');
145
- if (target === head) return void dropHeadCommit(top);
146
- dropOlderCommit(top, target);
166
+ return target;
167
+ };
168
+
169
+ const dropCommit = (top, sha) => {
170
+ const target = dropTarget(top, sha);
171
+ if (target === resolveCommit(top, 'HEAD')) return void dropHeadCommit(top);
172
+ runRebase(top, ['rebase', '--onto', `${target}^`, target]);
147
173
  };
148
174
 
149
175
  const dropCommitAsync = async (top, sha) => {
150
- const spec = namedCommit(sha);
151
- const target = resolveCommit(top, spec);
152
- const parentArgs = ['rev-parse', '--verify', `${target}^`];
153
- const parent = await runGitAsync(parentArgs, top);
154
- if (parent.status !== 0) throw new Error('cannot drop root commit');
155
- const head = resolveCommit(top, 'HEAD');
156
- if (target === head) return void dropHeadCommit(top);
157
- await dropOlderCommitAsync(top, target);
176
+ const target = dropTarget(top, sha);
177
+ if (target === resolveCommit(top, 'HEAD')) {
178
+ requireOk(await runGitAsync(['reset', '--soft', 'HEAD~1'], top));
179
+ return;
180
+ }
181
+ await runRebaseAsync(top, ['rebase', '--onto', `${target}^`, target]);
158
182
  };
159
183
 
160
184
  const FIXUP_MARK = 'fixup!';
@@ -197,61 +221,33 @@ const ancestorWithSubject = (top, sha, want) => {
197
221
  return '';
198
222
  };
199
223
 
200
- const applyAutosquash = (top, ontoArgs) => {
201
- const result = runGit([...AUTOSQUASH_ARGS, ...ontoArgs], top);
202
- const failed = rebaseFailed(result);
203
- if (!failed) return;
204
- runGit(['rebase', '--abort'], top);
205
- throw new Error(oneLine(failed));
206
- };
207
-
208
- const applyAutosquashAsync = async (top, ontoArgs) => {
209
- const result = await runGitAsync([...AUTOSQUASH_ARGS, ...ontoArgs], top);
210
- const failed = rebaseFailed(result);
211
- if (!failed) return;
212
- await runGitAsync(['rebase', '--abort'], top);
213
- throw new Error(oneLine(failed));
214
- };
215
-
216
- const applyFixupOnto = (top, original, applyRange) => {
217
- const parent = runGit(['rev-parse', '--verify', `${original}^`], top);
218
- if (parent.status !== 0) return applyRange(top, ['--root']);
219
- return applyRange(top, [(parent.stdout ?? '').trim()]);
224
+ const rebaseRange = (top, sha) => {
225
+ const parent = runGit(['rev-parse', '--verify', `${sha}^`], top);
226
+ return parent.status === 0 ? [parent.stdout.trim()] : ['--root'];
220
227
  };
221
228
 
222
- const applyFixup = (top, sha) => {
223
- const spec = namedCommit(sha);
224
- const commit = resolveCommit(top, spec);
229
+ const fixupArgs = (top, sha) => {
230
+ const commit = resolveCommit(top, namedCommit(sha));
225
231
  const subject = commitSubject(top, commit);
226
232
  if (!isFixupSubject(subject)) throw new Error('not a fixup commit');
227
233
  const want = fixupRest(subject);
228
234
  if (!want) throw new Error('no fixup target');
229
235
  const original = ancestorWithSubject(top, commit, want);
230
236
  if (!original) throw new Error('no fixup target');
231
- applyFixupOnto(top, original, applyAutosquash);
237
+ return [...AUTOSQUASH_ARGS, ...rebaseRange(top, original)];
232
238
  };
233
239
 
234
- const applyFixupAsync = async (top, sha) => {
235
- const spec = namedCommit(sha);
236
- const commit = resolveCommit(top, spec);
237
- const subject = commitSubject(top, commit);
238
- if (!isFixupSubject(subject)) throw new Error('not a fixup commit');
239
- const want = fixupRest(subject);
240
- if (!want) throw new Error('no fixup target');
241
- const original = ancestorWithSubject(top, commit, want);
242
- if (!original) throw new Error('no fixup target');
243
- await applyFixupOnto(top, original, applyAutosquashAsync);
244
- };
240
+ const applyFixup = (top, sha) => runRebase(top, fixupArgs(top, sha));
241
+
242
+ const applyFixupAsync = async (top, sha) =>
243
+ runRebaseAsync(top, fixupArgs(top, sha));
245
244
 
246
245
  const messageBody = (message) => {
247
246
  const text = `${message ?? ''}`;
248
247
  return text.endsWith('\n') ? text : `${text}\n`;
249
248
  };
250
249
 
251
- const rewordHead = (top, message) => {
252
- const args = ['commit', '--amend', '--only', '-F', '-'];
253
- requireOk(runGit(args, top, messageBody(message)));
254
- };
250
+ const REWORD_HEAD_ARGS = ['commit', '--amend', '--only', '-F', '-'];
255
251
 
256
252
  const REWORD_TODO = path.join(__dirname, 'git-reword-todo.js');
257
253
 
@@ -270,32 +266,11 @@ const prepareRewordEditor = (message) => {
270
266
  };
271
267
  };
272
268
 
273
- const rewordRange = (top, sha, env) => {
274
- const parent = runGit(['rev-parse', '--verify', `${sha}^`], top);
275
- const onto =
276
- parent.status !== 0 ? ['--root'] : [(parent.stdout ?? '').trim()];
277
- return runGit(['rebase', '--interactive', ...onto], top, undefined, { env });
278
- };
279
-
280
- const rewordRangeAsync = async (top, sha, env) => {
281
- const parent = runGit(['rev-parse', '--verify', `${sha}^`], top);
282
- const onto =
283
- parent.status !== 0 ? ['--root'] : [(parent.stdout ?? '').trim()];
284
- return runGitAsync(['rebase', '--interactive', ...onto], top, { env });
285
- };
286
-
287
- const finishRewordRebase = (top, result) => {
288
- const failed = rebaseFailed(result);
289
- if (!failed) return;
290
- runGit(['rebase', '--abort'], top);
291
- throw new Error(oneLine(failed));
292
- };
293
-
294
269
  const rewordOlder = (top, sha, message) => {
295
270
  const prepared = prepareRewordEditor(message);
296
271
  try {
297
- const result = rewordRange(top, sha, prepared.env);
298
- finishRewordRebase(top, result);
272
+ const args = ['rebase', '--interactive', ...rebaseRange(top, sha)];
273
+ runRebase(top, args, { env: prepared.env });
299
274
  } finally {
300
275
  fs.rmSync(prepared.dir, { recursive: true, force: true });
301
276
  }
@@ -304,29 +279,29 @@ const rewordOlder = (top, sha, message) => {
304
279
  const rewordOlderAsync = async (top, sha, message) => {
305
280
  const prepared = prepareRewordEditor(message);
306
281
  try {
307
- const result = await rewordRangeAsync(top, sha, prepared.env);
308
- const failed = rebaseFailed(result);
309
- if (!failed) return;
310
- await runGitAsync(['rebase', '--abort'], top);
311
- throw new Error(oneLine(failed));
282
+ const args = ['rebase', '--interactive', ...rebaseRange(top, sha)];
283
+ await runRebaseAsync(top, args, { env: prepared.env });
312
284
  } finally {
313
285
  fs.rmSync(prepared.dir, { recursive: true, force: true });
314
286
  }
315
287
  };
316
288
 
317
289
  const rewordCommit = (top, sha, message) => {
318
- const spec = namedCommit(sha);
319
- const commit = resolveCommit(top, spec);
320
- const head = resolveCommit(top, 'HEAD');
321
- if (commit === head) return void rewordHead(top, message);
290
+ const commit = resolveCommit(top, namedCommit(sha));
291
+ if (commit === resolveCommit(top, 'HEAD')) {
292
+ requireOk(runGit(REWORD_HEAD_ARGS, top, messageBody(message)));
293
+ return;
294
+ }
322
295
  rewordOlder(top, commit, message);
323
296
  };
324
297
 
325
298
  const rewordCommitAsync = async (top, sha, message) => {
326
- const spec = namedCommit(sha);
327
- const commit = resolveCommit(top, spec);
328
- const head = resolveCommit(top, 'HEAD');
329
- if (commit === head) return void rewordHead(top, message);
299
+ const commit = resolveCommit(top, namedCommit(sha));
300
+ if (commit === resolveCommit(top, 'HEAD')) {
301
+ const input = messageBody(message);
302
+ requireOk(await runGitAsync(REWORD_HEAD_ARGS, top, { input }));
303
+ return;
304
+ }
330
305
  await rewordOlderAsync(top, commit, message);
331
306
  };
332
307
 
@@ -348,7 +323,7 @@ const parseBranchLine = (line) => {
348
323
  if (!name) return null;
349
324
  const mark = parts[0] ?? '';
350
325
  const sha = (parts[2] ?? '').trim();
351
- const date = (parts[3] ?? '').trim();
326
+ const date = shortAge(parts[3] ?? '');
352
327
  const track = parseTrack(parts[4]);
353
328
  const subject = parts.slice(5).join('\t').trim();
354
329
  return {
@@ -393,13 +368,10 @@ const listBranches = (top) => {
393
368
  const names = new Set();
394
369
  for (const entry of branches) names.add(entry.name);
395
370
  const primary = defaultBranch(top, names);
396
- const { length } = branches;
397
- const listed = new Array(length);
398
- for (let i = 0; i < length; i++) {
399
- const entry = branches[i];
400
- listed[i] = { ...entry, isDefault: entry.name === primary };
401
- }
402
- return listed;
371
+ return branches.map((entry) => ({
372
+ ...entry,
373
+ isDefault: entry.name === primary,
374
+ }));
403
375
  };
404
376
 
405
377
  const namedBranch = (name) => {
@@ -430,23 +402,11 @@ const createBranchAsync = async (top, name) => {
430
402
  requireOk(await runGitAsync(args, top), 'checkout failed');
431
403
  };
432
404
 
433
- const rebaseBranch = (top, onto) => {
434
- const branch = namedBranch(onto);
435
- const result = runGit(['rebase', branch], top);
436
- const failed = rebaseFailed(result);
437
- if (!failed) return;
438
- runGit(['rebase', '--abort'], top);
439
- throw new Error(oneLine(failed));
440
- };
405
+ const rebaseBranch = (top, onto) =>
406
+ runRebase(top, ['rebase', namedBranch(onto)]);
441
407
 
442
- const rebaseBranchAsync = async (top, onto) => {
443
- const branch = namedBranch(onto);
444
- const result = await runGitAsync(['rebase', branch], top);
445
- const failed = rebaseFailed(result);
446
- if (!failed) return;
447
- await runGitAsync(['rebase', '--abort'], top);
448
- throw new Error(oneLine(failed));
449
- };
408
+ const rebaseBranchAsync = async (top, onto) =>
409
+ runRebaseAsync(top, ['rebase', namedBranch(onto)]);
450
410
 
451
411
  const dropBranch = (top, name) => {
452
412
  const branch = namedBranch(name);
@@ -510,36 +470,31 @@ const pushCmd = (force, branch) => {
510
470
  const noUpstream = (msg) =>
511
471
  `${msg ?? ''}`.toLowerCase().includes('no upstream');
512
472
 
513
- const finishPush = (result, force, branch, run) => {
473
+ const requirePushOk = (result) => {
514
474
  if (result.status === 0) return;
515
- const msg = result.stderr || result.stdout || 'push failed';
516
- if (!noUpstream(msg)) throw pushError(msg);
517
- const next = run(pushCmd(force, branch));
518
- if (next.status !== 0) {
519
- throw pushError(next.stderr || next.stdout || 'push failed');
520
- }
475
+ throw pushError(result.stderr || result.stdout || 'push failed');
521
476
  };
522
477
 
523
478
  const pushChanges = (top, force) => {
524
479
  const useForce = force === true;
525
480
  const branch = pushBranch(top);
526
481
  const tracked = hasUpstream(top) ? '' : branch;
527
- const result = runGit(pushCmd(useForce, tracked), top);
528
- finishPush(result, useForce, branch, (args) => runGit(args, top));
482
+ let result = runGit(pushCmd(useForce, tracked), top);
483
+ if (result.status !== 0 && noUpstream(result.stderr || result.stdout)) {
484
+ result = runGit(pushCmd(useForce, branch), top);
485
+ }
486
+ requirePushOk(result);
529
487
  };
530
488
 
531
489
  const pushChangesAsync = async (top, force) => {
532
490
  const useForce = force === true;
533
491
  const branch = pushBranch(top);
534
492
  const tracked = hasUpstream(top) ? '' : branch;
535
- const result = await runGitAsync(pushCmd(useForce, tracked), top);
536
- if (result.status === 0) return;
537
- const msg = result.stderr || result.stdout || 'push failed';
538
- if (!noUpstream(msg)) throw pushError(msg);
539
- const next = await runGitAsync(pushCmd(useForce, branch), top);
540
- if (next.status !== 0) {
541
- throw pushError(next.stderr || next.stdout || 'push failed');
493
+ let result = await runGitAsync(pushCmd(useForce, tracked), top);
494
+ if (result.status !== 0 && noUpstream(result.stderr || result.stdout)) {
495
+ result = await runGitAsync(pushCmd(useForce, branch), top);
542
496
  }
497
+ requirePushOk(result);
543
498
  };
544
499
 
545
500
  const hasStaged = (top) => {
@@ -551,9 +506,7 @@ const commitPlan = (kind, message) => {
551
506
  const args = ['commit'];
552
507
  if (kind === 'amend') args.push('--amend');
553
508
  args.push('-F', '-');
554
- const text = message ?? '';
555
- const body = text.endsWith('\n') ? text : `${text}\n`;
556
- return { args, body };
509
+ return { args, body: messageBody(message) };
557
510
  };
558
511
 
559
512
  const commitChanges = (top, kind, message) => {