rman 2.9.0 → 2.10.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.
- package/commands/changelog.command.d.ts +15 -0
- package/commands/version.command.js +13 -4
- package/constants.js +1 -1
- package/package.json +1 -1
- package/services/changelog.service.js +120 -14
- package/utils/version-group.d.ts +10 -0
- package/utils/version-group.js +12 -0
|
@@ -184,6 +184,21 @@ declare const config: {
|
|
|
184
184
|
* from commit type to section heading, and an option says `type: 'string'`.
|
|
185
185
|
*/
|
|
186
186
|
export interface ChangelogExtraKeys {
|
|
187
|
+
/**
|
|
188
|
+
* Where each **named** group's changelog is written under `groupBy: 'group'`, relative to the
|
|
189
|
+
* repository root - `{ core: 'packages/core/CHANGELOG.md', dialects: 'docs/dialects.md' }`.
|
|
190
|
+
* Read off the repository root only.
|
|
191
|
+
*
|
|
192
|
+
* A group not listed keeps the default rule, `CHANGELOG-<group>.md` beside the default file; the
|
|
193
|
+
* default group's own file is `changelog.filePath`. A key naming no group, a path outside the
|
|
194
|
+
* repository, and two groups resolving to one file are all refused before anything is written.
|
|
195
|
+
* An explicit `--file-path` overrides the whole map for that run.
|
|
196
|
+
*
|
|
197
|
+
* **Move an existing file with `git mv`, not by changing the path here.** A file's
|
|
198
|
+
* `documented-up-to` marker is what the next `--write` starts from; at a new path there is none,
|
|
199
|
+
* and the write reads the group's entire history.
|
|
200
|
+
*/
|
|
201
|
+
groupFiles?: ConfigValue<Record<string, string>>;
|
|
187
202
|
/**
|
|
188
203
|
* The heading each Conventional Commits type is listed under - `{ feat: 'New Features', dev:
|
|
189
204
|
* 'Development Changes' }`. Per-package cascaded.
|
|
@@ -5,6 +5,7 @@ import { registerCommand } from '../interfaces/rman-config.interface.js';
|
|
|
5
5
|
import { VersionPlanService } from '../services/version-plan.service.js';
|
|
6
6
|
import { assertAllowedBranch, branchGuardOptions, readBranchGuardOptions } from '../utils/branch-guard.js';
|
|
7
7
|
import { packageFilterOptions, readPackageFilterOptions } from '../utils/package-filter.js';
|
|
8
|
+
import { isSoloGroupKey } from '../utils/version-group.js';
|
|
8
9
|
/**
|
|
9
10
|
* Hoisted out of the metadata literal so the handler can be annotated against them - see
|
|
10
11
|
* `RmanConfig.ArgsOf` for why an inferred `argv` and the metadata's own typo checking cannot both
|
|
@@ -344,13 +345,21 @@ function planBlocks(entries) {
|
|
|
344
345
|
else
|
|
345
346
|
byGroup.set(e.groupKey, [e]);
|
|
346
347
|
}
|
|
348
|
+
/**
|
|
349
|
+
* **Classified by the key, not by how many members a group has.** This was `group.length > 1`,
|
|
350
|
+
* which stands in for "is this a group" and is wrong in exactly one case: a group the repository
|
|
351
|
+
* *named* that happens to have a single member. Reported on a real repository - a package alone in
|
|
352
|
+
* `group: "abisena-iomt"` landed among the ungrouped and printed an empty Group cell, reading
|
|
353
|
+
* exactly like a package that had opted out. Its name decides its changelog file and its tag, so
|
|
354
|
+
* it is a line of its own whatever its size.
|
|
355
|
+
*/
|
|
347
356
|
const shared = [];
|
|
348
357
|
const solo = [];
|
|
349
|
-
for (const group of byGroup
|
|
350
|
-
if (
|
|
351
|
-
|
|
358
|
+
for (const [key, group] of byGroup) {
|
|
359
|
+
if (isSoloGroupKey(key))
|
|
360
|
+
solo.push(...group);
|
|
352
361
|
else
|
|
353
|
-
|
|
362
|
+
shared.push({ members: group, shared: true });
|
|
354
363
|
}
|
|
355
364
|
return [
|
|
356
365
|
...(root.length ? [{ members: root, shared: false }] : []),
|
package/constants.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const version = '2.
|
|
1
|
+
export const version = '2.10.1';
|
package/package.json
CHANGED
|
@@ -600,33 +600,139 @@ function resolveFilePath(pkg, optionsFilePath) {
|
|
|
600
600
|
*/
|
|
601
601
|
function partitionTargets(repository, targets, options) {
|
|
602
602
|
const grouping = changelogGroupBy(repository, options);
|
|
603
|
-
const
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
else
|
|
610
|
-
byKey.set(key, [pkg]);
|
|
611
|
-
}
|
|
603
|
+
const groupFiles = grouping === 'group' ? readGroupFiles(repository, options) : {};
|
|
604
|
+
/** Checked against the **whole** repository, not against `targets`: a collision is a fact about
|
|
605
|
+
* the configuration, and a `--scope` that happens to leave one of the two groups out must not be
|
|
606
|
+
* what lets it through. */
|
|
607
|
+
if (grouping === 'group')
|
|
608
|
+
assertGroupFiles(repository, groupFiles, options);
|
|
612
609
|
const repositoryLabel = `${path.basename(repository.dirname)} repository`;
|
|
613
|
-
return [...
|
|
610
|
+
return [...groupTargets(targets, grouping)].map(([key, members]) => {
|
|
614
611
|
/** `targets` starts at the root, so a group holding it has it first - which is what makes the
|
|
615
612
|
* root the `home` of the default group without a second rule saying so. */
|
|
616
613
|
const home = members[0];
|
|
617
614
|
const grouped = !key.startsWith('solo:');
|
|
618
|
-
const named =
|
|
619
|
-
const filePkg = grouped ? repository.rootPackage : home;
|
|
615
|
+
const named = namedGroupOf(key);
|
|
620
616
|
return {
|
|
621
617
|
home,
|
|
622
|
-
filePkg,
|
|
618
|
+
filePkg: grouped ? repository.rootPackage : home,
|
|
623
619
|
grouped,
|
|
624
620
|
members: new Set(members),
|
|
625
621
|
label: named ?? (home === repository.rootPackage ? repositoryLabel : home.name),
|
|
626
|
-
file:
|
|
622
|
+
file: groupFile(repository, key, home, groupFiles, options),
|
|
627
623
|
};
|
|
628
624
|
});
|
|
629
625
|
}
|
|
626
|
+
/** The packages cut into what each file is about - by `.rmanrc group` under `groupBy: 'group'`, one
|
|
627
|
+
* package each otherwise. Insertion order, so the first member of each group is the first of
|
|
628
|
+
* `packages` to belong to it. */
|
|
629
|
+
function groupTargets(packages, grouping) {
|
|
630
|
+
const byKey = new Map();
|
|
631
|
+
for (const pkg of packages) {
|
|
632
|
+
const key = grouping === 'group' ? groupKeyOf(pkg) : `solo:${pkg.name}`;
|
|
633
|
+
const members = byKey.get(key);
|
|
634
|
+
if (members)
|
|
635
|
+
members.push(pkg);
|
|
636
|
+
else
|
|
637
|
+
byKey.set(key, [pkg]);
|
|
638
|
+
}
|
|
639
|
+
return byKey;
|
|
640
|
+
}
|
|
641
|
+
/** `named:core` -> `core`; `undefined` for the default group and for a package that is its own. */
|
|
642
|
+
function namedGroupOf(key) {
|
|
643
|
+
return key.startsWith('named:') ? key.slice('named:'.length) : undefined;
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* Where one group's changelog is written, absolute.
|
|
647
|
+
*
|
|
648
|
+
* In order: a named group listed in `changelog.groupFiles` goes where it says, relative to the
|
|
649
|
+
* repository root; any other named group takes the default file's name with its own suffixed
|
|
650
|
+
* (`CHANGELOG-core.md`), at the root; the default group takes the root's `changelog.filePath`; and a
|
|
651
|
+
* package that is a group of itself takes its own, in its own directory.
|
|
652
|
+
*
|
|
653
|
+
* **An explicit `--file-path` beats `groupFiles`**, as it already beats `changelog.filePath`: it
|
|
654
|
+
* names this run's file, and a run asking for one shape of name should not find half its groups
|
|
655
|
+
* somewhere a config put them. Named groups are still suffixed under it, so two cannot collide.
|
|
656
|
+
*/
|
|
657
|
+
function groupFile(repository, key, home, groupFiles, options) {
|
|
658
|
+
if (key.startsWith('solo:'))
|
|
659
|
+
return path.join(home.dirname, resolveFilePath(home, options.filePath));
|
|
660
|
+
const named = namedGroupOf(key);
|
|
661
|
+
const listed = named && !options.filePath ? groupFiles[named] : undefined;
|
|
662
|
+
if (listed)
|
|
663
|
+
return path.resolve(repository.dirname, listed);
|
|
664
|
+
return path.join(repository.dirname, suffixFileName(resolveFilePath(home, options.filePath), named));
|
|
665
|
+
}
|
|
666
|
+
/**
|
|
667
|
+
* `.rmanrc changelog.groupFiles`, off the repository root and nowhere else - a map from a named
|
|
668
|
+
* group to the file its changelog is written to, relative to the repository root.
|
|
669
|
+
*
|
|
670
|
+
* **The root's for the reason `groupBy` is the root's**: which file a group shares is a layout, and
|
|
671
|
+
* cascaded per package two members of one group could disagree about it, which has no answer.
|
|
672
|
+
*/
|
|
673
|
+
function readGroupFiles(repository, options) {
|
|
674
|
+
/** `--file-path` takes the whole decision - see `groupFile` - so the map is not even read. A
|
|
675
|
+
* mistake in it is still a mistake, but not one this run acts on. */
|
|
676
|
+
if (options.filePath)
|
|
677
|
+
return {};
|
|
678
|
+
const declared = repository.rootPackage.config?.changelog?.groupFiles;
|
|
679
|
+
if (declared === undefined || declared === null)
|
|
680
|
+
return {};
|
|
681
|
+
if (typeof declared !== 'object' || Array.isArray(declared)) {
|
|
682
|
+
throw new Error(`.rmanrc "changelog.groupFiles" must map a group name to a file path, ` +
|
|
683
|
+
`e.g. { core: "packages/core/CHANGELOG.md" }.`);
|
|
684
|
+
}
|
|
685
|
+
for (const [group, file] of Object.entries(declared)) {
|
|
686
|
+
if (typeof file !== 'string' || !file) {
|
|
687
|
+
throw new Error(`.rmanrc "changelog.groupFiles.${group}" must be a file path, relative to the repository root.`);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
return declared;
|
|
691
|
+
}
|
|
692
|
+
/**
|
|
693
|
+
* Refuses a `changelog.groupFiles` that would write somewhere wrong - before anything is written.
|
|
694
|
+
*
|
|
695
|
+
* Three ways, and each is silent otherwise:
|
|
696
|
+
*
|
|
697
|
+
* - **A key naming no group.** rman drops an unknown config key without a word, so a typo here would
|
|
698
|
+
* leave the group on the default rule and nothing would say so. Here the groups are known, so the
|
|
699
|
+
* key can be checked against them.
|
|
700
|
+
* - **A path leaving the repository.** `version --changelog` commits the file with the release; a
|
|
701
|
+
* file outside the working tree is one it cannot add.
|
|
702
|
+
* - **Two groups sharing one file**, including the default group and any package that is a group of
|
|
703
|
+
* itself. The `rman:documented-up-to` marker is one per file, so two groups writing into it would
|
|
704
|
+
* each read the other's marker as its own boundary - entries missing or written twice, with
|
|
705
|
+
* nothing failing. The default rule cannot produce a collision; an explicit map can, and an
|
|
706
|
+
* unlisted group's suffixed name can land on a listed one's path.
|
|
707
|
+
*/
|
|
708
|
+
function assertGroupFiles(repository, groupFiles, options) {
|
|
709
|
+
const all = groupTargets([repository.rootPackage, ...repository.packages], 'group');
|
|
710
|
+
const named = [...all.keys()].map(namedGroupOf).filter((name) => !!name);
|
|
711
|
+
for (const [group, file] of Object.entries(groupFiles)) {
|
|
712
|
+
if (!named.includes(group)) {
|
|
713
|
+
throw new Error(`.rmanrc "changelog.groupFiles" names a group "${group}" that no package belongs to. ` +
|
|
714
|
+
(named.length ? `Named groups here: ${named.join(', ')}.` : 'This repository has no named groups.'));
|
|
715
|
+
}
|
|
716
|
+
const rel = path.relative(repository.dirname, path.resolve(repository.dirname, file));
|
|
717
|
+
if (rel.startsWith('..') || path.isAbsolute(rel)) {
|
|
718
|
+
throw new Error(`.rmanrc "changelog.groupFiles.${group}" points outside the repository (${file}). ` +
|
|
719
|
+
`The file is committed with the release, so it has to be inside the working tree.`);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
const owner = new Map();
|
|
723
|
+
for (const [key, members] of all) {
|
|
724
|
+
const file = groupFile(repository, key, members[0], groupFiles, options);
|
|
725
|
+
const label = namedGroupOf(key) ?? (key === 'default' ? 'the default group' : members[0].name);
|
|
726
|
+
const other = owner.get(file);
|
|
727
|
+
if (other) {
|
|
728
|
+
throw new Error(`Two changelogs would be written to one file: ${other} and ${label} both resolve to ` +
|
|
729
|
+
`${path.relative(repository.dirname, file)}.\n` +
|
|
730
|
+
` Each file carries one "documented up to" marker, so two groups sharing it would read each ` +
|
|
731
|
+
`other's boundary. Give each its own path in .rmanrc "changelog.groupFiles".`);
|
|
732
|
+
}
|
|
733
|
+
owner.set(file, label);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
630
736
|
/** `CHANGELOG.md` + `core` -> `CHANGELOG-core.md`, keeping whatever directory and extension
|
|
631
737
|
* `changelog.filePath` chose (`docs/HISTORY.md` -> `docs/HISTORY-core.md`). */
|
|
632
738
|
function suffixFileName(filePath, suffix) {
|
package/utils/version-group.d.ts
CHANGED
|
@@ -16,6 +16,16 @@ import type { Package } from '../core/classes/package.js';
|
|
|
16
16
|
* batching; what it cannot do is leave the tag pattern behind, which is the point.
|
|
17
17
|
*/
|
|
18
18
|
export declare function groupKeyOf(pkg: Package): string;
|
|
19
|
+
/**
|
|
20
|
+
* Whether a group key belongs to a package that is in **no** group - `group: false`, a line of its
|
|
21
|
+
* own that nobody named.
|
|
22
|
+
*
|
|
23
|
+
* Here beside `groupKeyOf` so the key's shape is known in one place. The plan table asks it to decide
|
|
24
|
+
* whether the Group column is worth printing, and it has to ask *this* rather than count members: a
|
|
25
|
+
* group the repository named is a version line of its own whatever its size, while a package that
|
|
26
|
+
* opted out of grouping has no group to name.
|
|
27
|
+
*/
|
|
28
|
+
export declare function isSoloGroupKey(key: string): boolean;
|
|
19
29
|
/**
|
|
20
30
|
* How many distinct version lines this package's repository releases along - 1 when every package
|
|
21
31
|
* moves together, more once anything is grouped apart.
|
package/utils/version-group.js
CHANGED
|
@@ -22,6 +22,18 @@ export function groupKeyOf(pkg) {
|
|
|
22
22
|
return `named:${assertGroupName(g, pkg)}`;
|
|
23
23
|
return 'default';
|
|
24
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* Whether a group key belongs to a package that is in **no** group - `group: false`, a line of its
|
|
27
|
+
* own that nobody named.
|
|
28
|
+
*
|
|
29
|
+
* Here beside `groupKeyOf` so the key's shape is known in one place. The plan table asks it to decide
|
|
30
|
+
* whether the Group column is worth printing, and it has to ask *this* rather than count members: a
|
|
31
|
+
* group the repository named is a version line of its own whatever its size, while a package that
|
|
32
|
+
* opted out of grouping has no group to name.
|
|
33
|
+
*/
|
|
34
|
+
export function isSoloGroupKey(key) {
|
|
35
|
+
return key.startsWith('solo:');
|
|
36
|
+
}
|
|
25
37
|
/**
|
|
26
38
|
* How many distinct version lines this package's repository releases along - 1 when every package
|
|
27
39
|
* moves together, more once anything is grouped apart.
|