rman 2.11.3 → 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.
Files changed (32) hide show
  1. package/builtins/platforms/node/services/ci.service.d.ts +1 -1
  2. package/builtins/platforms/node/services/ci.service.js +2 -2
  3. package/builtins/platforms/node/services/clean.service.d.ts +1 -1
  4. package/builtins/platforms/node/services/clean.service.js +2 -2
  5. package/builtins/platforms/node/services/publish.service.d.ts +9 -0
  6. package/builtins/platforms/node/services/publish.service.js +45 -5
  7. package/builtins/publish-targets/npm/npm-publish-target.d.ts +5 -0
  8. package/builtins/publish-targets/npm/npm-publish-target.js +8 -0
  9. package/commands/changelog.command.js +1 -1
  10. package/commands/list.command.js +56 -1
  11. package/constants.js +1 -1
  12. package/core/classes/log-sink.d.ts +1 -1
  13. package/{utils → core/classes}/progress-panel.d.ts +1 -1
  14. package/core/classes/run-reporters.d.ts +1 -1
  15. package/core/interfaces/publish-target.d.ts +17 -0
  16. package/core/interfaces/publish-target.js +8 -0
  17. package/index.d.ts +2 -2
  18. package/index.js +2 -2
  19. package/package.json +1 -1
  20. package/services/changelog.service.d.ts +15 -0
  21. package/services/changelog.service.js +66 -6
  22. package/services/exec.service.js +1 -1
  23. package/services/list.service.d.ts +18 -0
  24. package/services/list.service.js +35 -2
  25. package/services/run.service.d.ts +1 -1
  26. package/services/run.service.js +1 -1
  27. package/utils/git.d.ts +17 -0
  28. package/utils/git.js +84 -0
  29. package/utils/status-region.js +1 -1
  30. /package/{utils → core/classes}/live-region.d.ts +0 -0
  31. /package/{utils → core/classes}/live-region.js +0 -0
  32. /package/{utils → core/classes}/progress-panel.js +0 -0
@@ -33,7 +33,7 @@ export declare namespace CiService {
33
33
  * `"ci"` script, runs that instead. Once every package is clean, installs once at the root
34
34
  * with the configured package manager (`npm`/`yarn`/`pnpm`/`bun`).
35
35
  *
36
- * Uses the same live progress panel as `run`/`build` (see `../utils/progress-panel.ts`) while it
36
+ * Uses the same live progress panel as `run`/`build` (see `core/classes/progress-panel.ts`) while it
37
37
  * runs, falling back to a plain rmdir/clean/run/install log line per step when the panel is off.
38
38
  *
39
39
  * Unlike `run`/`build`, it does *not* end with a per-package success tally: `ci`'s packages don't
@@ -2,9 +2,9 @@ import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import colors from 'ansi-colors';
4
4
  import { Logger, resolveRootLogLevel } from '../../../../core/classes/logger.js';
5
+ import { formatDuration, ProgressPanel } from '../../../../core/classes/progress-panel.js';
5
6
  import { exec } from '../../../../utils/exec.js';
6
7
  import { filterPackages } from '../../../../utils/package-filter.js';
7
- import { formatDuration, ProgressPanel } from '../../../../utils/progress-panel.js';
8
8
  export var CiService;
9
9
  (function (CiService) {
10
10
  /**
@@ -53,7 +53,7 @@ export var CiService;
53
53
  * `"ci"` script, runs that instead. Once every package is clean, installs once at the root
54
54
  * with the configured package manager (`npm`/`yarn`/`pnpm`/`bun`).
55
55
  *
56
- * Uses the same live progress panel as `run`/`build` (see `../utils/progress-panel.ts`) while it
56
+ * Uses the same live progress panel as `run`/`build` (see `core/classes/progress-panel.ts`) while it
57
57
  * runs, falling back to a plain rmdir/clean/run/install log line per step when the panel is off.
58
58
  *
59
59
  * Unlike `run`/`build`, it does *not* end with a per-package success tally: `ci`'s packages don't
@@ -46,7 +46,7 @@ export declare namespace CleanService {
46
46
  * Run from inside a single package's own directory, it only cleans that package unless
47
47
  * `options.fromRoot` says otherwise (see `Repository.currentPackage`).
48
48
  *
49
- * Uses the same live progress panel as `run`/`build` (see `../utils/progress-panel.ts`) - unlike
49
+ * Uses the same live progress panel as `run`/`build` (see `core/classes/progress-panel.ts`) - unlike
50
50
  * `ci`, cleaning genuinely is independent per-package work, so the final per-package tally stays.
51
51
  */
52
52
  function clean(repository: Repository, options?: Options): Promise<void>;
@@ -3,8 +3,8 @@ import path from 'node:path';
3
3
  import colors from 'ansi-colors';
4
4
  import fg from 'fast-glob';
5
5
  import { Logger, resolveRootLogLevel } from '../../../../core/classes/logger.js';
6
+ import { ProgressPanel } from '../../../../core/classes/progress-panel.js';
6
7
  import { filterPackages } from '../../../../utils/package-filter.js';
7
- import { ProgressPanel } from '../../../../utils/progress-panel.js';
8
8
  export var CleanService;
9
9
  (function (CleanService) {
10
10
  /**
@@ -38,7 +38,7 @@ export var CleanService;
38
38
  * Run from inside a single package's own directory, it only cleans that package unless
39
39
  * `options.fromRoot` says otherwise (see `Repository.currentPackage`).
40
40
  *
41
- * Uses the same live progress panel as `run`/`build` (see `../utils/progress-panel.ts`) - unlike
41
+ * Uses the same live progress panel as `run`/`build` (see `core/classes/progress-panel.ts`) - unlike
42
42
  * `ci`, cleaning genuinely is independent per-package work, so the final per-package tally stays.
43
43
  */
44
44
  async function clean(repository, options = {}) {
@@ -128,6 +128,15 @@ export declare namespace PublishService {
128
128
  * excludes it for a real monorepo (it only doubles as "the" package in a single-package repo,
129
129
  * where it's a normal candidate like any other).
130
130
  */
131
+ /**
132
+ * Why the npm target leaves `pkg` alone, from its manifest alone - `'private package'`, or
133
+ * `undefined` when it would be published. `rman list` asks this to grey a package out.
134
+ */
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;
131
140
  function getPlan(repository: Repository, options?: Options, deps?: Deps): Promise<Entry[]>;
132
141
  /**
133
142
  * Publishes every `'publish'` entry in `plan`, topological order (already `plan`'s own order -
@@ -153,6 +153,46 @@ export var PublishService;
153
153
  * excludes it for a real monorepo (it only doubles as "the" package in a single-package repo,
154
154
  * where it's a normal candidate like any other).
155
155
  */
156
+ /**
157
+ * Why the npm target leaves `pkg` alone, from its manifest alone - `'private package'`, or
158
+ * `undefined` when it would be published. `rman list` asks this to grey a package out.
159
+ */
160
+ /* **The one statement of npm's `private` rule**, called by `getPlan` and by `NpmPublishTarget.
161
+ * skipReason` both, so the plan and the list cannot disagree. Two cases, both private:
162
+ * - **no `publishConfig`** (`privateBySource`) - private in the published manifest too, wherever it
163
+ * is published from;
164
+ * - **published in place** - the source manifest *is* the published one, so its `private` stands
165
+ * whatever else it declares.
166
+ * The second sat after the build-output check in `getPlan` and moved ahead of it unchanged:
167
+ * `hasBuildOutput` is always true in place, so nothing reached it any differently. */
168
+ function skipReason(pkg, contentsOverride) {
169
+ if (!pkg.manifest.raw.private)
170
+ return undefined;
171
+ if (privateBySource(pkg) || publishesInPlace(pkg, contentsOverride))
172
+ return 'private package';
173
+ return undefined;
174
+ }
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;
156
196
  async function getPlan(repository, options = {}, deps = {}) {
157
197
  const git = new GitHelper({ cwd: repository.dirname });
158
198
  const packages = filterPackages(repository.getPackages({ toposort: true }), options);
@@ -173,6 +213,7 @@ export var PublishService;
173
213
  * finding that out after a round trip per package would be a slower way to the same
174
214
  * refusal. */
175
215
  const distTag = distTagFor(pkg, options.tag);
216
+ const privateReason = skipReason(pkg, options.contents);
176
217
  if (retiredDirectoryKey(pkg)) {
177
218
  entries.set(pkg.name, {
178
219
  package: pkg,
@@ -189,8 +230,8 @@ export var PublishService;
189
230
  reason: 'excluded via .rmanrc "publish.skip"',
190
231
  });
191
232
  }
192
- else if (privateBySource(pkg)) {
193
- entries.set(pkg.name, { package: pkg, version: pkg.version, status: 'skip', reason: 'private package' });
233
+ else if (privateReason) {
234
+ entries.set(pkg.name, { package: pkg, version: pkg.version, status: 'skip', reason: privateReason });
194
235
  }
195
236
  else if (!hasBuildOutput(pkg, options.contents)) {
196
237
  const dir = path.relative(pkg.dirname, resolvePublishDir(pkg, options.contents)) || '.';
@@ -201,9 +242,6 @@ export var PublishService;
201
242
  reason: `nothing to publish in "${dir}" - it is missing or empty; build the package first`,
202
243
  });
203
244
  }
204
- else if (publishesInPlace(pkg, options.contents) && pkg.manifest.raw.private) {
205
- entries.set(pkg.name, { package: pkg, version: pkg.version, status: 'skip', reason: 'private package' });
206
- }
207
245
  else if (distTag.error) {
208
246
  entries.set(pkg.name, { package: pkg, version: pkg.version, status: 'error', reason: distTag.error });
209
247
  }
@@ -413,6 +451,8 @@ function resolveRegistry(pkg, override) {
413
451
  const native = pkg.manifest.raw.publishConfig?.registry;
414
452
  return override || (typeof native === 'string' && native ? native : undefined);
415
453
  }
454
+ /** npm's own registry, which `PublishService.registryLabel` leaves shown as `npm`. */
455
+ const NPMJS_HOST = 'registry.npmjs.org';
416
456
  /**
417
457
  * The retired `publish.directory` spelling, if a config still carries it.
418
458
  *
@@ -1,3 +1,4 @@
1
+ import type { Package } from '../../../core/classes/package.js';
1
2
  import type { PublishTarget } from '../../../core/interfaces/publish-target.js';
2
3
  import { PublishService } from '../../platforms/node/services/publish.service.js';
3
4
  /** The name this target answers to in `publish.target` and `--target`. */
@@ -70,6 +71,10 @@ export declare class NpmPublishTarget implements PublishTarget {
70
71
  type: "string";
71
72
  };
72
73
  };
74
+ /** npm's `private` rule, the same function `getPlan` asks. */
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;
73
78
  getPlan(ctx: PublishTarget.Context): Promise<PublishService.Entry[]>;
74
79
  applyPlan(ctx: PublishTarget.Context, plan: PublishTarget.Entry[]): Promise<PublishService.Entry[]>;
75
80
  /** The shared filters plus the two flags both halves need - read here rather than in the
@@ -75,6 +75,14 @@ export class NpmPublishTarget {
75
75
  type: 'string',
76
76
  },
77
77
  };
78
+ /** npm's `private` rule, the same function `getPlan` asks. */
79
+ skipReason(pkg) {
80
+ return PublishService.skipReason(pkg);
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
+ }
78
86
  getPlan(ctx) {
79
87
  return PublishService.getPlan(ctx.repository, this.planOptions(ctx));
80
88
  }
@@ -1,10 +1,10 @@
1
1
  import path from 'node:path';
2
2
  import colors from 'ansi-colors';
3
3
  import { Logger, resolveRootLogLevel } from '../core/classes/logger.js';
4
+ import { ProgressPanel } from '../core/classes/progress-panel.js';
4
5
  import { registerCommand } from '../interfaces/rman-config.interface.js';
5
6
  import { ChangeHashService } from '../services/change-hash.service.js';
6
7
  import { fromRootOption, packageFilterOptions, readPackageFilterOptions } from '../utils/package-filter.js';
7
- import { ProgressPanel } from '../utils/progress-panel.js';
8
8
  const COMMAND = 'changelog';
9
9
  const config = {
10
10
  ...packageFilterOptions,
@@ -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,6 +102,55 @@ function statusLabel(status) {
100
102
  return '';
101
103
  }
102
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. */
109
+ function publishLabel(it) {
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 '';
153
+ }
103
154
  /**
104
155
  * **The tree, as a table**: the root first, then each package indented by how far below it sits.
105
156
  *
@@ -120,12 +171,16 @@ function statusLabel(status) {
120
171
  */
121
172
  function printTable(items, withRoot) {
122
173
  const table = new EasyTable();
174
+ const withGroup = showsGroups(items);
123
175
  for (const it of items) {
124
176
  const indent = ' '.repeat(it.depth);
125
177
  table.cell('Package', indent + (it.isRoot ? colors.whiteBright(it.selector) : colors.yellowBright(it.selector)));
126
178
  table.cell('Version', colors.yellow(it.version));
179
+ if (withGroup)
180
+ table.cell('Group', groupCell(it));
127
181
  table.cell('Platform', it.platform ? colors.cyan(it.platform) : '');
128
182
  table.cell('Private', it.private ? colors.magentaBright('yes') : '');
183
+ table.cell('Publish', publishLabel(it));
129
184
  table.cell('Changed', statusLabel(it.status));
130
185
  table.cell('Path', it.location);
131
186
  table.newRow();
package/constants.js CHANGED
@@ -1 +1 @@
1
- export const version = '2.11.3';
1
+ export const version = '2.13.0';
@@ -1,5 +1,5 @@
1
- import type { ProgressItem } from '../../utils/progress-panel.js';
2
1
  import type { LogLevel } from './logger.js';
2
+ import type { ProgressItem } from './progress-panel.js';
3
3
  /**
4
4
  * One thing that happened during a run, in the order it happened. The JSON form is exactly this
5
5
  * object with a `time` added; the text form is one line per event.
@@ -1,5 +1,5 @@
1
+ import type { StatusRegion, TerminalRegion } from '../../utils/status-region.js';
1
2
  import { LiveRegion } from './live-region.js';
2
- import type { StatusRegion, TerminalRegion } from './status-region.js';
3
3
  export type ProgressStatus = 'pending' | 'running' | 'success' | 'failed' | 'skipped';
4
4
  export interface ProgressItem {
5
5
  readonly name: string;
@@ -1,6 +1,6 @@
1
- import type { ProgressPanel } from '../../utils/progress-panel.js';
2
1
  import type { LogEvent, Reporter, ReportOrigin } from './log-sink.js';
3
2
  import { type LogLevel } from './logger.js';
3
+ import type { ProgressPanel } from './progress-panel.js';
4
4
  /**
5
5
  * The screen with no live panel: each line a step prints, led by its package, and one line per step
6
6
  * when it ends - the output `--no-progress`, a pipe or CI gets.
@@ -61,6 +61,19 @@ export interface PublishTarget {
61
61
  * let through.
62
62
  */
63
63
  claims?(pkg: Package): boolean;
64
+ /**
65
+ * Why this target would leave a package it ships to alone, or `undefined` when it would not -
66
+ * answered from the config and the manifest alone, with no registry and no build.
67
+ *
68
+ * `publish.skip` is the core's and is asked before this (see `skipReasonFor`); a target answers
69
+ * only what is its own, such as npm's `private`.
70
+ */
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;
64
77
  /** What this target *would* do - never publishes. Called even under `--dry-run`, which is the
65
78
  * whole point of the split. */
66
79
  getPlan(ctx: PublishTarget.Context): Promise<PublishTarget.Entry[]>;
@@ -131,6 +144,10 @@ export declare function shipsTo(pkg: Package, target: PublishTarget): boolean;
131
144
  /** Every registered target `pkg` ships to, in registration order. The single answer to "where does
132
145
  * this package go", so `publish` and `list --json` cannot disagree about it. */
133
146
  export declare function targetsOf(app: RmanApplication, pkg: Package): PublishTarget[];
147
+ /** Why `target` would leave `pkg` alone, or `undefined` when it would publish it: `.rmanrc
148
+ * "publish.skip"`, which excludes a package from every target, and then the target's own
149
+ * `skipReason`. */
150
+ export declare function skipReasonFor(pkg: Package, target: PublishTarget): string | undefined;
134
151
  /** Names in a package's `publish.target` that no registered target answers to - a misconfiguration
135
152
  * that used to be caught by a `choices: ['npm', 'docker']` the core can no longer write down. */
136
153
  export declare function unknownTargets(app: RmanApplication, pkg: Package): string[];
@@ -32,6 +32,14 @@ export function shipsTo(pkg, target) {
32
32
  export function targetsOf(app, pkg) {
33
33
  return app.publishTargets.all.filter(target => shipsTo(pkg, target));
34
34
  }
35
+ /** Why `target` would leave `pkg` alone, or `undefined` when it would publish it: `.rmanrc
36
+ * "publish.skip"`, which excludes a package from every target, and then the target's own
37
+ * `skipReason`. */
38
+ export function skipReasonFor(pkg, target) {
39
+ if (pkg.config.publish?.skip)
40
+ return 'excluded via .rmanrc "publish.skip"';
41
+ return target.skipReason?.(pkg);
42
+ }
35
43
  /** Names in a package's `publish.target` that no registered target answers to - a misconfiguration
36
44
  * that used to be caught by a `choices: ['npm', 'docker']` the core can no longer write down. */
37
45
  export function unknownTargets(app, pkg) {
package/index.d.ts CHANGED
@@ -65,7 +65,7 @@ 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
- export { declaredTargets, type PublishTarget, shipsTo, targetsOf, unknownTargets, } from './core/interfaces/publish-target.js';
68
+ export { declaredTargets, type PublishTarget, shipsTo, skipReasonFor, targetsOf, unknownTargets, } from './core/interfaces/publish-target.js';
69
69
  export type { RunConditionFn, RunStepContext, RunStepFn, RunStepObject, RunStepValue, } from './core/interfaces/run-step.js';
70
70
  /**
71
71
  * **`Platform` is one technology, whole**; **`Plugin` is whatever a package contributes**, platforms
@@ -145,7 +145,7 @@ export { GitHelper } from './utils/git.js';
145
145
  export { applyFromRootOption, applyPackageFilterOptions, filterPackages, fromRootOption, type PackageFilterOptions, packageFilterOptions, readFromRootOption, readPackageFilterOptions, ROOT_SELECTOR, } from './utils/package-filter.js';
146
146
  /** The live panel `run`/`build`/`clean` print - a plugin's per-package command looks like the rest
147
147
  * of rman instead of like a script someone bolted on. */
148
- export { formatDuration, type ProgressItem, ProgressPanel, type ProgressStatus, type ProgressSummary, } from './utils/progress-panel.js';
148
+ export { formatDuration, type ProgressItem, ProgressPanel, type ProgressStatus, type ProgressSummary, } from './core/classes/progress-panel.js';
149
149
  /** Version stamping helpers a `Plugin.stampVersion` can delegate to - the quoted-constant
150
150
  * pattern most languages share, and the OCI Dockerfile label (which `version` stamps itself, since
151
151
  * the label's value is by specification the package's version). */
package/index.js CHANGED
@@ -71,7 +71,7 @@ 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
- export { declaredTargets, shipsTo, targetsOf, unknownTargets, } from './core/interfaces/publish-target.js';
74
+ export { declaredTargets, shipsTo, skipReasonFor, targetsOf, unknownTargets, } from './core/interfaces/publish-target.js';
75
75
  export { basePlatform, definePlatform, definePlugin, isPlatform, } from './core/interfaces/plugin.js';
76
76
  /** The numbering seam. `VersionScheme` is abstract - `highestVersion`/`highestBump`/`smallestBump`
77
77
  * are implemented from the members around them, so a scheme states only what it must and still
@@ -128,7 +128,7 @@ export { GitHelper } from './utils/git.js';
128
128
  export { applyFromRootOption, applyPackageFilterOptions, filterPackages, fromRootOption, packageFilterOptions, readFromRootOption, readPackageFilterOptions, ROOT_SELECTOR, } from './utils/package-filter.js';
129
129
  /** The live panel `run`/`build`/`clean` print - a plugin's per-package command looks like the rest
130
130
  * of rman instead of like a script someone bolted on. */
131
- export { formatDuration, ProgressPanel, } from './utils/progress-panel.js';
131
+ export { formatDuration, ProgressPanel, } from './core/classes/progress-panel.js';
132
132
  export { CiService } from './builtins/platforms/node/services/ci.service.js';
133
133
  export { CleanService } from './builtins/platforms/node/services/clean.service.js';
134
134
  export { PublishService } from './builtins/platforms/node/services/publish.service.js';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "rman",
3
3
  "description": "Repository manager",
4
- "version": "2.11.3",
4
+ "version": "2.13.0",
5
5
  "author": "Panates",
6
6
  "license": "MIT",
7
7
  "dependencies": {
@@ -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 rel = path.relative(pkg.dirname, file);
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 && pkg.dirname.length > best.dirname.length)
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 &&
@@ -2,10 +2,10 @@ import os from 'node:os';
2
2
  import colors from 'ansi-colors';
3
3
  import { Task } from 'power-tasks';
4
4
  import { Logger, resolveRootLogLevel } from '../core/classes/logger.js';
5
+ import { ProgressPanel } from '../core/classes/progress-panel.js';
5
6
  import { Service } from '../core/classes/service.js';
6
7
  import { exec as execCommand } from '../utils/exec.js';
7
8
  import { filterPackages } from '../utils/package-filter.js';
8
- import { ProgressPanel } from '../utils/progress-panel.js';
9
9
  /**
10
10
  * A service class - see `ListService` for the shape and `Service` for the three measured
11
11
  * consequences a namespace had. `repository` left the signature because the application carries it.
@@ -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;
@@ -76,6 +85,15 @@ export declare namespace ListService {
76
85
  * declares one, otherwise every registered target that claims it, which is the same question
77
86
  * `publish` asks. Empty in a repository whose plugins contribute no target the package fits. */
78
87
  publishTargets: PublishTargetName[];
88
+ /** The entries of `publishTargets` that `publish` would leave this package alone for, each with
89
+ * why - `.rmanrc "publish.skip"`, or the target's own rule (npm's `private`). Decided without the
90
+ * registry, so `{}` means "a candidate", not "will publish": whether the version is already out
91
+ * there is `publish --dry-run`'s answer. */
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>;
79
97
  /** Present only when `"docker"` is one of `publishTargets` and `publish.docker` is configured -
80
98
  * the raw `.rmanrc` config, unresolved (no namespace prefixing - see `DockerPublishService`). */
81
99
  docker?: DockerPublishOptions;
@@ -1,7 +1,8 @@
1
1
  import path from 'path';
2
2
  import { Service } from '../core/classes/service.js';
3
- import { targetsOf } from '../core/interfaces/publish-target.js';
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
  *
@@ -33,19 +34,41 @@ export class ListService extends Service {
33
34
  /** **Asked, not assumed.** This read the config key directly and defaulted to `['npm']`, so a
34
35
  * Cargo package in a polyglot repository was reported as shipping to npm - which is what
35
36
  * `PublishTarget.claims` exists to answer, per target, from the ecosystem that knows. */
36
- const publishTargets = targetsOf(this.app, p).map(t => t.name);
37
+ /** **A monorepo's root ships nowhere**: `publish` never makes it a candidate
38
+ * (`repository.getPackages()` leaves it out), so listing the target a root *would* claim
39
+ * says something no command does. */
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);
45
+ const publishTargets = targets.map(t => t.name);
46
+ const skippedTargets = {};
47
+ const targetLabels = {};
48
+ for (const target of targets) {
49
+ const reason = skipReasonFor(p, target);
50
+ if (reason)
51
+ skippedTargets[target.name] = reason;
52
+ const label = target.labelFor?.(p);
53
+ if (label && label !== target.name)
54
+ targetLabels[target.name] = label;
55
+ }
37
56
  return {
38
57
  name: p.name,
39
58
  selector: p.selector,
40
59
  version: p.version,
41
60
  platform: p.provider,
42
61
  depth: depthOf(p),
62
+ groupKey,
63
+ group: groupNameOf(groupKey),
43
64
  isRoot: p === repository.rootPackage,
44
65
  location: path.relative(repository.dirname, p.dirname) || '.',
45
66
  private: p.isPrivate,
46
67
  status: status[p.selector],
47
68
  dependencies: p.dependencies.map(d => d.name),
48
69
  publishTargets: [...publishTargets],
70
+ skippedTargets,
71
+ targetLabels,
49
72
  docker: publishTargets.includes('docker') ? p.config.publish?.docker : undefined,
50
73
  };
51
74
  };
@@ -79,3 +102,13 @@ function depthOf(pkg) {
79
102
  depth++;
80
103
  return depth;
81
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
+ }
@@ -1,11 +1,11 @@
1
1
  import type { Reporter } from '../core/classes/log-sink.js';
2
2
  import { type LogLevel } from '../core/classes/logger.js';
3
3
  import type { Package } from '../core/classes/package.js';
4
+ import { ProgressPanel } from '../core/classes/progress-panel.js';
4
5
  import type { Repository } from '../core/classes/repository.js';
5
6
  import { Service } from '../core/classes/service.js';
6
7
  import type { RunStepContext, RunStepFn, RunStepValue } from '../core/interfaces/run-step.js';
7
8
  import { type PackageFilterOptions } from '../utils/package-filter.js';
8
- import { ProgressPanel } from '../utils/progress-panel.js';
9
9
  /**
10
10
  * A service class - see `ListService` for the shape and `Service` for the three measured
11
11
  * consequences a namespace had.
@@ -5,11 +5,11 @@ import colors from 'ansi-colors';
5
5
  import { tokenize } from 'fast-tokenizer';
6
6
  import { Task } from 'power-tasks';
7
7
  import { LOG_LEVELS, Logger, resolveRootLogLevel } from '../core/classes/logger.js';
8
+ import { ProgressPanel } from '../core/classes/progress-panel.js';
8
9
  import { PanelReporter, PlainReporter } from '../core/classes/run-reporters.js';
9
10
  import { Service } from '../core/classes/service.js';
10
11
  import { exec } from '../utils/exec.js';
11
12
  import { filterPackages } from '../utils/package-filter.js';
12
- import { ProgressPanel } from '../utils/progress-panel.js';
13
13
  import { runBin } from '../utils/run-bin.js';
14
14
  /**
15
15
  * A service class - see `ListService` for the shape and `Service` for the three measured
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) {
@@ -1,5 +1,5 @@
1
1
  import colors from 'ansi-colors';
2
- import { LiveRegion } from './live-region.js';
2
+ import { LiveRegion } from '../core/classes/live-region.js';
3
3
  export class StatusRegion {
4
4
  label;
5
5
  detail;
File without changes
File without changes
File without changes