rman 2.15.0 → 2.15.2
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/commands/deps.command.js +7 -3
- package/commands/version.command.js +2 -1
- package/constants.js +1 -1
- package/package.json +1 -1
- package/services/version.service.js +15 -2
- package/utils/git.d.ts +5 -3
- package/utils/git.js +15 -5
- package/utils/status-region.d.ts +14 -0
- package/utils/status-region.js +64 -6
package/commands/deps.command.js
CHANGED
|
@@ -200,9 +200,13 @@ function jsonPlan(entries) {
|
|
|
200
200
|
function printPlan(entries, root) {
|
|
201
201
|
const updated = entries.filter(e => e.status === 'update');
|
|
202
202
|
const behind = entries.filter(e => e.status === 'held' || e.status === 'skipped' || e.status === 'error' || (e.status === 'update' && e.reason));
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
203
|
+
/** Said first when nothing moves, so a run whose only lines are majors left behind does not read
|
|
204
|
+
* as a list of things to do. */
|
|
205
|
+
if (!updated.length) {
|
|
206
|
+
console.log(colors.green('All dependencies are up to date') +
|
|
207
|
+
(behind.length ? colors.gray(' - newer versions their settings leave out, or another rule holds back:') : '.'));
|
|
208
|
+
if (!behind.length)
|
|
209
|
+
return;
|
|
206
210
|
}
|
|
207
211
|
const shown = [...updated, ...behind];
|
|
208
212
|
const nameWidth = Math.max(...shown.map(e => e.name.length));
|
|
@@ -286,7 +286,8 @@ function printApplied(result) {
|
|
|
286
286
|
console.log(`${colors.green('tags')} ${release.map(label).join(', ')} ${colors.gray('(repository release)')}`);
|
|
287
287
|
}
|
|
288
288
|
console.log(result.pushed
|
|
289
|
-
? `${colors.green('push')} pushed
|
|
289
|
+
? `${colors.green('push')} pushed the branch` +
|
|
290
|
+
(result.tags.length ? ` and ${result.tags.map(t => t.name).join(', ')}, together` : '')
|
|
290
291
|
: `${colors.gray('push')} ${colors.gray('not pushed - run with --push, or push it yourself')}`);
|
|
291
292
|
}
|
|
292
293
|
function printPlan(entries) {
|
package/constants.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const version = '2.15.
|
|
1
|
+
export const version = '2.15.2';
|
package/package.json
CHANGED
|
@@ -254,8 +254,21 @@ export class VersionService extends Service {
|
|
|
254
254
|
tagged.push({ name: releaseTag, created: true, release: true });
|
|
255
255
|
}
|
|
256
256
|
const pushed = !!options.push && bumped.length > 0;
|
|
257
|
-
if (pushed)
|
|
258
|
-
|
|
257
|
+
if (pushed) {
|
|
258
|
+
/** Every tag this release names, including one that was already there: it is this release's
|
|
259
|
+
* tag all the same, and a remote that lacks it is missing part of the release. */
|
|
260
|
+
const tags = tagged.map(t => t.name);
|
|
261
|
+
try {
|
|
262
|
+
await git.push({ tags });
|
|
263
|
+
}
|
|
264
|
+
catch (e) {
|
|
265
|
+
throw new Error(`${e.message}\n\n` +
|
|
266
|
+
` Nothing was pushed - the branch and its tags go up together or not at all. Left behind ` +
|
|
267
|
+
`locally: ${commits.map(c => c.sha).join(', ') || 'no commit'}` +
|
|
268
|
+
(tags.length ? `, tagged ${tags.join(', ')}` : '') +
|
|
269
|
+
`.\n Once the cause is fixed, push them with: git push --atomic origin HEAD ${tags.join(' ')}`.trimEnd(), { cause: e });
|
|
270
|
+
}
|
|
271
|
+
}
|
|
259
272
|
return { entries: plan, updated: bumped, commits, tags: tagged, pushed };
|
|
260
273
|
}
|
|
261
274
|
}
|
package/utils/git.d.ts
CHANGED
|
@@ -121,11 +121,13 @@ export declare class GitHelper {
|
|
|
121
121
|
* Throws if a tag with that name already exists - callers wanting idempotent tagging should
|
|
122
122
|
* check `tagExists` first. */
|
|
123
123
|
createTag(name: string, message?: string): Promise<void>;
|
|
124
|
-
/**
|
|
125
|
-
*
|
|
124
|
+
/**
|
|
125
|
+
* Pushes the current branch to `remote` (default `"origin"`) together with `options.tags`, by
|
|
126
|
+
* name, in **one atomic push**: the branch and every tag go up, or none of them does.
|
|
127
|
+
*/
|
|
126
128
|
push(options?: {
|
|
127
129
|
remote?: string;
|
|
128
|
-
tags?:
|
|
130
|
+
tags?: readonly string[];
|
|
129
131
|
}): Promise<void>;
|
|
130
132
|
/** `git remote get-url <remote>` (default `"origin"`) - `undefined` when that remote isn't
|
|
131
133
|
* configured at all, which is a perfectly normal state for a local-only repository. */
|
package/utils/git.js
CHANGED
|
@@ -394,14 +394,24 @@ export class GitHelper {
|
|
|
394
394
|
throw new Error(`Unable to create tag "${name}": ${e.message}`, { cause: e });
|
|
395
395
|
}
|
|
396
396
|
}
|
|
397
|
-
/**
|
|
398
|
-
*
|
|
397
|
+
/**
|
|
398
|
+
* Pushes the current branch to `remote` (default `"origin"`) together with `options.tags`, by
|
|
399
|
+
* name, in **one atomic push**: the branch and every tag go up, or none of them does.
|
|
400
|
+
*/
|
|
401
|
+
/* **Named tags, not `--tags`.** `git push <remote> --tags` sends every tag in the clone, so one
|
|
402
|
+
* stale tag from months ago - re-created locally, diverged from the remote's - failed every
|
|
403
|
+
* release from then on, and failed it *after* the new tag had already gone up. Measured on
|
|
404
|
+
* `postgrejs` with 2.15.0: `v3.13.0` pushed, `v3.10.0` rejected, exit 1, and in CI the publish
|
|
405
|
+
* step after it never ran.
|
|
406
|
+
*
|
|
407
|
+
* **One command, `--atomic`.** The branch used to go first and the tags second, so a failure in
|
|
408
|
+
* the second - GitHub's own `fatal error in commit_refs` did it to the same repository an hour
|
|
409
|
+
* earlier - left a pushed version bump with no tag, which a re-run cannot repair because it has
|
|
410
|
+
* nothing left to bump. Atomic, a failure leaves the remote exactly as it was. */
|
|
399
411
|
async push(options) {
|
|
400
412
|
const remote = options?.remote ?? 'origin';
|
|
401
413
|
try {
|
|
402
|
-
await execFileAsync('git', ['push', remote], { cwd: this.cwd });
|
|
403
|
-
if (options?.tags !== false)
|
|
404
|
-
await execFileAsync('git', ['push', remote, '--tags'], { cwd: this.cwd });
|
|
414
|
+
await execFileAsync('git', ['push', '--atomic', remote, 'HEAD', ...(options?.tags ?? [])], { cwd: this.cwd });
|
|
405
415
|
}
|
|
406
416
|
catch (e) {
|
|
407
417
|
throw new Error(`Unable to push to "${remote}": ${e.message}`, { cause: e });
|
package/utils/status-region.d.ts
CHANGED
|
@@ -33,6 +33,12 @@ export declare class StatusRegion implements TerminalRegion {
|
|
|
33
33
|
* there, so `runBin` keeps asking one object (`app.statusRegion`) whatever is actually drawing.
|
|
34
34
|
*/
|
|
35
35
|
private takeover?;
|
|
36
|
+
/** Puts back the stream writes `guardWrites` replaced. */
|
|
37
|
+
private restoreWrites?;
|
|
38
|
+
/** Set while this region is writing its own frame, so the guard lets it through untouched. */
|
|
39
|
+
private drawing;
|
|
40
|
+
/** The last write from outside ended mid-line, so no frame is drawn until the line is finished. */
|
|
41
|
+
private partial;
|
|
36
42
|
constructor(label: string, detail?: string, enabled?: boolean);
|
|
37
43
|
/** Whether the spinner is actually drawn. `false` leaves `passThrough` a plain write and `stop`
|
|
38
44
|
* its single line, which is what a non-TTY wants. */
|
|
@@ -69,6 +75,14 @@ export declare class StatusRegion implements TerminalRegion {
|
|
|
69
75
|
passThrough(text: string): void;
|
|
70
76
|
/** Erases the spinner and leaves one line saying how it went. */
|
|
71
77
|
stop(outcome: 'ok' | 'fail'): void;
|
|
78
|
+
/**
|
|
79
|
+
* Moves every write to stdout and stderr above the spinner: erase it, write, draw it again below.
|
|
80
|
+
* Undone by `stop`.
|
|
81
|
+
*/
|
|
82
|
+
private guardWrites;
|
|
83
|
+
/** Runs `write` as one of this region's own writes, so the guard passes it through. */
|
|
84
|
+
private own;
|
|
85
|
+
private clear;
|
|
72
86
|
private draw;
|
|
73
87
|
/** `0.4s` while running and `950ms` / `4.1s` / `2m 03s` at the end - one formatter, because a
|
|
74
88
|
* duration that changes shape as it ticks reads as a glitch. */
|
package/utils/status-region.js
CHANGED
|
@@ -12,6 +12,12 @@ export class StatusRegion {
|
|
|
12
12
|
* there, so `runBin` keeps asking one object (`app.statusRegion`) whatever is actually drawing.
|
|
13
13
|
*/
|
|
14
14
|
takeover;
|
|
15
|
+
/** Puts back the stream writes `guardWrites` replaced. */
|
|
16
|
+
restoreWrites;
|
|
17
|
+
/** Set while this region is writing its own frame, so the guard lets it through untouched. */
|
|
18
|
+
drawing = false;
|
|
19
|
+
/** The last write from outside ended mid-line, so no frame is drawn until the line is finished. */
|
|
20
|
+
partial = false;
|
|
15
21
|
constructor(label, detail = '', enabled) {
|
|
16
22
|
this.label = label;
|
|
17
23
|
this.detail = detail;
|
|
@@ -26,6 +32,7 @@ export class StatusRegion {
|
|
|
26
32
|
start() {
|
|
27
33
|
if (!this.region.enabled)
|
|
28
34
|
return;
|
|
35
|
+
this.guardWrites();
|
|
29
36
|
this.draw();
|
|
30
37
|
/** **`unref`, so a spinner never holds the process open.** A command that finishes its work and
|
|
31
38
|
* leaves nothing else pending would otherwise wait out the interval before exiting. */
|
|
@@ -55,7 +62,7 @@ export class StatusRegion {
|
|
|
55
62
|
if (this.timer)
|
|
56
63
|
clearInterval(this.timer);
|
|
57
64
|
this.timer = undefined;
|
|
58
|
-
this.
|
|
65
|
+
this.clear();
|
|
59
66
|
}
|
|
60
67
|
/**
|
|
61
68
|
* Takes the terminal back - a no-op when it was never suspended.
|
|
@@ -88,8 +95,8 @@ export class StatusRegion {
|
|
|
88
95
|
process.stderr.write(text);
|
|
89
96
|
return;
|
|
90
97
|
}
|
|
91
|
-
this.
|
|
92
|
-
process.stderr.write(text);
|
|
98
|
+
this.clear();
|
|
99
|
+
this.own(() => process.stderr.write(text));
|
|
93
100
|
this.draw();
|
|
94
101
|
}
|
|
95
102
|
/** Erases the spinner and leaves one line saying how it went. */
|
|
@@ -98,11 +105,62 @@ export class StatusRegion {
|
|
|
98
105
|
if (this.timer)
|
|
99
106
|
clearInterval(this.timer);
|
|
100
107
|
this.timer = undefined;
|
|
101
|
-
this.
|
|
108
|
+
this.clear();
|
|
109
|
+
this.restoreWrites?.();
|
|
110
|
+
this.restoreWrites = undefined;
|
|
102
111
|
const mark = outcome === 'ok' ? colors.green('✔') : colors.red('✖');
|
|
103
|
-
|
|
112
|
+
const lead = this.partial ? '\n' : '';
|
|
113
|
+
process.stderr.write(`${lead}${mark} ${colors.bold(this.label)} ${colors.gray(this.elapsed())}\n`);
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Moves every write to stdout and stderr above the spinner: erase it, write, draw it again below.
|
|
117
|
+
* Undone by `stop`.
|
|
118
|
+
*/
|
|
119
|
+
/* **Here, once, rather than in every command.** Measured with a pseudo-terminal: `rman deps`
|
|
120
|
+
* printed its plan with `console.log`, the spinner's next erase moved up one row from *below* the
|
|
121
|
+
* plan, and the plan's last line - the one dependency it was reporting - was gone from the screen,
|
|
122
|
+
* leaving a "not updated" heading with nothing under it. `version --show` lost its closing
|
|
123
|
+
* "Nothing to version." the same way. Asking each command to route its output through
|
|
124
|
+
* `passThrough` is a rule every new command would have to remember; a command's own `console.log`
|
|
125
|
+
* is the ordinary thing to write.
|
|
126
|
+
*
|
|
127
|
+
* **Left alone while suspended**: the region that took over draws to stdout itself, and it owns the
|
|
128
|
+
* terminal until `resume`. **A write ending mid-line holds the next frame back** (`partial`), or the
|
|
129
|
+
* frame's `\r` + erase would wipe the half-written line. */
|
|
130
|
+
guardWrites() {
|
|
131
|
+
const streams = [process.stdout, process.stderr];
|
|
132
|
+
const originals = streams.map(stream => stream.write);
|
|
133
|
+
streams.forEach((stream, i) => {
|
|
134
|
+
const original = originals[i];
|
|
135
|
+
stream.write = ((chunk, ...rest) => {
|
|
136
|
+
if (this.drawing || this.takeover)
|
|
137
|
+
return original.call(stream, chunk, ...rest);
|
|
138
|
+
this.clear();
|
|
139
|
+
const written = this.own(() => original.call(stream, chunk, ...rest));
|
|
140
|
+
this.partial = !String(chunk).endsWith('\n');
|
|
141
|
+
this.draw();
|
|
142
|
+
return written;
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
this.restoreWrites = () => streams.forEach((stream, i) => (stream.write = originals[i]));
|
|
146
|
+
}
|
|
147
|
+
/** Runs `write` as one of this region's own writes, so the guard passes it through. */
|
|
148
|
+
own(write) {
|
|
149
|
+
const was = this.drawing;
|
|
150
|
+
this.drawing = true;
|
|
151
|
+
try {
|
|
152
|
+
return write();
|
|
153
|
+
}
|
|
154
|
+
finally {
|
|
155
|
+
this.drawing = was;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
clear() {
|
|
159
|
+
this.own(() => this.region.clear());
|
|
104
160
|
}
|
|
105
161
|
draw() {
|
|
162
|
+
if (this.partial)
|
|
163
|
+
return;
|
|
106
164
|
const spinner = colors.cyan(FRAMES[this.frame % FRAMES.length]);
|
|
107
165
|
const parts = [
|
|
108
166
|
spinner,
|
|
@@ -110,7 +168,7 @@ export class StatusRegion {
|
|
|
110
168
|
this.detail && colors.gray(this.detail),
|
|
111
169
|
colors.gray(this.elapsed()),
|
|
112
170
|
];
|
|
113
|
-
this.region.render([parts.filter(Boolean).join(' ')]);
|
|
171
|
+
this.own(() => this.region.render([parts.filter(Boolean).join(' ')]));
|
|
114
172
|
}
|
|
115
173
|
/** `0.4s` while running and `950ms` / `4.1s` / `2m 03s` at the end - one formatter, because a
|
|
116
174
|
* duration that changes shape as it ticks reads as a glitch. */
|