rman 2.0.1 → 2.1.0

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.
@@ -47,6 +47,23 @@ declare const config: {
47
47
  describe: string;
48
48
  type: "string";
49
49
  };
50
+ /**
51
+ * **Default `true`, so the flag that does something is `--no-commit-hash`.** GitHub autolinks a
52
+ * bare abbreviated sha wherever it renders Markdown inside a repository, so the plain
53
+ * `(a1b2c3d)` this writes is a link on the page and stays readable in a terminal - which a
54
+ * hand-built `[a1b2c3d](https://.../commit/a1b2c3d)` would not be, and which rman could not build
55
+ * anyway without knowing the forge.
56
+ *
57
+ * `'both'` rather than config-only: it is a lasting preference, and it is also the one thing a
58
+ * caller may want to turn off for a single run - `github-release` bodies are rendered in the same
59
+ * repository, but notes pasted somewhere else lose the autolinking and keep the noise.
60
+ */
61
+ commitHash: {
62
+ target: "both";
63
+ cliName: string;
64
+ describe: string;
65
+ type: "boolean";
66
+ };
50
67
  tagPattern: {
51
68
  target: "config";
52
69
  describe: string;
@@ -208,6 +225,23 @@ declare const changelogCommand: (app: import("../index.js").RmanApplication) =>
208
225
  describe: string;
209
226
  type: "string";
210
227
  };
228
+ /**
229
+ * **Default `true`, so the flag that does something is `--no-commit-hash`.** GitHub autolinks a
230
+ * bare abbreviated sha wherever it renders Markdown inside a repository, so the plain
231
+ * `(a1b2c3d)` this writes is a link on the page and stays readable in a terminal - which a
232
+ * hand-built `[a1b2c3d](https://.../commit/a1b2c3d)` would not be, and which rman could not build
233
+ * anyway without knowing the forge.
234
+ *
235
+ * `'both'` rather than config-only: it is a lasting preference, and it is also the one thing a
236
+ * caller may want to turn off for a single run - `github-release` bodies are rendered in the same
237
+ * repository, but notes pasted somewhere else lose the autolinking and keep the noise.
238
+ */
239
+ commitHash: {
240
+ target: "both";
241
+ cliName: string;
242
+ describe: string;
243
+ type: "boolean";
244
+ };
211
245
  tagPattern: {
212
246
  target: "config";
213
247
  describe: string;
@@ -62,6 +62,29 @@ const config = {
62
62
  "{{package}}/{{version}} placeholders are that file's content, not config expressions.",
63
63
  type: 'string',
64
64
  },
65
+ /**
66
+ * **Default `true`, so the flag that does something is `--no-commit-hash`.** GitHub autolinks a
67
+ * bare abbreviated sha wherever it renders Markdown inside a repository, so the plain
68
+ * `(a1b2c3d)` this writes is a link on the page and stays readable in a terminal - which a
69
+ * hand-built `[a1b2c3d](https://.../commit/a1b2c3d)` would not be, and which rman could not build
70
+ * anyway without knowing the forge.
71
+ *
72
+ * `'both'` rather than config-only: it is a lasting preference, and it is also the one thing a
73
+ * caller may want to turn off for a single run - `github-release` bodies are rendered in the same
74
+ * repository, but notes pasted somewhere else lose the autolinking and keep the noise.
75
+ */
76
+ /* **No `default: true` here, and that is the trap `unreleased` already avoids** - the default
77
+ * lives in `withCommitHash`. Declared as a yargs default, an omitted flag arrives as `true`
78
+ * rather than `undefined`, so the flag would win over `.rmanrc changelog.commitHash` on every run
79
+ * and the config key could never turn it off. The precedence rule everywhere is *the flag wins
80
+ * when it was given*, which needs an unset flag to stay unset. */
81
+ commitHash: {
82
+ target: 'both',
83
+ cliName: 'commit-hash',
84
+ describe: "Append each commit's short sha to its line (default true) - pass --no-commit-hash for " +
85
+ 'notes that are read outside the repository, where GitHub does not autolink it',
86
+ type: 'boolean',
87
+ },
65
88
  tagPattern: {
66
89
  target: 'config',
67
90
  describe: "The release tag naming scheme each package's changelog boundary is detected from - " +
@@ -141,6 +164,7 @@ const changelogCommand = registerCommand(app => {
141
164
  version: args.releaseVersion,
142
165
  startingAt: args.startingAt,
143
166
  unreleased: args.unreleased,
167
+ commitHash: args.commitHash,
144
168
  };
145
169
  const changelog = app.getService('changelog');
146
170
  const entries = write ? await changelog.generateToFile(options) : await changelog.getEntries(options);
package/constants.js CHANGED
@@ -1 +1 @@
1
- export const version = '2.0.1';
1
+ export const version = '2.1.0';
@@ -491,7 +491,11 @@ export class Repository extends Package {
491
491
  app.publishTargets.add(target);
492
492
  }
493
493
  const packages = workspace.packages;
494
- const repo = new Repository(app, rootDir, packages.length > 0, packages, from, workspace.rootPackage.platform);
494
+ /** `workspace.monorepo`, not `packages.length > 0` again: the workspace already had to answer
495
+ * this during the cascade - it is what decides whether a `"[glob]"` speaks for the root - and
496
+ * two copies of the derivation could disagree silently, resolving a config under one answer
497
+ * and building the package list under the other. */
498
+ const repo = new Repository(app, rootDir, workspace.monorepo, packages, from, workspace.rootPackage.platform);
495
499
  repo._adopt(workspace);
496
500
  app.attachRepository(repo);
497
501
  return Repository._init(repo);
@@ -45,6 +45,13 @@ export declare class Workspace {
45
45
  static create(rootDir: string, options: Workspace.Options): Promise<Workspace>;
46
46
  /** Use {@link Workspace.create}. */
47
47
  protected constructor(rootDir: string, options: Workspace.Options);
48
+ /**
49
+ * Whether the walk found packages *below* the root - i.e. whether this is a monorepo.
50
+ *
51
+ * `false` means the root is the repository's one and only package, which is what decides whether
52
+ * a `"[glob]"` selector speaks for it (see {@link Workspace._speaksFor}).
53
+ */
54
+ get monorepo(): boolean;
48
55
  /** The package at a directory, root included, or `undefined` for one that holds none. */
49
56
  packageAt(dirname: string): Package | undefined;
50
57
  /** The whole resolution, in the one order the steps admit. */
@@ -109,8 +116,8 @@ export declare class Workspace {
109
116
  /**
110
117
  * Whether one selector block speaks for this package.
111
118
  *
112
- * `"[/]"` asks only whether this *is* the root. Everything else matches packages **below** the
113
- * root and never the root itself, which is addressed structurally or not at all.
119
+ * `"[/]"` asks only whether this *is* the root. A `"[glob]"` matches packages **below** the root -
120
+ * and, in a repository that has none, the root itself, because there it *is* the one package.
114
121
  */
115
122
  protected _speaksFor(scope: ConfigReader.ParsedSelector['scope'], test: (value: string) => boolean, audience: Workspace.Audience): boolean;
116
123
  /**
@@ -74,6 +74,24 @@ export class Workspace {
74
74
  this.plugins = [...(options.plugins ?? [])];
75
75
  this.platforms = [...(options.platforms ?? [])];
76
76
  }
77
+ /**
78
+ * Whether the walk found packages *below* the root - i.e. whether this is a monorepo.
79
+ *
80
+ * `false` means the root is the repository's one and only package, which is what decides whether
81
+ * a `"[glob]"` selector speaks for it (see {@link Workspace._speaksFor}).
82
+ */
83
+ /* **One derivation, read from two places.** `Repository.create` computed `packages.length > 0`
84
+ * itself and passed it in as the `monorepo` constructor argument; `_speaksFor` now needs the same
85
+ * fact *during* the cascade, which runs before any `Repository` exists. Two copies of a one-line
86
+ * derivation is two places to disagree, and the disagreement would be silent - a config resolved
87
+ * under one answer and a command's package list built under the other. `Repository.create` reads
88
+ * this getter now.
89
+ *
90
+ * Safe to ask at cascade time because `_discover` has already run: `_init` walks the tree first
91
+ * and only then resolves selectors and configs, so `packages` is final by the time either asks. */
92
+ get monorepo() {
93
+ return this.packages.length > 0;
94
+ }
77
95
  /** The package at a directory, root included, or `undefined` for one that holds none. */
78
96
  packageAt(dirname) {
79
97
  const resolved = path.resolve(dirname);
@@ -369,17 +387,47 @@ export class Workspace {
369
387
  /**
370
388
  * Whether one selector block speaks for this package.
371
389
  *
372
- * `"[/]"` asks only whether this *is* the root. Everything else matches packages **below** the
373
- * root and never the root itself, which is addressed structurally or not at all.
390
+ * `"[/]"` asks only whether this *is* the root. A `"[glob]"` matches packages **below** the root -
391
+ * and, in a repository that has none, the root itself, because there it *is* the one package.
374
392
  */
375
- /* **A glob never matching the root removes two traps at once**: `"[my-*]"` cannot quietly pick up
376
- * a repository whose root package is called `my-repo`, and `"[*]"` cannot hand a package-shaped
377
- * setting to a root with no build directory to apply it to. Neither trap exists for a platform
378
- * block - `platform:node` is not a name and cannot match by accident, and it is not a catch-all -
379
- * so **a platform link answers about the root like any other package**, and what keeps a plain
380
- * `"[platform:node]"` off the root is `_matchingSelectors`' chain rule rather than a refusal here.
381
- * Asking `isRoot` first was what made `"[platform:node]" > "[/]"` unanswerable: the pair says *the
382
- * root, when it is a node package*, and the short-circuit never let the platform question be put.
393
+ /* **A glob is held off a MONOREPO root, and only a monorepo root** - which is narrower than the
394
+ * rule this replaces and is the whole of the change. Both justifications for the old blanket
395
+ * refusal are statements about a monorepo root, and neither survives being read in a
396
+ * single-package repository:
397
+ *
398
+ * - `"[my-*]"` must not quietly pick up a repository whose root package is called `my-repo`.
399
+ * That is an *accidental* name collision between a container and the things it contains.
400
+ * With nothing contained, the root is not a container that happens to share a name shape -
401
+ * it is the package, and `"[postgrejs]"` naming it is the author saying what they mean.
402
+ * - `"[*]"` must not hand a package-shaped setting to a root with no build directory to apply
403
+ * it to. A single-package root *has* the build directory: it is where the package is built.
404
+ *
405
+ * What the old rule cost, measured on `panates/postgrejs` before this: `@panates/rman-preset`
406
+ * declares its whole package block under `"[platform:node]" > "[*]"` - `run.build`,
407
+ * `publish.npm.directory`, `version.stamp`, the build directory's `clean.include` - and *none* of
408
+ * it reached that repository. `rman build` answered `No package defines a "build" script.` and
409
+ * `rman config` showed the four keys simply absent, with nothing reporting that a block had
410
+ * matched nobody. The workaround would have been for every single-package repository to restate
411
+ * the preset's block under `"[/]"`, which is the duplication a shared preset exists to remove -
412
+ * and it is not even writable in YAML, since two of those values are JavaScript functions the
413
+ * preset keeps to itself.
414
+ *
415
+ * **`"[/]"` still means the root and is still the right spelling for a repo-wide setting**, so in
416
+ * a single-package repository both selectors reach the one package and are layered in declaration
417
+ * order like any two siblings. That overlap is not a conflict to resolve: `"[/]"` says *the
418
+ * repository* and `"[*]"` says *its packages*, and in a repository of one those are the same
419
+ * object - which is the fact this rule finally states.
420
+ *
421
+ * Neither trap exists for a platform block - `platform:node` is not a name and cannot match by
422
+ * accident, and it is not a catch-all - so **a platform link answers about the root like any other
423
+ * package**, and what keeps a plain `"[platform:node]"` off the root is `_matchingSelectors`'
424
+ * chain rule rather than a refusal here. Asking `isRoot` first was what made
425
+ * `"[platform:node]" > "[/]"` unanswerable: the pair says *the root, when it is a node package*,
426
+ * and the short-circuit never let the platform question be put.
427
+ *
428
+ * **The CLI moves with this, and has to**: `--scope '*'` and `"[*]"` are documented as the same
429
+ * set, so `package-filter.ts`'s `selector()` carries the identical clause. Changing one alone
430
+ * would make a repository's config and its `--scope` disagree about what a package is.
383
431
  *
384
432
  * A package no technology claimed carries `basePlatform`, whose name is `''`, so it matches no
385
433
  * platform block at all - `parseSelector`'s test refuses an empty name rather than letting an
@@ -389,7 +437,9 @@ export class Workspace {
389
437
  return audience.isRoot;
390
438
  if (scope === 'platform')
391
439
  return test(audience.platform);
392
- return !audience.isRoot && audience.selector !== undefined && test(audience.selector);
440
+ if (audience.isRoot && this.monorepo)
441
+ return false;
442
+ return audience.selector !== undefined && test(audience.selector);
393
443
  }
394
444
  /**
395
445
  * The platform that claims a directory - the one its config names, or the first loaded platform
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "rman",
3
3
  "description": "Repository manager",
4
- "version": "2.0.1",
4
+ "version": "2.1.0",
5
5
  "author": "Panates",
6
6
  "license": "MIT",
7
7
  "dependencies": {
@@ -24,8 +24,8 @@ export declare class ChangelogService extends Service {
24
24
  * outside every package) - unless it's broad enough to count as a repo-wide change (see
25
25
  * `BROAD_COMMIT_THRESHOLD`/`ownersOf`), in which case it goes to root alone instead of being
26
26
  * repeated verbatim across most of the repository. Subject lines are grouped ✨ Features/🐛 Bug
27
- * Fixes/🔧 Other Changes on a best-effort Conventional Commits read; anything that doesn't parse
28
- * just lands in Other Changes as-is, so a repo that doesn't follow that convention still gets a
27
+ * Fixes/💬 General Changes on a best-effort Conventional Commits read; anything that doesn't parse
28
+ * just lands in General Changes as-is, so a repo that doesn't follow that convention still gets a
29
29
  * usable list. `.rmanrc changelog.ignoreTypes` (cascaded, e.g. `[chore, dev]`) drops commits of
30
30
  * those types entirely instead - see `ignoreTypesConfig`. A release marker - a bare version-bump
31
31
  * commit (`"6.0.1"`), or any of the messages `version` itself writes - is always dropped
@@ -104,6 +104,9 @@ export declare namespace ChangelogService {
104
104
  * omitted, and `true` when neither says. A caller passing `version` always gets it, since that
105
105
  * names the release being cut; see `resolveUnreleased`. */
106
106
  unreleased?: boolean;
107
+ /** Whether each line ends with its commit's short sha - `.rmanrc changelog.commitHash` when
108
+ * omitted, and `true` when neither says. See `withCommitHash`. */
109
+ commitHash?: boolean;
107
110
  }
108
111
  /** One package's (root included) generated changelog entry - what `getEntries`/`generate`
109
112
  * return. */
@@ -48,8 +48,8 @@ export class ChangelogService extends Service {
48
48
  * outside every package) - unless it's broad enough to count as a repo-wide change (see
49
49
  * `BROAD_COMMIT_THRESHOLD`/`ownersOf`), in which case it goes to root alone instead of being
50
50
  * repeated verbatim across most of the repository. Subject lines are grouped ✨ Features/🐛 Bug
51
- * Fixes/🔧 Other Changes on a best-effort Conventional Commits read; anything that doesn't parse
52
- * just lands in Other Changes as-is, so a repo that doesn't follow that convention still gets a
51
+ * Fixes/💬 General Changes on a best-effort Conventional Commits read; anything that doesn't parse
52
+ * just lands in General Changes as-is, so a repo that doesn't follow that convention still gets a
53
53
  * usable list. `.rmanrc changelog.ignoreTypes` (cascaded, e.g. `[chore, dev]`) drops commits of
54
54
  * those types entirely instead - see `ignoreTypesConfig`. A release marker - a bare version-bump
55
55
  * commit (`"6.0.1"`), or any of the messages `version` itself writes - is always dropped
@@ -143,7 +143,7 @@ export class ChangelogService extends Service {
143
143
  const ownCommits = dropVersionBumps(segment.commits).filter(c => ownersOf(repository, c).has(pkg));
144
144
  if (!ownCommits.length)
145
145
  continue;
146
- const sections = groupCommits(ownCommits.map(c => c.subject), ignoreTypesConfig(pkg), titles, order);
146
+ const sections = groupCommits(ownCommits, ignoreTypesConfig(pkg), titles, order, withCommitHash(pkg, options));
147
147
  // every commit could have been dropped by ignoreTypes - skip this package's entry entirely
148
148
  // rather than rendering a heading with nothing real underneath it.
149
149
  if (!sections.length)
@@ -168,9 +168,9 @@ export class ChangelogService extends Service {
168
168
  }
169
169
  }
170
170
  /** `.rmanrc changelog.ignoreTypes` (cascaded, per-package overridable) - Conventional Commits
171
- * `type`s to drop entirely (e.g. `[chore, dev]`), not just fold into "Other Changes". Only
171
+ * `type`s to drop entirely (e.g. `[chore, dev]`), not just fold into "General Changes". Only
172
172
  * applies to commits that actually parse as `type: ...` - a non-conventional message always
173
- * still lands in Other Changes, since it has no `type` to match against. */
173
+ * still lands in General Changes, since it has no `type` to match against. */
174
174
  function ignoreTypesConfig(pkg) {
175
175
  const v = pkg.config?.changelog?.ignoreTypes;
176
176
  return new Set(Array.isArray(v) ? v.map(t => String(t).toLowerCase()) : []);
@@ -204,36 +204,62 @@ function resolveTitles(pkg) {
204
204
  *
205
205
  * **The type prefix is stripped for every section, not just two.** It used to be `push(line)` for
206
206
  * `feat`/`fix` and `push(subject)` for everything else, so Features read `- the first feature`
207
- * while Other Changes read `- chore: write changelog` - the heading naming the type and the bullet
208
- * repeating it. With every type able to have a heading of its own that asymmetry has no defence
209
- * left. A subject that is not Conventional Commits at all has no prefix to strip and is kept whole.
207
+ * while General Changes read `- chore: write changelog` - the heading naming the type and the
208
+ * bullet repeating it. With every type able to have a heading of its own that asymmetry has no
209
+ * defence left. A subject that is not Conventional Commits at all has no prefix to strip and is
210
+ * kept whole.
210
211
  */
211
- function groupCommits(subjects, ignoreTypes, titles, order) {
212
+ /* **A message repeated inside one section is written once.** Real histories hold runs of identical
213
+ * subjects - measured on `panates/postgrejs`, whose v2.22.1 entry read `Updated config` five times
214
+ * under one heading - and the repetition states nothing the first line did not. Deduplicated **per
215
+ * section**, not per entry: `feat: x` and `fix: x` are different claims that land under different
216
+ * headings, and collapsing those would lose a fact rather than a repetition.
217
+ *
218
+ * **Keyed on the message, before the sha is appended**, which is the whole subtlety: with the sha on
219
+ * the line every duplicate is textually unique and the check would never fire. So the survivor keeps
220
+ * the *first* commit's sha - and since `listCommits` returns oldest-first, that is the earliest of
221
+ * the run. The others are dropped entirely, which is the point rather than a cost: the entry says
222
+ * what changed, and `git log` says how many times it was committed.
223
+ *
224
+ * **The sha is written plain, never as a Markdown link.** GitHub autolinks an abbreviated sha
225
+ * wherever it renders Markdown inside the repository, so `(a1b2c3d)` is already a link on the page
226
+ * and still readable in a terminal; a hand-built URL would need to know the forge, which nothing
227
+ * here does. Seven characters because that is what `git log --abbrev-commit` and GitHub both show.
228
+ */
229
+ function groupCommits(commits, ignoreTypes, titles, order, commitHash) {
212
230
  const CATCH_ALL = '*';
213
231
  const byTitle = new Map();
232
+ const seenByTitle = new Map();
214
233
  const titleFor = (type) => titles.get(type) ?? titles.get(CATCH_ALL) ?? DEFAULT_TITLES.get(CATCH_ALL);
215
- const push = (title, line) => {
234
+ const push = (title, message, sha) => {
235
+ let seen = seenByTitle.get(title);
236
+ if (!seen)
237
+ seenByTitle.set(title, (seen = new Set()));
238
+ if (seen.has(message))
239
+ return;
240
+ seen.add(message);
241
+ const line = commitHash ? `${message} (${sha.slice(0, SHORT_SHA_LENGTH)})` : message;
216
242
  const lines = byTitle.get(title);
217
243
  if (lines)
218
244
  lines.push(line);
219
245
  else
220
246
  byTitle.set(title, [line]);
221
247
  };
222
- for (const subject of subjects) {
248
+ for (const { subject, sha } of commits) {
223
249
  const parsed = ConventionalCommitsService.parseSubject(subject);
224
250
  if (!parsed) {
225
- push(titleFor(CATCH_ALL), subject);
251
+ push(titleFor(CATCH_ALL), subject, sha);
226
252
  continue;
227
253
  }
228
254
  const { type, scope, description } = parsed;
229
255
  if (ignoreTypes.has(type))
230
256
  continue;
231
- push(titleFor(type), scope ? `**${scope}:** ${description}` : description);
257
+ push(titleFor(type), scope ? `**${scope}:** ${description}` : description, sha);
232
258
  }
233
259
  /**
234
260
  * Only a type with a heading of its own takes a position. A type nobody named resolves to the
235
261
  * catch-all, so ordering by it would drag the catch-all to wherever that type was listed -
236
- * measured: `sortTitles: ['docs', 'fix', 'feat']` with no `docs` heading put "Other Changes"
262
+ * measured: `sortTitles: ['docs', 'fix', 'feat']` with no `docs` heading put "General Changes"
237
263
  * first and swallowed the sections after it.
238
264
  */
239
265
  const ordered = [...new Set(order.filter(t => t !== CATCH_ALL && titles.has(t)).map(titleFor))];
@@ -286,11 +312,57 @@ function legacyBuckets(sections, titles) {
286
312
  }
287
313
  /** `feat`/`fix`/everything else, which is what rman shipped before `changelog.titles` existed and
288
314
  * so is what a repository that sets nothing still gets, wording and order alike. */
315
+ /**
316
+ * The heading each commit type is listed under, and - because `resolveOrder` falls back to
317
+ * `titles.keys()` - the order the sections come out in. `.rmanrc changelog.titles` is merged over
318
+ * this **per key**, so a repository renaming one keeps the rest and keeps its position.
319
+ */
320
+ /* **Three entries was too few, and the measurement is what says so.** `feat`, `fix` and a catch-all
321
+ * were all rman named, so every other type fell into one heap: counted across four of this
322
+ * organization's repositories (rman, postgrejs, panates-javascript, syncbridge-public), that heap
323
+ * held 194 `chore`, 86 `docs`, 68 `refactor`, 29 `test`, 27 `ci` and 24 `perf` - more commits than
324
+ * the two named types put together, under a heading that says only "not one of the two".
325
+ *
326
+ * The set is Conventional Commits' own standard types and stops there. `dev` (32 across the same
327
+ * four) and `bench` (9) are this organization's inventions, and a repository names those in its own
328
+ * `changelog.titles` - or a shared preset does it once for all of them. rman shipping a heading for
329
+ * a type no convention defines would be guessing on everyone else's behalf.
330
+ *
331
+ * **These add sections; they do not hide anything.** `ignoreTypes` is what drops a type, and it
332
+ * stays the only thing that does - which is the one place this parts from conventional-changelog,
333
+ * whose default preset silently hides `chore`, `ci`, `build`, `style` and `test`. A changelog that
334
+ * quietly omits a third of the history is the failure mode worth avoiding; a reader who does not
335
+ * want those sections can say so in one key.
336
+ *
337
+ * Order: Features, Bug Fixes, Performance and Reverts first, as conventional-changelog has them -
338
+ * the four a reader is looking for - then the rest, then the catch-all, which `groupCommits` pins
339
+ * last whatever anyone writes.
340
+ *
341
+ * **Every emoji here is checked for U+FE0F and none carries it.** A variation selector survives
342
+ * github-slugger, so `### ♻️ Refactoring` gets an anchor nobody types by hand and every link to it
343
+ * silently lands at the top of the page - the trap already recorded for `⬆️`. That is why refactor
344
+ * is `🔧` (U+1F527, free now that the catch-all is `💬`) rather than `♻️`, and ci is `🤖` rather
345
+ * than `⚙️`.
346
+ *
347
+ * "General Changes", not "Other Changes", for the catch-all: even with eleven named types it holds
348
+ * whatever convention a repository invented for itself, and "Other" reads as the leftovers after
349
+ * the interesting part. */
289
350
  const DEFAULT_TITLES = new Map([
290
351
  ['feat', '✨ Features'],
291
352
  ['fix', '🐛 Bug Fixes'],
292
- ['*', '🔧 Other Changes'],
353
+ ['perf', '⚡ Performance and Optimizations'],
354
+ ['revert', '⏪ Reverts'],
355
+ ['refactor', '🔧 Refactoring'],
356
+ ['docs', '📚 Documentation'],
357
+ ['test', '🧪 Tests'],
358
+ ['build', '📦 Build System'],
359
+ ['ci', '🤖 Continuous Integration'],
360
+ ['chore', '🧹 Chores'],
361
+ ['style', '🎨 Code Style'],
362
+ ['*', '💬 General Changes'],
293
363
  ]);
364
+ /** What `git log --abbrev-commit` prints and what GitHub autolinks. */
365
+ const SHORT_SHA_LENGTH = 7;
294
366
  function bulletList(lines) {
295
367
  return lines.map(l => `- ${l}`).join('\n');
296
368
  }
@@ -353,6 +425,12 @@ function resolveUnreleased(pkg, options) {
353
425
  const cfg = pkg.config?.changelog?.unreleased;
354
426
  return options.unreleased ?? (typeof cfg === 'boolean' ? cfg : true);
355
427
  }
428
+ /** Whether each line carries its commit's short sha - `.rmanrc changelog.commitHash`, or the flag,
429
+ * which wins as everywhere. Defaults to `true`; `--no-commit-hash` is what turns it off. */
430
+ function withCommitHash(pkg, options) {
431
+ const cfg = pkg.config?.changelog?.commitHash;
432
+ return options.commitHash ?? (typeof cfg === 'boolean' ? cfg : true);
433
+ }
356
434
  /**
357
435
  * **Where this package's changelog begins** - `.rmanrc changelog.startingAt`, or the CLI flag,
358
436
  * which wins as everywhere.
@@ -13,7 +13,7 @@
13
13
  export declare namespace ConventionalCommitsService {
14
14
  /**
15
15
  * `type(scope): description`, optionally with a `!` breaking-change marker - Conventional
16
- * Commits' subject-line shape. Anything that doesn't match falls into "Other Changes" as-is (for
16
+ * Commits' subject-line shape. Anything that doesn't match falls into "General Changes" as-is (for
17
17
  * changelog entries) or defaults to a patch-level change (for version bump severity) - see
18
18
  * `parseSubject`.
19
19
  */
@@ -14,7 +14,7 @@ export var ConventionalCommitsService;
14
14
  (function (ConventionalCommitsService) {
15
15
  /**
16
16
  * `type(scope): description`, optionally with a `!` breaking-change marker - Conventional
17
- * Commits' subject-line shape. Anything that doesn't match falls into "Other Changes" as-is (for
17
+ * Commits' subject-line shape. Anything that doesn't match falls into "General Changes" as-is (for
18
18
  * changelog entries) or defaults to a patch-level change (for version bump severity) - see
19
19
  * `parseSubject`.
20
20
  */
@@ -46,12 +46,17 @@ export interface PackageFilterOptions {
46
46
  /**
47
47
  * **`--scope /` is the repository's own root package**, the one selector that is not a glob.
48
48
  *
49
- * The same `/` `.rmanrc`'s `"[/]"` block uses, and for the same reason stated there: *the root is
50
- * never selected by name.* A glob matches package names, and a name can be anything - so
49
+ * The same `/` `.rmanrc`'s `"[/]"` block uses, and for the same reason stated there: *a monorepo's
50
+ * root is never selected by name.* A glob matches package names, and a name can be anything - so
51
51
  * `--scope rman-repo` happened to work (measured) while being exactly the name-based addressing the
52
52
  * config selectors were redesigned to remove. `/` is structural, cannot collide with a package
53
53
  * (nothing can be named it), and gives "the root" one spelling across config and CLI.
54
54
  *
55
+ * **In a single-package repository a glob does reach the root**, because there it is not a container
56
+ * that happens to have a name - it is the one package, and `--scope '*'` meaning "no packages" was
57
+ * the same silent emptiness `"[*]"` had in a `.rmanrc`. `/` still selects it, so nothing that worked
58
+ * stops working; see `Workspace._speaksFor`, which carries the identical clause.
59
+ *
55
60
  * Accepted by `--ignore` too. The asymmetry would be the thing to remember, and `--ignore /` -
56
61
  * every package but the root - is a real thing to want of `clean`.
57
62
  *
@@ -2,12 +2,17 @@ import micromatch from 'micromatch';
2
2
  /**
3
3
  * **`--scope /` is the repository's own root package**, the one selector that is not a glob.
4
4
  *
5
- * The same `/` `.rmanrc`'s `"[/]"` block uses, and for the same reason stated there: *the root is
6
- * never selected by name.* A glob matches package names, and a name can be anything - so
5
+ * The same `/` `.rmanrc`'s `"[/]"` block uses, and for the same reason stated there: *a monorepo's
6
+ * root is never selected by name.* A glob matches package names, and a name can be anything - so
7
7
  * `--scope rman-repo` happened to work (measured) while being exactly the name-based addressing the
8
8
  * config selectors were redesigned to remove. `/` is structural, cannot collide with a package
9
9
  * (nothing can be named it), and gives "the root" one spelling across config and CLI.
10
10
  *
11
+ * **In a single-package repository a glob does reach the root**, because there it is not a container
12
+ * that happens to have a name - it is the one package, and `--scope '*'` meaning "no packages" was
13
+ * the same silent emptiness `"[*]"` had in a `.rmanrc`. `/` still selects it, so nothing that worked
14
+ * stops working; see `Workspace._speaksFor`, which carries the identical clause.
15
+ *
11
16
  * Accepted by `--ignore` too. The asymmetry would be the thing to remember, and `--ignore /` -
12
17
  * every package but the root - is a real thing to want of `clean`.
13
18
  *
@@ -297,5 +302,12 @@ function selector(value) {
297
302
  * `/` above already is. It was `pkg.name`, and the two coincide for every Node repository; a
298
303
  * package having a name at all is an ecosystem's promise, and a repository can assign a selector
299
304
  * where its own does not offer one. */
300
- return pkg => (pkg.isRoot ? wantsRoot : globs.length > 0 && micromatch.isMatch(pkg.selector, globs));
305
+ /** **A glob reaches the root in a single-package repository**, exactly as `"[*]"` does in a
306
+ * `.rmanrc` - the two are documented as the same set, so they move together or a repository's
307
+ * config and its `--scope` disagree about what a package is. In a monorepo the root is still
308
+ * addressed by `/` alone; there it is a container that happens to have a name, and a glob picking
309
+ * it up by that name is the trap `/` was introduced to remove. `pkg.repository` is safe to read
310
+ * under `isRoot`, which is `false` until `Repository.create` assigns it. */
311
+ const matchesGlob = (pkg) => globs.length > 0 && micromatch.isMatch(pkg.selector, globs);
312
+ return pkg => (pkg.isRoot ? wantsRoot || (!pkg.repository.monorepo && matchesGlob(pkg)) : matchesGlob(pkg));
301
313
  }