rman 2.5.0 → 2.6.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.
@@ -72,7 +72,7 @@ export var CiService;
72
72
  const progress = options.progress ?? true;
73
73
  const panel = new ProgressPanel('CI', !!process.stdout.isTTY && progress);
74
74
  const runStartedAt = Date.now();
75
- panel.start();
75
+ panel.start(repository.app.statusRegion);
76
76
  const items = [];
77
77
  let failed = false;
78
78
  try {
@@ -53,7 +53,7 @@ export var CleanService;
53
53
  const progress = options.progress ?? true;
54
54
  const logger = new Logger(options.logLevel ?? resolveRootLogLevel(repository));
55
55
  const panel = new ProgressPanel(dryRun ? 'CLEAN (dry-run)' : 'CLEAN', !!process.stdout.isTTY && progress);
56
- panel.start();
56
+ panel.start(repository.app.statusRegion);
57
57
  let failed = false;
58
58
  try {
59
59
  const results = await Promise.allSettled(targets.map(pkg => {
@@ -98,6 +98,48 @@ declare const config: {
98
98
  describe: string;
99
99
  type: "boolean";
100
100
  };
101
+ /**
102
+ * **What one changelog file is about.** `'package'` gives every package its own; `'group'` gives
103
+ * one to each set of packages that versions and releases together (`.rmanrc group`), written at
104
+ * the repository root - `CHANGELOG.md` for the default group, `CHANGELOG-<name>.md` for a named
105
+ * one. A `group: false` package is a group of itself either way, so its file stays its own.
106
+ *
107
+ * **`'package'` is the default because changing a repository's layout silently is not something a
108
+ * minor release may do**, not because it is the better answer. For a repository releasing along
109
+ * one line it is measurably the worse one: every package is bumped together, so most of them have
110
+ * no commit of their own and get a file that only ever says "Updated dependencies" - measured on
111
+ * `panates/sqb`, fifteen of seventeen. The whole release is then readable nowhere.
112
+ *
113
+ * Read off the repository root and nowhere else - see `changelogGroupBy`.
114
+ */
115
+ /**
116
+ * **The panel is drawn on stderr, not stdout**, which is the whole reason this needed a change
117
+ * rather than a call: `changelog` is `printsDocument`, so `rman changelog > NOTES.md` would
118
+ * otherwise capture the panel's cursor-movement codes into the notes. Same reasoning the status
119
+ * region already carries.
120
+ */
121
+ /**
122
+ * **Implies `--write`, because there is nothing else it could mean.** "Rebuild the changelog"
123
+ * with no file to rebuild would either print (which `rman changelog --from <first-tag>` already
124
+ * does) or silently do nothing; neither is what was asked for.
125
+ */
126
+ rebuild: {
127
+ target: "cli";
128
+ describe: string;
129
+ type: "boolean";
130
+ };
131
+ progress: {
132
+ target: "both";
133
+ describe: string;
134
+ type: "boolean";
135
+ };
136
+ groupBy: {
137
+ target: "both";
138
+ cliName: string;
139
+ describe: string;
140
+ type: "string";
141
+ choices: readonly ["package", "group"];
142
+ };
101
143
  startingAt: {
102
144
  target: "both";
103
145
  cliName: string;
@@ -278,6 +320,48 @@ declare const changelogCommand: (app: import("../index.js").RmanApplication) =>
278
320
  describe: string;
279
321
  type: "boolean";
280
322
  };
323
+ /**
324
+ * **What one changelog file is about.** `'package'` gives every package its own; `'group'` gives
325
+ * one to each set of packages that versions and releases together (`.rmanrc group`), written at
326
+ * the repository root - `CHANGELOG.md` for the default group, `CHANGELOG-<name>.md` for a named
327
+ * one. A `group: false` package is a group of itself either way, so its file stays its own.
328
+ *
329
+ * **`'package'` is the default because changing a repository's layout silently is not something a
330
+ * minor release may do**, not because it is the better answer. For a repository releasing along
331
+ * one line it is measurably the worse one: every package is bumped together, so most of them have
332
+ * no commit of their own and get a file that only ever says "Updated dependencies" - measured on
333
+ * `panates/sqb`, fifteen of seventeen. The whole release is then readable nowhere.
334
+ *
335
+ * Read off the repository root and nowhere else - see `changelogGroupBy`.
336
+ */
337
+ /**
338
+ * **The panel is drawn on stderr, not stdout**, which is the whole reason this needed a change
339
+ * rather than a call: `changelog` is `printsDocument`, so `rman changelog > NOTES.md` would
340
+ * otherwise capture the panel's cursor-movement codes into the notes. Same reasoning the status
341
+ * region already carries.
342
+ */
343
+ /**
344
+ * **Implies `--write`, because there is nothing else it could mean.** "Rebuild the changelog"
345
+ * with no file to rebuild would either print (which `rman changelog --from <first-tag>` already
346
+ * does) or silently do nothing; neither is what was asked for.
347
+ */
348
+ rebuild: {
349
+ target: "cli";
350
+ describe: string;
351
+ type: "boolean";
352
+ };
353
+ progress: {
354
+ target: "both";
355
+ describe: string;
356
+ type: "boolean";
357
+ };
358
+ groupBy: {
359
+ target: "both";
360
+ cliName: string;
361
+ describe: string;
362
+ type: "string";
363
+ choices: readonly ["package", "group"];
364
+ };
281
365
  startingAt: {
282
366
  target: "both";
283
367
  cliName: string;
@@ -1,8 +1,10 @@
1
+ import path from 'node:path';
1
2
  import colors from 'ansi-colors';
2
3
  import { registerCommand } from '../interfaces/rman-config.interface.js';
3
4
  import { ChangeHashService } from '../services/change-hash.service.js';
4
5
  import { Logger, resolveRootLogLevel } from '../utils/logger.js';
5
6
  import { fromRootOption, packageFilterOptions, readPackageFilterOptions } from '../utils/package-filter.js';
7
+ import { ProgressPanel } from '../utils/progress-panel.js';
6
8
  const COMMAND = 'changelog';
7
9
  const config = {
8
10
  ...packageFilterOptions,
@@ -122,6 +124,50 @@ const config = {
122
124
  '--no-unreleased for a changelog of released history only',
123
125
  type: 'boolean',
124
126
  },
127
+ /**
128
+ * **What one changelog file is about.** `'package'` gives every package its own; `'group'` gives
129
+ * one to each set of packages that versions and releases together (`.rmanrc group`), written at
130
+ * the repository root - `CHANGELOG.md` for the default group, `CHANGELOG-<name>.md` for a named
131
+ * one. A `group: false` package is a group of itself either way, so its file stays its own.
132
+ *
133
+ * **`'package'` is the default because changing a repository's layout silently is not something a
134
+ * minor release may do**, not because it is the better answer. For a repository releasing along
135
+ * one line it is measurably the worse one: every package is bumped together, so most of them have
136
+ * no commit of their own and get a file that only ever says "Updated dependencies" - measured on
137
+ * `panates/sqb`, fifteen of seventeen. The whole release is then readable nowhere.
138
+ *
139
+ * Read off the repository root and nowhere else - see `changelogGroupBy`.
140
+ */
141
+ /**
142
+ * **The panel is drawn on stderr, not stdout**, which is the whole reason this needed a change
143
+ * rather than a call: `changelog` is `printsDocument`, so `rman changelog > NOTES.md` would
144
+ * otherwise capture the panel's cursor-movement codes into the notes. Same reasoning the status
145
+ * region already carries.
146
+ */
147
+ /**
148
+ * **Implies `--write`, because there is nothing else it could mean.** "Rebuild the changelog"
149
+ * with no file to rebuild would either print (which `rman changelog --from <first-tag>` already
150
+ * does) or silently do nothing; neither is what was asked for.
151
+ */
152
+ rebuild: {
153
+ target: 'cli',
154
+ describe: 'Regenerate each changelog file from the whole history instead of appending to it - ' +
155
+ 'implies --write, and replaces what is in the file rather than prepending to it',
156
+ type: 'boolean',
157
+ },
158
+ progress: {
159
+ target: 'both',
160
+ describe: 'Show a live progress panel (default: true; auto-disabled when stderr is not a TTY)',
161
+ type: 'boolean',
162
+ },
163
+ groupBy: {
164
+ target: 'both',
165
+ cliName: 'group-by',
166
+ describe: 'What one changelog file covers: "package" (default, one per package) or "group" (one per ' +
167
+ 'set of packages released together, at the repository root)',
168
+ type: 'string',
169
+ choices: ['package', 'group'],
170
+ },
125
171
  startingAt: {
126
172
  target: 'both',
127
173
  cliName: 'starting-at',
@@ -148,13 +194,20 @@ const changelogCommand = registerCommand(app => {
148
194
  ],
149
195
  handler: async (args) => {
150
196
  const from = args.from;
151
- const write = args.write;
197
+ const rebuild = !!args.rebuild;
198
+ const write = args.write || rebuild;
152
199
  const logger = new Logger(args.logLevel ?? resolveRootLogLevel(repository));
153
- if (!from || from === ChangeHashService.AUTO) {
154
- // Auto-detection is mostly local git work, but the registry fallback it can reach for
155
- // (only when a package has no tag at all, and only if the package's own ecosystem provides
156
- // one) is a network round trip per package - without this, the command looks hung for that
157
- // stretch instead of just busy.
200
+ /**
201
+ * **The panel replaces the static line, and only where it can actually draw.** That line
202
+ * ("Detecting each package's last release...") answered "did it start" and then said nothing
203
+ * for the rest of the run - which is the longer half: measured on a `panates/sqb` backfill,
204
+ * boundary detection is 1.2s and reading the commits is 6.6s, one target at a time.
205
+ *
206
+ * Off it falls back to the line, because the two cases it is off in are exactly the two the
207
+ * line is for: a pipe, and `--no-progress`.
208
+ */
209
+ const panel = new ProgressPanel('CHANGELOG', (args.progress ?? resolveProgressConfig(repository)) !== false && !!process.stderr.isTTY, process.stderr);
210
+ if (!panel.enabled && (!from || from === ChangeHashService.AUTO)) {
158
211
  logger.info(colors.gray("Detecting each package's last release..."));
159
212
  }
160
213
  const options = {
@@ -167,20 +220,110 @@ const changelogCommand = registerCommand(app => {
167
220
  startingAt: args.startingAt,
168
221
  unreleased: args.unreleased,
169
222
  commitHash: args.commitHash,
223
+ groupBy: args.groupBy,
224
+ rebuild,
225
+ progress: panelReporter(panel),
170
226
  };
171
227
  const changelog = app.getService('changelog');
172
- const entries = write ? await changelog.generateToFile(options) : await changelog.getEntries(options);
228
+ panel.start(app.statusRegion);
229
+ let entries;
230
+ try {
231
+ entries = write ? await changelog.generateToFile(options) : await changelog.getEntries(options);
232
+ }
233
+ finally {
234
+ /** In a `finally`, or a throw leaves the redraw interval running and the cursor parked
235
+ * inside a half-drawn block - the process then prints its error over the panel. */
236
+ panel.stop();
237
+ }
173
238
  if (!entries.length) {
174
239
  logger.info(colors.gray('No unreleased changes.'));
175
240
  return;
176
241
  }
177
- for (const entry of entries) {
178
- if (write)
179
- logger.info(colors.green('updated'), colors.cyan(entry.label), entry.filePath);
180
- else
242
+ if (!write) {
243
+ for (const entry of entries)
181
244
  console.log(entry.content);
245
+ return;
246
+ }
247
+ /**
248
+ * **One line per file, not one per entry.** An entry is a *release*, so a backfill produces
249
+ * several for the same file and printed per entry the run repeats `updated ... CHANGELOG.md`
250
+ * once per release it found - twelve identical lines on a real repository, saying nothing
251
+ * the first did not.
252
+ *
253
+ * The path is repository-relative and the label is gone with the repetition: `packages/core/
254
+ * CHANGELOG.md` already says which package it belongs to, and under
255
+ * `changelog.groupBy: 'group'` the label names a group while the path names the file that
256
+ * was actually written - which is what the reader is being told about.
257
+ */
258
+ for (const file of new Set(entries.map(e => e.file))) {
259
+ logger.info(colors.green('updated'), path.relative(repository.dirname, file));
182
260
  }
183
261
  },
184
262
  };
185
263
  });
264
+ /** `.rmanrc changelog.progress`, off the **root** - one panel for the run, so one answer for the
265
+ * repository, the rule `run`'s `concurrency` already follows. */
266
+ function resolveProgressConfig(repository) {
267
+ const v = repository.rootPackage.config?.changelog?.progress;
268
+ return typeof v === 'boolean' ? v : undefined;
269
+ }
270
+ /**
271
+ * The panel, driven by what `ChangelogService` reports.
272
+ *
273
+ * A label sits at `pending` between its two phases on purpose: `detect` finishing does not finish
274
+ * the label, and the header's bar counts anything not pending as done - so marking it otherwise
275
+ * would show the run complete while the slow half had not started.
276
+ */
277
+ const PHASE_LABELS = {
278
+ detect: 'detecting last release',
279
+ commits: 'reading commits',
280
+ render: 'writing changelog',
281
+ };
282
+ function panelReporter(panel) {
283
+ const items = new Map();
284
+ return {
285
+ start(labels) {
286
+ for (const label of labels)
287
+ items.set(label, panel.addItem(label));
288
+ },
289
+ step(label, phase) {
290
+ const item = items.get(label);
291
+ if (!item)
292
+ return;
293
+ item.currentStep = PHASE_LABELS[phase];
294
+ item.startedAt ??= Date.now();
295
+ /**
296
+ * **`detect` leaves the row pending on purpose.** Every label enters it at once and it is
297
+ * over in milliseconds; showing all of them "running" would fill the panel with rows that
298
+ * are about to go quiet again, and the header counts anything not pending as done.
299
+ */
300
+ item.status = phase === 'detect' ? 'pending' : 'running';
301
+ /** The counter belongs to `commits` alone - there it counts real work, and a `(1/3)` over
302
+ * the other two would be counting phases, which the label already names. */
303
+ item.stepIndex = undefined;
304
+ item.stepsTotal = undefined;
305
+ },
306
+ commits(label, done, total) {
307
+ const item = items.get(label);
308
+ if (!item)
309
+ return;
310
+ /** **This is what the panel is for.** The row sat at one line for the whole of the slow
311
+ * phase - measured on `panates/sqb`, minutes for a full rebuild's 1825 commits - because
312
+ * the unit shown was files to write, and there is exactly one of those under
313
+ * `changelog.groupBy: 'group'`. The commits are the work. */
314
+ item.stepsTotal = total;
315
+ /** 0-based: the panel renders `stepIndex + 1`, the way `RunService` sets it. */
316
+ item.stepIndex = Math.max(0, done - 1);
317
+ },
318
+ done(label, wrote) {
319
+ const item = items.get(label);
320
+ if (!item)
321
+ return;
322
+ /** Nothing to document is not a failure and not a success - it is the third thing the panel
323
+ * already has a word for. */
324
+ item.status = wrote ? 'success' : 'skipped';
325
+ item.finishedAt = Date.now();
326
+ },
327
+ };
328
+ }
186
329
  export default changelogCommand;
package/constants.js CHANGED
@@ -1 +1 @@
1
- export const version = '2.5.0';
1
+ export const version = '2.6.1';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "rman",
3
3
  "description": "Repository manager",
4
- "version": "2.5.0",
4
+ "version": "2.6.1",
5
5
  "author": "Panates",
6
6
  "license": "MIT",
7
7
  "dependencies": {
@@ -1,4 +1,5 @@
1
1
  import type { Package } from '../core/classes/package.js';
2
+ import type { Repository } from '../core/classes/repository.js';
2
3
  import { Service } from '../core/classes/service.js';
3
4
  import { type PackageFilterOptions } from '../utils/package-filter.js';
4
5
  /**
@@ -55,6 +56,18 @@ export declare class ChangelogService extends Service {
55
56
  */
56
57
  getEntries(options?: ChangelogService.Options): Promise<ChangelogService.Entry[]>;
57
58
  }
59
+ /**
60
+ * What one changelog file is about - `.rmanrc changelog.groupBy`, `'package'` when nothing says.
61
+ *
62
+ * **Read off the repository root and nowhere else.** It decides a *layout*, so there is one answer
63
+ * for the whole repository, exactly as `run`'s `concurrency` is the root's because there is one
64
+ * scheduler. Cascaded per package it would mean two packages of one group disagreeing about which
65
+ * file they share, which has no answer.
66
+ *
67
+ * `VersionService` asks too, so that `version --changelog` makes one call per file rather than one
68
+ * per package - the same entry prepended once per member otherwise.
69
+ */
70
+ export declare function changelogGroupBy(repository: Repository, options?: ChangelogService.Options): 'package' | 'group';
58
71
  export declare namespace ChangelogService {
59
72
  interface Options extends PackageFilterOptions {
60
73
  /** Generate the changelog since this commit/hash - applied the same way to every package.
@@ -96,6 +109,10 @@ export declare namespace ChangelogService {
96
109
  * different question and deliberately ignores all of it.
97
110
  */
98
111
  write?: boolean;
112
+ /** Regenerate each file from the whole history rather than appending to what is there: the
113
+ * file's own marker and `catchUpFile` are both ignored, and every file this run writes is
114
+ * emptied first. Only meaningful with `write`; an explicit `from` still wins over it. */
115
+ rebuild?: boolean;
99
116
  /** Where this package's changelog begins - a version or release tag, a `YYYY-MM-DD` date, or a
100
117
  * commit. Releases below it are left out. `.rmanrc changelog.startingAt` when omitted; see
101
118
  * `resolveStartingPoint` for how the three forms are told apart. */
@@ -107,13 +124,58 @@ export declare namespace ChangelogService {
107
124
  /** Whether each line ends with its commit's short sha - `.rmanrc changelog.commitHash` when
108
125
  * omitted, and `true` when neither says. See `withCommitHash`. */
109
126
  commitHash?: boolean;
127
+ /** What one changelog file is about - a package, or the group of packages that release
128
+ * together. `.rmanrc changelog.groupBy` (read off the root) when omitted, and `'package'` when
129
+ * neither says. See `changelogGroupBy` and `partitionTargets`. */
130
+ groupBy?: 'package' | 'group';
131
+ /** Where this run reports what it is doing, so a caller can draw a panel. Left out, nothing is
132
+ * reported and nothing is printed - see `Progress`. */
133
+ progress?: Progress;
134
+ }
135
+ /**
136
+ * **What a run reports, for a caller that wants to render it.** `ChangelogService` prints
137
+ * nothing itself - `version --changelog` drives it in the middle of its own output, so a panel
138
+ * drawn from inside would land on top of that. The CLI implements this against `ProgressPanel`;
139
+ * every other caller leaves it out and the calls cost nothing.
140
+ *
141
+ * A label names a **file** rather than a package, since that is the unit under
142
+ * `changelog.groupBy: 'group'` - it is the same string `Entry.label` carries.
143
+ */
144
+ interface Progress {
145
+ /** Every label this run will work through, before any of it starts. */
146
+ start(labels: string[]): void;
147
+ /**
148
+ * A label entered a phase, in this order:
149
+ *
150
+ * - `'detect'` - resolving its boundary. Every label enters this at once, and it is quick.
151
+ * - `'commits'` - reading and parsing the range. **The slow one**, two `git show` per commit,
152
+ * one commit at a time; `commits()` counts it.
153
+ * - `'render'` - cutting into releases, grouping and templating. No git, so however long the
154
+ * list is this is the fast one.
155
+ */
156
+ step(label: string, phase: 'detect' | 'commits' | 'render'): void;
157
+ /**
158
+ * How far the `'commits'` phase has got - `done` of `total`, called with `(0, total)` first so
159
+ * the total is known while the count is still zero.
160
+ *
161
+ * Optional because it is the only member that fires per *commit* rather than per label: a
162
+ * reporter that only wants the phases leaves it out and pays nothing.
163
+ */
164
+ commits?(label: string, done: number, total: number): void;
165
+ /** A label is finished. `wrote` is false when the range produced no entry for it. */
166
+ done(label: string, wrote: boolean): void;
110
167
  }
111
168
  /** One package's (root included) generated changelog entry - what `getEntries`/`generate`
112
169
  * return. */
113
170
  interface Entry {
171
+ /** **Whose directory holds the file**, which under `groupBy: 'group'` is the repository root
172
+ * for every group of more than one - the entry is about all of them. The member whose config
173
+ * and tags the entry was resolved from is not carried: it is an implementation detail of the
174
+ * group, and naming one member here would read as the entry being that package's. */
114
175
  package: Package;
115
- /** Display name for this entry's heading - `"<repo dir name> repository"` for the root
116
- * package, its own name otherwise (see `getEntries`'s doc comment on `{{package}}`). */
176
+ /** Display name for this entry's heading - the group's name under `groupBy: 'group'`,
177
+ * `"<repo dir name> repository"` for the root package, its own name otherwise (see
178
+ * `getEntries`'s doc comment on `{{package}}`). */
117
179
  label: string;
118
180
  /** `options.version` when the caller gave one, otherwise resolved from git tags rather than
119
181
  * package.json - see `resolveVersion`. */
@@ -136,8 +198,12 @@ export declare namespace ChangelogService {
136
198
  other: string[];
137
199
  /** The fully rendered entry, via `.rmanrc changelog.template` (or the built-in default). */
138
200
  content: string;
139
- /** Where this entry would be (or, with `options.write`, was) written, relative to the
140
- * package's own directory - see `GetOptions.filePath`. */
201
+ /** Where this entry would be (or, with `options.write`, was) written - **absolute**, and what
202
+ * `generateToFile` writes to. A caller needing the path resolves it from here rather than
203
+ * rebuilding it from `package` and `filePath`, which a group's file cannot be rebuilt from:
204
+ * its name carries the group. */
205
+ file: string;
206
+ /** `file` relative to `package`'s own directory, for display - see `Options.filePath`. */
141
207
  filePath: string;
142
208
  }
143
209
  }