rman 2.12.0 → 2.13.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.
- package/builtins/platforms/node/services/publish.service.d.ts +4 -0
- package/builtins/platforms/node/services/publish.service.js +22 -0
- package/builtins/publish-targets/npm/npm-publish-target.d.ts +2 -0
- package/builtins/publish-targets/npm/npm-publish-target.js +4 -0
- package/commands/list.command.js +53 -4
- package/constants.js +1 -1
- package/core/interfaces/publish-target.d.ts +5 -0
- package/package.json +1 -1
- package/services/changelog.service.d.ts +15 -0
- package/services/changelog.service.js +66 -6
- package/services/list.service.d.ts +13 -0
- package/services/list.service.js +23 -1
- package/utils/git.d.ts +17 -0
- package/utils/git.js +84 -0
|
@@ -133,6 +133,10 @@ export declare namespace PublishService {
|
|
|
133
133
|
* `undefined` when it would be published. `rman list` asks this to grey a package out.
|
|
134
134
|
*/
|
|
135
135
|
function skipReason(pkg: Package, contentsOverride?: string): string | undefined;
|
|
136
|
+
/** The host of a package's `publishConfig.registry` - `npm.pkg.github.com` for GitHub Packages -
|
|
137
|
+
* or `undefined` where it names none, or names npm's own registry. What `rman list` shows in
|
|
138
|
+
* place of `npm`. */
|
|
139
|
+
function registryLabel(pkg: Package): string | undefined;
|
|
136
140
|
function getPlan(repository: Repository, options?: Options, deps?: Deps): Promise<Entry[]>;
|
|
137
141
|
/**
|
|
138
142
|
* Publishes every `'publish'` entry in `plan`, topological order (already `plan`'s own order -
|
|
@@ -173,6 +173,26 @@ export var PublishService;
|
|
|
173
173
|
return undefined;
|
|
174
174
|
}
|
|
175
175
|
PublishService.skipReason = skipReason;
|
|
176
|
+
/** The host of a package's `publishConfig.registry` - `npm.pkg.github.com` for GitHub Packages -
|
|
177
|
+
* or `undefined` where it names none, or names npm's own registry. What `rman list` shows in
|
|
178
|
+
* place of `npm`. */
|
|
179
|
+
/* **`publishConfig.registry` only**, which is one package's own statement. A scoped
|
|
180
|
+
* `@owner:registry=` in `.npmrc` also sends a package elsewhere, and is not read: npm resolves it
|
|
181
|
+
* itself (see `resolveRegistry`), so such a package still shows `npm`. A value that is not a URL
|
|
182
|
+
* shows `npm` too - `npm publish` is what will say it is wrong. */
|
|
183
|
+
function registryLabel(pkg) {
|
|
184
|
+
const registry = resolveRegistry(pkg, undefined);
|
|
185
|
+
if (!registry)
|
|
186
|
+
return undefined;
|
|
187
|
+
try {
|
|
188
|
+
const host = new URL(registry).hostname;
|
|
189
|
+
return host && host !== NPMJS_HOST ? host : undefined;
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
return undefined;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
PublishService.registryLabel = registryLabel;
|
|
176
196
|
async function getPlan(repository, options = {}, deps = {}) {
|
|
177
197
|
const git = new GitHelper({ cwd: repository.dirname });
|
|
178
198
|
const packages = filterPackages(repository.getPackages({ toposort: true }), options);
|
|
@@ -431,6 +451,8 @@ function resolveRegistry(pkg, override) {
|
|
|
431
451
|
const native = pkg.manifest.raw.publishConfig?.registry;
|
|
432
452
|
return override || (typeof native === 'string' && native ? native : undefined);
|
|
433
453
|
}
|
|
454
|
+
/** npm's own registry, which `PublishService.registryLabel` leaves shown as `npm`. */
|
|
455
|
+
const NPMJS_HOST = 'registry.npmjs.org';
|
|
434
456
|
/**
|
|
435
457
|
* The retired `publish.directory` spelling, if a config still carries it.
|
|
436
458
|
*
|
|
@@ -73,6 +73,8 @@ export declare class NpmPublishTarget implements PublishTarget {
|
|
|
73
73
|
};
|
|
74
74
|
/** npm's `private` rule, the same function `getPlan` asks. */
|
|
75
75
|
skipReason(pkg: Package): string | undefined;
|
|
76
|
+
/** The registry's host where `publishConfig.registry` names one other than npm's own. */
|
|
77
|
+
labelFor(pkg: Package): string | undefined;
|
|
76
78
|
getPlan(ctx: PublishTarget.Context): Promise<PublishService.Entry[]>;
|
|
77
79
|
applyPlan(ctx: PublishTarget.Context, plan: PublishTarget.Entry[]): Promise<PublishService.Entry[]>;
|
|
78
80
|
/** The shared filters plus the two flags both halves need - read here rather than in the
|
|
@@ -79,6 +79,10 @@ export class NpmPublishTarget {
|
|
|
79
79
|
skipReason(pkg) {
|
|
80
80
|
return PublishService.skipReason(pkg);
|
|
81
81
|
}
|
|
82
|
+
/** The registry's host where `publishConfig.registry` names one other than npm's own. */
|
|
83
|
+
labelFor(pkg) {
|
|
84
|
+
return PublishService.registryLabel(pkg);
|
|
85
|
+
}
|
|
82
86
|
getPlan(ctx) {
|
|
83
87
|
return PublishService.getPlan(ctx.repository, this.planOptions(ctx));
|
|
84
88
|
}
|
package/commands/list.command.js
CHANGED
|
@@ -2,6 +2,7 @@ import colors from 'ansi-colors';
|
|
|
2
2
|
import EasyTable from 'easy-table';
|
|
3
3
|
import { registerCommand } from '../interfaces/rman-config.interface.js';
|
|
4
4
|
import { packageFilterOptions, readPackageFilterOptions } from '../utils/package-filter.js';
|
|
5
|
+
import { isNamedGroupKey, isSoloGroupKey, ROOT_GROUP_KEY } from '../utils/version-group.js';
|
|
5
6
|
/** `[options...]` is yargs-meaningless - its variadic marker is two dots, and this command declares
|
|
6
7
|
* no positional for it. Left as it is rather than fixed in passing: dropping it makes
|
|
7
8
|
* `rman list foo` start failing, which is a behaviour change and belongs in its own commit. */
|
|
@@ -82,8 +83,9 @@ const listCommand = registerCommand(app => {
|
|
|
82
83
|
* so counting rows that are not the root reported `0 Package(s) found` there (measured). The
|
|
83
84
|
* condition is the same one the service applies, and it is one line.
|
|
84
85
|
*/
|
|
86
|
+
/** Grouped, except where the order *is* the answer: `--toposort` is dependency order. */
|
|
85
87
|
else
|
|
86
|
-
printTable(items, table && app.repository.monorepo);
|
|
88
|
+
printTable(args.toposort ? items : byGroup(items), table && app.repository.monorepo);
|
|
87
89
|
},
|
|
88
90
|
};
|
|
89
91
|
});
|
|
@@ -100,10 +102,54 @@ function statusLabel(status) {
|
|
|
100
102
|
return '';
|
|
101
103
|
}
|
|
102
104
|
}
|
|
103
|
-
/** Where a package ships, one target
|
|
104
|
-
*
|
|
105
|
+
/** Where a package ships, one target each - its label where it has one (`Item.targetLabels`, npm's
|
|
106
|
+
* registry host). A target `publish` would skip it for (`Item.skippedTargets`) is left out, and a
|
|
107
|
+
* package left with none reads `-`, so the column never reads as a promise the release will not
|
|
108
|
+
* keep. Blank where no target claims the package at all - a monorepo's root, for one. */
|
|
105
109
|
function publishLabel(it) {
|
|
106
|
-
|
|
110
|
+
if (!it.publishTargets.length)
|
|
111
|
+
return '';
|
|
112
|
+
const shipping = it.publishTargets.filter(name => !it.skippedTargets[name]);
|
|
113
|
+
if (!shipping.length)
|
|
114
|
+
return colors.gray('-');
|
|
115
|
+
return shipping.map(name => colors.green(it.targetLabels[name] ?? name)).join(', ');
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* The rows with each release group's members together - the root first, then the groups in the
|
|
119
|
+
* order their first member appears, then the packages in no group (`group: false`) - the order
|
|
120
|
+
* `version`'s plan prints them in. Stable within a group, so the lexical order holds there.
|
|
121
|
+
*/
|
|
122
|
+
/* The cost, stated: a package nested inside another can land away from it, when the two are in
|
|
123
|
+
* different groups - its indentation still says how deep it sits, not under which row. */
|
|
124
|
+
function byGroup(items) {
|
|
125
|
+
const root = items.filter(it => it.groupKey === ROOT_GROUP_KEY);
|
|
126
|
+
const groups = new Map();
|
|
127
|
+
const solo = [];
|
|
128
|
+
for (const it of items) {
|
|
129
|
+
if (it.groupKey === ROOT_GROUP_KEY)
|
|
130
|
+
continue;
|
|
131
|
+
if (isSoloGroupKey(it.groupKey))
|
|
132
|
+
solo.push(it);
|
|
133
|
+
else
|
|
134
|
+
groups.set(it.groupKey, [...(groups.get(it.groupKey) ?? []), it]);
|
|
135
|
+
}
|
|
136
|
+
return [...root, ...[...groups.values()].flat(), ...solo];
|
|
137
|
+
}
|
|
138
|
+
/** Whether the Group column says anything: a group the repository named, or the default group
|
|
139
|
+
* beside packages in none. One line for everything, or no grouping at all, would put the same
|
|
140
|
+
* answer - or nothing - on every row. */
|
|
141
|
+
function showsGroups(items) {
|
|
142
|
+
const keys = items.map(it => it.groupKey).filter(key => key !== ROOT_GROUP_KEY);
|
|
143
|
+
return keys.some(isNamedGroupKey) || (keys.includes('default') && keys.some(isSoloGroupKey));
|
|
144
|
+
}
|
|
145
|
+
/** A named group by its name, the default one as a grey `(default)` - `version`'s plan table reads
|
|
146
|
+
* the same way. Blank for the root and for a package in no group. */
|
|
147
|
+
function groupCell(it) {
|
|
148
|
+
if (isNamedGroupKey(it.groupKey))
|
|
149
|
+
return colors.cyan(it.group);
|
|
150
|
+
if (it.groupKey === 'default')
|
|
151
|
+
return colors.gray('(default)');
|
|
152
|
+
return '';
|
|
107
153
|
}
|
|
108
154
|
/**
|
|
109
155
|
* **The tree, as a table**: the root first, then each package indented by how far below it sits.
|
|
@@ -125,10 +171,13 @@ function publishLabel(it) {
|
|
|
125
171
|
*/
|
|
126
172
|
function printTable(items, withRoot) {
|
|
127
173
|
const table = new EasyTable();
|
|
174
|
+
const withGroup = showsGroups(items);
|
|
128
175
|
for (const it of items) {
|
|
129
176
|
const indent = ' '.repeat(it.depth);
|
|
130
177
|
table.cell('Package', indent + (it.isRoot ? colors.whiteBright(it.selector) : colors.yellowBright(it.selector)));
|
|
131
178
|
table.cell('Version', colors.yellow(it.version));
|
|
179
|
+
if (withGroup)
|
|
180
|
+
table.cell('Group', groupCell(it));
|
|
132
181
|
table.cell('Platform', it.platform ? colors.cyan(it.platform) : '');
|
|
133
182
|
table.cell('Private', it.private ? colors.magentaBright('yes') : '');
|
|
134
183
|
table.cell('Publish', publishLabel(it));
|
package/constants.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const version = '2.
|
|
1
|
+
export const version = '2.13.0';
|
|
@@ -69,6 +69,11 @@ export interface PublishTarget {
|
|
|
69
69
|
* only what is its own, such as npm's `private`.
|
|
70
70
|
*/
|
|
71
71
|
skipReason?(pkg: Package): string | undefined;
|
|
72
|
+
/**
|
|
73
|
+
* The name `rman list` shows for this package when it says more than `name` does - npm's is the
|
|
74
|
+
* registry's host, `npm.pkg.github.com` for GitHub Packages. `undefined` shows `name`.
|
|
75
|
+
*/
|
|
76
|
+
labelFor?(pkg: Package): string | undefined;
|
|
72
77
|
/** What this target *would* do - never publishes. Called even under `--dry-run`, which is the
|
|
73
78
|
* whole point of the split. */
|
|
74
79
|
getPlan(ctx: PublishTarget.Context): Promise<PublishTarget.Entry[]>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Package } from '../core/classes/package.js';
|
|
2
2
|
import type { Repository } from '../core/classes/repository.js';
|
|
3
3
|
import { Service } from '../core/classes/service.js';
|
|
4
|
+
import { GitHelper } from '../utils/git.js';
|
|
4
5
|
import { type PackageFilterOptions } from '../utils/package-filter.js';
|
|
5
6
|
/**
|
|
6
7
|
* A service class - see `ListService` for the shape and `Service` for the three measured
|
|
@@ -55,6 +56,12 @@ export declare class ChangelogService extends Service {
|
|
|
55
56
|
* `options.fromRoot` says otherwise (see `Repository.currentPackage`).
|
|
56
57
|
*/
|
|
57
58
|
getEntries(options?: ChangelogService.Options): Promise<ChangelogService.Entry[]>;
|
|
59
|
+
/**
|
|
60
|
+
* Where each member package's directory was over time, newest first - `since` is the commit that
|
|
61
|
+
* moved it there, absent for the oldest. A package that never moved has one entry, its own
|
|
62
|
+
* directory. Read from the moves of the package's manifest (`GitHelper.moveHistory`).
|
|
63
|
+
*/
|
|
64
|
+
protected packageHomes(git: GitHelper): Promise<PackageHomes>;
|
|
58
65
|
}
|
|
59
66
|
/**
|
|
60
67
|
* What one changelog file is about - `.rmanrc changelog.groupBy`, `'package'` when nothing says.
|
|
@@ -68,6 +75,13 @@ export declare class ChangelogService extends Service {
|
|
|
68
75
|
* per package - the same entry prepended once per member otherwise.
|
|
69
76
|
*/
|
|
70
77
|
export declare function changelogGroupBy(repository: Repository, options?: ChangelogService.Options): 'package' | 'group';
|
|
78
|
+
/** Where a package's directory was over time, newest first - see `ChangelogService.packageHomes`. */
|
|
79
|
+
type PackageHomes = Map<Package, Home[]>;
|
|
80
|
+
interface Home {
|
|
81
|
+
dir: string;
|
|
82
|
+
/** The commit that moved the package here; absent for its first directory. */
|
|
83
|
+
since?: string;
|
|
84
|
+
}
|
|
71
85
|
export declare namespace ChangelogService {
|
|
72
86
|
interface Options extends PackageFilterOptions {
|
|
73
87
|
/** Generate the changelog since this commit/hash - applied the same way to every package.
|
|
@@ -212,3 +226,4 @@ declare module '../core/classes/service.js' {
|
|
|
212
226
|
changelog: ChangelogService;
|
|
213
227
|
}
|
|
214
228
|
}
|
|
229
|
+
export {};
|
|
@@ -154,6 +154,9 @@ export class ChangelogService extends Service {
|
|
|
154
154
|
progress?.step(group.label, 'commits');
|
|
155
155
|
return listCommitsCached(from, group.label);
|
|
156
156
|
}));
|
|
157
|
+
/** Where each package's directory was over time - so a commit made before a package moved is
|
|
158
|
+
* still its own. See `packageHomes`. */
|
|
159
|
+
const homes = await this.packageHomes(git);
|
|
157
160
|
const entries = [];
|
|
158
161
|
for (let i = 0; i < groups.length; i++) {
|
|
159
162
|
const group = groups[i];
|
|
@@ -177,6 +180,7 @@ export class ChangelogService extends Service {
|
|
|
177
180
|
* falls inside the range and there is exactly one segment - which is what `version
|
|
178
181
|
* --changelog` and `github-release` see too, since both pass an explicit boundary.
|
|
179
182
|
*/
|
|
183
|
+
const dirsAt = homesAt(homes, commitsByTarget[i]);
|
|
180
184
|
const floor = await resolveStartingPoint(git, pkg, options);
|
|
181
185
|
const floorIndex = floor?.sha ? commitsByTarget[i].findIndex(c => c.sha === floor.sha) : -1;
|
|
182
186
|
const withUnreleased = resolveUnreleased(pkg, options);
|
|
@@ -189,7 +193,7 @@ export class ChangelogService extends Service {
|
|
|
189
193
|
continue;
|
|
190
194
|
/** A commit belongs to a group when it belongs to **any** of its members - one line for a
|
|
191
195
|
* commit touching two of them, which is the whole reason a group shares a file. */
|
|
192
|
-
const ownCommits = dropVersionBumps(segment.commits).filter(c => [...ownersOf(repository, c)].some(owner => group.members.has(owner)));
|
|
196
|
+
const ownCommits = dropVersionBumps(segment.commits).filter(c => [...ownersOf(repository, c, dirsAt(c))].some(owner => group.members.has(owner)));
|
|
193
197
|
if (!ownCommits.length)
|
|
194
198
|
continue;
|
|
195
199
|
const sections = groupCommits(ownCommits, ignoreTypesConfig(pkg), titles, order, withCommitHash(pkg, options));
|
|
@@ -219,6 +223,33 @@ export class ChangelogService extends Service {
|
|
|
219
223
|
}
|
|
220
224
|
return entries;
|
|
221
225
|
}
|
|
226
|
+
/**
|
|
227
|
+
* Where each member package's directory was over time, newest first - `since` is the commit that
|
|
228
|
+
* moved it there, absent for the oldest. A package that never moved has one entry, its own
|
|
229
|
+
* directory. Read from the moves of the package's manifest (`GitHelper.moveHistory`).
|
|
230
|
+
*/
|
|
231
|
+
/* **Why a commit cannot simply be matched against today's directories**: measured on
|
|
232
|
+
* `panates/syncbridge`, whose last commit moved every package under `packages/<scope>/`. Every
|
|
233
|
+
* earlier commit touched the old paths, which no package sits at any more, so all of them were
|
|
234
|
+
* attributed to the root - `changelog --write` under `groupBy: package` wrote one root file and
|
|
235
|
+
* nothing for any package, while git still held 259 commits of hl7's and 283 of iomt's.
|
|
236
|
+
*
|
|
237
|
+
* **The manifest, not the files**: one file per package says where its directory was, so this is
|
|
238
|
+
* one `git log --follow` per package, run at once - 40ms each there, 120ms on `panates/sqb`. A
|
|
239
|
+
* single-package repository has nothing to follow: its package is the root, which does not move. */
|
|
240
|
+
async packageHomes(git) {
|
|
241
|
+
const homes = new Map();
|
|
242
|
+
const packages = this.repository.monorepo ? this.repository.packages : [];
|
|
243
|
+
await Promise.all(packages.map(async (pkg) => {
|
|
244
|
+
if (!pkg.manifestFileName)
|
|
245
|
+
return;
|
|
246
|
+
const moves = await git.moveHistory(pkg.manifestFileName);
|
|
247
|
+
const periods = [{ dir: pkg.dirname, since: moves[0]?.sha }];
|
|
248
|
+
moves.forEach((move, k) => periods.push({ dir: path.dirname(move.from), since: moves[k + 1]?.sha }));
|
|
249
|
+
homes.set(pkg, periods);
|
|
250
|
+
}));
|
|
251
|
+
return homes;
|
|
252
|
+
}
|
|
222
253
|
}
|
|
223
254
|
/**
|
|
224
255
|
* What one changelog file is about - `.rmanrc changelog.groupBy`, `'package'` when nothing says.
|
|
@@ -856,16 +887,45 @@ async function splitByRelease(git, pkg, commits) {
|
|
|
856
887
|
* fallback for anything outside every package (e.g. root-level config files). Mirrors
|
|
857
888
|
* `Repository.currentPackage`'s longest-prefix logic, but always resolves to *something*
|
|
858
889
|
* (root), rather than `undefined`, since every file belongs to some changelog. */
|
|
859
|
-
function owningPackage(repository, file) {
|
|
890
|
+
function owningPackage(repository, file, dirOf) {
|
|
860
891
|
let best = repository.rootPackage;
|
|
892
|
+
let bestDir = repository.rootPackage.dirname;
|
|
861
893
|
for (const pkg of repository.packages) {
|
|
862
|
-
const
|
|
894
|
+
const dir = dirOf(pkg);
|
|
895
|
+
const rel = path.relative(dir, file);
|
|
863
896
|
const isSelfOrDescendant = rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
|
|
864
|
-
if (isSelfOrDescendant &&
|
|
897
|
+
if (isSelfOrDescendant && dir.length > bestDir.length) {
|
|
865
898
|
best = pkg;
|
|
899
|
+
bestDir = dir;
|
|
900
|
+
}
|
|
866
901
|
}
|
|
867
902
|
return best;
|
|
868
903
|
}
|
|
904
|
+
/**
|
|
905
|
+
* For one fetched commit list, the function that answers where each package's directory was when a
|
|
906
|
+
* given commit was made.
|
|
907
|
+
*/
|
|
908
|
+
/* **Ordered by position in the list, not by asking git**: the list is oldest first and runs to HEAD,
|
|
909
|
+
* so a move inside it is compared by index, and a move *not* in it happened at or before the range's
|
|
910
|
+
* boundary - every commit in the range is after it. One `merge-base --is-ancestor` per commit and
|
|
911
|
+
* package would be the exact answer and costs a process each; this costs a Map. */
|
|
912
|
+
function homesAt(homes, commits) {
|
|
913
|
+
const index = new Map(commits.map((c, i) => [c.sha, i]));
|
|
914
|
+
return commit => pkg => {
|
|
915
|
+
const periods = homes.get(pkg);
|
|
916
|
+
if (!periods)
|
|
917
|
+
return pkg.dirname;
|
|
918
|
+
const at = index.get(commit.sha) ?? commits.length;
|
|
919
|
+
for (const period of periods) {
|
|
920
|
+
if (!period.since)
|
|
921
|
+
return period.dir;
|
|
922
|
+
const movedAt = index.get(period.since);
|
|
923
|
+
if (movedAt === undefined || movedAt <= at)
|
|
924
|
+
return period.dir;
|
|
925
|
+
}
|
|
926
|
+
return periods[periods.length - 1].dir;
|
|
927
|
+
};
|
|
928
|
+
}
|
|
869
929
|
/** A commit touching more than this fraction of all packages (a repo-wide relicense, a doc
|
|
870
930
|
* update stamped into every package's README, ...) is treated as a repo-wide change rather than
|
|
871
931
|
* attributed to each of them - see `ownersOf`. Without this, one such commit would show up
|
|
@@ -883,10 +943,10 @@ const BROAD_COMMIT_MIN_PACKAGES = 3;
|
|
|
883
943
|
* doc pass across every package, ...), not something that belongs in each package's own release
|
|
884
944
|
* notes individually - it's attributed to the root alone.
|
|
885
945
|
*/
|
|
886
|
-
function ownersOf(repository, commit) {
|
|
946
|
+
function ownersOf(repository, commit, dirOf) {
|
|
887
947
|
const owners = new Set();
|
|
888
948
|
for (const f of commit.files)
|
|
889
|
-
owners.add(owningPackage(repository, f));
|
|
949
|
+
owners.add(owningPackage(repository, f, dirOf));
|
|
890
950
|
const totalPackages = repository.packages.length;
|
|
891
951
|
const nonRootOwners = [...owners].filter(p => p !== repository.rootPackage).length;
|
|
892
952
|
if (totalPackages > 0 &&
|
|
@@ -63,6 +63,15 @@ export declare namespace ListService {
|
|
|
63
63
|
* `--toposort` - which reorders the rows and leaves the nesting where it is.
|
|
64
64
|
*/
|
|
65
65
|
depth: number;
|
|
66
|
+
/**
|
|
67
|
+
* Which release group the package versions with - `.rmanrc group`, the key `version` batches its
|
|
68
|
+
* plan by: `default` for the implicit repo-wide group, `named:<name>` for a named one,
|
|
69
|
+
* `solo:<package>` for `group: false`, and `__root__` for a monorepo's root, which belongs to
|
|
70
|
+
* none. The same spelling `version --json` reports.
|
|
71
|
+
*/
|
|
72
|
+
groupKey: string;
|
|
73
|
+
/** `groupKey`'s name - the group's own name, `default`, the package's for a solo one, `root`. */
|
|
74
|
+
group: string;
|
|
66
75
|
/** Whether this row *is* the root package - present only when `includeRoot` asked for it, or in
|
|
67
76
|
* a single-package repository where the root is the one member. */
|
|
68
77
|
isRoot: boolean;
|
|
@@ -81,6 +90,10 @@ export declare namespace ListService {
|
|
|
81
90
|
* registry, so `{}` means "a candidate", not "will publish": whether the version is already out
|
|
82
91
|
* there is `publish --dry-run`'s answer. */
|
|
83
92
|
skippedTargets: Record<PublishTargetName, string>;
|
|
93
|
+
/** What the table shows in place of a target's name, where the target says more for this
|
|
94
|
+
* package - npm's registry host when `publishConfig.registry` names one other than npm's own
|
|
95
|
+
* (`{ npm: 'npm.pkg.github.com' }`). A label only: `publishTargets` and `--target` keep the name. */
|
|
96
|
+
targetLabels: Record<PublishTargetName, string>;
|
|
84
97
|
/** Present only when `"docker"` is one of `publishTargets` and `publish.docker` is configured -
|
|
85
98
|
* the raw `.rmanrc` config, unresolved (no namespace prefixing - see `DockerPublishService`). */
|
|
86
99
|
docker?: DockerPublishOptions;
|
package/services/list.service.js
CHANGED
|
@@ -2,6 +2,7 @@ import path from 'path';
|
|
|
2
2
|
import { Service } from '../core/classes/service.js';
|
|
3
3
|
import { skipReasonFor, targetsOf } from '../core/interfaces/publish-target.js';
|
|
4
4
|
import { filterPackages } from '../utils/package-filter.js';
|
|
5
|
+
import { groupKeyOf, isNamedGroupKey, isSoloGroupKey, ROOT_GROUP_KEY } from '../utils/version-group.js';
|
|
5
6
|
/**
|
|
6
7
|
* **A class, and the shape every service follows now.**
|
|
7
8
|
*
|
|
@@ -36,13 +37,21 @@ export class ListService extends Service {
|
|
|
36
37
|
/** **A monorepo's root ships nowhere**: `publish` never makes it a candidate
|
|
37
38
|
* (`repository.getPackages()` leaves it out), so listing the target a root *would* claim
|
|
38
39
|
* says something no command does. */
|
|
39
|
-
const
|
|
40
|
+
const monorepoRoot = p === repository.rootPackage && repository.monorepo;
|
|
41
|
+
const targets = monorepoRoot ? [] : targetsOf(this.app, p);
|
|
42
|
+
/** The root's own key, as in `version`'s plan: a monorepo's root is never a group member - its
|
|
43
|
+
* number is the repository's identity. */
|
|
44
|
+
const groupKey = monorepoRoot ? ROOT_GROUP_KEY : groupKeyOf(p);
|
|
40
45
|
const publishTargets = targets.map(t => t.name);
|
|
41
46
|
const skippedTargets = {};
|
|
47
|
+
const targetLabels = {};
|
|
42
48
|
for (const target of targets) {
|
|
43
49
|
const reason = skipReasonFor(p, target);
|
|
44
50
|
if (reason)
|
|
45
51
|
skippedTargets[target.name] = reason;
|
|
52
|
+
const label = target.labelFor?.(p);
|
|
53
|
+
if (label && label !== target.name)
|
|
54
|
+
targetLabels[target.name] = label;
|
|
46
55
|
}
|
|
47
56
|
return {
|
|
48
57
|
name: p.name,
|
|
@@ -50,6 +59,8 @@ export class ListService extends Service {
|
|
|
50
59
|
version: p.version,
|
|
51
60
|
platform: p.provider,
|
|
52
61
|
depth: depthOf(p),
|
|
62
|
+
groupKey,
|
|
63
|
+
group: groupNameOf(groupKey),
|
|
53
64
|
isRoot: p === repository.rootPackage,
|
|
54
65
|
location: path.relative(repository.dirname, p.dirname) || '.',
|
|
55
66
|
private: p.isPrivate,
|
|
@@ -57,6 +68,7 @@ export class ListService extends Service {
|
|
|
57
68
|
dependencies: p.dependencies.map(d => d.name),
|
|
58
69
|
publishTargets: [...publishTargets],
|
|
59
70
|
skippedTargets,
|
|
71
|
+
targetLabels,
|
|
60
72
|
docker: publishTargets.includes('docker') ? p.config.publish?.docker : undefined,
|
|
61
73
|
};
|
|
62
74
|
};
|
|
@@ -90,3 +102,13 @@ function depthOf(pkg) {
|
|
|
90
102
|
depth++;
|
|
91
103
|
return depth;
|
|
92
104
|
}
|
|
105
|
+
/** The name behind a group key - the same reading `VersionPlanService.groupLabel` makes. */
|
|
106
|
+
function groupNameOf(key) {
|
|
107
|
+
if (key === ROOT_GROUP_KEY)
|
|
108
|
+
return 'root';
|
|
109
|
+
if (isNamedGroupKey(key))
|
|
110
|
+
return key.slice('named:'.length);
|
|
111
|
+
if (isSoloGroupKey(key))
|
|
112
|
+
return key.slice('solo:'.length);
|
|
113
|
+
return key;
|
|
114
|
+
}
|
package/utils/git.d.ts
CHANGED
|
@@ -12,6 +12,8 @@ export interface CommitInfo {
|
|
|
12
12
|
}
|
|
13
13
|
export declare class GitHelper {
|
|
14
14
|
cwd: string;
|
|
15
|
+
/** `movedShare`'s per-commit rename lists. */
|
|
16
|
+
private _renames;
|
|
15
17
|
constructor(options?: GitOptions);
|
|
16
18
|
/** The current branch name, or `undefined` in detached HEAD state (or outside a git repository) -
|
|
17
19
|
* what `branch-guard.ts` checks `allowBranch`/`ignoreBranch` against. */
|
|
@@ -67,6 +69,21 @@ export declare class GitHelper {
|
|
|
67
69
|
* never been committed. Answers "since when has this file last been updated", independent of
|
|
68
70
|
* any tag/version scheme - e.g. to check whether a changelog file has fallen behind. */
|
|
69
71
|
lastCommitTouching(file: string): Promise<string | undefined>;
|
|
72
|
+
/**
|
|
73
|
+
* Every time `file` was **moved together with its directory**, newest first - `{ sha, from, to }`,
|
|
74
|
+
* absolute paths of the file, and the commit that moved it. Empty for a file that never moved, or
|
|
75
|
+
* one git has no history of. Meant for a package manifest: where it went, its package went.
|
|
76
|
+
*/
|
|
77
|
+
moveHistory(file: string): Promise<{
|
|
78
|
+
sha: string;
|
|
79
|
+
from: string;
|
|
80
|
+
to: string;
|
|
81
|
+
}[]>;
|
|
82
|
+
/**
|
|
83
|
+
* Of the files `fromDir` held just before `sha`, the share `sha` renamed into `toDir` - `0` to `1`.
|
|
84
|
+
* Both directories relative to `cwd`, in git's `/` form.
|
|
85
|
+
*/
|
|
86
|
+
movedShare(sha: string, fromDir: string, toDir: string): Promise<number>;
|
|
70
87
|
/** The best common ancestor of `a` and `b` - `undefined` if none exists (unrelated histories,
|
|
71
88
|
* an invalid ref, ...). */
|
|
72
89
|
mergeBase(a: string, b: string): Promise<string | undefined>;
|
package/utils/git.js
CHANGED
|
@@ -3,6 +3,8 @@ import path from 'node:path';
|
|
|
3
3
|
import { promisify } from 'node:util';
|
|
4
4
|
export class GitHelper {
|
|
5
5
|
cwd;
|
|
6
|
+
/** `movedShare`'s per-commit rename lists. */
|
|
7
|
+
_renames = new Map();
|
|
6
8
|
constructor(options) {
|
|
7
9
|
this.cwd = options?.cwd || process.cwd();
|
|
8
10
|
}
|
|
@@ -203,6 +205,88 @@ export class GitHelper {
|
|
|
203
205
|
return undefined;
|
|
204
206
|
}
|
|
205
207
|
}
|
|
208
|
+
/**
|
|
209
|
+
* Every time `file` was **moved together with its directory**, newest first - `{ sha, from, to }`,
|
|
210
|
+
* absolute paths of the file, and the commit that moved it. Empty for a file that never moved, or
|
|
211
|
+
* one git has no history of. Meant for a package manifest: where it went, its package went.
|
|
212
|
+
*/
|
|
213
|
+
/* **Renames only, and the walk stops at the first copy.** `git log --follow` also crosses a *copy*
|
|
214
|
+
* (`C`), and for a manifest that is the dangerous case: package manifests resemble one another, so a
|
|
215
|
+
* new package created from an old one's `package.json` is followed into that package's history.
|
|
216
|
+
* Measured on `panates/syncbridge`: `@syncbridge/hl7`'s manifest came back as `C054` from
|
|
217
|
+
* `packages/common/package.json` - a different package, `@panates-prv/syncbridge-common`, which was
|
|
218
|
+
* not even deleted in that commit - and following it would have handed hl7 137 commits of another
|
|
219
|
+
* package's history. A rename (`R`) means the old file is gone, which a copy does not.
|
|
220
|
+
*
|
|
221
|
+
* **And a rename counts only where the directory went too** - more than half of the files the old
|
|
222
|
+
* directory held landing in the new one (`movedShare`). Two manifests are similar enough to pair
|
|
223
|
+
* when a directory is *split*: in the same repository `packages/builtins` was broken into ten
|
|
224
|
+
* packages in one commit, git paired its `package.json` with `packages-ext/serialport`'s at
|
|
225
|
+
* `R052`, and serialport had received 3 of its 43 files. The old directory belongs to no one of
|
|
226
|
+
* them, so the walk stops there and its history stays the root's - unattributed rather than
|
|
227
|
+
* guessed. A real move clears this easily: `iomt-connectors` took 101 of its 110 files. */
|
|
228
|
+
async moveHistory(file) {
|
|
229
|
+
let stdout;
|
|
230
|
+
try {
|
|
231
|
+
({ stdout } = await execFileAsync('git', ['log', '--follow', '--format=%x00%H', '--name-status', '--', path.relative(this.cwd, file)], { cwd: this.cwd, maxBuffer: 64 * 1024 * 1024 }));
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
234
|
+
return [];
|
|
235
|
+
}
|
|
236
|
+
const moves = [];
|
|
237
|
+
for (const entry of stdout.split('\0').filter(Boolean)) {
|
|
238
|
+
const [sha, ...lines] = entry.split(/\r?\n/).filter(Boolean);
|
|
239
|
+
const [status = '', from, to] = (lines[0] ?? '').split('\t');
|
|
240
|
+
if (status.startsWith('R') && from && to) {
|
|
241
|
+
const fromDir = path.posix.dirname(from);
|
|
242
|
+
const toDir = path.posix.dirname(to);
|
|
243
|
+
/** Renamed within its own directory: the package did not move. */
|
|
244
|
+
if (fromDir === toDir)
|
|
245
|
+
continue;
|
|
246
|
+
if ((await this.movedShare(sha.trim(), fromDir, toDir)) <= 0.5)
|
|
247
|
+
break;
|
|
248
|
+
moves.push({ sha: sha.trim(), from: path.join(this.cwd, from), to: path.join(this.cwd, to) });
|
|
249
|
+
}
|
|
250
|
+
else if (status.startsWith('C') || status === 'A')
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
253
|
+
return moves;
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Of the files `fromDir` held just before `sha`, the share `sha` renamed into `toDir` - `0` to `1`.
|
|
257
|
+
* Both directories relative to `cwd`, in git's `/` form.
|
|
258
|
+
*/
|
|
259
|
+
/* The rename list is cached per commit: a commit that moves every package is asked about once per
|
|
260
|
+
* package, and it is one `git show` however many ask. */
|
|
261
|
+
async movedShare(sha, fromDir, toDir) {
|
|
262
|
+
let renames = this._renames.get(sha);
|
|
263
|
+
if (!renames) {
|
|
264
|
+
renames = execFileAsync('git', ['show', '-M', '--name-status', '--format=', sha], {
|
|
265
|
+
cwd: this.cwd,
|
|
266
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
267
|
+
}).then(({ stdout }) => stdout
|
|
268
|
+
.split(/\r?\n/)
|
|
269
|
+
.map(line => line.split('\t'))
|
|
270
|
+
.filter(([status, from, to]) => status?.startsWith('R') && from && to)
|
|
271
|
+
.map(([, from, to]) => ({ from, to })), () => []);
|
|
272
|
+
this._renames.set(sha, renames);
|
|
273
|
+
}
|
|
274
|
+
let held;
|
|
275
|
+
try {
|
|
276
|
+
const { stdout } = await execFileAsync('git', ['ls-tree', '-r', '--name-only', `${sha}^`, '--', `${fromDir}/`], {
|
|
277
|
+
cwd: this.cwd,
|
|
278
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
279
|
+
});
|
|
280
|
+
held = stdout.split(/\r?\n/).filter(Boolean);
|
|
281
|
+
}
|
|
282
|
+
catch {
|
|
283
|
+
return 0;
|
|
284
|
+
}
|
|
285
|
+
if (!held.length)
|
|
286
|
+
return 0;
|
|
287
|
+
const into = (await renames).filter(r => r.from.startsWith(`${fromDir}/`) && r.to.startsWith(`${toDir}/`)).length;
|
|
288
|
+
return into / held.length;
|
|
289
|
+
}
|
|
206
290
|
/** The best common ancestor of `a` and `b` - `undefined` if none exists (unrelated histories,
|
|
207
291
|
* an invalid ref, ...). */
|
|
208
292
|
async mergeBase(a, b) {
|