rman 2.12.0 → 2.14.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/node-dependency-updater.d.ts +116 -0
- package/builtins/platforms/node/node-dependency-updater.js +452 -0
- package/builtins/platforms/node/node.platform.d.ts +2 -0
- package/builtins/platforms/node/node.platform.js +2 -0
- 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/builtins/publish-targets/npm/npm-view.d.ts +31 -0
- package/builtins/publish-targets/npm/npm-view.js +47 -0
- package/commands/deps.command.d.ts +175 -0
- package/commands/deps.command.js +252 -0
- package/commands/list.command.js +53 -4
- package/commands.d.ts +2 -0
- package/commands.js +1 -0
- package/constants.js +1 -1
- package/core/interfaces/dependency-updater.d.ts +114 -0
- package/core/interfaces/dependency-updater.js +61 -0
- package/core/interfaces/plugin.d.ts +4 -0
- package/core/interfaces/publish-target.d.ts +5 -0
- package/index.d.ts +2 -0
- package/index.js +2 -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
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import type { RmanApplication } from '../application.js';
|
|
2
|
+
import type { Package } from '../classes/package.js';
|
|
3
|
+
import type { Repository } from '../classes/repository.js';
|
|
4
|
+
/**
|
|
5
|
+
* **How one technology answers "are this package's dependencies current, and what may they move
|
|
6
|
+
* to"** - the half of `rman deps` that is an ecosystem's.
|
|
7
|
+
*
|
|
8
|
+
* The command owns the repository's half: which packages are asked, printing the plan, `--json`,
|
|
9
|
+
* writing only under `--upgrade`, and undoing the writes when `verify` refuses them.
|
|
10
|
+
*/
|
|
11
|
+
export interface DependencyUpdater {
|
|
12
|
+
/** What each dependency of `packages` would move to - never writes. */
|
|
13
|
+
getPlan(ctx: DependencyUpdater.Context, packages: readonly Package[]): Promise<DependencyUpdater.Entry[]>;
|
|
14
|
+
/** Writes every `'update'` entry of `plan` to its manifest, and returns how to take that back. */
|
|
15
|
+
applyPlan(ctx: DependencyUpdater.Context, plan: readonly DependencyUpdater.Entry[]): Promise<DependencyUpdater.Applied>;
|
|
16
|
+
/**
|
|
17
|
+
* Asks the ecosystem's own resolver whether the manifests as they now stand can be installed,
|
|
18
|
+
* once `applyPlan` has written them. Returns why not, or `undefined` when they can.
|
|
19
|
+
*
|
|
20
|
+
* Omit it when the ecosystem has no resolver to ask; the plan's own checks then stand alone.
|
|
21
|
+
*/
|
|
22
|
+
verify?(ctx: DependencyUpdater.Context, packages: readonly Package[]): Promise<string | undefined>;
|
|
23
|
+
}
|
|
24
|
+
export declare namespace DependencyUpdater {
|
|
25
|
+
/** What one call is handed. */
|
|
26
|
+
interface Context {
|
|
27
|
+
readonly app: RmanApplication;
|
|
28
|
+
readonly repository: Repository;
|
|
29
|
+
readonly options: Options;
|
|
30
|
+
}
|
|
31
|
+
/** The run's own settings, read off the command line. A package's `.rmanrc "deps"` block supplies
|
|
32
|
+
* the rest - see `settingsFor`. */
|
|
33
|
+
interface Options {
|
|
34
|
+
/** Only the dependencies whose names match one of these globs. */
|
|
35
|
+
names?: string[];
|
|
36
|
+
/** Overrides every package's `deps.target`. */
|
|
37
|
+
target?: string;
|
|
38
|
+
/** Added to every package's `deps.reject`. */
|
|
39
|
+
reject?: string[];
|
|
40
|
+
/** Overrides every package's `deps.minAge`. */
|
|
41
|
+
minAge?: number;
|
|
42
|
+
/** How many registry lookups run at once. */
|
|
43
|
+
concurrency: number;
|
|
44
|
+
/** Reported as each dependency name's registry lookup finishes. */
|
|
45
|
+
onProgress?(done: number, total: number): void;
|
|
46
|
+
}
|
|
47
|
+
/** One package's settings, once the command line has been laid over its config. */
|
|
48
|
+
interface Settings {
|
|
49
|
+
/** The largest move allowed, as one of the package's version scheme's `bumpNames` - `minor`
|
|
50
|
+
* keeps every dependency inside its major. */
|
|
51
|
+
target: string;
|
|
52
|
+
/** `deps.targets`: a dependency-name glob, and the largest move for the names it matches -
|
|
53
|
+
* ahead of `target`. Empty when `--target` was given, which overrides both. */
|
|
54
|
+
targets: Record<string, string>;
|
|
55
|
+
reject: string[];
|
|
56
|
+
minAge: number;
|
|
57
|
+
/** The dependency kinds to look at, in the ecosystem's own words - `undefined` is every one. */
|
|
58
|
+
types?: string[];
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* One dependency of one package.
|
|
62
|
+
*
|
|
63
|
+
* - `'update'`: `target` is the new range; `reason` says why it is lower than `latest`, if it is.
|
|
64
|
+
* - `'held'`: a newer version exists and is allowed, but another dependency's rule refuses it -
|
|
65
|
+
* `reason` names that rule.
|
|
66
|
+
* - `'skipped'`: a newer version exists, and the package's own settings leave it out - a major
|
|
67
|
+
* under `target: minor`, or one younger than `minAge`. `reason` says which.
|
|
68
|
+
* - `'up-to-date'`: nothing newer exists.
|
|
69
|
+
* - `'error'`: the registry gave no answer.
|
|
70
|
+
*/
|
|
71
|
+
interface Entry {
|
|
72
|
+
package: Package;
|
|
73
|
+
/** The dependency's name. */
|
|
74
|
+
name: string;
|
|
75
|
+
/** Where the package declares it, in the ecosystem's own words - `devDependencies`. */
|
|
76
|
+
types: string[];
|
|
77
|
+
/** The range as the manifest declares it. */
|
|
78
|
+
current: string;
|
|
79
|
+
status: 'update' | 'held' | 'skipped' | 'up-to-date' | 'error';
|
|
80
|
+
/** The range it is rewritten to, on an `'update'`. */
|
|
81
|
+
target?: string;
|
|
82
|
+
/** The newest version the package's own settings allow, before any other dependency's rule. */
|
|
83
|
+
latest?: string;
|
|
84
|
+
/** The newest version there is, whatever the settings say - what a reader compares against to
|
|
85
|
+
* see what was left behind. */
|
|
86
|
+
available?: string;
|
|
87
|
+
/** How far `target` moves, in the version scheme's own words - `major`. */
|
|
88
|
+
bump?: string;
|
|
89
|
+
reason?: string;
|
|
90
|
+
}
|
|
91
|
+
/** What `applyPlan` wrote. */
|
|
92
|
+
interface Applied {
|
|
93
|
+
/** Every file it changed, absolute. */
|
|
94
|
+
files: string[];
|
|
95
|
+
/** Puts every one of them back as it was. */
|
|
96
|
+
restore(): void;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Every move but the largest - `minor` under semver. The largest is the one a scheme reserves for a
|
|
100
|
+
* break, and taking a break is a decision a person makes, not a default.
|
|
101
|
+
*/
|
|
102
|
+
function defaultTarget(bumps: readonly string[]): string;
|
|
103
|
+
/**
|
|
104
|
+
* The largest move `settings` allow for the dependency `name`, and the key that said so - the
|
|
105
|
+
* **last** `deps.targets` glob matching it, else `deps.target`.
|
|
106
|
+
*/
|
|
107
|
+
function targetFor(settings: Settings, name: string): {
|
|
108
|
+
size: string;
|
|
109
|
+
from: string;
|
|
110
|
+
};
|
|
111
|
+
/** The settings `pkg` is planned with: `options` from the command line over its `.rmanrc "deps"`,
|
|
112
|
+
* and the defaults under both. */
|
|
113
|
+
function settingsFor(pkg: Package, options: Options): Settings;
|
|
114
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import micromatch from 'micromatch';
|
|
2
|
+
export var DependencyUpdater;
|
|
3
|
+
(function (DependencyUpdater) {
|
|
4
|
+
/**
|
|
5
|
+
* Every move but the largest - `minor` under semver. The largest is the one a scheme reserves for a
|
|
6
|
+
* break, and taking a break is a decision a person makes, not a default.
|
|
7
|
+
*/
|
|
8
|
+
/* **It was `latest` - every move - and that was the wrong default**, measured the moment it ran:
|
|
9
|
+
* a `^5.3.0` came back as `^7.0.2`. The values are the scheme's own `bumpNames` rather than a list
|
|
10
|
+
* written here, for the reason `version` gives: `patch`/`minor`/`major` are semver's words, and a
|
|
11
|
+
* four-part scheme would offer four. */
|
|
12
|
+
function defaultTarget(bumps) {
|
|
13
|
+
return bumps[Math.max(0, bumps.length - 2)];
|
|
14
|
+
}
|
|
15
|
+
DependencyUpdater.defaultTarget = defaultTarget;
|
|
16
|
+
/**
|
|
17
|
+
* The largest move `settings` allow for the dependency `name`, and the key that said so - the
|
|
18
|
+
* **last** `deps.targets` glob matching it, else `deps.target`.
|
|
19
|
+
*/
|
|
20
|
+
/* Last rather than first, because that is how two `"[selector]"` blocks layer: what is written
|
|
21
|
+
* later wins. */
|
|
22
|
+
function targetFor(settings, name) {
|
|
23
|
+
let found;
|
|
24
|
+
for (const [glob, size] of Object.entries(settings.targets)) {
|
|
25
|
+
if (micromatch.isMatch(name, glob))
|
|
26
|
+
found = { size, from: `deps.targets["${glob}"]` };
|
|
27
|
+
}
|
|
28
|
+
return found ?? { size: settings.target, from: 'deps.target' };
|
|
29
|
+
}
|
|
30
|
+
DependencyUpdater.targetFor = targetFor;
|
|
31
|
+
/** The settings `pkg` is planned with: `options` from the command line over its `.rmanrc "deps"`,
|
|
32
|
+
* and the defaults under both. */
|
|
33
|
+
function settingsFor(pkg, options) {
|
|
34
|
+
const config = pkg.config.deps ?? {};
|
|
35
|
+
const bumps = pkg.versionScheme.bumpNames;
|
|
36
|
+
const target = options.target ?? config.target ?? defaultTarget(bumps);
|
|
37
|
+
if (!bumps.includes(target)) {
|
|
38
|
+
throw new Error(`Invalid "deps.target" for "${pkg.name}": "${target}" (expected one of: ${bumps.join(', ')}, ` +
|
|
39
|
+
`the "${pkg.versionScheme.name}" scheme's own sizes)`);
|
|
40
|
+
}
|
|
41
|
+
const targets = options.target ? {} : { ...(config.targets ?? {}) };
|
|
42
|
+
for (const [glob, size] of Object.entries(targets)) {
|
|
43
|
+
if (!bumps.includes(size)) {
|
|
44
|
+
throw new Error(`Invalid "deps.targets" entry "${glob}" for "${pkg.name}": "${size}" (expected one of: ${bumps.join(', ')})`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
target,
|
|
49
|
+
targets,
|
|
50
|
+
reject: [...toList(config.reject), ...(options.reject ?? [])],
|
|
51
|
+
minAge: options.minAge ?? config.minAge ?? 0,
|
|
52
|
+
types: config.types?.length ? [...config.types] : undefined,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
DependencyUpdater.settingsFor = settingsFor;
|
|
56
|
+
})(DependencyUpdater || (DependencyUpdater = {}));
|
|
57
|
+
function toList(value) {
|
|
58
|
+
if (value === undefined)
|
|
59
|
+
return [];
|
|
60
|
+
return Array.isArray(value) ? [...value] : [value];
|
|
61
|
+
}
|
|
@@ -4,6 +4,7 @@ import type { BinPath } from '../../utils/bin-path.js';
|
|
|
4
4
|
import type { RmanApplication } from '../application.js';
|
|
5
5
|
import type { Repository } from '../classes/repository.js';
|
|
6
6
|
import type { Workspace } from '../classes/workspace.js';
|
|
7
|
+
import type { DependencyUpdater } from './dependency-updater.js';
|
|
7
8
|
import type { ManifestProvider } from './manifest.js';
|
|
8
9
|
/**
|
|
9
10
|
* **One technology, as a whole** - Node, Cargo, Maven. Everything rman needs in order to treat a
|
|
@@ -75,6 +76,9 @@ export interface Platform {
|
|
|
75
76
|
/** How this technology's releases are planned - where a package's change boundary comes from
|
|
76
77
|
* when it has no release tag, and how far into its group a bump reaches. */
|
|
77
78
|
versionPlanner?: VersionPlanService;
|
|
79
|
+
/** How this technology's dependencies are checked against their registry and moved forward -
|
|
80
|
+
* what `rman deps` asks. Absent, `deps` leaves this technology's packages alone. */
|
|
81
|
+
dependencyUpdater?: DependencyUpdater;
|
|
78
82
|
}
|
|
79
83
|
/**
|
|
80
84
|
* **A plugin is whatever a package contributes; a platform is one of the things it can contribute.**
|
|
@@ -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/index.d.ts
CHANGED
|
@@ -65,6 +65,8 @@ export { Manifest } from './core/interfaces/manifest.js';
|
|
|
65
65
|
export { Registry } from './core/classes/registry.js';
|
|
66
66
|
export { Repository } from './core/classes/repository.js';
|
|
67
67
|
export { Service, type ServiceFactory, type ServiceMap } from './core/classes/service.js';
|
|
68
|
+
/** The `deps` seam: how a technology checks its dependencies against a registry and moves them. */
|
|
69
|
+
export { DependencyUpdater } from './core/interfaces/dependency-updater.js';
|
|
68
70
|
export { declaredTargets, type PublishTarget, shipsTo, skipReasonFor, targetsOf, unknownTargets, } from './core/interfaces/publish-target.js';
|
|
69
71
|
export type { RunConditionFn, RunStepContext, RunStepFn, RunStepObject, RunStepValue, } from './core/interfaces/run-step.js';
|
|
70
72
|
/**
|
package/index.js
CHANGED
|
@@ -71,6 +71,8 @@ export { Manifest } from './core/interfaces/manifest.js';
|
|
|
71
71
|
export { Registry } from './core/classes/registry.js';
|
|
72
72
|
export { Repository } from './core/classes/repository.js';
|
|
73
73
|
export { Service } from './core/classes/service.js';
|
|
74
|
+
/** The `deps` seam: how a technology checks its dependencies against a registry and moves them. */
|
|
75
|
+
export { DependencyUpdater } from './core/interfaces/dependency-updater.js';
|
|
74
76
|
export { declaredTargets, shipsTo, skipReasonFor, targetsOf, unknownTargets, } from './core/interfaces/publish-target.js';
|
|
75
77
|
export { basePlatform, definePlatform, definePlugin, isPlatform, } from './core/interfaces/plugin.js';
|
|
76
78
|
/** The numbering seam. `VersionScheme` is abstract - `highestVersion`/`highestBump`/`smallestBump`
|
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) {
|