reslop 0.2.1 → 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 (81) hide show
  1. package/CHANGELOG.md +27 -1
  2. package/README.md +19 -4
  3. package/bin/reslop.js +4 -2
  4. package/lib/ansi.js +120 -4
  5. package/lib/api.js +2 -1
  6. package/lib/cli.js +21 -9
  7. package/lib/clipboard.js +6 -3
  8. package/lib/deps-diff.js +61 -115
  9. package/lib/deps.js +57 -54
  10. package/lib/detect.js +4 -2
  11. package/lib/diff/align.js +2 -1
  12. package/lib/diff/diff.js +6 -3
  13. package/lib/diff/display.js +2 -1
  14. package/lib/diff/inline.js +2 -1
  15. package/lib/diff/patch.js +2 -1
  16. package/lib/diff/unit.js +4 -2
  17. package/lib/editor.js +22 -4
  18. package/lib/files.js +38 -12
  19. package/lib/git-branches.js +150 -165
  20. package/lib/git-deps.js +20 -60
  21. package/lib/git-worktree.js +508 -0
  22. package/lib/git.js +23 -580
  23. package/lib/github.js +2 -1
  24. package/lib/gitlab.js +2 -1
  25. package/lib/highlight/bash.js +2 -1
  26. package/lib/highlight/css.js +2 -1
  27. package/lib/highlight/csv.js +2 -1
  28. package/lib/highlight/dot.js +2 -1
  29. package/lib/highlight/html.js +2 -1
  30. package/lib/highlight/json.js +2 -1
  31. package/lib/highlight/log.js +2 -1
  32. package/lib/highlight/txt.js +2 -1
  33. package/lib/highlight.js +2 -1
  34. package/lib/keys.js +12 -6
  35. package/lib/manifest.js +4 -2
  36. package/lib/npm-commands.js +592 -0
  37. package/lib/remote.js +20 -13
  38. package/lib/render/branches.js +25 -41
  39. package/lib/render/chrome.js +163 -128
  40. package/lib/render/commits.js +727 -65
  41. package/lib/render/diff.js +130 -47
  42. package/lib/render/files.js +4 -12
  43. package/lib/render/frame.js +41 -13
  44. package/lib/render/notes.js +4 -11
  45. package/lib/render/npm.js +216 -0
  46. package/lib/render/present.js +2 -1
  47. package/lib/render/primitives.js +34 -50
  48. package/lib/render/render.js +2 -1
  49. package/lib/render/unit.js +14 -11
  50. package/lib/report-model.js +129 -0
  51. package/lib/report-parse.js +89 -0
  52. package/lib/report-render.js +104 -0
  53. package/lib/report-run.js +166 -0
  54. package/lib/report-test.js +507 -0
  55. package/lib/report-tools.js +337 -0
  56. package/lib/review-merge.js +2 -1
  57. package/lib/review.js +6 -2
  58. package/lib/session/actions.js +77 -44
  59. package/lib/session/branches.js +13 -49
  60. package/lib/session/change-actions.js +2 -1
  61. package/lib/session/commit.js +113 -53
  62. package/lib/session/compose.js +83 -14
  63. package/lib/session/dispatch.js +42 -22
  64. package/lib/session/files-pane.js +16 -21
  65. package/lib/session/items.js +6 -3
  66. package/lib/session/lifecycle.js +12 -5
  67. package/lib/session/load.js +4 -2
  68. package/lib/session/navigation.js +2 -0
  69. package/lib/session/npm.js +388 -0
  70. package/lib/session/ops.js +15 -1
  71. package/lib/session/pointer.js +30 -3
  72. package/lib/session/review.js +2 -1
  73. package/lib/session/terminal.js +4 -2
  74. package/lib/session/update.js +2 -1
  75. package/lib/session/view.js +41 -4
  76. package/lib/session.js +44 -22
  77. package/lib/source.js +6 -3
  78. package/lib/update.js +5 -9
  79. package/lib/utilities.js +4 -2
  80. package/lib/wrap.js +3 -1
  81. package/package.json +6 -6
@@ -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)',
@@ -33,36 +36,95 @@ const commitMessage = (top, sha) => {
33
36
 
34
37
  const lastMessage = (top) => commitMessage(top, 'HEAD');
35
38
 
36
- const COMMIT_FORMAT = ['%H', '%h', '%an', '%ar', '%s'].join('%x1f');
39
+ const COMMIT_FIELDS = ['%H', '%h', '%an', '%ae', '%ar', '%ci', '%D', '%s'];
40
+ const COMMIT_FORMAT = `${COMMIT_FIELDS.join('%x1f')}%x1e%B`;
37
41
  const COMMIT_LIMIT = 256;
38
42
  const UNIT = '\x1f';
43
+ const RECORD = '\x1e';
44
+ const TAG_REF = /^tag: /;
45
+ const REMOTE_HEAD = /\/HEAD(?: -> |$)/;
46
+
47
+ const formatCommitRefs = (raw) => {
48
+ const text = `${raw ?? ''}`.trim();
49
+ if (!text) return '';
50
+ const names = [];
51
+ for (const part of text.split(', ')) {
52
+ const name = part.trim();
53
+ if (!name || TAG_REF.test(name)) continue;
54
+ if (name !== 'HEAD' && REMOTE_HEAD.test(name)) continue;
55
+ names.push(name);
56
+ }
57
+ return names.join(', ');
58
+ };
39
59
 
40
- const parseCommitLine = (line, index) => {
41
- const parts = line.split(UNIT);
42
- if (parts.length < 5) return null;
60
+ const parseCommitRecord = (record, index) => {
61
+ const cut = record.indexOf(RECORD);
62
+ if (cut < 0) return null;
63
+ const parts = record.slice(0, cut).split(UNIT);
64
+ if (parts.length < COMMIT_FIELDS.length) return null;
43
65
  const sha = (parts[0] ?? '').trim();
44
66
  if (!sha) return null;
67
+ const body = record.slice(cut + 1).replace(/\s+$/, '');
68
+ const read = (spec) => {
69
+ const at = COMMIT_FIELDS.indexOf(spec);
70
+ return (parts[at] ?? '').trim();
71
+ };
72
+ const subject = read('%s') || body.split('\n')[0].trim();
45
73
  return {
46
74
  sha,
47
- shortSha: (parts[1] ?? '').trim() || sha.slice(0, 7),
48
- author: (parts[2] ?? '').trim(),
49
- date: (parts[3] ?? '').trim(),
50
- subject: parts.slice(4).join(UNIT).trim(),
75
+ shortSha: read('%h') || sha.slice(0, 7),
76
+ author: read('%an'),
77
+ email: read('%ae'),
78
+ date: shortAge(read('%ar')),
79
+ when: read('%ci'),
80
+ refs: formatCommitRefs(read('%D')),
81
+ subject,
82
+ body,
83
+ added: 0,
84
+ removed: 0,
51
85
  head: index === 0,
52
86
  };
53
87
  };
54
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
+
55
108
  const listCommits = (top) => {
56
109
  const args = [
57
110
  'log',
111
+ '-z',
112
+ '--shortstat',
58
113
  `--format=${COMMIT_FORMAT}`,
59
114
  `--max-count=${COMMIT_LIMIT}`,
60
115
  ];
61
116
  const result = requireOk(runGit(args, top), 'log failed');
62
117
  const commits = [];
63
- for (const line of result.stdout.split('\n')) {
64
- if (!line) continue;
65
- const entry = parseCommitLine(line, 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);
66
128
  if (entry) commits.push(entry);
67
129
  }
68
130
  return commits;
@@ -83,48 +145,40 @@ const dropHeadCommit = (top) => {
83
145
  requireOk(runGit(['reset', '--soft', 'HEAD~1'], top), 'drop failed');
84
146
  };
85
147
 
86
- const rebaseFailed = (result) => {
87
- if (result.status === 0) return '';
88
- return result.stderr || result.stdout || 'rebase failed';
89
- };
90
-
91
- const dropOlderCommit = (top, sha) => {
92
- const onto = `${sha}^`;
93
- const result = runGit(['rebase', '--onto', onto, sha], top);
94
- const failed = rebaseFailed(result);
95
- if (!failed) return;
148
+ const runRebase = (top, args, extra = {}) => {
149
+ const result = runGit(args, top, undefined, extra);
150
+ if (result.status === 0) return;
96
151
  runGit(['rebase', '--abort'], top);
97
- throw new Error(oneLine(failed));
152
+ requireOk(result, 'rebase failed');
98
153
  };
99
154
 
100
- const dropOlderCommitAsync = async (top, sha) => {
101
- const onto = `${sha}^`;
102
- const result = await runGitAsync(['rebase', '--onto', onto, sha], top);
103
- const failed = rebaseFailed(result);
104
- if (!failed) return;
155
+ const runRebaseAsync = async (top, args, extra = {}) => {
156
+ const result = await runGitAsync(args, top, extra);
157
+ if (result.status === 0) return;
105
158
  await runGitAsync(['rebase', '--abort'], top);
106
- throw new Error(oneLine(failed));
159
+ requireOk(result, 'rebase failed');
107
160
  };
108
161
 
109
- const dropCommit = (top, sha) => {
110
- const spec = namedCommit(sha);
111
- const target = resolveCommit(top, spec);
162
+ const dropTarget = (top, sha) => {
163
+ const target = resolveCommit(top, namedCommit(sha));
112
164
  const parent = runGit(['rev-parse', '--verify', `${target}^`], top);
113
165
  if (parent.status !== 0) throw new Error('cannot drop root commit');
114
- const head = resolveCommit(top, 'HEAD');
115
- if (target === head) return void dropHeadCommit(top);
116
- 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]);
117
173
  };
118
174
 
119
175
  const dropCommitAsync = async (top, sha) => {
120
- const spec = namedCommit(sha);
121
- const target = resolveCommit(top, spec);
122
- const parentArgs = ['rev-parse', '--verify', `${target}^`];
123
- const parent = await runGitAsync(parentArgs, top);
124
- if (parent.status !== 0) throw new Error('cannot drop root commit');
125
- const head = resolveCommit(top, 'HEAD');
126
- if (target === head) return void dropHeadCommit(top);
127
- 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]);
128
182
  };
129
183
 
130
184
  const FIXUP_MARK = 'fixup!';
@@ -154,72 +208,46 @@ const commitSubject = (top, sha) => {
154
208
  };
155
209
 
156
210
  const ancestorWithSubject = (top, sha, want) => {
157
- const args = ['log', `--format=${COMMIT_FORMAT}`, `${sha}^`];
211
+ const args = ['log', '--format=%H%x1f%s', `${sha}^`];
158
212
  const result = runGit(args, top);
159
213
  if (result.status !== 0) return '';
160
214
  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;
215
+ const cut = line.indexOf(UNIT);
216
+ if (cut < 0) continue;
217
+ const subject = line.slice(cut + 1).trim();
218
+ if (subject !== want) continue;
219
+ return line.slice(0, cut).trim();
164
220
  }
165
221
  return '';
166
222
  };
167
223
 
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()]);
224
+ const rebaseRange = (top, sha) => {
225
+ const parent = runGit(['rev-parse', '--verify', `${sha}^`], top);
226
+ return parent.status === 0 ? [parent.stdout.trim()] : ['--root'];
188
227
  };
189
228
 
190
- const applyFixup = (top, sha) => {
191
- const spec = namedCommit(sha);
192
- const commit = resolveCommit(top, spec);
229
+ const fixupArgs = (top, sha) => {
230
+ const commit = resolveCommit(top, namedCommit(sha));
193
231
  const subject = commitSubject(top, commit);
194
232
  if (!isFixupSubject(subject)) throw new Error('not a fixup commit');
195
233
  const want = fixupRest(subject);
196
234
  if (!want) throw new Error('no fixup target');
197
235
  const original = ancestorWithSubject(top, commit, want);
198
236
  if (!original) throw new Error('no fixup target');
199
- applyFixupOnto(top, original, applyAutosquash);
237
+ return [...AUTOSQUASH_ARGS, ...rebaseRange(top, original)];
200
238
  };
201
239
 
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
- };
240
+ const applyFixup = (top, sha) => runRebase(top, fixupArgs(top, sha));
241
+
242
+ const applyFixupAsync = async (top, sha) =>
243
+ runRebaseAsync(top, fixupArgs(top, sha));
213
244
 
214
245
  const messageBody = (message) => {
215
246
  const text = `${message ?? ''}`;
216
247
  return text.endsWith('\n') ? text : `${text}\n`;
217
248
  };
218
249
 
219
- const rewordHead = (top, message) => {
220
- const args = ['commit', '--amend', '--only', '-F', '-'];
221
- requireOk(runGit(args, top, messageBody(message)));
222
- };
250
+ const REWORD_HEAD_ARGS = ['commit', '--amend', '--only', '-F', '-'];
223
251
 
224
252
  const REWORD_TODO = path.join(__dirname, 'git-reword-todo.js');
225
253
 
@@ -238,32 +266,11 @@ const prepareRewordEditor = (message) => {
238
266
  };
239
267
  };
240
268
 
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
269
  const rewordOlder = (top, sha, message) => {
263
270
  const prepared = prepareRewordEditor(message);
264
271
  try {
265
- const result = rewordRange(top, sha, prepared.env);
266
- finishRewordRebase(top, result);
272
+ const args = ['rebase', '--interactive', ...rebaseRange(top, sha)];
273
+ runRebase(top, args, { env: prepared.env });
267
274
  } finally {
268
275
  fs.rmSync(prepared.dir, { recursive: true, force: true });
269
276
  }
@@ -272,29 +279,29 @@ const rewordOlder = (top, sha, message) => {
272
279
  const rewordOlderAsync = async (top, sha, message) => {
273
280
  const prepared = prepareRewordEditor(message);
274
281
  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));
282
+ const args = ['rebase', '--interactive', ...rebaseRange(top, sha)];
283
+ await runRebaseAsync(top, args, { env: prepared.env });
280
284
  } finally {
281
285
  fs.rmSync(prepared.dir, { recursive: true, force: true });
282
286
  }
283
287
  };
284
288
 
285
289
  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
+ const commit = resolveCommit(top, namedCommit(sha));
291
+ if (commit === resolveCommit(top, 'HEAD')) {
292
+ requireOk(runGit(REWORD_HEAD_ARGS, top, messageBody(message)));
293
+ return;
294
+ }
290
295
  rewordOlder(top, commit, message);
291
296
  };
292
297
 
293
298
  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);
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
+ }
298
305
  await rewordOlderAsync(top, commit, message);
299
306
  };
300
307
 
@@ -316,7 +323,7 @@ const parseBranchLine = (line) => {
316
323
  if (!name) return null;
317
324
  const mark = parts[0] ?? '';
318
325
  const sha = (parts[2] ?? '').trim();
319
- const date = (parts[3] ?? '').trim();
326
+ const date = shortAge(parts[3] ?? '');
320
327
  const track = parseTrack(parts[4]);
321
328
  const subject = parts.slice(5).join('\t').trim();
322
329
  return {
@@ -361,13 +368,10 @@ const listBranches = (top) => {
361
368
  const names = new Set();
362
369
  for (const entry of branches) names.add(entry.name);
363
370
  const primary = defaultBranch(top, names);
364
- const { length } = branches;
365
- const listed = new Array(length);
366
- for (let i = 0; i < length; i++) {
367
- const entry = branches[i];
368
- listed[i] = { ...entry, isDefault: entry.name === primary };
369
- }
370
- return listed;
371
+ return branches.map((entry) => ({
372
+ ...entry,
373
+ isDefault: entry.name === primary,
374
+ }));
371
375
  };
372
376
 
373
377
  const namedBranch = (name) => {
@@ -398,23 +402,11 @@ const createBranchAsync = async (top, name) => {
398
402
  requireOk(await runGitAsync(args, top), 'checkout failed');
399
403
  };
400
404
 
401
- const rebaseBranch = (top, onto) => {
402
- const branch = namedBranch(onto);
403
- const result = runGit(['rebase', branch], top);
404
- const failed = rebaseFailed(result);
405
- if (!failed) return;
406
- runGit(['rebase', '--abort'], top);
407
- throw new Error(oneLine(failed));
408
- };
405
+ const rebaseBranch = (top, onto) =>
406
+ runRebase(top, ['rebase', namedBranch(onto)]);
409
407
 
410
- const rebaseBranchAsync = async (top, onto) => {
411
- const branch = namedBranch(onto);
412
- const result = await runGitAsync(['rebase', branch], top);
413
- const failed = rebaseFailed(result);
414
- if (!failed) return;
415
- await runGitAsync(['rebase', '--abort'], top);
416
- throw new Error(oneLine(failed));
417
- };
408
+ const rebaseBranchAsync = async (top, onto) =>
409
+ runRebaseAsync(top, ['rebase', namedBranch(onto)]);
418
410
 
419
411
  const dropBranch = (top, name) => {
420
412
  const branch = namedBranch(name);
@@ -478,36 +470,31 @@ const pushCmd = (force, branch) => {
478
470
  const noUpstream = (msg) =>
479
471
  `${msg ?? ''}`.toLowerCase().includes('no upstream');
480
472
 
481
- const finishPush = (result, force, branch, run) => {
473
+ const requirePushOk = (result) => {
482
474
  if (result.status === 0) return;
483
- const msg = result.stderr || result.stdout || 'push failed';
484
- if (!noUpstream(msg)) throw pushError(msg);
485
- const next = run(pushCmd(force, branch));
486
- if (next.status !== 0) {
487
- throw pushError(next.stderr || next.stdout || 'push failed');
488
- }
475
+ throw pushError(result.stderr || result.stdout || 'push failed');
489
476
  };
490
477
 
491
478
  const pushChanges = (top, force) => {
492
479
  const useForce = force === true;
493
480
  const branch = pushBranch(top);
494
481
  const tracked = hasUpstream(top) ? '' : branch;
495
- const result = runGit(pushCmd(useForce, tracked), top);
496
- 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);
497
487
  };
498
488
 
499
489
  const pushChangesAsync = async (top, force) => {
500
490
  const useForce = force === true;
501
491
  const branch = pushBranch(top);
502
492
  const tracked = hasUpstream(top) ? '' : branch;
503
- const result = await runGitAsync(pushCmd(useForce, tracked), top);
504
- if (result.status === 0) return;
505
- const msg = result.stderr || result.stdout || 'push failed';
506
- if (!noUpstream(msg)) throw pushError(msg);
507
- const next = await runGitAsync(pushCmd(useForce, branch), top);
508
- if (next.status !== 0) {
509
- 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);
510
496
  }
497
+ requirePushOk(result);
511
498
  };
512
499
 
513
500
  const hasStaged = (top) => {
@@ -519,9 +506,7 @@ const commitPlan = (kind, message) => {
519
506
  const args = ['commit'];
520
507
  if (kind === 'amend') args.push('--amend');
521
508
  args.push('-F', '-');
522
- const text = message ?? '';
523
- const body = text.endsWith('\n') ? text : `${text}\n`;
524
- return { args, body };
509
+ return { args, body: messageBody(message) };
525
510
  };
526
511
 
527
512
  const commitChanges = (top, kind, message) => {
package/lib/git-deps.js CHANGED
@@ -8,12 +8,14 @@ const { foldDepItems, mergeDepFile, depFileMeta } = deps;
8
8
  const { parseAuditReport, parseOutdatedReport } = deps;
9
9
  const { mergeProposedItems, MANIFEST, LOCKFILE } = deps;
10
10
  const { collectUsedNames, collectExportMeta } = deps;
11
- const { itemPath, isPathInScope } = require('./files.js');
11
+ const files = require('./files.js');
12
+ const { itemPath, isPathInScope } = files;
12
13
  const proc = require('./utilities.js');
13
14
  const { abortError, npmBin, npmOpts, runProc, runProcSync } = proc;
14
- const git = require('./git.js');
15
+ const git = require('./git-worktree.js');
15
16
  const { oneLine, requireOk, runGit } = git;
16
17
  const { gitText, worktreeText, readDepSides, revertOnePath } = git;
18
+ const { writeFile, writeIndex } = git;
17
19
 
18
20
  const AUDIT_MS = 15000;
19
21
  const INSTALL_MS = 120000;
@@ -30,12 +32,12 @@ const npmProcOptions = (cwd, extra = {}) => ({
30
32
  const runNpm = (cwd, args) =>
31
33
  runProcSync(npmBin(), args, npmProcOptions(cwd, { timeout: INSTALL_MS }));
32
34
 
33
- const runNpmAsync = (cwd, args, extra = {}) =>
34
- runProc(
35
- npmBin(),
36
- args,
37
- npmProcOptions(cwd, { timeout: INSTALL_MS, signal: extra.signal }),
38
- );
35
+ const runNpmAsync = (cwd, args, extra = {}) => {
36
+ const bin = npmBin();
37
+ const signal = extra.signal;
38
+ const options = npmProcOptions(cwd, { timeout: INSTALL_MS, signal });
39
+ return runProc(bin, args, options);
40
+ };
39
41
 
40
42
  const runNpmJson = (top, args, timeout) => {
41
43
  const result = runProcSync(npmBin(), args, npmProcOptions(top, { timeout }));
@@ -104,18 +106,6 @@ const hasProposed = (item) => {
104
106
  return !!(change && change.propose);
105
107
  };
106
108
 
107
- const relatedFiles = (items) => {
108
- let size = 0;
109
- for (const item of items) size += depPaths(item).length;
110
- const files = new Array(size);
111
- let i = 0;
112
- for (const item of items) {
113
- const paths = depPaths(item);
114
- for (let j = 0; j < paths.length; j++) files[i++] = paths[j];
115
- }
116
- return files;
117
- };
118
-
119
109
  const itemsForPath = (rel, items) => {
120
110
  const group = [];
121
111
  for (const item of items) {
@@ -287,12 +277,6 @@ const loadExtras = async (snapshot, extra = {}) => {
287
277
  };
288
278
  };
289
279
 
290
- const writeWorktreeFile = (top, rel, text) => {
291
- const abs = path.join(top, rel);
292
- fs.mkdirSync(path.dirname(abs), { recursive: true });
293
- fs.writeFileSync(abs, text);
294
- };
295
-
296
280
  const captureFile = (top, rel) => {
297
281
  const abs = path.join(top, rel);
298
282
  try {
@@ -327,16 +311,6 @@ const restoreFiles = (top, owned) => {
327
311
  for (const saved of owned) restoreFile(top, saved);
328
312
  };
329
313
 
330
- const writeIndexFile = (top, rel, text) => {
331
- const saved = captureFile(top, rel);
332
- writeWorktreeFile(top, rel, text);
333
- try {
334
- requireOk(runGit(['add', '--', rel], top));
335
- } finally {
336
- restoreFile(top, saved);
337
- }
338
- };
339
-
340
314
  const mergeDepLive = (top, rel, change, origin, base, side, kind) => {
341
315
  const sides = readDepSides(top, null, origin, rel);
342
316
  return mergeDepFile(base, sides.oldText, sides.newText, change, side, kind);
@@ -347,7 +321,7 @@ const applyDepIndex = (top, rel, change, origin, side) => {
347
321
  if (!meta) return;
348
322
  const indexed = gitText(top, `:${rel}`) || gitText(top, `HEAD:${rel}`);
349
323
  const text = mergeDepLive(top, rel, change, origin, indexed, side, meta.kind);
350
- writeIndexFile(top, rel, text);
324
+ writeIndex(top, rel, text);
351
325
  };
352
326
 
353
327
  const applyDepWorktree = (top, rel, change, origin) => {
@@ -363,7 +337,7 @@ const applyDepWorktree = (top, rel, change, origin) => {
363
337
  'old',
364
338
  meta.kind,
365
339
  );
366
- writeWorktreeFile(top, rel, text);
340
+ writeFile(top, rel, text);
367
341
  };
368
342
 
369
343
  const ownedRelsOf = (plan) => {
@@ -399,7 +373,7 @@ const applyPreparation = (top, item, plan) => {
399
373
  'new',
400
374
  'manifest',
401
375
  );
402
- writeWorktreeFile(top, plan.manifest, text);
376
+ writeFile(top, plan.manifest, text);
403
377
  };
404
378
 
405
379
  const stagePlan = (top, plan) => {
@@ -418,26 +392,14 @@ const finishPlan = (top, plan, result, stage) => {
418
392
  stageFn(top, plan);
419
393
  };
420
394
 
421
- const syncInstaller = (plan, extra) => {
422
- if (extra.run) return extra.run;
423
- if (plan.runner) return plan.runner;
424
- return (dir) => runNpm(dir, plan.npmArgs);
425
- };
426
-
427
- const asyncInstaller = (plan, extra) => {
428
- if (extra.run) return extra.run;
429
- if (plan.runner) return (dir) => Promise.resolve(plan.runner(dir));
430
- return (dir) => runNpmAsync(dir, plan.npmArgs);
431
- };
432
-
433
395
  const applyProposedUpdate = (top, item, extra = {}) => {
434
396
  const plan = proposedNpmPlan(top, item);
435
397
  if (!plan) return;
436
398
  const owned = captureFiles(top, ownedRelsOf(plan));
437
399
  try {
438
400
  applyPreparation(top, item, plan);
439
- const install = syncInstaller(plan, extra);
440
- const result = install(plan.cwd);
401
+ const install = extra.run ?? plan.runner ?? runNpm;
402
+ const result = install(plan.cwd, plan.npmArgs);
441
403
  finishPlan(top, plan, result, extra.stage);
442
404
  } catch (error) {
443
405
  restoreFiles(top, owned);
@@ -451,8 +413,8 @@ const applyProposedUpdateAsync = async (top, item, extra = {}) => {
451
413
  const owned = captureFiles(top, ownedRelsOf(plan));
452
414
  try {
453
415
  applyPreparation(top, item, plan);
454
- const run = asyncInstaller(plan, extra);
455
- const result = await run(plan.cwd);
416
+ const run = extra.run ?? plan.runner ?? runNpmAsync;
417
+ const result = await run(plan.cwd, plan.npmArgs);
456
418
  finishPlan(top, plan, result, extra.stage);
457
419
  } catch (error) {
458
420
  restoreFiles(top, owned);
@@ -525,7 +487,9 @@ const revertDepItem = (top, item) => {
525
487
 
526
488
  const revertFile = (top, rel, items) => {
527
489
  const paths = new Set([rel]);
528
- for (const file of relatedFiles(items)) paths.add(file);
490
+ for (const item of items) {
491
+ for (const file of depPaths(item)) paths.add(file);
492
+ }
529
493
  for (const file of paths) {
530
494
  const group = itemsForPath(file, items);
531
495
  const targets = group.length ? group : items;
@@ -533,12 +497,8 @@ const revertFile = (top, rel, items) => {
533
497
  }
534
498
  };
535
499
 
536
- const handles = (item) => !!(item && item.dep);
537
-
538
500
  module.exports = {
539
501
  hasProposed,
540
- handles,
541
- relatedFiles,
542
502
  itemsForPath,
543
503
  finishLoad,
544
504
  loadExtras,