@cellajs/cli 0.4.2 → 0.6.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/README.md +171 -113
- package/dist/config.d.ts +15 -11
- package/dist/config.js +4 -2
- package/dist/config.js.map +1 -1
- package/dist/index.js +1471 -818
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env tsx
|
|
2
2
|
|
|
3
3
|
// src/cella-cli.ts
|
|
4
|
-
import { existsSync as
|
|
4
|
+
import { existsSync as existsSync14, statSync as statSync3 } from "fs";
|
|
5
5
|
import { join as join19, resolve as resolve7 } from "path";
|
|
6
|
-
import
|
|
6
|
+
import process11 from "process";
|
|
7
7
|
|
|
8
8
|
// src/cli.ts
|
|
9
|
-
import
|
|
9
|
+
import process6 from "process";
|
|
10
10
|
import { select } from "@inquirer/prompts";
|
|
11
11
|
import { Command } from "commander";
|
|
12
12
|
|
|
@@ -57,7 +57,7 @@ import { z as z2 } from "zod";
|
|
|
57
57
|
|
|
58
58
|
// src/config/types.ts
|
|
59
59
|
import { z } from "zod";
|
|
60
|
-
var
|
|
60
|
+
var PACKAGE_JSON_KEYS = [
|
|
61
61
|
"dependencies",
|
|
62
62
|
"devDependencies",
|
|
63
63
|
"peerDependencies",
|
|
@@ -75,7 +75,9 @@ var cellaConfigSchema = z.object({
|
|
|
75
75
|
upstreamBranch: z.string().min(1).optional(),
|
|
76
76
|
upstreamTrack: z.enum(["release", "branch"]).optional(),
|
|
77
77
|
releaseBase: z.string().min(1).optional(),
|
|
78
|
-
|
|
78
|
+
packageJsonKeys: z.array(z.enum(PACKAGE_JSON_KEYS)).optional(),
|
|
79
|
+
mergePackageJson: z.boolean().optional(),
|
|
80
|
+
packageJsonSync: z.array(z.enum(PACKAGE_JSON_KEYS)).optional(),
|
|
79
81
|
syncWithPackages: z.boolean().optional(),
|
|
80
82
|
fileLinkMode: z.enum(["commit", "file"]).optional()
|
|
81
83
|
}).strict(),
|
|
@@ -156,13 +158,18 @@ function resolveAppModuleFolders(repoPath) {
|
|
|
156
158
|
// src/utils/config.ts
|
|
157
159
|
var DEFAULT_UPSTREAM_REMOTE = "cella-upstream";
|
|
158
160
|
var DEFAULT_BRANCH = "main";
|
|
159
|
-
var
|
|
160
|
-
var
|
|
161
|
+
var UPGRADE_BRANCH_PREFIX = "cella/upgrade";
|
|
162
|
+
var TEMPORARY_BRANCH_PREFIXES = [UPGRADE_BRANCH_PREFIX, "cella/sync"];
|
|
163
|
+
var DEFAULT_PACKAGE_JSON_KEYS = ["dependencies", "devDependencies"];
|
|
164
|
+
var RENAMED_SETTINGS = { packageJsonSync: "packageJsonKeys", syncWithPackages: "mergePackageJson" };
|
|
165
|
+
function renamedSettingWarnings(config) {
|
|
166
|
+
return Object.entries(RENAMED_SETTINGS).filter(([old]) => old in config.settings).map(([old, current]) => `settings.${old} is renamed to ${current}: rename it in ${CONFIG_FILE}`);
|
|
167
|
+
}
|
|
161
168
|
function isUpstreamRepo(repoPath) {
|
|
162
169
|
return JSON.parse(readFileSync(join2(repoPath, "package.json"), "utf8")).name === "cella";
|
|
163
170
|
}
|
|
164
171
|
function isTemporarySyncBranch(branch) {
|
|
165
|
-
return branch.startsWith(`${
|
|
172
|
+
return TEMPORARY_BRANCH_PREFIXES.some((prefix) => branch.startsWith(`${prefix}/`));
|
|
166
173
|
}
|
|
167
174
|
function resolveReleaseBase(settings) {
|
|
168
175
|
return settings.releaseBase ?? DEFAULT_BRANCH;
|
|
@@ -171,7 +178,7 @@ function buildTemporarySyncBranch() {
|
|
|
171
178
|
const now = /* @__PURE__ */ new Date();
|
|
172
179
|
const pad2 = (n) => String(n).padStart(2, "0");
|
|
173
180
|
const stamp = `${now.getFullYear()}${pad2(now.getMonth() + 1)}${pad2(now.getDate())}-${pad2(now.getHours())}${pad2(now.getMinutes())}`;
|
|
174
|
-
return `${
|
|
181
|
+
return `${UPGRADE_BRANCH_PREFIX}/${stamp}`;
|
|
175
182
|
}
|
|
176
183
|
function resolveUpstream(settings) {
|
|
177
184
|
const track = settings.upstreamTrack ?? "release";
|
|
@@ -201,6 +208,8 @@ async function loadConfig(forkPath) {
|
|
|
201
208
|
${z2.prettifyError(result.error)}`);
|
|
202
209
|
}
|
|
203
210
|
const config = result.data;
|
|
211
|
+
config.settings.packageJsonKeys ??= config.settings.packageJsonSync;
|
|
212
|
+
config.settings.mergePackageJson ??= config.settings.syncWithPackages;
|
|
204
213
|
const appModuleFolders = resolveAppModuleFolders(realForkPath);
|
|
205
214
|
if (appModuleFolders.length > 0) {
|
|
206
215
|
const existing = config.overrides?.ignored ?? [];
|
|
@@ -213,16 +222,16 @@ ${z2.prettifyError(result.error)}`);
|
|
|
213
222
|
}
|
|
214
223
|
|
|
215
224
|
// src/utils/display.ts
|
|
216
|
-
import { resolve as
|
|
217
|
-
import
|
|
225
|
+
import { resolve as resolve3 } from "path";
|
|
226
|
+
import process5 from "process";
|
|
218
227
|
|
|
219
228
|
// package.json
|
|
220
229
|
var package_default = {
|
|
221
230
|
name: "@cellajs/cli",
|
|
222
|
-
version: "0.
|
|
231
|
+
version: "0.6.0",
|
|
223
232
|
private: false,
|
|
224
233
|
license: "MIT",
|
|
225
|
-
description: "CLI for managing your monorepo and
|
|
234
|
+
description: "CLI for managing your monorepo and upgrading it with upstream Cella.",
|
|
226
235
|
repository: {
|
|
227
236
|
type: "git",
|
|
228
237
|
url: "https://github.com/cellajs/cella-cli"
|
|
@@ -334,377 +343,19 @@ var package_default = {
|
|
|
334
343
|
200
|
|
335
344
|
],
|
|
336
345
|
"footer-max-line-length": [
|
|
337
|
-
0,
|
|
338
|
-
"always",
|
|
339
|
-
200
|
|
340
|
-
]
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
};
|
|
344
|
-
|
|
345
|
-
// src/utils/display.ts
|
|
346
|
-
var VERSION = package_default.version;
|
|
347
|
-
var DIVIDER = "\u2500".repeat(60);
|
|
348
|
-
var MENU_DIVIDER = "\u2500".repeat(40);
|
|
349
|
-
var COMMIT_LIST_MAX = 50;
|
|
350
|
-
var warningMark = colors_default.yellow("\u26A0");
|
|
351
|
-
var checkMark = colors_default.green("\u2713");
|
|
352
|
-
var activeSpinner = null;
|
|
353
|
-
var jsonMode = false;
|
|
354
|
-
function setJsonMode(enabled) {
|
|
355
|
-
jsonMode = enabled;
|
|
356
|
-
if (!enabled) return;
|
|
357
|
-
const toStderr = (...args) => {
|
|
358
|
-
process4.stderr.write(`${args.map((a) => typeof a === "string" ? a : String(a)).join(" ")}
|
|
359
|
-
`);
|
|
360
|
-
};
|
|
361
|
-
console.info = toStderr;
|
|
362
|
-
console.warn = toStderr;
|
|
363
|
-
}
|
|
364
|
-
function writeStdout(text) {
|
|
365
|
-
process4.stdout.write(text.endsWith("\n") ? text : `${text}
|
|
366
|
-
`);
|
|
367
|
-
}
|
|
368
|
-
function printHeader() {
|
|
369
|
-
const left = "\u29C8 cella cli";
|
|
370
|
-
const right = "cellajs.com";
|
|
371
|
-
const padding = Math.max(1, 60 - `${left} v${VERSION}`.length - right.length);
|
|
372
|
-
console.info();
|
|
373
|
-
console.info(`${colors_default.cyan(left)}${colors_default.dim(` \xB7 v${VERSION}`)}${colors_default.cyan(`${" ".repeat(padding)}${right}`)}`);
|
|
374
|
-
console.info(DIVIDER);
|
|
375
|
-
console.info();
|
|
376
|
-
}
|
|
377
|
-
function createSpinner(text) {
|
|
378
|
-
const enabled = !jsonMode && !getEnv("VITEST") && getEnv("NODE_ENV") !== "test" && !!process4.stdout.isTTY;
|
|
379
|
-
const frames = ["-", "\\", "|", "/"];
|
|
380
|
-
let frame = 0;
|
|
381
|
-
let timer;
|
|
382
|
-
const spinner = {
|
|
383
|
-
text,
|
|
384
|
-
stop() {
|
|
385
|
-
clearInterval(timer);
|
|
386
|
-
if (enabled) process4.stdout.write("\r\x1B[2K");
|
|
387
|
-
}
|
|
388
|
-
};
|
|
389
|
-
const render = () => process4.stdout.write(`\r${colors_default.cyan(frames[frame++ % frames.length])} ${spinner.text}`);
|
|
390
|
-
if (enabled) {
|
|
391
|
-
render();
|
|
392
|
-
timer = setInterval(render, 80);
|
|
393
|
-
}
|
|
394
|
-
activeSpinner = spinner;
|
|
395
|
-
return spinner;
|
|
396
|
-
}
|
|
397
|
-
function spinnerSuccess(message, detail) {
|
|
398
|
-
stopSpinner();
|
|
399
|
-
if (!message) return;
|
|
400
|
-
console.info(`${checkMark} ${message}`);
|
|
401
|
-
if (detail) {
|
|
402
|
-
console.info(` ${colors_default.dim(detail)}`);
|
|
403
|
-
console.info();
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
function spinnerFail(message) {
|
|
407
|
-
stopSpinner();
|
|
408
|
-
console.info(`${colors_default.red("\u2717")} ${message}`);
|
|
409
|
-
}
|
|
410
|
-
function spinnerText(text) {
|
|
411
|
-
if (activeSpinner) {
|
|
412
|
-
activeSpinner.text = text;
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
function stopSpinner() {
|
|
416
|
-
if (activeSpinner) {
|
|
417
|
-
activeSpinner.stop();
|
|
418
|
-
activeSpinner = null;
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
function hyperlink(label, url) {
|
|
422
|
-
if (!url) return label;
|
|
423
|
-
const safeUrl = url.replace(/[\x00-\x1f\x7f]/g, "");
|
|
424
|
-
return `\x1B]8;;${safeUrl}\x07${label}\x1B]8;;\x07`;
|
|
425
|
-
}
|
|
426
|
-
function formatFetchedUpstreamDetail(commitCount, commits, upstreamGitHubUrl, maxShownCommits = COMMIT_LIST_MAX) {
|
|
427
|
-
const lines = [`${plural(commitCount, "new commit")} since last merge`];
|
|
428
|
-
const shown = commits.slice(0, maxShownCommits);
|
|
429
|
-
for (const commit of shown) {
|
|
430
|
-
const shortHash = commit.hash.slice(0, 7);
|
|
431
|
-
const commitUrl = upstreamGitHubUrl ? `${upstreamGitHubUrl}/commit/${commit.hash}` : void 0;
|
|
432
|
-
const hashLabel = hyperlink(shortHash, commitUrl);
|
|
433
|
-
lines.push(` ${hashLabel} "${commit.message}" (${commit.date})`);
|
|
434
|
-
}
|
|
435
|
-
if (commitCount > shown.length) {
|
|
436
|
-
lines.push(` ... and ${commitCount - shown.length} more`);
|
|
437
|
-
}
|
|
438
|
-
return lines.join("\n");
|
|
439
|
-
}
|
|
440
|
-
function printSectionHeader(title) {
|
|
441
|
-
console.info();
|
|
442
|
-
console.info(title);
|
|
443
|
-
console.info(DIVIDER);
|
|
444
|
-
console.info();
|
|
445
|
-
}
|
|
446
|
-
function formatFileDateInfo(filePath, commit, date, options) {
|
|
447
|
-
if (!date) return "";
|
|
448
|
-
const { upstreamGitHubUrl: url, upstreamBranch, fileLinkMode = "commit" } = options;
|
|
449
|
-
let link = commit?.slice(0, 9) ?? "";
|
|
450
|
-
if (url && fileLinkMode === "file" && upstreamBranch) {
|
|
451
|
-
link = hyperlink(filePath.split("/").pop() || filePath, `${url}/blob/${upstreamBranch}/${filePath}`);
|
|
452
|
-
} else if (url) {
|
|
453
|
-
link = commit ? hyperlink(link, `${url}/commit/${commit}`) : "";
|
|
454
|
-
}
|
|
455
|
-
return colors_default.dim(` \u2260 ${date} ${link}`);
|
|
456
|
-
}
|
|
457
|
-
function formatMergeInProgressDetail(conflictCount, autoMergedFiles, linkOptions, maxFiles = 100) {
|
|
458
|
-
const lines = [`${conflictCount} conflicts to resolve in IDE`];
|
|
459
|
-
if (autoMergedFiles.length === 0) {
|
|
460
|
-
return lines.join("\n");
|
|
461
|
-
}
|
|
462
|
-
lines.push("");
|
|
463
|
-
lines.push(` auto-merged files (${autoMergedFiles.length}):`);
|
|
464
|
-
const { forkPath } = linkOptions;
|
|
465
|
-
const shown = autoMergedFiles.slice(0, maxFiles);
|
|
466
|
-
for (const filePath of shown) {
|
|
467
|
-
lines.push(` - ${forkPath ? hyperlink(filePath, `vscode://file${resolve2(forkPath, filePath)}`) : filePath}`);
|
|
468
|
-
}
|
|
469
|
-
if (autoMergedFiles.length > shown.length) {
|
|
470
|
-
lines.push(` ... + ${autoMergedFiles.length - shown.length} more`);
|
|
471
|
-
}
|
|
472
|
-
return lines.join("\n");
|
|
473
|
-
}
|
|
474
|
-
var statusConfig = {
|
|
475
|
-
behind: { icon: colors_default.cyan("\u2193"), label: "behind", color: colors_default.cyan, description: "upstream changed, will sync" },
|
|
476
|
-
diverged: { icon: colors_default.magenta("\u21C5"), label: "diverged", color: colors_default.magenta, description: "both changed, will merge" },
|
|
477
|
-
drifted: { icon: colors_default.yellow("!"), label: "drifted", color: colors_default.yellow, description: "fork changed (at risk)" },
|
|
478
|
-
ahead: { icon: colors_default.blue("\u2191"), label: "ahead", color: colors_default.blue, description: "fork changed (protected)" },
|
|
479
|
-
local: { icon: colors_default.green("+"), label: "local", color: colors_default.green, description: "local file in cella folders" },
|
|
480
|
-
pinned: { icon: colors_default.green("\u2A00"), label: "pinned", color: colors_default.green, description: "both changed, fork wins" },
|
|
481
|
-
ignored: { icon: colors_default.gray("\u2A02"), label: "ignored", color: colors_default.gray },
|
|
482
|
-
identical: { icon: colors_default.gray("\u2713"), label: "identical", color: colors_default.gray, description: "no changes" },
|
|
483
|
-
deleted: { icon: colors_default.red("\u2717"), label: "deleted", color: colors_default.red },
|
|
484
|
-
renamed: { icon: colors_default.blue("\u2192"), label: "renamed", color: colors_default.blue }
|
|
485
|
-
};
|
|
486
|
-
var summaryStatusConfig = {
|
|
487
|
-
...statusConfig,
|
|
488
|
-
managed: {
|
|
489
|
-
icon: colors_default.cyan("\u25C7"),
|
|
490
|
-
label: "managed",
|
|
491
|
-
color: colors_default.cyan,
|
|
492
|
-
description: "package.jsons & cella/cella.config.ts handled separately"
|
|
493
|
-
},
|
|
494
|
-
ignored: { ...statusConfig.ignored, description: "protected by ignored config" }
|
|
495
|
-
};
|
|
496
|
-
function printSummary(summary, title = "summary") {
|
|
497
|
-
printSectionHeader(colors_default.cyan(title));
|
|
498
|
-
const maxCount = Math.max(...Object.values(summary));
|
|
499
|
-
const countWidth = String(maxCount).length + 1;
|
|
500
|
-
const groups = [
|
|
501
|
-
["managed", "ignored"],
|
|
502
|
-
["identical"],
|
|
503
|
-
["ahead", "local", "drifted"],
|
|
504
|
-
["diverged", "pinned"],
|
|
505
|
-
["behind"]
|
|
506
|
-
];
|
|
507
|
-
for (const group of groups) {
|
|
508
|
-
if (group !== groups[0]) console.info();
|
|
509
|
-
for (const status of group) {
|
|
510
|
-
const { icon, label, color, description } = summaryStatusConfig[status];
|
|
511
|
-
const countStr = color(String(summary[status]).padStart(countWidth));
|
|
512
|
-
console.info(` ${icon} ${countStr} ${label.padEnd(15)}${description ? colors_default.dim(description) : ""}`);
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
function printFileSection(files, title, linkOptions, options) {
|
|
517
|
-
if (files.length === 0) return;
|
|
518
|
-
printSectionHeader(`${title} ${colors_default.dim(`\xB7 ${files.length} files`)}`);
|
|
519
|
-
const useUpstream = options.dateSource === "upstream";
|
|
520
|
-
const maxLines = 100;
|
|
521
|
-
const shown = files.length > maxLines ? files.slice(0, maxLines) : files;
|
|
522
|
-
for (const file of shown) {
|
|
523
|
-
const icon = typeof options.icon === "string" ? options.icon : options.icon(file);
|
|
524
|
-
const commit = useUpstream ? file.upstreamCommit : file.changedCommit;
|
|
525
|
-
const date = useUpstream ? file.upstreamChangedAt : file.changedAt;
|
|
526
|
-
const dateInfo = formatFileDateInfo(file.path, commit, date, linkOptions);
|
|
527
|
-
console.info(` ${icon} ${file.path}${dateInfo}${options.suffix?.(file) ?? ""}`);
|
|
528
|
-
}
|
|
529
|
-
if (files.length > maxLines) {
|
|
530
|
-
console.info(colors_default.dim(` ... + ${files.length - maxLines} more`));
|
|
531
|
-
}
|
|
532
|
-
if (options.hint) {
|
|
533
|
-
console.info();
|
|
534
|
-
for (const line of Array.isArray(options.hint) ? options.hint : [options.hint]) {
|
|
535
|
-
console.info(colors_default.dim(` ${line}`));
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
|
-
function filesWithStatus(files, status) {
|
|
540
|
-
return files.filter((f) => f.status === status && !isManagedFile(f.path));
|
|
541
|
-
}
|
|
542
|
-
function printFileGroup(files, status, linkOptions, options) {
|
|
543
|
-
const config = statusConfig[status];
|
|
544
|
-
const title = options?.title ?? `${config.icon} ${config.label}`;
|
|
545
|
-
printFileSection(filesWithStatus(files, status), title, linkOptions, { ...options, icon: config.icon });
|
|
546
|
-
}
|
|
547
|
-
function findProtectedBehind(files) {
|
|
548
|
-
return files.filter((file) => file.upstreamChanged && !isManagedFile(file.path));
|
|
549
|
-
}
|
|
550
|
-
var PROTECTED_BEHIND_HINT = "pinned/ignored files where upstream also changed since the last sync; the fork side wins on conflict, so review what upstream changed and adopt what you need.";
|
|
551
|
-
function protectionIcon(file) {
|
|
552
|
-
return file.isIgnored ? statusConfig.ignored.icon : statusConfig.pinned.icon;
|
|
553
|
-
}
|
|
554
|
-
function formatUpstreamChangedLines(file) {
|
|
555
|
-
if (file.upstreamChangedLines === void 0) return "";
|
|
556
|
-
return colors_default.yellow(` \xB7 ${plural(file.upstreamChangedLines, "line")} changed upstream`);
|
|
557
|
-
}
|
|
558
|
-
function formatUpstreamLinesAbsent(file) {
|
|
559
|
-
const n = file.upstreamLinesAbsent ?? 0;
|
|
560
|
-
return n > 0 ? colors_default.yellow(` \xB7 ${plural(n, "upstream line")} absent`) : "";
|
|
561
|
-
}
|
|
562
|
-
function printAnalysisFileGroups(files, linkOptions) {
|
|
563
|
-
const protectedBehind = findProtectedBehind(files);
|
|
564
|
-
printFileGroup(files, "behind", linkOptions, {
|
|
565
|
-
title: colors_default.cyan("\u2193 behind on upstream")
|
|
566
|
-
});
|
|
567
|
-
printFileGroup(files, "ahead", linkOptions, {
|
|
568
|
-
title: `${colors_default.blue("\u2191 protected in fork")}`,
|
|
569
|
-
suffix: formatUpstreamLinesAbsent,
|
|
570
|
-
hint: [
|
|
571
|
-
"these files have fork changes but are protected (pinned); upstream did not change them since the last sync.",
|
|
572
|
-
"pinned files keep the fork copy whole; a count here is upstream content the fork never received, deliberately or not: `analyze --unpinned` shows what a merge would bring in."
|
|
573
|
-
]
|
|
574
|
-
});
|
|
575
|
-
printFileSection(protectedBehind, `${warningMark} ${colors_default.yellow("protected but behind upstream")}`, linkOptions, {
|
|
576
|
-
icon: protectionIcon,
|
|
577
|
-
suffix: formatUpstreamChangedLines,
|
|
578
|
-
dateSource: "upstream",
|
|
579
|
-
hint: PROTECTED_BEHIND_HINT
|
|
580
|
-
});
|
|
581
|
-
printFileGroup(files, "drifted", linkOptions, {
|
|
582
|
-
title: `${warningMark} ${colors_default.yellow("drifted from upstream")}`,
|
|
583
|
-
hint: "these files have fork changes but are not pinned or ignored."
|
|
584
|
-
});
|
|
585
|
-
printFileGroup(files, "diverged", linkOptions, {
|
|
586
|
-
title: `${colors_default.magenta("\u21C5 diverged")}`,
|
|
587
|
-
hint: "both fork and upstream changed.",
|
|
588
|
-
dateSource: "upstream"
|
|
589
|
-
});
|
|
590
|
-
}
|
|
591
|
-
function printSyncComplete(result, options = {}) {
|
|
592
|
-
const updated = result.files.filter((file) => file.status === "behind" || file.status === "diverged").length;
|
|
593
|
-
const diverged = result.files.filter((file) => file.status === "diverged").length;
|
|
594
|
-
const merged = result.autoMergedFiles?.length ?? Math.max(0, diverged - result.conflicts.length);
|
|
595
|
-
const conflicts = result.conflicts.length;
|
|
596
|
-
console.info();
|
|
597
|
-
if (options.stagedBranch) {
|
|
598
|
-
console.info(`${checkMark} sync merge staged on '${options.stagedBranch}'`);
|
|
599
|
-
console.info(
|
|
600
|
-
colors_default.dim(` ${updated} files updated, ${merged} auto-merged, ${conflicts} conflicts. committing next...`)
|
|
601
|
-
);
|
|
602
|
-
} else {
|
|
603
|
-
console.info(`${checkMark} sync complete`);
|
|
604
|
-
console.info(colors_default.dim(` ${updated} files updated, ${merged} auto-merged, ${conflicts} conflicts`));
|
|
605
|
-
}
|
|
606
|
-
printProtectedConflicts(result);
|
|
607
|
-
console.info();
|
|
608
|
-
}
|
|
609
|
-
function printProtectedConflicts(result) {
|
|
610
|
-
const paths = result.protectedConflicts ?? [];
|
|
611
|
-
if (paths.length === 0) return;
|
|
612
|
-
const byPath = new Map(result.files.map((file) => [file.path, file]));
|
|
613
|
-
console.info();
|
|
614
|
-
console.info(
|
|
615
|
-
`${warningMark} ${colors_default.yellow(
|
|
616
|
-
`${plural(paths.length, "protected file kept", "protected files kept")} the fork version, dropping upstream changes:`
|
|
617
|
-
)}`
|
|
618
|
-
);
|
|
619
|
-
for (const path of paths) {
|
|
620
|
-
const file = byPath.get(path);
|
|
621
|
-
const icon = file ? protectionIcon(file) : statusConfig.pinned.icon;
|
|
622
|
-
console.info(` ${icon} ${path}${file ? formatUpstreamChangedLines(file) : ""}`);
|
|
623
|
-
}
|
|
624
|
-
console.info(colors_default.dim(` ${PROTECTED_BEHIND_HINT}`));
|
|
625
|
-
}
|
|
626
|
-
function plural(count3, singular, pluralForm = `${singular}s`) {
|
|
627
|
-
return `${count3} ${count3 === 1 ? singular : pluralForm}`;
|
|
628
|
-
}
|
|
629
|
-
function printIgnoredUpstreamChanges(result) {
|
|
630
|
-
const groups = result.ignoredUpstreamChanges ?? [];
|
|
631
|
-
if (groups.length === 0) return;
|
|
632
|
-
const total = groups.reduce((count3, group) => count3 + group.paths.length, 0);
|
|
633
|
-
console.info();
|
|
634
|
-
console.info(
|
|
635
|
-
`${warningMark} ${colors_default.yellow(`ignored paths changed upstream \xB7 ${plural(total, "file")} under ${plural(groups.length, "entry", "entries")}`)}`
|
|
636
|
-
);
|
|
637
|
-
for (const group of groups) {
|
|
638
|
-
const counts = [plural(group.paths.length, "file")];
|
|
639
|
-
if (group.added > 0) counts.push(`${group.added} new`);
|
|
640
|
-
if (group.deleted > 0) counts.push(`${group.deleted} deleted`);
|
|
641
|
-
console.info(` ${statusConfig.ignored.icon} ${group.entry} ${colors_default.dim(`\xB7 ${counts.join(", ")}`)}`);
|
|
642
|
-
}
|
|
643
|
-
console.info(colors_default.dim(" the fork left these untouched and ignored paths never sync: review and adopt what you need."));
|
|
644
|
-
}
|
|
645
|
-
function printUpstreamOverrideChanges(result) {
|
|
646
|
-
const report = result.upstreamOverrides;
|
|
647
|
-
if (!report) return;
|
|
648
|
-
console.info();
|
|
649
|
-
if (report.kind === "unreadable") {
|
|
650
|
-
const side = report.side === "base" ? "the last sync point" : "the incoming ref";
|
|
651
|
-
console.info(
|
|
652
|
-
colors_default.dim(`upstream sync config not compared: upstream ${CONFIG_FILE} is missing or unreadable at ${side}`)
|
|
653
|
-
);
|
|
654
|
-
return;
|
|
655
|
-
}
|
|
656
|
-
const lines = [];
|
|
657
|
-
for (const list of ["pinned", "ignored", "packageJsonSync"]) {
|
|
658
|
-
for (const entry of report[list].added) lines.push(` ${colors_default.green("+")} ${list}: ${entry}`);
|
|
659
|
-
for (const entry of report[list].removed) {
|
|
660
|
-
lines.push(` ${colors_default.red("\u2212")} ${list}: ${entry}${colors_default.dim(" \xB7 dropped upstream, still in your config")}`);
|
|
661
|
-
}
|
|
662
|
-
}
|
|
663
|
-
console.info(
|
|
664
|
-
`${warningMark} ${colors_default.yellow(`upstream changed its sync config \xB7 ${plural(lines.length, "entry", "entries")} to review`)}`
|
|
665
|
-
);
|
|
666
|
-
for (const line of lines) console.info(line);
|
|
667
|
-
console.info(colors_default.dim(` ${CONFIG_FILE} never syncs: add or drop these by hand where they fit your app.`));
|
|
668
|
-
}
|
|
669
|
-
function findMaskingPins(files) {
|
|
670
|
-
return files.filter(
|
|
671
|
-
(file) => file.isPinned && !isManagedFile(file.path) && file.status === "behind" && file.existsInFork && file.existsInUpstream
|
|
672
|
-
);
|
|
673
|
-
}
|
|
674
|
-
function printMaskingPinWarning(files) {
|
|
675
|
-
const masking = findMaskingPins(files);
|
|
676
|
-
if (masking.length === 0) return;
|
|
677
|
-
console.info();
|
|
678
|
-
console.info(
|
|
679
|
-
`${warningMark} ${colors_default.yellow(
|
|
680
|
-
`${plural(masking.length, "pinned file matches", "pinned files match")} the previous upstream but changed upstream:`
|
|
681
|
-
)}`
|
|
682
|
-
);
|
|
683
|
-
console.info(colors_default.yellow(" the pin keeps the old fork copy and silently drops those upstream changes:"));
|
|
684
|
-
for (const file of masking) {
|
|
685
|
-
console.info(colors_default.dim(` \u2A00 ${file.path}`));
|
|
686
|
-
}
|
|
687
|
-
console.info(
|
|
688
|
-
colors_default.yellow(" if the fork never customized these, unpin them in cella/cella.config.ts to take upstream.")
|
|
689
|
-
);
|
|
690
|
-
console.info();
|
|
691
|
-
}
|
|
692
|
-
function printEngineReports(result, title) {
|
|
693
|
-
printSummary(result.summary, title);
|
|
694
|
-
printIgnoredUpstreamChanges(result);
|
|
695
|
-
printUpstreamOverrideChanges(result);
|
|
696
|
-
printMaskingPinWarning(result.files);
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
// src/utils/overrides.ts
|
|
700
|
-
import { existsSync as existsSync5 } from "fs";
|
|
701
|
-
import { join as join6 } from "path";
|
|
346
|
+
0,
|
|
347
|
+
"always",
|
|
348
|
+
200
|
|
349
|
+
]
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
};
|
|
702
353
|
|
|
703
354
|
// src/utils/git.ts
|
|
704
355
|
import { execFile } from "child_process";
|
|
705
356
|
import { existsSync as existsSync3 } from "fs";
|
|
706
357
|
import { mkdir as mkdir2, readdir, rmdir, unlink } from "fs/promises";
|
|
707
|
-
import { dirname as dirname2, join as join4, resolve as
|
|
358
|
+
import { dirname as dirname2, join as join4, resolve as resolve2 } from "path";
|
|
708
359
|
import { promisify } from "util";
|
|
709
360
|
|
|
710
361
|
// src/utils/manifest.ts
|
|
@@ -840,7 +491,7 @@ async function commitSquash(cwd, message) {
|
|
|
840
491
|
await git(["commit", "-m", message], cwd, { skipEditor: true });
|
|
841
492
|
}
|
|
842
493
|
async function gitPath(cwd, file) {
|
|
843
|
-
return
|
|
494
|
+
return resolve2(cwd, await git(["rev-parse", "--git-path", file], cwd));
|
|
844
495
|
}
|
|
845
496
|
async function stageAll(cwd) {
|
|
846
497
|
await git(["add", "-A"], cwd);
|
|
@@ -848,6 +499,10 @@ async function stageAll(cwd) {
|
|
|
848
499
|
async function mergeInProgress(cwd) {
|
|
849
500
|
return await revParse(cwd, "MERGE_HEAD") !== null;
|
|
850
501
|
}
|
|
502
|
+
function splitPrNumber(subject) {
|
|
503
|
+
const match = /\s*\(#(\d+)\)\s*$/.exec(subject);
|
|
504
|
+
return match ? { subject: subject.slice(0, match.index), pr: Number(match[1]) } : { subject };
|
|
505
|
+
}
|
|
851
506
|
async function getShortSha(cwd, ref) {
|
|
852
507
|
return git(["rev-parse", "--short", ref], cwd);
|
|
853
508
|
}
|
|
@@ -920,16 +575,16 @@ async function countCommitsBetween(cwd, fromRef, toRef) {
|
|
|
920
575
|
return Number.parseInt(output.trim(), 10) || 0;
|
|
921
576
|
}
|
|
922
577
|
async function listCommitsBetween(cwd, fromRef, toRef, limit) {
|
|
923
|
-
const args = ["log", "--format=%H%x1f%s%x1f%ar", "--reverse", `${fromRef}..${toRef}`];
|
|
578
|
+
const args = ["log", "--format=%H%x1f%s%x1f%ar%x1f%ct", "--reverse", `${fromRef}..${toRef}`];
|
|
924
579
|
if (limit) args.push(`-n${limit}`);
|
|
925
580
|
const output = await git(args, cwd, { ignoreErrors: true });
|
|
926
581
|
if (!output) return [];
|
|
927
582
|
const commits = [];
|
|
928
583
|
for (const line of output.split("\n")) {
|
|
929
584
|
if (!line) continue;
|
|
930
|
-
const [hash, message, date] = line.split("");
|
|
585
|
+
const [hash, message, date, timestamp] = line.split("");
|
|
931
586
|
if (!hash || !message || !date) continue;
|
|
932
|
-
commits.push({ hash, message, date });
|
|
587
|
+
commits.push({ hash, message, date, timestamp: Number(timestamp) || void 0 });
|
|
933
588
|
}
|
|
934
589
|
return commits;
|
|
935
590
|
}
|
|
@@ -1100,16 +755,25 @@ async function cleanupEmptyParentDirs(cwd, relativePath) {
|
|
|
1100
755
|
} catch {
|
|
1101
756
|
}
|
|
1102
757
|
}
|
|
758
|
+
var LAST_UPGRADE_REF = "refs/cella/last-upgrade";
|
|
759
|
+
var LEGACY_LAST_UPGRADE_REF = "refs/cella/last-sync";
|
|
1103
760
|
async function storeLastSyncRef(cwd, upstreamHash) {
|
|
1104
|
-
await git(["update-ref",
|
|
761
|
+
await git(["update-ref", LAST_UPGRADE_REF, upstreamHash], cwd);
|
|
1105
762
|
const head = await revParse(cwd, "HEAD");
|
|
1106
|
-
if (head) await git(["update-ref",
|
|
763
|
+
if (head) await git(["update-ref", `${LAST_UPGRADE_REF}-head`, head], cwd);
|
|
1107
764
|
}
|
|
1108
765
|
async function stagePath(cwd, path) {
|
|
1109
766
|
await git(["add", "--", path], cwd, { ignoreErrors: true });
|
|
1110
767
|
}
|
|
768
|
+
async function readStoredSyncRefs(cwd) {
|
|
769
|
+
for (const name of [LAST_UPGRADE_REF, LEGACY_LAST_UPGRADE_REF]) {
|
|
770
|
+
const ref = await revParse(cwd, name);
|
|
771
|
+
if (ref) return { ref, head: await revParse(cwd, `${name}-head`) };
|
|
772
|
+
}
|
|
773
|
+
return null;
|
|
774
|
+
}
|
|
1111
775
|
async function getStoredSyncRef(cwd) {
|
|
1112
|
-
return
|
|
776
|
+
return (await readStoredSyncRefs(cwd))?.ref ?? null;
|
|
1113
777
|
}
|
|
1114
778
|
async function readManifestAtRef(cwd, ref) {
|
|
1115
779
|
const raw = await git(["show", `${ref}:${MANIFEST_FILE}`], cwd, { ignoreErrors: true });
|
|
@@ -1158,11 +822,11 @@ async function unshallowRepository(cwd) {
|
|
|
1158
822
|
${error.message}` : "";
|
|
1159
823
|
throw new Error(
|
|
1160
824
|
`this is a shallow clone and its full history could not be restored automatically.
|
|
1161
|
-
|
|
825
|
+
An upgrade needs the complete history to find the common ancestor with upstream. Restore it with:
|
|
1162
826
|
|
|
1163
827
|
git fetch --unshallow
|
|
1164
828
|
|
|
1165
|
-
then re-run
|
|
829
|
+
then re-run the upgrade.${detail}`
|
|
1166
830
|
);
|
|
1167
831
|
}
|
|
1168
832
|
}
|
|
@@ -1188,20 +852,20 @@ async function ensureSyncBase(cwd, headRef, upstreamRef) {
|
|
|
1188
852
|
}
|
|
1189
853
|
if (!baseSha) {
|
|
1190
854
|
throw new Error(
|
|
1191
|
-
`no common ancestor between the fork and '${upstreamRef}', and no
|
|
855
|
+
`no common ancestor between the fork and '${upstreamRef}', and no upgrade base could be determined.
|
|
1192
856
|
|
|
1193
857
|
This fork has unrelated history with upstream (typical for a create-cella scaffold, or after
|
|
1194
858
|
upstream squashed its history) and no recorded base commit exists after fetching
|
|
1195
|
-
upstream. Seed it manually with the upstream commit the fork was created from, then re-run
|
|
859
|
+
upstream. Seed it manually with the upstream commit the fork was created from, then re-run the upgrade:
|
|
1196
860
|
|
|
1197
|
-
git update-ref
|
|
861
|
+
git update-ref ${LAST_UPGRADE_REF} <upstream-sha>
|
|
1198
862
|
`
|
|
1199
863
|
);
|
|
1200
864
|
}
|
|
1201
865
|
const root = await getRootCommit(cwd, headRef);
|
|
1202
|
-
if (!root) throw new Error("could not determine the fork's root commit to bootstrap
|
|
866
|
+
if (!root) throw new Error("could not determine the fork's root commit to bootstrap upgrade ancestry");
|
|
1203
867
|
await git(["replace", "-f", "--graft", root, baseSha], cwd);
|
|
1204
|
-
await git(["update-ref",
|
|
868
|
+
await git(["update-ref", LAST_UPGRADE_REF, baseSha], cwd);
|
|
1205
869
|
}
|
|
1206
870
|
async function isAncestor(cwd, ancestor, descendant) {
|
|
1207
871
|
try {
|
|
@@ -1212,14 +876,13 @@ async function isAncestor(cwd, ancestor, descendant) {
|
|
|
1212
876
|
}
|
|
1213
877
|
}
|
|
1214
878
|
async function getLegacySyncRef(cwd, headRef) {
|
|
1215
|
-
const
|
|
1216
|
-
if (!
|
|
1217
|
-
|
|
1218
|
-
if (storedHead) {
|
|
879
|
+
const stored = await readStoredSyncRefs(cwd);
|
|
880
|
+
if (!stored) return null;
|
|
881
|
+
if (stored.head) {
|
|
1219
882
|
const currentHead = await revParse(cwd, headRef);
|
|
1220
|
-
if (currentHead && currentHead ===
|
|
883
|
+
if (currentHead && currentHead === stored.head) return null;
|
|
1221
884
|
}
|
|
1222
|
-
return
|
|
885
|
+
return stored.ref;
|
|
1223
886
|
}
|
|
1224
887
|
async function getEffectiveMergeBase(cwd, headRef, upstreamRef) {
|
|
1225
888
|
const gitBase = await getMergeBase(cwd, headRef, upstreamRef);
|
|
@@ -1231,36 +894,432 @@ async function getEffectiveMergeBase(cwd, headRef, upstreamRef) {
|
|
|
1231
894
|
if (recordedIsNewer && recorded !== gitBase) {
|
|
1232
895
|
return { base: recorded, isStale: true, storedRef: recorded };
|
|
1233
896
|
}
|
|
1234
|
-
return { base: gitBase, isStale: false, storedRef: recorded };
|
|
897
|
+
return { base: gitBase, isStale: false, storedRef: recorded };
|
|
898
|
+
}
|
|
899
|
+
async function withTemporarySyncBaseGraft(cwd, headRef, baseSha, fn) {
|
|
900
|
+
const headSha = await revParse(cwd, headRef);
|
|
901
|
+
if (!headSha || !await commitObjectExists(cwd, baseSha)) return fn();
|
|
902
|
+
if (await isAncestor(cwd, baseSha, headSha)) return fn();
|
|
903
|
+
const existingReplacement = await git(["replace", "-l", headSha], cwd, { ignoreErrors: true });
|
|
904
|
+
if (existingReplacement) return fn();
|
|
905
|
+
const parentsRaw = await git(["log", "-1", "--format=%P", headSha], cwd, { ignoreErrors: true });
|
|
906
|
+
const parents = parentsRaw ? parentsRaw.split(/\s+/) : [];
|
|
907
|
+
await git(["replace", "-f", "--graft", headSha, ...parents, baseSha], cwd);
|
|
908
|
+
try {
|
|
909
|
+
return await fn();
|
|
910
|
+
} finally {
|
|
911
|
+
await git(["replace", "-d", headSha], cwd, { ignoreErrors: true });
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
// src/utils/layout.ts
|
|
916
|
+
import process4 from "process";
|
|
917
|
+
import { stripVTControlCharacters } from "util";
|
|
918
|
+
function terminalColumns() {
|
|
919
|
+
const columns = process4.stdout.isTTY ? process4.stdout.columns : void 0;
|
|
920
|
+
return Math.max(60, columns || 80);
|
|
921
|
+
}
|
|
922
|
+
function visibleWidth(text) {
|
|
923
|
+
return stripVTControlCharacters(text).length;
|
|
924
|
+
}
|
|
925
|
+
function padVisible(text, width, align = "start") {
|
|
926
|
+
const padding = " ".repeat(Math.max(0, width - visibleWidth(text)));
|
|
927
|
+
return align === "start" ? `${text}${padding}` : `${padding}${text}`;
|
|
928
|
+
}
|
|
929
|
+
function fitText(text, width) {
|
|
930
|
+
if (text.length <= width) return text;
|
|
931
|
+
return `${text.slice(0, Math.max(0, width - 1)).trimEnd()}\u2026`;
|
|
932
|
+
}
|
|
933
|
+
var AGE_STEPS = [
|
|
934
|
+
[90 * 60, 60, "m"],
|
|
935
|
+
[36 * 3600, 3600, "h"],
|
|
936
|
+
[14 * 86400, 86400, "d"],
|
|
937
|
+
[70 * 86400, 7 * 86400, "w"],
|
|
938
|
+
[365 * 86400, 30 * 86400, "mo"]
|
|
939
|
+
];
|
|
940
|
+
function shortAge(epochSeconds, nowMs = Date.now()) {
|
|
941
|
+
const seconds = Math.max(0, Math.round(nowMs / 1e3 - epochSeconds));
|
|
942
|
+
if (seconds < 60) return "now";
|
|
943
|
+
for (const [below, unit, suffix] of AGE_STEPS) {
|
|
944
|
+
if (seconds < below) return `${Math.max(1, Math.round(seconds / unit))}${suffix}`;
|
|
945
|
+
}
|
|
946
|
+
return `${Math.floor(seconds / (365 * 86400))}y`;
|
|
947
|
+
}
|
|
948
|
+
function compactCount(count3) {
|
|
949
|
+
if (count3 < 1e4) return String(count3);
|
|
950
|
+
if (count3 < 1e6) return `${(count3 / 1e3).toFixed(1)}k`;
|
|
951
|
+
return `${(count3 / 1e6).toFixed(1)}M`;
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
// src/utils/display.ts
|
|
955
|
+
var VERSION = package_default.version;
|
|
956
|
+
var DIVIDER = "\u2500".repeat(60);
|
|
957
|
+
var MENU_DIVIDER = "\u2500".repeat(40);
|
|
958
|
+
var COMMIT_LIST_MAX = 50;
|
|
959
|
+
var COMMIT_STEP_MAX = 10;
|
|
960
|
+
var STEP_LABEL_WIDTH = 9;
|
|
961
|
+
var STEP_INDENT = 2 + STEP_LABEL_WIDTH + 2;
|
|
962
|
+
var warningMark = colors_default.yellow("\u26A0");
|
|
963
|
+
var checkMark = colors_default.green("\u2713");
|
|
964
|
+
var activeSpinner = null;
|
|
965
|
+
var jsonMode = false;
|
|
966
|
+
function setJsonMode(enabled) {
|
|
967
|
+
jsonMode = enabled;
|
|
968
|
+
if (!enabled) return;
|
|
969
|
+
const toStderr = (...args) => {
|
|
970
|
+
process5.stderr.write(`${args.map((a) => typeof a === "string" ? a : String(a)).join(" ")}
|
|
971
|
+
`);
|
|
972
|
+
};
|
|
973
|
+
console.info = toStderr;
|
|
974
|
+
console.warn = toStderr;
|
|
975
|
+
}
|
|
976
|
+
function writeStdout(text) {
|
|
977
|
+
process5.stdout.write(text.endsWith("\n") ? text : `${text}
|
|
978
|
+
`);
|
|
979
|
+
}
|
|
980
|
+
function printHeader() {
|
|
981
|
+
const left = "\u29C8 cella cli";
|
|
982
|
+
const version = ` \xB7 v${VERSION}`;
|
|
983
|
+
const right = "cellajs.com";
|
|
984
|
+
const padding = Math.max(1, DIVIDER.length - `${left}${version}${right}`.length);
|
|
985
|
+
console.info();
|
|
986
|
+
console.info(`${colors_default.cyan(left)}${colors_default.dim(version)}${colors_default.cyan(`${" ".repeat(padding)}${right}`)}`);
|
|
987
|
+
console.info(DIVIDER);
|
|
988
|
+
console.info();
|
|
989
|
+
}
|
|
990
|
+
function createSpinner(text) {
|
|
991
|
+
const enabled = !jsonMode && !getEnv("VITEST") && getEnv("NODE_ENV") !== "test" && !!process5.stdout.isTTY;
|
|
992
|
+
const frames = ["-", "\\", "|", "/"];
|
|
993
|
+
let frame = 0;
|
|
994
|
+
let timer;
|
|
995
|
+
const spinner = {
|
|
996
|
+
text,
|
|
997
|
+
stop() {
|
|
998
|
+
clearInterval(timer);
|
|
999
|
+
if (enabled) process5.stdout.write("\r\x1B[2K");
|
|
1000
|
+
}
|
|
1001
|
+
};
|
|
1002
|
+
const render = () => process5.stdout.write(`\r${colors_default.cyan(frames[frame++ % frames.length])} ${spinner.text}`);
|
|
1003
|
+
if (enabled) {
|
|
1004
|
+
render();
|
|
1005
|
+
timer = setInterval(render, 80);
|
|
1006
|
+
}
|
|
1007
|
+
activeSpinner = spinner;
|
|
1008
|
+
return spinner;
|
|
1009
|
+
}
|
|
1010
|
+
function printStep(label, detail, mark = checkMark) {
|
|
1011
|
+
stopSpinner();
|
|
1012
|
+
const [first = "", ...rest] = (detail ?? "").split("\n");
|
|
1013
|
+
console.info(`${mark} ${colors_default.bold(label.padEnd(STEP_LABEL_WIDTH))} ${first}`.trimEnd());
|
|
1014
|
+
for (const line of rest) console.info(line ? `${" ".repeat(STEP_INDENT)}${line}` : "");
|
|
1015
|
+
}
|
|
1016
|
+
function spinnerSuccess(message, detail) {
|
|
1017
|
+
stopSpinner();
|
|
1018
|
+
if (!message) return;
|
|
1019
|
+
console.info(`${checkMark} ${message}`);
|
|
1020
|
+
if (detail) {
|
|
1021
|
+
console.info(` ${colors_default.dim(detail)}`);
|
|
1022
|
+
console.info();
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
function spinnerFail(message) {
|
|
1026
|
+
stopSpinner();
|
|
1027
|
+
console.info(`${colors_default.red("\u2717")} ${message}`);
|
|
1028
|
+
}
|
|
1029
|
+
function spinnerText(text) {
|
|
1030
|
+
if (activeSpinner) {
|
|
1031
|
+
activeSpinner.text = text;
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
function stopSpinner() {
|
|
1035
|
+
if (activeSpinner) {
|
|
1036
|
+
activeSpinner.stop();
|
|
1037
|
+
activeSpinner = null;
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
function hyperlink(label, url) {
|
|
1041
|
+
if (!url) return label;
|
|
1042
|
+
const safeUrl = url.replace(/[\x00-\x1f\x7f]/g, "");
|
|
1043
|
+
return `\x1B]8;;${safeUrl}\x07${label}\x1B]8;;\x07`;
|
|
1044
|
+
}
|
|
1045
|
+
function styleSubject(subject) {
|
|
1046
|
+
if (/^chore(\([^)]*\))?: release\b/.test(subject)) return colors_default.dim(subject);
|
|
1047
|
+
const breaking = /^([a-z]+(\([^)]*\))?!:)/.exec(subject);
|
|
1048
|
+
return breaking ? `${colors_default.yellow(breaking[1])}${subject.slice(breaking[1].length)}` : subject;
|
|
1049
|
+
}
|
|
1050
|
+
function formatCheckoutStep(checkout) {
|
|
1051
|
+
const { forkPath, branch, headSha, changeCount, upstream } = checkout;
|
|
1052
|
+
const parts = [
|
|
1053
|
+
changeCount === 0 ? "clean" : `${plural(changeCount, "uncommitted change")}, not included in the analysis`
|
|
1054
|
+
];
|
|
1055
|
+
if (upstream.upstream && (upstream.ahead > 0 || upstream.behind > 0)) {
|
|
1056
|
+
parts.push(`${upstream.ahead} ahead, ${upstream.behind} behind ${upstream.upstream}`);
|
|
1057
|
+
}
|
|
1058
|
+
const line = `${branch} @ ${headSha}${colors_default.dim(` \xB7 ${parts.join(" \xB7 ")}`)}`;
|
|
1059
|
+
return resolve3(forkPath) === resolve3(process5.cwd()) ? line : `${line}
|
|
1060
|
+
${colors_default.dim(forkPath)}`;
|
|
1061
|
+
}
|
|
1062
|
+
function formatUpstreamStep(upstream) {
|
|
1063
|
+
const { label, sha, baseSha, pinned, commitCount, commits, githubUrl } = upstream;
|
|
1064
|
+
const since = commitCount === 0 ? "no new commits since the last upgrade" : `${plural(commitCount, "new commit")} since the last upgrade at ${baseSha}`;
|
|
1065
|
+
return [
|
|
1066
|
+
`${label} @ ${sha}${colors_default.dim(`${pinned ? " \xB7 --ref" : ""} \xB7 ${since}`)}`,
|
|
1067
|
+
...formatUpstreamCommits(commitCount, commits, githubUrl)
|
|
1068
|
+
].join("\n");
|
|
1069
|
+
}
|
|
1070
|
+
function formatUpstreamCommits(commitCount, commits, upstreamGitHubUrl, options = {}) {
|
|
1071
|
+
const { columns = terminalColumns(), max = COMMIT_STEP_MAX, nowMs } = options;
|
|
1072
|
+
const shown = commits.slice(-max).reverse();
|
|
1073
|
+
const rows = shown.map((commit) => ({ commit, ...splitPrNumber(commit.message) }));
|
|
1074
|
+
const prWidth = Math.max(0, ...rows.map((row) => row.pr ? `#${row.pr}`.length : 0));
|
|
1075
|
+
const ages = rows.map((row) => row.commit.timestamp ? shortAge(row.commit.timestamp, nowMs) : "");
|
|
1076
|
+
const ageWidth = Math.max(0, ...ages.map((age) => age.length));
|
|
1077
|
+
const subjectWidth = Math.max(
|
|
1078
|
+
20,
|
|
1079
|
+
columns - STEP_INDENT - 7 - 2 - (prWidth ? prWidth + 2 : 0) - (ageWidth ? ageWidth + 2 : 0)
|
|
1080
|
+
);
|
|
1081
|
+
const lines = rows.map((row, index) => {
|
|
1082
|
+
const hash = hyperlink(
|
|
1083
|
+
row.commit.hash.slice(0, 7),
|
|
1084
|
+
upstreamGitHubUrl && `${upstreamGitHubUrl}/commit/${row.commit.hash}`
|
|
1085
|
+
);
|
|
1086
|
+
const subject = fitText(row.subject, subjectWidth);
|
|
1087
|
+
let line = `${colors_default.dim(hash)} ${padVisible(styleSubject(subject), subjectWidth)}`;
|
|
1088
|
+
if (prWidth) {
|
|
1089
|
+
const pr = row.pr ? hyperlink(`#${row.pr}`, upstreamGitHubUrl && `${upstreamGitHubUrl}/pull/${row.pr}`) : "";
|
|
1090
|
+
line += ` ${colors_default.dim(padVisible(pr, prWidth))}`;
|
|
1091
|
+
}
|
|
1092
|
+
if (ageWidth) line += ` ${colors_default.dim(ages[index].padStart(ageWidth))}`;
|
|
1093
|
+
return line.trimEnd();
|
|
1094
|
+
});
|
|
1095
|
+
if (commitCount > shown.length) lines.push(colors_default.dim(`+ ${commitCount - shown.length} earlier`));
|
|
1096
|
+
return lines;
|
|
1097
|
+
}
|
|
1098
|
+
function printNextSteps(steps) {
|
|
1099
|
+
if (steps.length === 0) return;
|
|
1100
|
+
printSectionHeader(colors_default.cyan("next steps"));
|
|
1101
|
+
for (const [index, step] of steps.entries()) {
|
|
1102
|
+
const label = step.label.padEnd(STEP_INDENT - 5);
|
|
1103
|
+
console.info(` ${colors_default.dim(String(index + 1))} ${colors_default.bold(label)} ${step.line}`.trimEnd());
|
|
1104
|
+
for (const detail of step.details ?? []) console.info(`${" ".repeat(STEP_INDENT)}${detail}`);
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
function printSectionHeader(title) {
|
|
1108
|
+
console.info();
|
|
1109
|
+
console.info(title);
|
|
1110
|
+
console.info(DIVIDER);
|
|
1111
|
+
console.info();
|
|
1112
|
+
}
|
|
1113
|
+
function formatMergeInProgressDetail(conflictCount, autoMergedFiles, linkOptions, maxFiles = 100) {
|
|
1114
|
+
const lines = [`${plural(conflictCount, "conflict")} to resolve in your editor`];
|
|
1115
|
+
if (autoMergedFiles.length === 0) {
|
|
1116
|
+
return lines.join("\n");
|
|
1117
|
+
}
|
|
1118
|
+
lines.push("");
|
|
1119
|
+
lines.push(` auto-merged files (${autoMergedFiles.length}):`);
|
|
1120
|
+
const { forkPath } = linkOptions;
|
|
1121
|
+
const shown = autoMergedFiles.slice(0, maxFiles);
|
|
1122
|
+
for (const filePath of shown) {
|
|
1123
|
+
lines.push(` - ${forkPath ? hyperlink(filePath, `vscode://file${resolve3(forkPath, filePath)}`) : filePath}`);
|
|
1124
|
+
}
|
|
1125
|
+
if (autoMergedFiles.length > shown.length) {
|
|
1126
|
+
lines.push(` ... + ${autoMergedFiles.length - shown.length} more`);
|
|
1127
|
+
}
|
|
1128
|
+
return lines.join("\n");
|
|
1129
|
+
}
|
|
1130
|
+
var statusConfig = {
|
|
1131
|
+
behind: { icon: colors_default.cyan("\u2193"), label: "behind", color: colors_default.cyan, description: "upstream changed, will update" },
|
|
1132
|
+
diverged: { icon: colors_default.magenta("\u21C5"), label: "diverged", color: colors_default.magenta, description: "both changed, will merge" },
|
|
1133
|
+
drifted: { icon: colors_default.yellow("!"), label: "drifted", color: colors_default.yellow, description: "fork changed (at risk)" },
|
|
1134
|
+
ahead: { icon: colors_default.blue("\u2191"), label: "ahead", color: colors_default.blue, description: "fork changed (protected)" },
|
|
1135
|
+
local: { icon: colors_default.green("+"), label: "local", color: colors_default.green, description: "local file in cella folders" },
|
|
1136
|
+
pinned: { icon: colors_default.green("\u2A00"), label: "pinned", color: colors_default.green, description: "both changed, fork wins" },
|
|
1137
|
+
ignored: { icon: colors_default.gray("\u2A02"), label: "ignored", color: colors_default.gray },
|
|
1138
|
+
identical: { icon: colors_default.gray("\u2713"), label: "identical", color: colors_default.gray, description: "no changes" },
|
|
1139
|
+
deleted: { icon: colors_default.red("\u2717"), label: "deleted", color: colors_default.red },
|
|
1140
|
+
renamed: { icon: colors_default.blue("\u2192"), label: "renamed", color: colors_default.blue }
|
|
1141
|
+
};
|
|
1142
|
+
var summaryStatusConfig = {
|
|
1143
|
+
...statusConfig,
|
|
1144
|
+
managed: {
|
|
1145
|
+
icon: colors_default.cyan("\u25C7"),
|
|
1146
|
+
label: "managed",
|
|
1147
|
+
color: colors_default.cyan,
|
|
1148
|
+
description: "package.jsons & cella/cella.config.ts handled separately"
|
|
1149
|
+
},
|
|
1150
|
+
ignored: { ...statusConfig.ignored, description: "protected by ignored config" }
|
|
1151
|
+
};
|
|
1152
|
+
function printSummary(summary, title = "summary") {
|
|
1153
|
+
printSectionHeader(colors_default.cyan(title));
|
|
1154
|
+
const maxCount = Math.max(...Object.values(summary));
|
|
1155
|
+
const countWidth = String(maxCount).length + 1;
|
|
1156
|
+
const groups = [
|
|
1157
|
+
["managed", "ignored"],
|
|
1158
|
+
["identical"],
|
|
1159
|
+
["ahead", "local", "drifted"],
|
|
1160
|
+
["diverged", "pinned"],
|
|
1161
|
+
["behind"]
|
|
1162
|
+
];
|
|
1163
|
+
for (const group of groups) {
|
|
1164
|
+
if (group !== groups[0]) console.info();
|
|
1165
|
+
for (const status of group) {
|
|
1166
|
+
const { icon, label, color, description } = summaryStatusConfig[status];
|
|
1167
|
+
const countStr = color(String(summary[status]).padStart(countWidth));
|
|
1168
|
+
console.info(` ${icon} ${countStr} ${label.padEnd(15)}${description ? colors_default.dim(description) : ""}`);
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
function filesWithStatus(files, status) {
|
|
1173
|
+
return files.filter((f) => f.status === status && !isManagedFile(f.path));
|
|
1174
|
+
}
|
|
1175
|
+
function findProtectedBehind(files) {
|
|
1176
|
+
return files.filter((file) => file.upstreamChanged && !isManagedFile(file.path) && !isGeneratedFile(file.path));
|
|
1177
|
+
}
|
|
1178
|
+
function findProtectedBehindUpstream(result) {
|
|
1179
|
+
const ignoredOnly = new Set((result.ignoredUpstreamChanges ?? []).flatMap((group) => group.paths));
|
|
1180
|
+
return [...findProtectedBehind(result.files), ...result.files.filter((file) => ignoredOnly.has(file.path))];
|
|
1181
|
+
}
|
|
1182
|
+
var PROTECTED_BEHIND_HINT = "pinned/ignored files where upstream also changed since the last upgrade; the fork side wins on conflict, so review what upstream changed and adopt what you need.";
|
|
1183
|
+
function protectionIcon(file) {
|
|
1184
|
+
return file.isIgnored ? statusConfig.ignored.icon : statusConfig.pinned.icon;
|
|
1185
|
+
}
|
|
1186
|
+
function formatUpstreamChangedLines(file) {
|
|
1187
|
+
if (file.upstreamChangedLines === void 0) return "";
|
|
1188
|
+
return colors_default.yellow(` \xB7 ${plural(file.upstreamChangedLines, "line")} changed upstream`);
|
|
1189
|
+
}
|
|
1190
|
+
function printSyncComplete(result, options = {}) {
|
|
1191
|
+
const updated = result.files.filter((file) => file.status === "behind" || file.status === "diverged").length;
|
|
1192
|
+
const diverged = result.files.filter((file) => file.status === "diverged").length;
|
|
1193
|
+
const merged = result.autoMergedFiles?.length ?? Math.max(0, diverged - result.conflicts.length);
|
|
1194
|
+
const conflicts = result.conflicts.length;
|
|
1195
|
+
const counts = `${plural(updated, "file")} updated, ${merged} auto-merged, ${plural(conflicts, "conflict")}`;
|
|
1196
|
+
console.info();
|
|
1197
|
+
if (options.stagedBranch) {
|
|
1198
|
+
console.info(`${checkMark} upgrade merge staged on '${options.stagedBranch}'`);
|
|
1199
|
+
console.info(colors_default.dim(` ${counts}. committing next...`));
|
|
1200
|
+
} else if (conflicts > 0) {
|
|
1201
|
+
console.info(`${warningMark} upgrade merge staged with conflicts`);
|
|
1202
|
+
console.info(colors_default.dim(` ${counts}`));
|
|
1203
|
+
} else {
|
|
1204
|
+
console.info(`${checkMark} upgrade merge complete`);
|
|
1205
|
+
console.info(colors_default.dim(` ${counts}`));
|
|
1206
|
+
}
|
|
1207
|
+
printProtectedConflicts(result);
|
|
1208
|
+
console.info();
|
|
1209
|
+
}
|
|
1210
|
+
function printProtectedConflicts(result) {
|
|
1211
|
+
const paths = result.protectedConflicts ?? [];
|
|
1212
|
+
if (paths.length === 0) return;
|
|
1213
|
+
const byPath2 = new Map(result.files.map((file) => [file.path, file]));
|
|
1214
|
+
console.info();
|
|
1215
|
+
console.info(
|
|
1216
|
+
`${warningMark} ${colors_default.yellow(
|
|
1217
|
+
`${plural(paths.length, "protected file kept", "protected files kept")} the fork version, dropping upstream changes:`
|
|
1218
|
+
)}`
|
|
1219
|
+
);
|
|
1220
|
+
for (const path of paths) {
|
|
1221
|
+
const file = byPath2.get(path);
|
|
1222
|
+
const icon = file ? protectionIcon(file) : statusConfig.pinned.icon;
|
|
1223
|
+
console.info(` ${icon} ${path}${file ? formatUpstreamChangedLines(file) : ""}`);
|
|
1224
|
+
}
|
|
1225
|
+
console.info(colors_default.dim(` ${PROTECTED_BEHIND_HINT}`));
|
|
1226
|
+
}
|
|
1227
|
+
function plural(count3, singular, pluralForm = `${singular}s`) {
|
|
1228
|
+
return `${count3} ${count3 === 1 ? singular : pluralForm}`;
|
|
1229
|
+
}
|
|
1230
|
+
function printIgnoredUpstreamChanges(result) {
|
|
1231
|
+
const groups = result.ignoredUpstreamChanges ?? [];
|
|
1232
|
+
if (groups.length === 0) return;
|
|
1233
|
+
const total = groups.reduce((count3, group) => count3 + group.paths.length, 0);
|
|
1234
|
+
console.info();
|
|
1235
|
+
console.info(
|
|
1236
|
+
`${warningMark} ${colors_default.yellow(`ignored paths changed upstream \xB7 ${plural(total, "file")} under ${plural(groups.length, "entry", "entries")}`)}`
|
|
1237
|
+
);
|
|
1238
|
+
for (const group of groups) {
|
|
1239
|
+
const counts = [plural(group.paths.length, "file")];
|
|
1240
|
+
if (group.added > 0) counts.push(`${group.added} new`);
|
|
1241
|
+
if (group.deleted > 0) counts.push(`${group.deleted} deleted`);
|
|
1242
|
+
console.info(` ${statusConfig.ignored.icon} ${group.entry} ${colors_default.dim(`\xB7 ${counts.join(", ")}`)}`);
|
|
1243
|
+
}
|
|
1244
|
+
console.info(
|
|
1245
|
+
colors_default.dim(" the fork left these untouched and ignored paths never merge: review and adopt what you need.")
|
|
1246
|
+
);
|
|
1247
|
+
}
|
|
1248
|
+
function printUpstreamOverrideChanges(result) {
|
|
1249
|
+
const report = result.upstreamOverrides;
|
|
1250
|
+
if (!report) return;
|
|
1251
|
+
console.info();
|
|
1252
|
+
if (report.kind === "unreadable") {
|
|
1253
|
+
const side = report.side === "base" ? "the last upgrade" : "the incoming ref";
|
|
1254
|
+
console.info(
|
|
1255
|
+
colors_default.dim(`upstream CLI config not compared: upstream ${CONFIG_FILE} is missing or unreadable at ${side}`)
|
|
1256
|
+
);
|
|
1257
|
+
return;
|
|
1258
|
+
}
|
|
1259
|
+
const lines = [];
|
|
1260
|
+
for (const list of ["pinned", "ignored", "packageJsonKeys"]) {
|
|
1261
|
+
for (const entry of report[list].added) lines.push(` ${colors_default.green("+")} ${list}: ${entry}`);
|
|
1262
|
+
for (const entry of report[list].removed) {
|
|
1263
|
+
lines.push(` ${colors_default.red("\u2212")} ${list}: ${entry}${colors_default.dim(" \xB7 dropped upstream, still in your config")}`);
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
console.info(
|
|
1267
|
+
`${warningMark} ${colors_default.yellow(`upstream changed its CLI config \xB7 ${plural(lines.length, "entry", "entries")} to review`)}`
|
|
1268
|
+
);
|
|
1269
|
+
for (const line of lines) console.info(line);
|
|
1270
|
+
console.info(colors_default.dim(` ${CONFIG_FILE} never merges: add or drop these by hand where they fit your app.`));
|
|
1235
1271
|
}
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
const
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1272
|
+
function findMaskingPins(files) {
|
|
1273
|
+
return files.filter(
|
|
1274
|
+
(file) => file.isPinned && !isManagedFile(file.path) && file.status === "behind" && file.existsInFork && file.existsInUpstream
|
|
1275
|
+
);
|
|
1276
|
+
}
|
|
1277
|
+
function printMaskingPinWarning(files) {
|
|
1278
|
+
const masking = findMaskingPins(files);
|
|
1279
|
+
if (masking.length === 0) return;
|
|
1280
|
+
console.info();
|
|
1281
|
+
console.info(
|
|
1282
|
+
`${warningMark} ${colors_default.yellow(
|
|
1283
|
+
`${plural(masking.length, "pinned file matches", "pinned files match")} the previous upstream but changed upstream:`
|
|
1284
|
+
)}`
|
|
1285
|
+
);
|
|
1286
|
+
console.info(colors_default.yellow(" the pin keeps the old fork copy and silently drops those upstream changes:"));
|
|
1287
|
+
for (const file of masking) {
|
|
1288
|
+
console.info(colors_default.dim(` \u2A00 ${file.path}`));
|
|
1249
1289
|
}
|
|
1290
|
+
console.info(
|
|
1291
|
+
colors_default.yellow(" if the fork never customized these, unpin them in cella/cella.config.ts to take upstream.")
|
|
1292
|
+
);
|
|
1293
|
+
console.info();
|
|
1294
|
+
}
|
|
1295
|
+
function printEngineReports(result, title) {
|
|
1296
|
+
printSummary(result.summary, title);
|
|
1297
|
+
printIgnoredUpstreamChanges(result);
|
|
1298
|
+
printUpstreamOverrideChanges(result);
|
|
1299
|
+
printMaskingPinWarning(result.files);
|
|
1250
1300
|
}
|
|
1251
1301
|
|
|
1252
|
-
// src/utils/
|
|
1302
|
+
// src/utils/overrides.ts
|
|
1303
|
+
import { existsSync as existsSync5 } from "fs";
|
|
1304
|
+
import { join as join6 } from "path";
|
|
1305
|
+
|
|
1306
|
+
// src/utils/alignments.ts
|
|
1253
1307
|
import { existsSync as existsSync4 } from "fs";
|
|
1254
1308
|
import { mkdir as mkdir3, readFile as readFile2, rm, writeFile as writeFile2 } from "fs/promises";
|
|
1255
1309
|
import { dirname as dirname3, join as join5 } from "path";
|
|
1256
|
-
var
|
|
1257
|
-
var
|
|
1258
|
-
var
|
|
1310
|
+
var ALIGNMENTS_DIR = "cella/alignments";
|
|
1311
|
+
var UPSTREAM_ONLY_DIRS = [ALIGNMENTS_DIR, "cella/migrations"];
|
|
1312
|
+
var PENDING_FILE = "cella/cella.alignments.json";
|
|
1313
|
+
var ALIGNMENT_ID = /^\d{8}T\d{4}-[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
1314
|
+
var CAUGHT_BY = ["compiler", "tests", "nothing"];
|
|
1259
1315
|
function isCodemodFile(name) {
|
|
1260
1316
|
return name.endsWith(".ts") && !name.endsWith(".test.ts");
|
|
1261
1317
|
}
|
|
1318
|
+
function splitList(value) {
|
|
1319
|
+
return (value ?? "").split(",").map((item) => item.trim()).filter(Boolean);
|
|
1320
|
+
}
|
|
1262
1321
|
function isUpstreamOnly(filePath) {
|
|
1263
|
-
return filePath ===
|
|
1322
|
+
return UPSTREAM_ONLY_DIRS.some((dir) => filePath === dir || filePath.startsWith(`${dir}/`));
|
|
1264
1323
|
}
|
|
1265
1324
|
function bodyLines(lines) {
|
|
1266
1325
|
let inComment = false;
|
|
@@ -1270,7 +1329,7 @@ function bodyLines(lines) {
|
|
|
1270
1329
|
return false;
|
|
1271
1330
|
});
|
|
1272
1331
|
}
|
|
1273
|
-
function
|
|
1332
|
+
function parseAlignment(id, source, fileNames) {
|
|
1274
1333
|
const lines = source.replace(/\r\n/g, "\n").split("\n");
|
|
1275
1334
|
const close = lines[0] === "---" ? lines.indexOf("---", 1) : -1;
|
|
1276
1335
|
const meta = /* @__PURE__ */ new Map();
|
|
@@ -1282,31 +1341,75 @@ function parseNote(id, source, fileNames) {
|
|
|
1282
1341
|
const blocks = body.split(/\n\s*\n/).map((block) => block.trim()).filter(Boolean);
|
|
1283
1342
|
const title = /^# (.+)$/.exec(blocks[0] ?? "")?.[1].trim();
|
|
1284
1343
|
const summary = title && blocks[1] && !/^(#|>|[-*|]|```)/.test(blocks[1]) ? blocks[1].replace(/\s*\n\s*/g, " ") : "";
|
|
1344
|
+
const caughtBy = CAUGHT_BY.find((value) => value === meta.get("caughtBy")) ?? null;
|
|
1285
1345
|
return {
|
|
1286
1346
|
id,
|
|
1287
1347
|
title: title || id,
|
|
1288
1348
|
summary,
|
|
1289
|
-
|
|
1349
|
+
// `syncBreaking` is the field's name in alignments up to cella 0.27
|
|
1350
|
+
breaking: (meta.get("breaking") ?? meta.get("syncBreaking")) === "true",
|
|
1290
1351
|
clientCacheBump: meta.get("clientCacheBump") === "true",
|
|
1291
|
-
|
|
1352
|
+
caughtBy,
|
|
1353
|
+
generate: meta.get("generate") === "true",
|
|
1354
|
+
grep: splitList(meta.get("grep")),
|
|
1355
|
+
roots: splitList(meta.get("roots")),
|
|
1292
1356
|
codemod: fileNames.find(isCodemodFile) ?? null
|
|
1293
1357
|
};
|
|
1294
1358
|
}
|
|
1295
|
-
async function
|
|
1296
|
-
const out = await git(["ls-tree", "--name-only", `${ref}:${
|
|
1297
|
-
return out.split("\n").filter((name) =>
|
|
1359
|
+
async function listAlignmentIds(cwd, ref) {
|
|
1360
|
+
const out = await git(["ls-tree", "--name-only", `${ref}:${ALIGNMENTS_DIR}`], cwd, { ignoreErrors: true });
|
|
1361
|
+
return out.split("\n").filter((name) => ALIGNMENT_ID.test(name)).sort();
|
|
1298
1362
|
}
|
|
1299
|
-
async function
|
|
1300
|
-
const source = await git(["show", `${ref}:${
|
|
1363
|
+
async function readAlignment(cwd, ref, id) {
|
|
1364
|
+
const source = await git(["show", `${ref}:${ALIGNMENTS_DIR}/${id}/README.md`], cwd, { ignoreErrors: true });
|
|
1301
1365
|
if (!source) return null;
|
|
1302
|
-
const files = await git(["ls-tree", "--name-only", `${ref}:${
|
|
1303
|
-
return
|
|
1366
|
+
const files = await git(["ls-tree", "--name-only", `${ref}:${ALIGNMENTS_DIR}/${id}`], cwd, { ignoreErrors: true });
|
|
1367
|
+
return parseAlignment(id, source, files.split("\n").filter(Boolean));
|
|
1304
1368
|
}
|
|
1305
|
-
async function
|
|
1306
|
-
const [before, after] = await Promise.all([
|
|
1369
|
+
async function arrivedAlignmentIds(cwd, from, to) {
|
|
1370
|
+
const [before, after] = await Promise.all([listAlignmentIds(cwd, from), listAlignmentIds(cwd, to)]);
|
|
1307
1371
|
const known = new Set(before);
|
|
1308
1372
|
return after.filter((id) => !known.has(id));
|
|
1309
1373
|
}
|
|
1374
|
+
async function readAlignmentSource(cwd, ref, id) {
|
|
1375
|
+
const out = await git(
|
|
1376
|
+
["log", "-1", "--diff-filter=A", "--format=%H%x09%s", ref, "--", `${ALIGNMENTS_DIR}/${id}/README.md`],
|
|
1377
|
+
cwd,
|
|
1378
|
+
{ ignoreErrors: true }
|
|
1379
|
+
);
|
|
1380
|
+
if (!out) return null;
|
|
1381
|
+
const [commit, subject = ""] = out.split(" ");
|
|
1382
|
+
const { pr = null } = splitPrNumber(subject);
|
|
1383
|
+
const nulSeparated = (text) => text.split("\0").filter(Boolean);
|
|
1384
|
+
const [changed, added] = await Promise.all([
|
|
1385
|
+
git(["diff-tree", "--root", "--no-commit-id", "--name-only", "--no-renames", "-r", "-z", commit], cwd),
|
|
1386
|
+
git(
|
|
1387
|
+
[
|
|
1388
|
+
"diff-tree",
|
|
1389
|
+
"--root",
|
|
1390
|
+
"--no-commit-id",
|
|
1391
|
+
"--name-only",
|
|
1392
|
+
"--diff-filter=A",
|
|
1393
|
+
"-r",
|
|
1394
|
+
"-z",
|
|
1395
|
+
commit,
|
|
1396
|
+
"--",
|
|
1397
|
+
ALIGNMENTS_DIR
|
|
1398
|
+
],
|
|
1399
|
+
cwd
|
|
1400
|
+
)
|
|
1401
|
+
]);
|
|
1402
|
+
const readmeId = new RegExp(`^${ALIGNMENTS_DIR}/([^/]+)/README\\.md$`);
|
|
1403
|
+
const sharedCommit = nulSeparated(added).map((path) => readmeId.exec(path)?.[1]).filter((other) => !!other && other !== id && ALIGNMENT_ID.test(other)).sort();
|
|
1404
|
+
return { commit, pr, sharedCommit, changedFiles: nulSeparated(changed) };
|
|
1405
|
+
}
|
|
1406
|
+
async function grepAlignmentTerms(cwd, terms, inFiles) {
|
|
1407
|
+
if (terms.length === 0) return [];
|
|
1408
|
+
const out = await git(["grep", "-n", "-I", "-w", "-F", "-z", ...terms.flatMap((term) => ["-e", term]), "--"], cwd, {
|
|
1409
|
+
ignoreErrors: true
|
|
1410
|
+
});
|
|
1411
|
+
return out.split("\n").map((match) => match.split("\0")).filter(([file, line]) => file && line && inFiles(file)).map(([file, line]) => ({ file, line: Number(line) }));
|
|
1412
|
+
}
|
|
1310
1413
|
async function readPending(cwd) {
|
|
1311
1414
|
const path = join5(cwd, PENDING_FILE);
|
|
1312
1415
|
if (!existsSync4(path)) return [];
|
|
@@ -1324,14 +1427,13 @@ async function writePending(cwd, ids) {
|
|
|
1324
1427
|
await writeFile2(path, `${JSON.stringify({ pending }, null, 2)}
|
|
1325
1428
|
`, "utf8");
|
|
1326
1429
|
}
|
|
1327
|
-
function
|
|
1328
|
-
return upstreamGitHubUrl ? `${upstreamGitHubUrl}/blob/${commit}/${
|
|
1430
|
+
function alignmentUrl(upstreamGitHubUrl, commit, id) {
|
|
1431
|
+
return upstreamGitHubUrl ? `${upstreamGitHubUrl}/blob/${commit}/${ALIGNMENTS_DIR}/${id}/README.md` : void 0;
|
|
1329
1432
|
}
|
|
1330
|
-
function
|
|
1433
|
+
function formatAlignmentsLine(total, open) {
|
|
1331
1434
|
const parts = [open === 0 ? `all ${total} handled` : `${Math.max(0, total - open)} of ${total} handled`];
|
|
1332
|
-
if (
|
|
1333
|
-
|
|
1334
|
-
return `migration notes: ${parts.join(" \xB7 ")}`;
|
|
1435
|
+
if (open > 0) parts.push(`${open} open: pnpm cella align`);
|
|
1436
|
+
return `alignments: ${parts.join(" \xB7 ")}`;
|
|
1335
1437
|
}
|
|
1336
1438
|
|
|
1337
1439
|
// src/utils/overrides.ts
|
|
@@ -1450,11 +1552,12 @@ var forkOption = ["--fork <name>", "pre-select a fork by name (skips the fork se
|
|
|
1450
1552
|
var serviceDefinitions = [
|
|
1451
1553
|
{
|
|
1452
1554
|
name: "analyze",
|
|
1453
|
-
description: "dry run to see what would change on
|
|
1555
|
+
description: "dry run to see what would change on upgrade",
|
|
1454
1556
|
options: [
|
|
1455
1557
|
["--list", "non-interactive output for tooling (one file per line)"],
|
|
1456
1558
|
jsonOption,
|
|
1457
1559
|
["--scope <scope>", "analyze scope for --list/--json: all|risk|protected"],
|
|
1560
|
+
["--all", "list every file instead of collapsing large groups"],
|
|
1458
1561
|
trackOption,
|
|
1459
1562
|
refOption,
|
|
1460
1563
|
unpinnedOption
|
|
@@ -1462,30 +1565,31 @@ var serviceDefinitions = [
|
|
|
1462
1565
|
includeInMenu: (context) => !context.isUpstreamRepo
|
|
1463
1566
|
},
|
|
1464
1567
|
{
|
|
1465
|
-
name: "
|
|
1466
|
-
description: "merge upstream changes onto a fresh branch,
|
|
1568
|
+
name: "upgrade",
|
|
1569
|
+
description: "merge upstream changes onto a fresh branch, merge package.json and open a squash-merge PR",
|
|
1570
|
+
aliases: ["sync"],
|
|
1467
1571
|
options: [
|
|
1468
1572
|
trackOption,
|
|
1469
1573
|
refOption,
|
|
1470
|
-
["--keep-config", "merge with your
|
|
1574
|
+
["--keep-config", "merge with your CLI config as it stands when upstream changed its own"],
|
|
1471
1575
|
unpinnedOption
|
|
1472
1576
|
],
|
|
1473
1577
|
includeInMenu: (context) => !context.isUpstreamRepo
|
|
1474
1578
|
},
|
|
1475
1579
|
{
|
|
1476
|
-
name: "
|
|
1477
|
-
description: "list the upstream
|
|
1580
|
+
name: "align",
|
|
1581
|
+
description: "list the upstream alignments this app has not handled yet",
|
|
1478
1582
|
options: [
|
|
1479
|
-
["--all", "list every upstream
|
|
1583
|
+
["--all", "list every upstream alignment, handled or not"],
|
|
1480
1584
|
jsonOption,
|
|
1481
|
-
["--show <id>", "print one
|
|
1482
|
-
["--extract <id>", "write one
|
|
1483
|
-
["--run <id>", "run one
|
|
1484
|
-
["--mark <ids...>", "record
|
|
1585
|
+
["--show <id>", "print one alignment's README and what git tells about it"],
|
|
1586
|
+
["--extract <id>", "write one alignment's folder under node_modules/.cache to run its codemod"],
|
|
1587
|
+
["--run <id>", "run one alignment's codemod; files identical to upstream stay as they are"],
|
|
1588
|
+
["--mark <ids...>", "record alignments as handled"]
|
|
1485
1589
|
],
|
|
1486
1590
|
operands: [
|
|
1487
1591
|
"[codemodArgs...]",
|
|
1488
|
-
"with --run, after `--`: arguments for the codemod (default: inventory and the
|
|
1592
|
+
"with --run, after `--`: arguments for the codemod (default: inventory and the alignment's roots)"
|
|
1489
1593
|
],
|
|
1490
1594
|
includeInMenu: (context) => !context.isUpstreamRepo
|
|
1491
1595
|
},
|
|
@@ -1496,10 +1600,10 @@ var serviceDefinitions = [
|
|
|
1496
1600
|
},
|
|
1497
1601
|
{
|
|
1498
1602
|
name: "forks",
|
|
1499
|
-
description: "run
|
|
1603
|
+
description: "run the upgrade inside local fork repositories",
|
|
1500
1604
|
options: [
|
|
1501
1605
|
forkOption,
|
|
1502
|
-
["--keep-config", "merge with the fork's
|
|
1606
|
+
["--keep-config", "merge with the fork's CLI config as it stands when upstream changed its own"]
|
|
1503
1607
|
],
|
|
1504
1608
|
includeInMenu: (context) => context.hasForks
|
|
1505
1609
|
},
|
|
@@ -1538,17 +1642,21 @@ function buildServiceChoices(context) {
|
|
|
1538
1642
|
];
|
|
1539
1643
|
}
|
|
1540
1644
|
function addServiceCommand(program, definition, setSelection) {
|
|
1541
|
-
const
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1645
|
+
for (const name of [definition.name, ...definition.aliases ?? []]) {
|
|
1646
|
+
const renamedFrom = name === definition.name ? void 0 : name;
|
|
1647
|
+
const command = program.command(name, { hidden: !!renamedFrom }).description(definition.description);
|
|
1648
|
+
for (const option of definition.options ?? []) command.option(...option);
|
|
1649
|
+
if (definition.operands) command.argument(...definition.operands);
|
|
1650
|
+
command.action((...args) => {
|
|
1651
|
+
const operands = definition.operands ? args[0] : [];
|
|
1652
|
+
const opts = definition.operands ? args[1] : args[0];
|
|
1653
|
+
setSelection({
|
|
1654
|
+
service: definition.name,
|
|
1655
|
+
renamedFrom,
|
|
1656
|
+
options: readOptions(opts, operands)
|
|
1657
|
+
});
|
|
1550
1658
|
});
|
|
1551
|
-
}
|
|
1659
|
+
}
|
|
1552
1660
|
}
|
|
1553
1661
|
function buildProgram(setSelection) {
|
|
1554
1662
|
const program = new Command("cella").version(VERSION, "-v, --version", "output the current version").usage("[service] [options]").helpOption("-h, --help", "display help for a command").showHelpAfterError().addHelpText(
|
|
@@ -1558,10 +1666,10 @@ function buildProgram(setSelection) {
|
|
|
1558
1666
|
"Examples:",
|
|
1559
1667
|
" $ cella analyze",
|
|
1560
1668
|
" $ cella analyze --json --scope risk",
|
|
1561
|
-
" $ cella
|
|
1562
|
-
" $ cella
|
|
1563
|
-
" $ cella
|
|
1564
|
-
" $ cella
|
|
1669
|
+
" $ cella upgrade --track branch",
|
|
1670
|
+
" $ cella upgrade --ref 4f7d87c",
|
|
1671
|
+
" $ cella align --run 20261001T2116-tailwind-class-conventions -- rewrite frontend/src",
|
|
1672
|
+
" $ cella align --mark 20261002T0614-config-switch",
|
|
1565
1673
|
" $ cella audit --list",
|
|
1566
1674
|
" $ cella stats --since origin/main --md",
|
|
1567
1675
|
" $ cella contributions --fork raak --json"
|
|
@@ -1592,7 +1700,7 @@ async function promptForService(userConfig, forkPath) {
|
|
|
1592
1700
|
if (selected === "exit") {
|
|
1593
1701
|
console.info(colors_default.dim("exiting..."));
|
|
1594
1702
|
console.info();
|
|
1595
|
-
|
|
1703
|
+
process6.exit(0);
|
|
1596
1704
|
}
|
|
1597
1705
|
console.info();
|
|
1598
1706
|
return selected;
|
|
@@ -1601,12 +1709,18 @@ function buildRuntimeConfig(userConfig, forkPath, selection, upstreamRef) {
|
|
|
1601
1709
|
return { ...userConfig, forkPath, upstreamRef, service: selection.service ?? "analyze", ...selection.options };
|
|
1602
1710
|
}
|
|
1603
1711
|
async function parseCli(userConfig, forkPath) {
|
|
1604
|
-
const selection = parseCommandLine(
|
|
1712
|
+
const selection = parseCommandLine(process6.argv);
|
|
1605
1713
|
const machineList = selection.options.list && selection.service !== "audit";
|
|
1606
1714
|
if (selection.options.json || selection.options.md || machineList) setJsonMode(true);
|
|
1607
1715
|
printHeader();
|
|
1608
1716
|
const { branchRef } = resolveUpstream(userConfig.settings);
|
|
1609
|
-
const warnings =
|
|
1717
|
+
const warnings = [
|
|
1718
|
+
...renamedSettingWarnings(userConfig),
|
|
1719
|
+
...await validateOverrides(userConfig, forkPath, branchRef)
|
|
1720
|
+
];
|
|
1721
|
+
if (selection.renamedFrom) {
|
|
1722
|
+
warnings.unshift(`\`cella ${selection.renamedFrom}\` is renamed to \`cella ${selection.service}\``);
|
|
1723
|
+
}
|
|
1610
1724
|
for (const warning of warnings) console.info(`${warningMark} ${warning}`);
|
|
1611
1725
|
if (warnings.length > 0) console.info();
|
|
1612
1726
|
if (!selection.service) {
|
|
@@ -1615,6 +1729,12 @@ async function parseCli(userConfig, forkPath) {
|
|
|
1615
1729
|
return buildRuntimeConfig(userConfig, forkPath, selection, branchRef);
|
|
1616
1730
|
}
|
|
1617
1731
|
|
|
1732
|
+
// src/services/align.ts
|
|
1733
|
+
import { execFileSync, spawnSync } from "child_process";
|
|
1734
|
+
import { mkdir as mkdir4, writeFile as writeFile3 } from "fs/promises";
|
|
1735
|
+
import { dirname as dirname4, join as join8 } from "path";
|
|
1736
|
+
import process7 from "process";
|
|
1737
|
+
|
|
1618
1738
|
// src/services/merge-engine.ts
|
|
1619
1739
|
import { existsSync as existsSync6 } from "fs";
|
|
1620
1740
|
import { rm as rm2 } from "fs/promises";
|
|
@@ -1689,7 +1809,7 @@ function cliVersionMismatch(running, range) {
|
|
|
1689
1809
|
const current = parseComparableVersion(running);
|
|
1690
1810
|
if (!required || !current || compareVersions(current, required) >= 0) return null;
|
|
1691
1811
|
return [
|
|
1692
|
-
`upstream needs ${CLI_PACKAGE} ${range}, this is ${running}: an older CLI misses fixes to the
|
|
1812
|
+
`upstream needs ${CLI_PACKAGE} ${range}, this is ${running}: an older CLI misses fixes to the upgrade itself.`,
|
|
1693
1813
|
` update it, commit, then rerun: pnpm add -D -w '${CLI_PACKAGE}@${range}'`
|
|
1694
1814
|
].join("\n");
|
|
1695
1815
|
}
|
|
@@ -1743,12 +1863,13 @@ function readStringEntries(node) {
|
|
|
1743
1863
|
}
|
|
1744
1864
|
return entries;
|
|
1745
1865
|
}
|
|
1746
|
-
function
|
|
1866
|
+
function readPackageJsonKeys(config, sourceFile) {
|
|
1747
1867
|
const settings = readProperty(config, "settings", sourceFile);
|
|
1748
|
-
if (settings === void 0) return [...
|
|
1868
|
+
if (settings === void 0) return [...DEFAULT_PACKAGE_JSON_KEYS];
|
|
1749
1869
|
if (!settings || !Node.isObjectLiteralExpression(settings)) return null;
|
|
1750
|
-
const
|
|
1751
|
-
|
|
1870
|
+
const current = readProperty(settings, "packageJsonKeys", sourceFile);
|
|
1871
|
+
const keys = current === void 0 ? readProperty(settings, "packageJsonSync", sourceFile) : current;
|
|
1872
|
+
if (keys === void 0) return [...DEFAULT_PACKAGE_JSON_KEYS];
|
|
1752
1873
|
return keys ? readStringEntries(keys) : null;
|
|
1753
1874
|
}
|
|
1754
1875
|
function parseOverrideLists(source) {
|
|
@@ -1760,7 +1881,7 @@ function parseOverrideLists(source) {
|
|
|
1760
1881
|
const lists = {
|
|
1761
1882
|
pinned: [],
|
|
1762
1883
|
ignored: [],
|
|
1763
|
-
|
|
1884
|
+
packageJsonKeys: readPackageJsonKeys(config, sourceFile)
|
|
1764
1885
|
};
|
|
1765
1886
|
const overrides = readProperty(config, "overrides", sourceFile);
|
|
1766
1887
|
if (overrides === void 0) return lists;
|
|
@@ -1788,14 +1909,14 @@ function diffOverrideLists(base, incoming, fork) {
|
|
|
1788
1909
|
};
|
|
1789
1910
|
};
|
|
1790
1911
|
const diffKeys = () => {
|
|
1791
|
-
const [baseKeys, incomingKeys, forkKeys] = [base.
|
|
1912
|
+
const [baseKeys, incomingKeys, forkKeys] = [base.packageJsonKeys, incoming.packageJsonKeys, fork.packageJsonKeys];
|
|
1792
1913
|
if (!baseKeys || !incomingKeys || !forkKeys) return { added: [], removed: [] };
|
|
1793
1914
|
return {
|
|
1794
1915
|
added: [...new Set(incomingKeys)].filter((key) => !baseKeys.includes(key) && !forkKeys.includes(key)),
|
|
1795
1916
|
removed: [...new Set(baseKeys)].filter((key) => !incomingKeys.includes(key) && forkKeys.includes(key))
|
|
1796
1917
|
};
|
|
1797
1918
|
};
|
|
1798
|
-
return { pinned: diff("pinned"), ignored: diff("ignored"),
|
|
1919
|
+
return { pinned: diff("pinned"), ignored: diff("ignored"), packageJsonKeys: diffKeys() };
|
|
1799
1920
|
}
|
|
1800
1921
|
async function compareUpstreamOverrides(repoPath, mergeBaseRef, incomingRef, forkConfig) {
|
|
1801
1922
|
const [baseSource, incomingSource] = await Promise.all(
|
|
@@ -1809,7 +1930,7 @@ async function compareUpstreamOverrides(repoPath, mergeBaseRef, incomingRef, for
|
|
|
1809
1930
|
const fork = {
|
|
1810
1931
|
pinned: forkConfig.overrides?.pinned ?? [],
|
|
1811
1932
|
ignored: forkConfig.overrides?.ignored ?? [],
|
|
1812
|
-
|
|
1933
|
+
packageJsonKeys: forkConfig.settings.packageJsonKeys ?? DEFAULT_PACKAGE_JSON_KEYS
|
|
1813
1934
|
};
|
|
1814
1935
|
const changes = diffOverrideLists(base, incoming, fork);
|
|
1815
1936
|
const count3 = Object.values(changes).reduce((n, list) => n + list.added.length + list.removed.length, 0);
|
|
@@ -1936,7 +2057,9 @@ async function analyzeRefs(repoPath, localRef, incomingRef, mergeBaseRef, predic
|
|
|
1936
2057
|
const upstreamOnly = fileIsIgnored && isReportableIgnored(filePath) && upstreamHash !== baseHash && forkHash === baseHash;
|
|
1937
2058
|
push(status, { upstreamChanged: upstreamChanged || void 0, upstreamOnly: upstreamOnly || void 0 });
|
|
1938
2059
|
}
|
|
1939
|
-
const flagged = analyzedFiles.filter(
|
|
2060
|
+
const flagged = analyzedFiles.filter(
|
|
2061
|
+
(file) => file.upstreamChanged && !renamedProtected.has(file.path) || file.upstreamOnly
|
|
2062
|
+
);
|
|
1940
2063
|
if (flagged.length > 0) {
|
|
1941
2064
|
const stat = await getDiffStat(
|
|
1942
2065
|
repoPath,
|
|
@@ -1983,7 +2106,7 @@ async function enrichChangeInfo(repoPath, files, mergeBaseRef, localRef, incomin
|
|
|
1983
2106
|
setChanged(file, forkInfo.get(file.path));
|
|
1984
2107
|
} else if (file.status === "behind") {
|
|
1985
2108
|
setChanged(file, upstreamInfo.get(file.path));
|
|
1986
|
-
} else if (file.status === "diverged" || file.status === "pinned" || file.upstreamChanged) {
|
|
2109
|
+
} else if (file.status === "diverged" || file.status === "pinned" || file.upstreamChanged || file.upstreamOnly) {
|
|
1987
2110
|
setChanged(file, forkInfo.get(file.path));
|
|
1988
2111
|
const info = upstreamInfo.get(file.path);
|
|
1989
2112
|
if (info) {
|
|
@@ -2003,7 +2126,7 @@ var UpstreamConfigChangedError = class extends Error {
|
|
|
2003
2126
|
constructor(report) {
|
|
2004
2127
|
super(
|
|
2005
2128
|
[
|
|
2006
|
-
"upstream changed its
|
|
2129
|
+
"upstream changed its CLI config and yours does not follow it: stopped before the merge, nothing changed.",
|
|
2007
2130
|
` add or drop the entries that fit your app in ${CONFIG_FILE}, commit, then rerun.`,
|
|
2008
2131
|
" to merge with your config as it stands, rerun with --keep-config."
|
|
2009
2132
|
].join("\n")
|
|
@@ -2015,6 +2138,10 @@ function getGitHubBaseUrl(remoteUrl) {
|
|
|
2015
2138
|
const match = remoteUrl.match(/github\.com[:/]([^/]+)\/([^/]+?)(?:\.git)?$/);
|
|
2016
2139
|
return match ? `https://github.com/${match[1]}/${match[2]}` : null;
|
|
2017
2140
|
}
|
|
2141
|
+
async function readOriginGitHubUrl(forkPath) {
|
|
2142
|
+
const origin = await git(["remote", "get-url", "origin"], forkPath, { ignoreErrors: true });
|
|
2143
|
+
return getGitHubBaseUrl(origin.trim()) ?? void 0;
|
|
2144
|
+
}
|
|
2018
2145
|
function isProtectedFile(file) {
|
|
2019
2146
|
return file.isIgnored || file.isPinned;
|
|
2020
2147
|
}
|
|
@@ -2150,11 +2277,6 @@ function calculateSummary(files) {
|
|
|
2150
2277
|
}
|
|
2151
2278
|
return summary;
|
|
2152
2279
|
}
|
|
2153
|
-
function formatLocalCheckoutDetail(forkPath, branch, headSha, changeCount, upstream) {
|
|
2154
|
-
const tracking = upstream.upstream ? `, tracking ${upstream.upstream} (${upstream.ahead} ahead, ${upstream.behind} behind)` : ", no tracking branch";
|
|
2155
|
-
const workingTree = changeCount === 0 ? " working tree clean" : ` ${changeCount} uncommitted change${changeCount === 1 ? "" : "s"} in working tree, not included in analysis`;
|
|
2156
|
-
return [`${forkPath}`, ` ${branch} @ ${headSha}${tracking}`, workingTree].join("\n");
|
|
2157
|
-
}
|
|
2158
2280
|
async function resolvePinnedRef(forkPath, config, remoteName, branchRef, ref) {
|
|
2159
2281
|
const resolved = await resolveUpstreamCommit(forkPath, remoteName, ref);
|
|
2160
2282
|
if (!resolved) {
|
|
@@ -2164,7 +2286,7 @@ async function resolvePinnedRef(forkPath, config, remoteName, branchRef, ref) {
|
|
|
2164
2286
|
}
|
|
2165
2287
|
if (!await isPublishedUpstream(forkPath, remoteName, branchRef, resolved.sha)) {
|
|
2166
2288
|
throw new Error(
|
|
2167
|
-
`--ref '${ref}' (${resolved.sha.slice(0, 7)}) is not on ${branchRef} or in an upstream release.
|
|
2289
|
+
`--ref '${ref}' (${resolved.sha.slice(0, 7)}) is not on ${branchRef} or in an upstream release. Upgrade only to commits upstream has published there.`
|
|
2168
2290
|
);
|
|
2169
2291
|
}
|
|
2170
2292
|
return resolved.release ? { ref: resolved.release.ref, releaseTag: resolved.release.tag } : { ref: resolved.sha };
|
|
@@ -2178,7 +2300,7 @@ async function prepareUpstream(config, onProgress, onStep) {
|
|
|
2178
2300
|
getWorkingTreeChangeCount(forkPath),
|
|
2179
2301
|
getUpstreamStatus(forkPath)
|
|
2180
2302
|
]);
|
|
2181
|
-
onStep?.("
|
|
2303
|
+
onStep?.("checkout", formatCheckoutStep({ forkPath, branch, headSha, changeCount, upstream: upstreamStatus }));
|
|
2182
2304
|
onProgress?.("setting up upstream remote...");
|
|
2183
2305
|
const { track: configTrack, branchRef } = resolveUpstream(config.settings);
|
|
2184
2306
|
const remoteName = DEFAULT_UPSTREAM_REMOTE;
|
|
@@ -2206,7 +2328,6 @@ async function prepareUpstream(config, onProgress, onStep) {
|
|
|
2206
2328
|
}
|
|
2207
2329
|
const upstreamLabel = releaseTag ?? (config.ref ? `--ref ${config.ref}` : upstreamRef);
|
|
2208
2330
|
config.upstreamRef = upstreamRef;
|
|
2209
|
-
onStep?.("remote configured", `${upstreamLabel} \u2192 ${config.settings.upstreamUrl}`);
|
|
2210
2331
|
const cliRange = await readUpstreamCliRange(forkPath, upstreamRef);
|
|
2211
2332
|
const cliMismatch = cliRange ? cliVersionMismatch(VERSION, cliRange) : null;
|
|
2212
2333
|
if (cliMismatch) throw new Error(cliMismatch);
|
|
@@ -2217,20 +2338,35 @@ async function prepareUpstream(config, onProgress, onStep) {
|
|
|
2217
2338
|
const mergeRef = config.unpinned ? await getMergeBase(forkPath, "HEAD", upstreamRef) : mergeBase;
|
|
2218
2339
|
const upstreamCommit = await getCommitInfo(forkPath, upstreamRef);
|
|
2219
2340
|
if (upstreamCommit.hash !== mergeBase && await isAncestor(forkPath, upstreamCommit.hash, mergeBase)) {
|
|
2220
|
-
const [
|
|
2341
|
+
const [upstreamShort2, syncShort] = await Promise.all([
|
|
2221
2342
|
getShortSha(forkPath, upstreamCommit.hash),
|
|
2222
2343
|
getShortSha(forkPath, mergeBase)
|
|
2223
2344
|
]);
|
|
2224
|
-
const
|
|
2345
|
+
const hint2 = config.ref ? "Pick a newer --ref." : releaseTag ? `Nothing to upgrade until a release past ${syncShort}; to follow the tip instead, run with --track branch.` : `Nothing to upgrade until '${branchRef}' moves past ${syncShort}.`;
|
|
2225
2346
|
throw new BehindSyncPointError(
|
|
2226
|
-
`upstream ${upstreamLabel} (${
|
|
2227
|
-
${
|
|
2347
|
+
`upstream ${upstreamLabel} (${upstreamShort2}) is behind the last upgrade at ${syncShort}: the fork already has it, and merging it would revert the upstream changes after it.
|
|
2348
|
+
${hint2}`
|
|
2228
2349
|
);
|
|
2229
2350
|
}
|
|
2230
2351
|
const commitCount = await countCommitsBetween(forkPath, mergeBase, upstreamRef);
|
|
2231
|
-
const upstreamCommits = commitCount > 0 ? await listCommitsBetween(forkPath, mergeBase, upstreamRef,
|
|
2232
|
-
|
|
2233
|
-
|
|
2352
|
+
const upstreamCommits = commitCount > 0 ? await listCommitsBetween(forkPath, mergeBase, upstreamRef, COMMIT_STEP_MAX) : [];
|
|
2353
|
+
const [upstreamShort, baseShort] = await Promise.all([
|
|
2354
|
+
getShortSha(forkPath, upstreamCommit.hash),
|
|
2355
|
+
getShortSha(forkPath, mergeBase)
|
|
2356
|
+
]);
|
|
2357
|
+
onStep?.(
|
|
2358
|
+
"upstream",
|
|
2359
|
+
formatUpstreamStep({
|
|
2360
|
+
label: releaseTag ?? branchRef.replace(`${remoteName}/`, ""),
|
|
2361
|
+
sha: upstreamShort,
|
|
2362
|
+
baseSha: baseShort,
|
|
2363
|
+
pinned: !!config.ref,
|
|
2364
|
+
commitCount,
|
|
2365
|
+
commits: upstreamCommits,
|
|
2366
|
+
githubUrl: upstreamGitHubUrl
|
|
2367
|
+
})
|
|
2368
|
+
);
|
|
2369
|
+
return { upstreamRef, releaseTag, mergeBase, mergeRef, upstreamGitHubUrl, upstreamCommit, commitCount };
|
|
2234
2370
|
}
|
|
2235
2371
|
async function assertSyncConfigFollowed(config, ctx) {
|
|
2236
2372
|
if (config.keepConfig) return;
|
|
@@ -2245,7 +2381,8 @@ function resultMeta(config, ctx) {
|
|
|
2245
2381
|
upstreamRef: ctx.upstreamRef,
|
|
2246
2382
|
upstreamTag: ctx.releaseTag,
|
|
2247
2383
|
upstreamGitHubUrl: ctx.upstreamGitHubUrl,
|
|
2248
|
-
upstreamCommit: ctx.upstreamCommit
|
|
2384
|
+
upstreamCommit: ctx.upstreamCommit,
|
|
2385
|
+
upstreamCommitCount: ctx.commitCount
|
|
2249
2386
|
};
|
|
2250
2387
|
}
|
|
2251
2388
|
async function upstreamOnlyReports(config, ctx, files) {
|
|
@@ -2266,33 +2403,40 @@ async function upstreamOnlyReports(config, ctx, files) {
|
|
|
2266
2403
|
return {};
|
|
2267
2404
|
}
|
|
2268
2405
|
}
|
|
2269
|
-
|
|
2406
|
+
function pendingAfterSync(alignments) {
|
|
2407
|
+
return [.../* @__PURE__ */ new Set([...alignments.pending, ...alignments.arrived])];
|
|
2408
|
+
}
|
|
2409
|
+
async function readAlignmentsInRange(config, ctx) {
|
|
2270
2410
|
const { forkPath } = config;
|
|
2271
2411
|
try {
|
|
2272
2412
|
const [total, arrived, pending] = await Promise.all([
|
|
2273
|
-
|
|
2274
|
-
|
|
2413
|
+
listAlignmentIds(forkPath, ctx.upstreamRef),
|
|
2414
|
+
arrivedAlignmentIds(forkPath, ctx.mergeBase, ctx.upstreamRef),
|
|
2275
2415
|
readPending(forkPath)
|
|
2276
2416
|
]);
|
|
2277
|
-
return { total: total.length, arrived, pending
|
|
2417
|
+
return { total: total.length, arrived, pending };
|
|
2278
2418
|
} catch {
|
|
2279
2419
|
return void 0;
|
|
2280
2420
|
}
|
|
2281
2421
|
}
|
|
2282
|
-
function
|
|
2283
|
-
|
|
2422
|
+
function alignmentsResult(alignments, options) {
|
|
2423
|
+
if (!alignments) return {};
|
|
2424
|
+
const open = options.dryRun ? alignments.pending : pendingAfterSync(alignments);
|
|
2425
|
+
return { alignments: { total: alignments.total, arrived: alignments.arrived, open } };
|
|
2284
2426
|
}
|
|
2285
2427
|
async function removeUpstreamOnlyCopy(forkPath, onStep) {
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2428
|
+
for (const dir of UPSTREAM_ONLY_DIRS) {
|
|
2429
|
+
if (!existsSync6(join7(forkPath, dir))) continue;
|
|
2430
|
+
await git(["rm", "-r", "-q", "--ignore-unmatch", "--", dir], forkPath);
|
|
2431
|
+
await rm2(join7(forkPath, dir), { recursive: true, force: true });
|
|
2432
|
+
onStep?.("alignments", `removed ${dir}/: alignments stay upstream, list them with pnpm cella align`);
|
|
2433
|
+
}
|
|
2290
2434
|
}
|
|
2291
2435
|
async function runSyncMerge(config, ctx, onProgress, onStep) {
|
|
2292
2436
|
const { forkPath } = config;
|
|
2293
2437
|
const { upstreamRef, releaseTag, mergeRef, upstreamGitHubUrl, upstreamCommit } = ctx;
|
|
2294
2438
|
await assertSyncConfigFollowed(config, ctx);
|
|
2295
|
-
const
|
|
2439
|
+
const alignments = await readAlignmentsInRange(config, ctx);
|
|
2296
2440
|
const { remainingConflicts, analyzedFiles, autoMergedFiles, protectedConflicts } = await applyDirectMerge(
|
|
2297
2441
|
forkPath,
|
|
2298
2442
|
upstreamRef,
|
|
@@ -2310,32 +2454,29 @@ async function runSyncMerge(config, ctx, onProgress, onStep) {
|
|
|
2310
2454
|
commit: upstreamCommit.hash,
|
|
2311
2455
|
release: releaseTag ?? null,
|
|
2312
2456
|
url: upstreamGitHubUrl ? releaseTag ? `${upstreamGitHubUrl}/releases/tag/${releaseTag}` : `${upstreamGitHubUrl}/commit/${upstreamCommit.hash}` : void 0,
|
|
2313
|
-
|
|
2457
|
+
upgradedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2314
2458
|
}
|
|
2315
2459
|
};
|
|
2316
2460
|
const recordSyncPoint = async () => {
|
|
2317
2461
|
await storeLastSyncRef(forkPath, upstreamCommit.hash);
|
|
2318
2462
|
await writeSyncManifest(forkPath, syncManifest);
|
|
2319
2463
|
await stagePath(forkPath, MANIFEST_FILE);
|
|
2320
|
-
if (
|
|
2321
|
-
await writePending(forkPath,
|
|
2464
|
+
if (alignments) {
|
|
2465
|
+
await writePending(forkPath, pendingAfterSync(alignments));
|
|
2322
2466
|
await stagePath(forkPath, PENDING_FILE);
|
|
2323
2467
|
}
|
|
2324
2468
|
await removeUpstreamOnlyCopy(forkPath, onStep);
|
|
2325
2469
|
};
|
|
2326
2470
|
if (remainingConflicts.length > 0) {
|
|
2327
2471
|
await recordSyncPoint();
|
|
2328
|
-
onStep?.(
|
|
2329
|
-
"merge in progress",
|
|
2330
|
-
formatMergeInProgressDetail(remainingConflicts.length, autoMergedFiles, { forkPath }, 100)
|
|
2331
|
-
);
|
|
2472
|
+
onStep?.("merged", formatMergeInProgressDetail(remainingConflicts.length, autoMergedFiles, { forkPath }, 100));
|
|
2332
2473
|
} else if (totalResolved > 0) {
|
|
2333
2474
|
const label = synced > 0 ? `${synced} files from upstream` : "upstream changes";
|
|
2334
2475
|
await recordSyncPoint();
|
|
2335
|
-
onStep?.("
|
|
2476
|
+
onStep?.("upgraded", `${label} (staged, commit to finish)`);
|
|
2336
2477
|
} else {
|
|
2337
2478
|
await mergeAbort(forkPath);
|
|
2338
|
-
onStep?.("up to date", "no upstream changes to
|
|
2479
|
+
onStep?.("up to date", "no upstream changes to merge");
|
|
2339
2480
|
}
|
|
2340
2481
|
return {
|
|
2341
2482
|
success: remainingConflicts.length === 0,
|
|
@@ -2346,12 +2487,12 @@ async function runSyncMerge(config, ctx, onProgress, onStep) {
|
|
|
2346
2487
|
protectedConflicts,
|
|
2347
2488
|
...resultMeta(config, ctx),
|
|
2348
2489
|
...await upstreamOnlyReports(config, ctx, analyzedFiles),
|
|
2349
|
-
...
|
|
2490
|
+
...alignmentsResult(alignments, { dryRun: false })
|
|
2350
2491
|
};
|
|
2351
2492
|
}
|
|
2352
2493
|
async function runAnalyzePreview(config, ctx, onProgress, onStep) {
|
|
2353
2494
|
const { forkPath } = config;
|
|
2354
|
-
const
|
|
2495
|
+
const alignments = await readAlignmentsInRange(config, ctx);
|
|
2355
2496
|
onProgress?.("analyzing files...");
|
|
2356
2497
|
const analyzedFiles = await analyzeRefs(
|
|
2357
2498
|
forkPath,
|
|
@@ -2364,7 +2505,7 @@ async function runAnalyzePreview(config, ctx, onProgress, onStep) {
|
|
|
2364
2505
|
await enrichChangeInfo(forkPath, analyzedFiles, ctx.mergeBase, "HEAD", ctx.upstreamRef);
|
|
2365
2506
|
const protectedConflicts = analyzedFiles.filter((f) => f.upstreamChanged).map((f) => f.path);
|
|
2366
2507
|
const reports = await upstreamOnlyReports(config, ctx, analyzedFiles);
|
|
2367
|
-
onStep?.("
|
|
2508
|
+
onStep?.("analyzed", `${plural(analyzedFiles.length, "file")}${colors_default.dim(" \xB7 dry run, nothing changed")}`);
|
|
2368
2509
|
return {
|
|
2369
2510
|
success: true,
|
|
2370
2511
|
files: analyzedFiles,
|
|
@@ -2374,25 +2515,23 @@ async function runAnalyzePreview(config, ctx, onProgress, onStep) {
|
|
|
2374
2515
|
protectedConflicts,
|
|
2375
2516
|
...resultMeta(config, ctx),
|
|
2376
2517
|
...reports,
|
|
2377
|
-
...
|
|
2518
|
+
...alignmentsResult(alignments, { dryRun: true })
|
|
2378
2519
|
};
|
|
2379
2520
|
}
|
|
2380
2521
|
async function runEngineWithSpinner(config, apply) {
|
|
2381
2522
|
const onProgress = (message) => spinnerText(message);
|
|
2382
2523
|
const onStep = (label, detail) => {
|
|
2383
|
-
|
|
2524
|
+
printStep(label, detail);
|
|
2384
2525
|
createSpinner("...");
|
|
2385
2526
|
};
|
|
2386
2527
|
const ctx = await prepareUpstream(config, onProgress, onStep);
|
|
2387
2528
|
return apply ? runSyncMerge(config, ctx, onProgress, onStep) : runAnalyzePreview(config, ctx, onProgress, onStep);
|
|
2388
2529
|
}
|
|
2389
2530
|
|
|
2390
|
-
// src/services/
|
|
2391
|
-
|
|
2392
|
-
import { mkdir as mkdir4, writeFile as writeFile3 } from "fs/promises";
|
|
2393
|
-
import { dirname as dirname4, join as join8 } from "path";
|
|
2394
|
-
var EXTRACT_DIR = "node_modules/.cache/cella/migrations";
|
|
2531
|
+
// src/services/align.ts
|
|
2532
|
+
var EXTRACT_DIR = "node_modules/.cache/cella/alignments";
|
|
2395
2533
|
var RESTORED_LIST_MAX = 20;
|
|
2534
|
+
var SHOW_LIST_MAX = 30;
|
|
2396
2535
|
async function ensureUpstreamCommit(config, sha) {
|
|
2397
2536
|
const { forkPath } = config;
|
|
2398
2537
|
const present = async () => !!await revParse(forkPath, `${sha}^{commit}`);
|
|
@@ -2402,60 +2541,158 @@ async function ensureUpstreamCommit(config, sha) {
|
|
|
2402
2541
|
if (!await present())
|
|
2403
2542
|
throw new Error(`upstream commit ${sha.slice(0, 7)} is not reachable from '${DEFAULT_UPSTREAM_REMOTE}'`);
|
|
2404
2543
|
}
|
|
2405
|
-
async function
|
|
2544
|
+
async function resolveAlignmentsRef(config) {
|
|
2406
2545
|
const { forkPath } = config;
|
|
2407
2546
|
const ref = await getStoredSyncRef(forkPath) ?? await readManifestBaseAtRef(forkPath, "HEAD");
|
|
2408
2547
|
if (!ref) return null;
|
|
2409
2548
|
await ensureUpstreamCommit(config, ref);
|
|
2410
2549
|
return ref;
|
|
2411
2550
|
}
|
|
2412
|
-
async function
|
|
2551
|
+
async function readOpenAlignments(config) {
|
|
2413
2552
|
try {
|
|
2414
|
-
const ref = await
|
|
2553
|
+
const ref = await resolveAlignmentsRef(config);
|
|
2415
2554
|
if (!ref) return null;
|
|
2416
|
-
|
|
2417
|
-
return ids.length > 0 || pending.length > 0 ? { total: ids.length, open: pending.length } : null;
|
|
2555
|
+
return { ref, open: await readPending(config.forkPath) };
|
|
2418
2556
|
} catch {
|
|
2419
2557
|
return null;
|
|
2420
2558
|
}
|
|
2421
2559
|
}
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
const
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2560
|
+
var STEP_ROWS_MAX = 5;
|
|
2561
|
+
function alignmentTags(alignment) {
|
|
2562
|
+
return [
|
|
2563
|
+
alignment.codemod ? "codemod" : "manual",
|
|
2564
|
+
alignment.breaking ? "breaking" : "not breaking",
|
|
2565
|
+
alignment.caughtBy ? `caught by ${alignment.caughtBy}` : null
|
|
2566
|
+
].filter(Boolean).join(" \xB7 ");
|
|
2567
|
+
}
|
|
2568
|
+
async function alignmentsStep(config, view) {
|
|
2569
|
+
const arriving = view.arriving ?? [];
|
|
2570
|
+
const open = view.open.filter((id) => !arriving.includes(id));
|
|
2571
|
+
const arrived = (view.arrived ?? []).filter((id) => open.includes(id));
|
|
2572
|
+
if (arriving.length === 0 && open.length === 0) return void 0;
|
|
2573
|
+
const line = arriving.length > 0 ? `${plural(arriving.length, "alignment")} ${arriving.length === 1 ? "arrives" : "arrive"} with this upgrade${open.length > 0 ? colors_default.dim(` \xB7 ${open.length} open`) : ""}` : `${plural(open.length, "alignment")} open${arrived.length > 0 ? colors_default.dim(` \xB7 ${arrived.length} arrived with this upgrade`) : ""}`;
|
|
2574
|
+
const githubUrl = getGitHubBaseUrl(config.settings.upstreamUrl) ?? void 0;
|
|
2575
|
+
const ids = [...arriving, ...open];
|
|
2576
|
+
const details = [];
|
|
2577
|
+
for (const id of ids.slice(0, STEP_ROWS_MAX)) {
|
|
2578
|
+
const alignment = await readAlignment(config.forkPath, view.ref, id).catch(() => null);
|
|
2579
|
+
const title = hyperlink(alignment?.title ?? id, alignmentUrl(githubUrl, view.ref, id));
|
|
2580
|
+
details.push(`${colors_default.cyan("\u25CB")} ${title}${alignment ? colors_default.dim(` \xB7 ${alignmentTags(alignment)}`) : ""}`);
|
|
2581
|
+
if (!process7.stdout.isTTY) details.push(colors_default.dim(` id ${id}`));
|
|
2582
|
+
}
|
|
2583
|
+
if (ids.length > STEP_ROWS_MAX) details.push(colors_default.dim(`+ ${ids.length - STEP_ROWS_MAX} more`));
|
|
2584
|
+
details.push(`${arriving.length > 0 ? colors_default.dim("after the upgrade: ") : ""}pnpm cella align`);
|
|
2585
|
+
return { label: "align", line, details };
|
|
2586
|
+
}
|
|
2587
|
+
async function readForkView(config, ref) {
|
|
2588
|
+
const { pinned = [], ignored = [] } = config.overrides ?? {};
|
|
2589
|
+
const counts = (path) => !isManagedFile(path) && !isGeneratedFile(path) && !isUpstreamOnly(path);
|
|
2590
|
+
const out = await git(["diff", "--name-only", "--no-renames", "-z", ref, "--"], config.forkPath);
|
|
2591
|
+
return {
|
|
2592
|
+
githubUrl: getGitHubBaseUrl(config.settings.upstreamUrl) ?? void 0,
|
|
2593
|
+
owns: (path) => counts(path) && isUnderAnyFolder(path, [...pinned, ...ignored]),
|
|
2594
|
+
changed: new Set(out.split("\0").filter((path) => path && counts(path)))
|
|
2595
|
+
};
|
|
2596
|
+
}
|
|
2597
|
+
async function describeAlignment(config, view, ref, alignment, open) {
|
|
2598
|
+
const { forkPath } = config;
|
|
2599
|
+
const [source, grepHits] = await Promise.all([
|
|
2600
|
+
readAlignmentSource(forkPath, ref, alignment.id),
|
|
2601
|
+
grepAlignmentTerms(forkPath, alignment.grep, (path) => view.changed.has(path))
|
|
2602
|
+
]);
|
|
2603
|
+
const pr = source?.pr ?? null;
|
|
2604
|
+
return {
|
|
2605
|
+
...alignment,
|
|
2606
|
+
kind: alignment.codemod ? "codemod" : "manual",
|
|
2607
|
+
open,
|
|
2608
|
+
url: alignmentUrl(view.githubUrl, ref, alignment.id) ?? null,
|
|
2609
|
+
commit: source?.commit ?? null,
|
|
2610
|
+
pr,
|
|
2611
|
+
prUrl: pr !== null && view.githubUrl ? `${view.githubUrl}/pull/${pr}` : null,
|
|
2612
|
+
sharedCommit: source?.sharedCommit ?? [],
|
|
2613
|
+
yourFiles: (source?.changedFiles ?? []).filter((path) => view.owns(path) || view.changed.has(path)),
|
|
2614
|
+
grepHits
|
|
2615
|
+
};
|
|
2616
|
+
}
|
|
2617
|
+
function grepSummary(alignment) {
|
|
2618
|
+
if (alignment.grep.length === 0) return null;
|
|
2619
|
+
const hits = alignment.grepHits.length;
|
|
2620
|
+
if (hits === 0) return "grep: no app hits";
|
|
2621
|
+
const files = new Set(alignment.grepHits.map((hit) => hit.file)).size;
|
|
2622
|
+
return `grep: ${hits} hit${hits === 1 ? "" : "s"} in ${files} app file${files === 1 ? "" : "s"}`;
|
|
2436
2623
|
}
|
|
2437
|
-
function
|
|
2624
|
+
function printAlignment(index, alignment) {
|
|
2438
2625
|
const tags = [
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2626
|
+
alignment.codemod ? "codemod" : "manual",
|
|
2627
|
+
alignment.breaking ? "breaking" : null,
|
|
2628
|
+
alignment.clientCacheBump ? "cache-bump" : null,
|
|
2629
|
+
alignment.caughtBy ? `caught by: ${alignment.caughtBy}` : null,
|
|
2630
|
+
alignment.generate ? "generate" : null
|
|
2442
2631
|
].filter(Boolean).join(", ");
|
|
2443
|
-
|
|
2444
|
-
console.info(colors_default.dim(`
|
|
2445
|
-
|
|
2446
|
-
console.info(
|
|
2447
|
-
if (
|
|
2448
|
-
|
|
2632
|
+
const grep = grepSummary(alignment);
|
|
2633
|
+
console.info(`${index}. ${alignment.title} ${colors_default.dim(`[${tags}]`)}`);
|
|
2634
|
+
console.info(colors_default.dim(` id: ${alignment.id}`));
|
|
2635
|
+
if (alignment.summary) console.info(` ${alignment.summary}`);
|
|
2636
|
+
if (alignment.pr !== null) {
|
|
2637
|
+
console.info(colors_default.dim(` added in #${alignment.pr}${alignment.prUrl ? ` ${alignment.prUrl}` : ""}`));
|
|
2638
|
+
}
|
|
2639
|
+
if (grep) console.info(colors_default.dim(` ${grep}`));
|
|
2640
|
+
console.info(colors_default.dim(` read: pnpm cella align --show ${alignment.id}`));
|
|
2641
|
+
if (alignment.url) console.info(colors_default.dim(` ${alignment.url}`));
|
|
2642
|
+
if (alignment.codemod) console.info(colors_default.dim(` codemod: pnpm cella align --run ${alignment.id}`));
|
|
2643
|
+
console.info();
|
|
2644
|
+
}
|
|
2645
|
+
function printShowBlock(title, items) {
|
|
2646
|
+
if (items.length === 0) return;
|
|
2647
|
+
console.info(colors_default.dim(`${title} (${items.length}):`));
|
|
2648
|
+
for (const item of items.slice(0, SHOW_LIST_MAX)) console.info(colors_default.dim(` ${item}`));
|
|
2649
|
+
if (items.length > SHOW_LIST_MAX) console.info(colors_default.dim(` ... + ${items.length - SHOW_LIST_MAX} more`));
|
|
2650
|
+
}
|
|
2651
|
+
async function showAlignment(config, ref, pending, id) {
|
|
2652
|
+
const { forkPath } = config;
|
|
2653
|
+
const upstreamBranch = `${DEFAULT_UPSTREAM_REMOTE}/${config.settings.upstreamBranch}`;
|
|
2654
|
+
let readRef = ref;
|
|
2655
|
+
let alignment = await readAlignment(forkPath, ref, id);
|
|
2656
|
+
if (!alignment && await revParse(forkPath, upstreamBranch)) {
|
|
2657
|
+
alignment = await readAlignment(forkPath, upstreamBranch, id);
|
|
2658
|
+
readRef = upstreamBranch;
|
|
2659
|
+
}
|
|
2660
|
+
if (!alignment) throw new Error(`no alignment '${id}' at upstream ${ref.slice(0, 7)} or ${upstreamBranch}`);
|
|
2661
|
+
const source = await git(["show", `${readRef}:${ALIGNMENTS_DIR}/${id}/README.md`], forkPath);
|
|
2662
|
+
const described = await describeAlignment(
|
|
2663
|
+
config,
|
|
2664
|
+
await readForkView(config, ref),
|
|
2665
|
+
readRef,
|
|
2666
|
+
alignment,
|
|
2667
|
+
pending.includes(id)
|
|
2668
|
+
);
|
|
2669
|
+
if (readRef !== ref) console.info(colors_default.dim(`not at the last upgrade ${ref.slice(0, 7)}: read at ${upstreamBranch}`));
|
|
2670
|
+
if (described.url) console.info(colors_default.dim(described.url));
|
|
2671
|
+
console.info(source);
|
|
2449
2672
|
console.info();
|
|
2673
|
+
if (described.commit) {
|
|
2674
|
+
const pr = described.pr !== null ? `#${described.pr} ` : "";
|
|
2675
|
+
console.info(
|
|
2676
|
+
colors_default.dim(`added in ${pr}(${described.commit.slice(0, 9)})${described.prUrl ? ` ${described.prUrl}` : ""}`)
|
|
2677
|
+
);
|
|
2678
|
+
}
|
|
2679
|
+
printShowBlock("added in the same commit", described.sharedCommit);
|
|
2680
|
+
printShowBlock("files of that commit the app pins, ignores or changed", described.yourFiles);
|
|
2681
|
+
const grep = grepSummary(described);
|
|
2682
|
+
if (grep) console.info(colors_default.dim(`${grep} (${described.grep.join(", ")})`));
|
|
2683
|
+
printShowBlock(
|
|
2684
|
+
"grep hits",
|
|
2685
|
+
described.grepHits.map((hit) => `${hit.file}:${hit.line}`)
|
|
2686
|
+
);
|
|
2450
2687
|
}
|
|
2451
|
-
async function
|
|
2688
|
+
async function writeAlignmentFolder(config, ref, id) {
|
|
2452
2689
|
const { forkPath } = config;
|
|
2453
|
-
const
|
|
2454
|
-
if (!
|
|
2455
|
-
const files = (await git(["ls-tree", "-r", "--name-only", `${ref}:${
|
|
2690
|
+
const alignment = await readAlignment(forkPath, ref, id);
|
|
2691
|
+
if (!alignment) throw new Error(`no alignment '${id}' at upstream ${ref.slice(0, 7)}`);
|
|
2692
|
+
const files = (await git(["ls-tree", "-r", "--name-only", `${ref}:${ALIGNMENTS_DIR}/${id}`], forkPath)).split("\n").filter(Boolean);
|
|
2456
2693
|
const target = join8(EXTRACT_DIR, id);
|
|
2457
2694
|
for (const file of files) {
|
|
2458
|
-
const content = execFileSync("git", ["show", `${ref}:${
|
|
2695
|
+
const content = execFileSync("git", ["show", `${ref}:${ALIGNMENTS_DIR}/${id}/${file}`], {
|
|
2459
2696
|
cwd: forkPath,
|
|
2460
2697
|
maxBuffer: MAX_BUFFER
|
|
2461
2698
|
});
|
|
@@ -2463,27 +2700,27 @@ async function writeNoteFolder(config, ref, id) {
|
|
|
2463
2700
|
await mkdir4(dirname4(path), { recursive: true });
|
|
2464
2701
|
await writeFile3(path, content);
|
|
2465
2702
|
}
|
|
2466
|
-
return {
|
|
2703
|
+
return { alignment, target, files };
|
|
2467
2704
|
}
|
|
2468
|
-
async function
|
|
2469
|
-
const {
|
|
2705
|
+
async function extractAlignment(config, ref, id) {
|
|
2706
|
+
const { alignment, target, files } = await writeAlignmentFolder(config, ref, id);
|
|
2470
2707
|
console.info(`${checkMark} extracted ${files.length} file(s) to ${target}/`);
|
|
2471
|
-
console.info(colors_default.dim(` the README's ${
|
|
2472
|
-
if (
|
|
2473
|
-
console.info(colors_default.dim(` pnpm exec tsx ${target}/${
|
|
2474
|
-
console.info(colors_default.dim(` or, keeping files identical to upstream as they are: pnpm cella
|
|
2708
|
+
console.info(colors_default.dim(` the README's ${ALIGNMENTS_DIR}/${id}/ paths are this folder.`));
|
|
2709
|
+
if (alignment.codemod) {
|
|
2710
|
+
console.info(colors_default.dim(` pnpm exec tsx ${target}/${alignment.codemod} inventory ${alignment.roots.join(" ")}`));
|
|
2711
|
+
console.info(colors_default.dim(` or, keeping files identical to upstream as they are: pnpm cella align --run ${id}`));
|
|
2475
2712
|
}
|
|
2476
2713
|
}
|
|
2477
|
-
async function
|
|
2714
|
+
async function runAlignment(config, ref, id) {
|
|
2478
2715
|
const { forkPath } = config;
|
|
2479
|
-
const {
|
|
2480
|
-
if (!
|
|
2716
|
+
const { alignment, target } = await writeAlignmentFolder(config, ref, id);
|
|
2717
|
+
if (!alignment.codemod) {
|
|
2481
2718
|
throw new Error(
|
|
2482
|
-
`
|
|
2719
|
+
`alignment '${id}' ships no codemod script: its steps are manual, read them with \`pnpm cella align --show ${id}\`.`
|
|
2483
2720
|
);
|
|
2484
2721
|
}
|
|
2485
|
-
const script = join8(target,
|
|
2486
|
-
const args = config.runArgs?.length ? config.runArgs : ["inventory", ...
|
|
2722
|
+
const script = join8(target, alignment.codemod);
|
|
2723
|
+
const args = config.runArgs?.length ? config.runArgs : ["inventory", ...alignment.roots];
|
|
2487
2724
|
const upstreamCommit = await readManifestBase(forkPath) ?? ref;
|
|
2488
2725
|
await ensureUpstreamCommit(config, upstreamCommit);
|
|
2489
2726
|
const identical = await listIdenticalToRef(forkPath, upstreamCommit);
|
|
@@ -2508,92 +2745,335 @@ async function runNote(config, ref, id) {
|
|
|
2508
2745
|
}
|
|
2509
2746
|
if (child.status !== 0) throw new Error(`the codemod exited with code ${child.status ?? "unknown"}`);
|
|
2510
2747
|
if (!config.runArgs?.length) {
|
|
2511
|
-
const apply = ["rewrite", ...
|
|
2512
|
-
console.info(colors_default.dim(`report only. to apply: pnpm cella
|
|
2748
|
+
const apply = ["rewrite", ...alignment.roots].join(" ");
|
|
2749
|
+
console.info(colors_default.dim(`report only. to apply: pnpm cella align --run ${id} -- ${apply}`));
|
|
2513
2750
|
}
|
|
2514
2751
|
}
|
|
2515
|
-
async function
|
|
2752
|
+
async function markAlignments(config, ref, pending, ids) {
|
|
2516
2753
|
const open = new Set(pending);
|
|
2517
2754
|
const unknown = ids.filter((id) => !open.has(id));
|
|
2518
2755
|
for (const id of ids) open.delete(id);
|
|
2519
2756
|
await writePending(config.forkPath, [...open]);
|
|
2520
2757
|
const marked = ids.length - unknown.length;
|
|
2521
|
-
if (marked > 0) console.info(`${checkMark} recorded ${marked}
|
|
2758
|
+
if (marked > 0) console.info(`${checkMark} recorded ${marked} alignment(s) as handled`);
|
|
2522
2759
|
if (unknown.length > 0) console.info(colors_default.dim(`not pending, left as is: ${unknown.join(", ")}`));
|
|
2523
|
-
console.info(colors_default.dim(
|
|
2760
|
+
console.info(colors_default.dim(formatAlignmentsLine((await listAlignmentIds(config.forkPath, ref)).length, open.size)));
|
|
2524
2761
|
}
|
|
2525
|
-
async function
|
|
2762
|
+
async function runAlign(config) {
|
|
2526
2763
|
const { forkPath } = config;
|
|
2527
|
-
const ref = await
|
|
2764
|
+
const ref = await resolveAlignmentsRef(config);
|
|
2528
2765
|
if (!ref) {
|
|
2529
|
-
console.info(colors_default.dim("no
|
|
2766
|
+
console.info(colors_default.dim("no upgrade recorded yet: alignments start with the first `pnpm cella upgrade`."));
|
|
2530
2767
|
return;
|
|
2531
2768
|
}
|
|
2532
2769
|
const pending = await readPending(forkPath);
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
if (config.
|
|
2536
|
-
if (config.
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
});
|
|
2542
|
-
if (!source) throw new Error(`no migration note '${config.show}' at upstream ${ref.slice(0, 7)}`);
|
|
2543
|
-
const url = urlOf(config.show);
|
|
2544
|
-
if (url) console.info(colors_default.dim(url));
|
|
2545
|
-
console.info(source);
|
|
2546
|
-
return;
|
|
2547
|
-
}
|
|
2548
|
-
const ids = config.all ? await listNoteIds(forkPath, ref) : pending;
|
|
2549
|
-
const notes = (await Promise.all(ids.map((id) => readNote(forkPath, ref, id)))).map(
|
|
2550
|
-
(note, i) => note ?? {
|
|
2770
|
+
if (config.mark?.length) return markAlignments(config, ref, pending, config.mark);
|
|
2771
|
+
if (config.run) return runAlignment(config, ref, config.run);
|
|
2772
|
+
if (config.extract) return extractAlignment(config, ref, config.extract);
|
|
2773
|
+
if (config.show) return showAlignment(config, ref, pending, config.show);
|
|
2774
|
+
const ids = config.all ? await listAlignmentIds(forkPath, ref) : pending;
|
|
2775
|
+
const read = await Promise.all(ids.map((id) => readAlignment(forkPath, ref, id)));
|
|
2776
|
+
const alignments = read.map(
|
|
2777
|
+
(alignment, i) => alignment ?? {
|
|
2551
2778
|
id: ids[i],
|
|
2552
2779
|
title: `${ids[i]} (no longer upstream)`,
|
|
2553
2780
|
summary: "",
|
|
2554
|
-
|
|
2781
|
+
breaking: false,
|
|
2555
2782
|
clientCacheBump: false,
|
|
2783
|
+
caughtBy: null,
|
|
2784
|
+
generate: false,
|
|
2785
|
+
grep: [],
|
|
2556
2786
|
roots: [],
|
|
2557
2787
|
codemod: null
|
|
2558
2788
|
}
|
|
2559
2789
|
);
|
|
2560
|
-
const total = (await
|
|
2790
|
+
const total = (await listAlignmentIds(forkPath, ref)).length;
|
|
2791
|
+
const open = new Set(pending);
|
|
2792
|
+
const view = await readForkView(config, ref);
|
|
2793
|
+
const described = await Promise.all(
|
|
2794
|
+
alignments.map((alignment) => describeAlignment(config, view, ref, alignment, open.has(alignment.id)))
|
|
2795
|
+
);
|
|
2561
2796
|
if (config.json) {
|
|
2562
|
-
|
|
2563
|
-
const out = {
|
|
2564
|
-
upstreamCommit: ref,
|
|
2565
|
-
total,
|
|
2566
|
-
open: pending.length,
|
|
2567
|
-
notes: notes.map((note) => ({
|
|
2568
|
-
...note,
|
|
2569
|
-
kind: note.codemod ? "codemod" : "manual",
|
|
2570
|
-
open: open.has(note.id),
|
|
2571
|
-
url: urlOf(note.id) ?? null
|
|
2572
|
-
}))
|
|
2573
|
-
};
|
|
2574
|
-
writeStdout(JSON.stringify(out, null, 2));
|
|
2797
|
+
writeStdout(JSON.stringify({ upstreamCommit: ref, total, open: pending.length, alignments: described }, null, 2));
|
|
2575
2798
|
return;
|
|
2576
2799
|
}
|
|
2577
|
-
console.info(colors_default.dim(
|
|
2578
|
-
if (
|
|
2800
|
+
console.info(colors_default.dim(formatAlignmentsLine(total, pending.length)));
|
|
2801
|
+
if (described.length === 0) return;
|
|
2579
2802
|
console.info();
|
|
2580
|
-
for (const [i,
|
|
2803
|
+
for (const [i, alignment] of described.entries()) printAlignment(i + 1, alignment);
|
|
2804
|
+
const schema = described.filter((alignment) => alignment.open && alignment.generate);
|
|
2805
|
+
if (schema.length > 1) {
|
|
2806
|
+
console.info(
|
|
2807
|
+
colors_default.dim(
|
|
2808
|
+
`${schema.length} open alignments change the schema: work their code steps first, then run pnpm generate once.`
|
|
2809
|
+
)
|
|
2810
|
+
);
|
|
2811
|
+
}
|
|
2581
2812
|
if (pending.length > 0) {
|
|
2582
|
-
console.info(colors_default.dim("work each
|
|
2583
|
-
console.info(colors_default.dim(` pnpm cella
|
|
2813
|
+
console.info(colors_default.dim("work each alignment's README, check with `pnpm check`, then record it:"));
|
|
2814
|
+
console.info(colors_default.dim(` pnpm cella align --mark ${pending.join(" ")}`));
|
|
2584
2815
|
}
|
|
2585
2816
|
}
|
|
2586
2817
|
|
|
2587
|
-
// src/services/
|
|
2818
|
+
// src/services/analyze-report.ts
|
|
2588
2819
|
import { basename as basename2 } from "path";
|
|
2589
|
-
|
|
2820
|
+
var BEHIND_FLAT_MAX = 15;
|
|
2821
|
+
var AHEAD_FLAT_MAX = 8;
|
|
2822
|
+
var ABSENT_SHOWN_MIN = 10;
|
|
2823
|
+
var ENTRY_COLLAPSE_MIN = 5;
|
|
2824
|
+
var SECTION_ROWS_MAX = 100;
|
|
2825
|
+
var UPSTREAM_CHANGE = { fields: "changed", repo: "upstream" };
|
|
2826
|
+
var UPSTREAM_SIDE = { fields: "upstream", repo: "upstream" };
|
|
2827
|
+
var FORK_SIDE = { fields: "changed", repo: "fork" };
|
|
2828
|
+
function cell(value, width, color, align) {
|
|
2829
|
+
if (!value) return " ".repeat(width);
|
|
2830
|
+
return padVisible(color ? color(value) : value, width, align === "end" ? "end" : "start");
|
|
2831
|
+
}
|
|
2832
|
+
function formatRows(rows, caption) {
|
|
2833
|
+
const iconWidth = rows.some((row) => row.icon) ? 2 : 0;
|
|
2834
|
+
const labelWidth = Math.max(...rows.map((row) => row.label.length + (row.note ? row.note.length + 1 : 0)));
|
|
2835
|
+
const countWidth = Math.max(0, ...rows.map((row) => row.count?.length ?? 0));
|
|
2836
|
+
const tagWidth = Math.max(0, ...rows.map((row) => row.tag?.length ?? 0));
|
|
2837
|
+
const ageWidth = Math.max(0, ...rows.map((row) => row.age?.length ?? 0));
|
|
2838
|
+
const lines = [];
|
|
2839
|
+
if (caption && countWidth > 0) lines.push(colors_default.dim(caption.padStart(2 + iconWidth + labelWidth + 2 + countWidth)));
|
|
2840
|
+
for (const row of rows) {
|
|
2841
|
+
const icon = iconWidth ? cell(row.icon, iconWidth) : "";
|
|
2842
|
+
const label = `${hyperlink(row.label, row.link)}${row.note ? ` ${colors_default.dim(row.note)}` : ""}`;
|
|
2843
|
+
const cells = [` ${icon}${padVisible(label, labelWidth)}`];
|
|
2844
|
+
if (countWidth) cells.push(cell(row.count, countWidth, row.countColor, "end"));
|
|
2845
|
+
if (tagWidth) cells.push(cell(row.tag, tagWidth, row.tagColor));
|
|
2846
|
+
if (ageWidth) cells.push(cell(row.age, ageWidth, colors_default.dim, "end"));
|
|
2847
|
+
lines.push(cells.join(" ").trimEnd());
|
|
2848
|
+
}
|
|
2849
|
+
return lines;
|
|
2850
|
+
}
|
|
2851
|
+
function formatCappedRows(rows, caption) {
|
|
2852
|
+
if (rows.length <= SECTION_ROWS_MAX) return formatRows(rows, caption);
|
|
2853
|
+
return [
|
|
2854
|
+
...formatRows(rows.slice(0, SECTION_ROWS_MAX), caption),
|
|
2855
|
+
colors_default.dim(` + ${rows.length - SECTION_ROWS_MAX} more`)
|
|
2856
|
+
];
|
|
2857
|
+
}
|
|
2858
|
+
function hint(text, command = "") {
|
|
2859
|
+
return ` ${colors_default.dim(text)}${command}`;
|
|
2860
|
+
}
|
|
2861
|
+
function printSection(title, count3, body, hints = []) {
|
|
2862
|
+
printSectionHeader(`${title} ${colors_default.dim(`\xB7 ${plural(count3, "file")}`)}`);
|
|
2863
|
+
for (const line of body) console.info(line);
|
|
2864
|
+
if (hints.length === 0) return;
|
|
2865
|
+
console.info();
|
|
2866
|
+
for (const line of hints) console.info(line);
|
|
2867
|
+
}
|
|
2868
|
+
function byPath(a, b) {
|
|
2869
|
+
return a.path.localeCompare(b.path);
|
|
2870
|
+
}
|
|
2871
|
+
function fileLink(file, side, links) {
|
|
2872
|
+
const { fileLinkMode = "commit", upstreamGitHubUrl, upstreamSha, forkGitHubUrl } = links;
|
|
2873
|
+
if (fileLinkMode === "file") {
|
|
2874
|
+
if (!upstreamGitHubUrl || !upstreamSha || !file.existsInUpstream) return void 0;
|
|
2875
|
+
return `${upstreamGitHubUrl}/blob/${upstreamSha}/${file.path}`;
|
|
2876
|
+
}
|
|
2877
|
+
const commit = side.fields === "upstream" ? file.upstreamCommit : file.changedCommit;
|
|
2878
|
+
const repo = side.repo === "upstream" ? upstreamGitHubUrl : forkGitHubUrl;
|
|
2879
|
+
return commit && repo ? `${repo}/commit/${commit}` : void 0;
|
|
2880
|
+
}
|
|
2881
|
+
function changeTimestamp(file, side) {
|
|
2882
|
+
return side.fields === "upstream" ? file.upstreamChangedTs : file.changedTs;
|
|
2883
|
+
}
|
|
2884
|
+
function changeKind(file) {
|
|
2885
|
+
if (!file.existsInFork) return "new";
|
|
2886
|
+
if (!file.existsInUpstream) return "deleted";
|
|
2887
|
+
return "changed";
|
|
2888
|
+
}
|
|
2889
|
+
var KIND_TAG = { new: { tag: "new", color: colors_default.green }, deleted: { tag: "deleted", color: colors_default.red } };
|
|
2890
|
+
function fileRow(file, side, options, extra = {}) {
|
|
2891
|
+
const timestamp = changeTimestamp(file, side);
|
|
2892
|
+
const kind = changeKind(file);
|
|
2893
|
+
return {
|
|
2894
|
+
label: file.path,
|
|
2895
|
+
link: fileLink(file, side, options.links),
|
|
2896
|
+
age: timestamp ? shortAge(timestamp, options.nowMs) : void 0,
|
|
2897
|
+
...kind === "changed" ? {} : { tag: KIND_TAG[kind].tag, tagColor: KIND_TAG[kind].color },
|
|
2898
|
+
...extra
|
|
2899
|
+
};
|
|
2900
|
+
}
|
|
2901
|
+
function commonFolder(paths) {
|
|
2902
|
+
const folders = paths.map((path) => path.split("/").slice(0, -1));
|
|
2903
|
+
const [first] = folders;
|
|
2904
|
+
let depth = 0;
|
|
2905
|
+
while (depth < first.length && folders.every((parts) => parts[depth] === first[depth])) depth++;
|
|
2906
|
+
return first.slice(0, depth).join("/");
|
|
2907
|
+
}
|
|
2908
|
+
function formatFolderRows(files, columns) {
|
|
2909
|
+
const groups = /* @__PURE__ */ new Map();
|
|
2910
|
+
for (const file of files) {
|
|
2911
|
+
const top = file.path.includes("/") ? file.path.split("/")[0] : file.path;
|
|
2912
|
+
groups.set(top, [...groups.get(top) ?? [], file]);
|
|
2913
|
+
}
|
|
2914
|
+
const rows = [...groups.values()].map((group) => ({
|
|
2915
|
+
label: group.length === 1 ? group[0].path : `${commonFolder(group.map((file) => file.path))}/`,
|
|
2916
|
+
files: group
|
|
2917
|
+
})).sort((a, b) => b.files.length - a.files.length || a.label.localeCompare(b.label));
|
|
2918
|
+
const labelWidth = Math.max(...rows.map((row) => row.label.length));
|
|
2919
|
+
const countWidth = Math.max(...rows.map((row) => String(row.files.length).length));
|
|
2920
|
+
const order = { new: 0, deleted: 1, changed: 2 };
|
|
2921
|
+
return rows.map((row) => {
|
|
2922
|
+
let line = ` ${row.label.padEnd(labelWidth)} ${colors_default.cyan(String(row.files.length).padStart(countWidth))}`;
|
|
2923
|
+
if (row.files.length === 1) return line.trimEnd();
|
|
2924
|
+
const seen = /* @__PURE__ */ new Map();
|
|
2925
|
+
for (const file of row.files) seen.set(basename2(file.path), (seen.get(basename2(file.path)) ?? 0) + 1);
|
|
2926
|
+
const names = row.files.map((file) => ({
|
|
2927
|
+
kind: changeKind(file),
|
|
2928
|
+
name: (seen.get(basename2(file.path)) ?? 0) > 1 ? file.path.slice(row.label.length) : basename2(file.path)
|
|
2929
|
+
})).sort((a, b) => order[a.kind] - order[b.kind] || a.name.localeCompare(b.name));
|
|
2930
|
+
let width = visibleWidth(line);
|
|
2931
|
+
for (const { kind, name } of names) {
|
|
2932
|
+
const text = kind === "new" ? `+${name}` : kind === "deleted" ? `\u2212${name}` : name;
|
|
2933
|
+
if (width + 2 + text.length > columns - 2) {
|
|
2934
|
+
line += colors_default.dim(" \u2026");
|
|
2935
|
+
break;
|
|
2936
|
+
}
|
|
2937
|
+
line += ` ${kind === "new" ? colors_default.green(text) : kind === "deleted" ? colors_default.red(text) : colors_default.dim(text)}`;
|
|
2938
|
+
width += 2 + text.length;
|
|
2939
|
+
}
|
|
2940
|
+
return line;
|
|
2941
|
+
});
|
|
2942
|
+
}
|
|
2943
|
+
function printBehind(files, options, columns) {
|
|
2944
|
+
const behind = filesWithStatus(files, "behind").sort(byPath);
|
|
2945
|
+
if (behind.length === 0) return;
|
|
2946
|
+
const title = colors_default.cyan("\u2193 behind on upstream");
|
|
2947
|
+
if (options.all || behind.length <= BEHIND_FLAT_MAX) {
|
|
2948
|
+
printSection(title, behind.length, formatCappedRows(behind.map((file) => fileRow(file, UPSTREAM_CHANGE, options))));
|
|
2949
|
+
return;
|
|
2950
|
+
}
|
|
2951
|
+
printSection(title, behind.length, formatFolderRows(behind, columns), [
|
|
2952
|
+
hint("every path: ", "pnpm cella analyze --all")
|
|
2953
|
+
]);
|
|
2954
|
+
}
|
|
2955
|
+
function printFlat(files, title, side, options) {
|
|
2956
|
+
if (files.length === 0) return;
|
|
2957
|
+
const rows = [...files].sort(byPath).map((file) => fileRow(file, side, options));
|
|
2958
|
+
printSection(title, files.length, formatCappedRows(rows));
|
|
2959
|
+
}
|
|
2960
|
+
function absentLines(file) {
|
|
2961
|
+
return file.upstreamLinesAbsent ?? 0;
|
|
2962
|
+
}
|
|
2963
|
+
function describeAbsent(files) {
|
|
2964
|
+
const counts = files.map(absentLines);
|
|
2965
|
+
const missing = counts.filter((count3) => count3 > 0);
|
|
2966
|
+
const parts = [];
|
|
2967
|
+
if (missing.length > 0) {
|
|
2968
|
+
const [low, high] = [Math.min(...missing), Math.max(...missing)];
|
|
2969
|
+
parts.push(`${missing.length} with ${low === high ? plural(low, "line") : `${low}\u2013${high} lines`} absent`);
|
|
2970
|
+
}
|
|
2971
|
+
if (missing.length < counts.length) parts.push(`${counts.length - missing.length} with none`);
|
|
2972
|
+
return parts.join(", ");
|
|
2973
|
+
}
|
|
2974
|
+
function printAhead(files, options) {
|
|
2975
|
+
const ahead = filesWithStatus(files, "ahead").sort((a, b) => absentLines(b) - absentLines(a) || byPath(a, b));
|
|
2976
|
+
if (ahead.length === 0) return;
|
|
2977
|
+
let shown = ahead;
|
|
2978
|
+
if (!options.all && ahead.length > AHEAD_FLAT_MAX) {
|
|
2979
|
+
const many = ahead.filter((file) => absentLines(file) >= ABSENT_SHOWN_MIN);
|
|
2980
|
+
shown = many.length > 0 ? many : ahead.slice(0, 5);
|
|
2981
|
+
}
|
|
2982
|
+
const rest = ahead.slice(shown.length);
|
|
2983
|
+
const rows = shown.map(
|
|
2984
|
+
(file) => fileRow(file, FORK_SIDE, options, {
|
|
2985
|
+
count: absentLines(file) > 0 ? String(absentLines(file)) : void 0,
|
|
2986
|
+
countColor: colors_default.yellow
|
|
2987
|
+
})
|
|
2988
|
+
);
|
|
2989
|
+
const body = formatCappedRows(rows, "upstream lines absent");
|
|
2990
|
+
if (rest.length > 0) body.push(colors_default.dim(` + ${rest.length} more: ${describeAbsent(rest)}`));
|
|
2991
|
+
const hints = ahead.some((file) => absentLines(file) > 0) ? [hint("what a merge would bring in: ", "pnpm cella analyze --unpinned")] : [];
|
|
2992
|
+
printSection(colors_default.blue("\u2191 protected in fork"), ahead.length, body, hints);
|
|
2993
|
+
}
|
|
2994
|
+
function entryFinder(overrides) {
|
|
2995
|
+
const entries = [...overrides?.ignored ?? [], ...overrides?.pinned ?? []].map(normalizeEntry).filter(Boolean).sort((a, b) => b.length - a.length);
|
|
2996
|
+
return (path) => entries.find((entry) => isUnderAnyFolder(path, [entry]));
|
|
2997
|
+
}
|
|
2998
|
+
var KIND_ORDER = { new: 0, changed: 1, deleted: 2 };
|
|
2999
|
+
function protectedRow(items, options) {
|
|
3000
|
+
const [{ file, kind, entry }] = items;
|
|
3001
|
+
const lines = items.map((item) => item.file.upstreamChangedLines);
|
|
3002
|
+
const total = lines.every((count3) => count3 !== void 0) ? lines.reduce((sum, count3) => sum + (count3 ?? 0), 0) : void 0;
|
|
3003
|
+
const sign = kind === "new" ? "+" : kind === "deleted" ? "\u2212" : "";
|
|
3004
|
+
const newest = Math.max(0, ...items.map((item) => changeTimestamp(item.file, UPSTREAM_SIDE) ?? 0));
|
|
3005
|
+
const row = {
|
|
3006
|
+
label: file.path,
|
|
3007
|
+
link: fileLink(file, UPSTREAM_SIDE, options.links),
|
|
3008
|
+
icon: protectionIcon(file),
|
|
3009
|
+
count: total === void 0 ? void 0 : `${sign}${compactCount(total)}`,
|
|
3010
|
+
countColor: kind === "new" ? colors_default.green : kind === "deleted" ? colors_default.red : colors_default.yellow,
|
|
3011
|
+
...kind === "changed" ? {} : { tag: KIND_TAG[kind].tag, tagColor: KIND_TAG[kind].color },
|
|
3012
|
+
age: newest ? shortAge(newest, options.nowMs) : void 0
|
|
3013
|
+
};
|
|
3014
|
+
if (items.length === 1 || !entry) return row;
|
|
3015
|
+
const names = new Set(items.map((item) => basename2(item.file.path)));
|
|
3016
|
+
const { upstreamGitHubUrl, upstreamSha } = options.links;
|
|
3017
|
+
return {
|
|
3018
|
+
...row,
|
|
3019
|
+
label: `${entry}/`,
|
|
3020
|
+
note: names.size === 1 ? `\xB7 ${items.length} \xD7 ${[...names][0]}` : `\xB7 ${items.length} files`,
|
|
3021
|
+
link: upstreamGitHubUrl && upstreamSha ? `${upstreamGitHubUrl}/tree/${upstreamSha}/${entry}` : void 0
|
|
3022
|
+
};
|
|
3023
|
+
}
|
|
3024
|
+
function printProtectedBehind(result, options) {
|
|
3025
|
+
const findEntry = entryFinder(options.overrides);
|
|
3026
|
+
const items = findProtectedBehindUpstream(result).map((file) => ({
|
|
3027
|
+
file,
|
|
3028
|
+
kind: file.upstreamOnly ? changeKind(file) : "changed",
|
|
3029
|
+
entry: findEntry(file.path)
|
|
3030
|
+
}));
|
|
3031
|
+
if (items.length === 0) return;
|
|
3032
|
+
const groups = /* @__PURE__ */ new Map();
|
|
3033
|
+
for (const item of items) {
|
|
3034
|
+
const key = options.all || !item.entry ? item.file.path : `${item.kind}\0${item.entry}`;
|
|
3035
|
+
groups.set(key, [...groups.get(key) ?? [], item]);
|
|
3036
|
+
}
|
|
3037
|
+
const single = (group) => group.length < ENTRY_COLLAPSE_MIN;
|
|
3038
|
+
const lines = (group) => group.reduce((sum, item) => sum + (item.file.upstreamChangedLines ?? 0), 0);
|
|
3039
|
+
const ordered = [...groups.values()].flatMap((group) => single(group) ? group.map((item) => [item]) : [group]).sort(
|
|
3040
|
+
(a, b) => Number(a.length > 1) - Number(b.length > 1) || KIND_ORDER[a[0].kind] - KIND_ORDER[b[0].kind] || lines(b) - lines(a) || byPath(a[0].file, b[0].file)
|
|
3041
|
+
);
|
|
3042
|
+
printSection(
|
|
3043
|
+
`${warningMark} ${colors_default.yellow("protected but behind upstream")}`,
|
|
3044
|
+
items.length,
|
|
3045
|
+
formatCappedRows(
|
|
3046
|
+
ordered.map((group) => protectedRow(group, options)),
|
|
3047
|
+
"lines changed upstream"
|
|
3048
|
+
),
|
|
3049
|
+
[hint("the fork side wins: adopt what you need from the review page")]
|
|
3050
|
+
);
|
|
3051
|
+
}
|
|
3052
|
+
function printAnalysisFileGroups(result, options) {
|
|
3053
|
+
const columns = options.columns ?? terminalColumns();
|
|
3054
|
+
printBehind(result.files, options, columns);
|
|
3055
|
+
printFlat(filesWithStatus(result.files, "diverged"), colors_default.magenta("\u21C5 diverged"), UPSTREAM_SIDE, options);
|
|
3056
|
+
printAhead(result.files, options);
|
|
3057
|
+
printFlat(
|
|
3058
|
+
filesWithStatus(result.files, "drifted"),
|
|
3059
|
+
`${warningMark} ${colors_default.yellow("drifted from upstream")}`,
|
|
3060
|
+
FORK_SIDE,
|
|
3061
|
+
options
|
|
3062
|
+
);
|
|
3063
|
+
printProtectedBehind(result, options);
|
|
3064
|
+
}
|
|
3065
|
+
|
|
3066
|
+
// src/services/review-page.ts
|
|
3067
|
+
import { existsSync as existsSync7, rmSync } from "fs";
|
|
3068
|
+
import { basename as basename3 } from "path";
|
|
3069
|
+
import process9 from "process";
|
|
2590
3070
|
import { pathToFileURL } from "url";
|
|
2591
3071
|
|
|
2592
3072
|
// src/utils/diff.ts
|
|
2593
3073
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
2594
3074
|
import { mkdirSync, writeFileSync } from "fs";
|
|
2595
|
-
import { join as join9 } from "path";
|
|
2596
|
-
import
|
|
3075
|
+
import { dirname as dirname5, join as join9 } from "path";
|
|
3076
|
+
import process8 from "process";
|
|
2597
3077
|
function gitDiffFile(cwd, range, filePath) {
|
|
2598
3078
|
try {
|
|
2599
3079
|
return execFileSync2("git", ["diff", "--no-color", range, "--", filePath], { cwd, maxBuffer: MAX_BUFFER });
|
|
@@ -2628,9 +3108,9 @@ ${prerenderedHTML}
|
|
|
2628
3108
|
for (const file of section.files) files.push(await renderFile(file));
|
|
2629
3109
|
const count3 = section.files.length + (section.omitted?.length ?? 0);
|
|
2630
3110
|
const title = section.title ? `<h2>${escapeHtml(section.title)} <span class="count">\xB7 ${count3} ${count3 === 1 ? "file" : "files"}</span></h2>` : "";
|
|
2631
|
-
const
|
|
3111
|
+
const hint2 = section.hint ? `<p class="hint">${escapeHtml(section.hint)}</p>` : "";
|
|
2632
3112
|
const omitted = section.omitted?.length ? `<p class="hint">Not shown here (binary, too large, or past the page limit):</p><ul class="omitted">${section.omitted.map((path) => `<li><code>${escapeHtml(path)}</code></li>`).join("")}</ul>` : "";
|
|
2633
|
-
blocks.push(`<section id="section-${index2 + 1}">${title}${
|
|
3113
|
+
blocks.push(`<section id="section-${index2 + 1}">${title}${hint2}${files.join("\n")}${omitted}</section>`);
|
|
2634
3114
|
}
|
|
2635
3115
|
const titled = sections.filter((section) => section.title);
|
|
2636
3116
|
const index = titled.length > 1 ? `<nav><ol>${sections.map(
|
|
@@ -2694,16 +3174,18 @@ ${blocks.join("\n")}
|
|
|
2694
3174
|
}
|
|
2695
3175
|
async function writeDiffPage(sections, meta, repoPath, name) {
|
|
2696
3176
|
const html = await renderDiffPage(sections, meta);
|
|
2697
|
-
const
|
|
2698
|
-
|
|
2699
|
-
mkdirSync(dir, { recursive: true });
|
|
2700
|
-
const outPath = join9(dir, `${name.replace(/[^\w.-]+/g, "-")}.html`);
|
|
3177
|
+
const outPath = diffPagePath(repoPath, name);
|
|
3178
|
+
mkdirSync(dirname5(outPath), { recursive: true });
|
|
2701
3179
|
writeFileSync(outPath, html);
|
|
2702
3180
|
return outPath;
|
|
2703
3181
|
}
|
|
3182
|
+
function diffPagePath(repoPath, name) {
|
|
3183
|
+
const gitDir = execFileSync2("git", ["rev-parse", "--absolute-git-dir"], { cwd: repoPath, encoding: "utf8" }).trim();
|
|
3184
|
+
return join9(gitDir, "cella-diff", `${name.replace(/[^\w.-]+/g, "-")}.html`);
|
|
3185
|
+
}
|
|
2704
3186
|
function openInBrowser(target) {
|
|
2705
3187
|
try {
|
|
2706
|
-
execFileSync2(
|
|
3188
|
+
execFileSync2(process8.platform === "darwin" ? "open" : "xdg-open", [target], { stdio: "ignore" });
|
|
2707
3189
|
return true;
|
|
2708
3190
|
} catch {
|
|
2709
3191
|
return false;
|
|
@@ -2711,49 +3193,50 @@ function openInBrowser(target) {
|
|
|
2711
3193
|
}
|
|
2712
3194
|
|
|
2713
3195
|
// src/services/review-page.ts
|
|
2714
|
-
var
|
|
3196
|
+
var PAGE_LIMITS = { files: 50, lines: 15e3, patchLines: 2e3 };
|
|
2715
3197
|
var MAX_PATCH_BYTES = 512 * 1024;
|
|
2716
3198
|
function reviewSections(result, command) {
|
|
2717
3199
|
const [base, upstream] = result.upstreamDiffRange?.split("..") ?? [];
|
|
2718
3200
|
if (!base || !upstream) return [];
|
|
2719
3201
|
const sinceSync = `${base}..${upstream}`;
|
|
2720
|
-
const
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
hint: "What upstream changed in these files since the last sync. They are pinned or ignored, so the sync keeps the fork version and none of it arrives: adopt what you need.",
|
|
2728
|
-
range: sinceSync,
|
|
2729
|
-
paths: findProtectedBehind(result.files).map((file) => file.path)
|
|
2730
|
-
};
|
|
2731
|
-
const sections = [
|
|
2732
|
-
dropped,
|
|
3202
|
+
const sections = command === "upgrade" ? [
|
|
3203
|
+
{
|
|
3204
|
+
title: "protected files kept the fork version",
|
|
3205
|
+
hint: "What upstream changed in these files since the last upgrade. The fork kept its own version, so none of it arrived: adopt what you need.",
|
|
3206
|
+
range: sinceSync,
|
|
3207
|
+
paths: result.protectedConflicts ?? []
|
|
3208
|
+
},
|
|
2733
3209
|
{
|
|
2734
3210
|
title: "ignored paths changed upstream",
|
|
2735
|
-
hint: "What upstream changed under ignored paths since the last
|
|
3211
|
+
hint: "What upstream changed under ignored paths since the last upgrade. The fork left these files untouched, and ignored paths never merge.",
|
|
2736
3212
|
range: sinceSync,
|
|
2737
3213
|
paths: (result.ignoredUpstreamChanges ?? []).flatMap((group) => group.paths)
|
|
2738
|
-
}
|
|
3214
|
+
}
|
|
3215
|
+
] : [
|
|
2739
3216
|
{
|
|
2740
|
-
title: "
|
|
2741
|
-
hint: "What upstream changed in these files since the last
|
|
3217
|
+
title: "protected but behind upstream",
|
|
3218
|
+
hint: "What upstream changed in these files since the last upgrade. They are pinned or ignored, so the upgrade keeps the fork version and none of it arrives: adopt what you need.",
|
|
2742
3219
|
range: sinceSync,
|
|
2743
|
-
paths:
|
|
3220
|
+
paths: findProtectedBehindUpstream(result).map((file) => file.path)
|
|
2744
3221
|
}
|
|
2745
3222
|
];
|
|
3223
|
+
sections.push({
|
|
3224
|
+
title: "pinned files that match the previous upstream",
|
|
3225
|
+
hint: "What upstream changed in these files since the last upgrade. The fork copy still equals the previous upstream, so the pin only drops these changes: unpin to take them.",
|
|
3226
|
+
range: sinceSync,
|
|
3227
|
+
paths: findMaskingPins(result.files).map((file) => file.path)
|
|
3228
|
+
});
|
|
2746
3229
|
if (command === "analyze") {
|
|
2747
3230
|
sections.push(
|
|
2748
3231
|
{
|
|
2749
3232
|
title: "protected in fork, upstream lines absent",
|
|
2750
|
-
hint: "The fork's version against upstream's, both as they are now: added lines are upstream content the fork does not have, dropped by an earlier
|
|
3233
|
+
hint: "The fork's version against upstream's, both as they are now: added lines are upstream content the fork does not have, dropped by an earlier upgrade or removed on purpose.",
|
|
2751
3234
|
range: `HEAD..${upstream}`,
|
|
2752
3235
|
paths: filesWithStatus(result.files, "ahead").filter((file) => (file.upstreamLinesAbsent ?? 0) > 0).map((file) => file.path)
|
|
2753
3236
|
},
|
|
2754
3237
|
{
|
|
2755
3238
|
title: "drifted from upstream",
|
|
2756
|
-
hint: "What the fork changed in these files since the last
|
|
3239
|
+
hint: "What the fork changed in these files since the last upgrade. They are not pinned or ignored, so upstream changes still merge into them.",
|
|
2757
3240
|
range: `${base}..HEAD`,
|
|
2758
3241
|
paths: filesWithStatus(result.files, "drifted").map((file) => file.path)
|
|
2759
3242
|
}
|
|
@@ -2761,55 +3244,100 @@ function reviewSections(result, command) {
|
|
|
2761
3244
|
}
|
|
2762
3245
|
return sections.filter((section) => section.paths.length > 0);
|
|
2763
3246
|
}
|
|
2764
|
-
|
|
3247
|
+
function countLines(patch) {
|
|
3248
|
+
let count3 = 0;
|
|
3249
|
+
for (let at = patch.indexOf(10); at !== -1; at = patch.indexOf(10, at + 1)) count3++;
|
|
3250
|
+
return count3;
|
|
3251
|
+
}
|
|
3252
|
+
async function writeReviewPage(forkPath, sections, meta, limits = PAGE_LIMITS) {
|
|
2765
3253
|
const [base, upstream] = meta.range.split("..");
|
|
3254
|
+
const pageSections = sections.map((section) => ({
|
|
3255
|
+
title: section.title,
|
|
3256
|
+
hint: section.hint,
|
|
3257
|
+
files: [],
|
|
3258
|
+
omitted: []
|
|
3259
|
+
}));
|
|
3260
|
+
const queues = sections.map((section) => [...section.paths]);
|
|
2766
3261
|
let diffs = 0;
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
const
|
|
2770
|
-
|
|
2771
|
-
if (
|
|
2772
|
-
|
|
3262
|
+
let lines = 0;
|
|
3263
|
+
while (queues.some((queue) => queue.length > 0)) {
|
|
3264
|
+
for (const [index, queue] of queues.entries()) {
|
|
3265
|
+
const path = queue.shift();
|
|
3266
|
+
if (path === void 0) continue;
|
|
3267
|
+
const pageSection = pageSections[index];
|
|
3268
|
+
if (diffs >= limits.files || lines >= limits.lines) {
|
|
3269
|
+
pageSection.omitted.push(path);
|
|
2773
3270
|
continue;
|
|
2774
3271
|
}
|
|
2775
|
-
const patch = gitDiffFile(forkPath,
|
|
3272
|
+
const patch = gitDiffFile(forkPath, sections[index].range, path);
|
|
2776
3273
|
if (patch.length === 0) continue;
|
|
2777
|
-
|
|
2778
|
-
|
|
3274
|
+
const patchLines = patch.length > MAX_PATCH_BYTES ? Number.POSITIVE_INFINITY : countLines(patch);
|
|
3275
|
+
if (patchLines > limits.patchLines || lines + patchLines > limits.lines || !patch.includes("\n@@ ")) {
|
|
3276
|
+
pageSection.omitted.push(path);
|
|
2779
3277
|
continue;
|
|
2780
3278
|
}
|
|
2781
3279
|
pageSection.files.push({ path, patch: patch.toString() });
|
|
2782
3280
|
diffs++;
|
|
3281
|
+
lines += patchLines;
|
|
2783
3282
|
}
|
|
2784
|
-
if (pageSection.files.length > 0 || pageSection.omitted?.length) pageSections.push(pageSection);
|
|
2785
3283
|
}
|
|
2786
3284
|
if (diffs === 0) return void 0;
|
|
3285
|
+
const shown = pageSections.filter((section) => section.files.length > 0 || section.omitted.length > 0);
|
|
2787
3286
|
const page = await writeDiffPage(
|
|
2788
|
-
|
|
2789
|
-
{ title: `${meta.command} review`, note: `${
|
|
3287
|
+
shown,
|
|
3288
|
+
{ title: `${meta.command} review`, note: `${basename3(forkPath)} \xB7 last upgrade ${base} \u2192 upstream ${upstream}` },
|
|
2790
3289
|
forkPath,
|
|
2791
3290
|
`${meta.command}-review`
|
|
2792
3291
|
);
|
|
2793
|
-
|
|
3292
|
+
const listed = shown.reduce((count3, section) => count3 + section.omitted.length, 0);
|
|
3293
|
+
return { path: page, diffs, listed, sections: shown.length };
|
|
2794
3294
|
}
|
|
2795
3295
|
async function reportReviewPage(forkPath, result, command) {
|
|
2796
3296
|
const sections = reviewSections(result, command);
|
|
2797
|
-
if (sections.length === 0 || !result.upstreamDiffRange) return;
|
|
2798
|
-
createSpinner("writing review page...");
|
|
2799
3297
|
let page;
|
|
3298
|
+
if (sections.length > 0 && result.upstreamDiffRange) {
|
|
3299
|
+
createSpinner("writing review page...");
|
|
3300
|
+
try {
|
|
3301
|
+
page = await writeReviewPage(forkPath, sections, { command, range: result.upstreamDiffRange });
|
|
3302
|
+
} catch {
|
|
3303
|
+
page = void 0;
|
|
3304
|
+
}
|
|
3305
|
+
spinnerSuccess();
|
|
3306
|
+
}
|
|
3307
|
+
if (!page) {
|
|
3308
|
+
if (command === "upgrade") removeSyncReviewPage(forkPath);
|
|
3309
|
+
return void 0;
|
|
3310
|
+
}
|
|
3311
|
+
const opened = command === "upgrade" && process9.stdout.isTTY === true && openInBrowser(page.path);
|
|
3312
|
+
return { ...page, command, opened };
|
|
3313
|
+
}
|
|
3314
|
+
function removeSyncReviewPage(forkPath) {
|
|
2800
3315
|
try {
|
|
2801
|
-
|
|
3316
|
+
rmSync(diffPagePath(forkPath, "upgrade-review"), { force: true });
|
|
2802
3317
|
} catch {
|
|
2803
|
-
page = void 0;
|
|
2804
3318
|
}
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
3319
|
+
}
|
|
3320
|
+
function existingSyncReviewPage(forkPath) {
|
|
3321
|
+
try {
|
|
3322
|
+
const path = diffPagePath(forkPath, "upgrade-review");
|
|
3323
|
+
return existsSync7(path) ? path : void 0;
|
|
3324
|
+
} catch {
|
|
3325
|
+
return void 0;
|
|
3326
|
+
}
|
|
3327
|
+
}
|
|
3328
|
+
function reviewStep(page) {
|
|
3329
|
+
const path = typeof page === "string" ? page : page.path;
|
|
3330
|
+
const url = pathToFileURL(path).href;
|
|
3331
|
+
let detail = colors_default.dim("upstream changes the upgrade did not bring in, from the merge run");
|
|
3332
|
+
if (typeof page !== "string") {
|
|
3333
|
+
const what = page.command === "upgrade" ? `${plural(page.diffs, "diff")} of upstream changes that did not arrive` : `${plural(page.diffs, "diff")} in ${plural(page.sections, "section")}`;
|
|
3334
|
+
const notes = [
|
|
3335
|
+
page.listed > 0 ? `${page.listed} listed without a diff` : "",
|
|
3336
|
+
page.opened ? "opened in the browser" : ""
|
|
3337
|
+
].filter(Boolean).map((note) => ` \xB7 ${note}`).join("");
|
|
3338
|
+
detail = `${what}${notes ? colors_default.dim(notes) : ""}`;
|
|
3339
|
+
}
|
|
3340
|
+
return { label: "review", line: hyperlink(colors_default.cyan(url), url), details: [detail] };
|
|
2813
3341
|
}
|
|
2814
3342
|
|
|
2815
3343
|
// src/services/analyze.ts
|
|
@@ -2844,8 +3372,8 @@ async function runAnalyze(config) {
|
|
|
2844
3372
|
upstreamChangedLines: f.upstreamChangedLines ?? null,
|
|
2845
3373
|
upstreamLinesAbsent: f.upstreamLinesAbsent ?? null,
|
|
2846
3374
|
upstreamOnly: f.upstreamOnly ?? false,
|
|
2847
|
-
// Only on the
|
|
2848
|
-
upstreamOverrides: overrides && f.path === CONFIG_FILE ? { pinned: overrides.pinned, ignored: overrides.ignored,
|
|
3375
|
+
// Only on the CLI config: upstream override entries and packageJsonKeys the fork config does not follow
|
|
3376
|
+
upstreamOverrides: overrides && f.path === CONFIG_FILE ? { pinned: overrides.pinned, ignored: overrides.ignored, packageJsonKeys: overrides.packageJsonKeys } : null
|
|
2849
3377
|
}));
|
|
2850
3378
|
writeStdout(JSON.stringify(out, null, 2));
|
|
2851
3379
|
return result;
|
|
@@ -2856,23 +3384,56 @@ async function runAnalyze(config) {
|
|
|
2856
3384
|
}
|
|
2857
3385
|
return result;
|
|
2858
3386
|
}
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
3387
|
+
printAnalysisFileGroups(result, {
|
|
3388
|
+
links: {
|
|
3389
|
+
upstreamGitHubUrl: result.upstreamGitHubUrl,
|
|
3390
|
+
upstreamSha: result.upstreamCommit?.hash,
|
|
3391
|
+
forkGitHubUrl: await readOriginGitHubUrl(config.forkPath),
|
|
3392
|
+
fileLinkMode: config.settings.fileLinkMode,
|
|
3393
|
+
forkPath: config.forkPath
|
|
3394
|
+
},
|
|
3395
|
+
all: config.all,
|
|
3396
|
+
overrides: config.overrides
|
|
3397
|
+
});
|
|
3398
|
+
printUpstreamOverrideChanges(result);
|
|
3399
|
+
printMaskingPinWarning(result.files);
|
|
3400
|
+
printSummary(result.summary, "analysis summary");
|
|
3401
|
+
const steps = [];
|
|
3402
|
+
const review = await reportReviewPage(config.forkPath, result, "analyze");
|
|
3403
|
+
if (review) steps.push(reviewStep(review));
|
|
3404
|
+
const sync = syncStep(result);
|
|
3405
|
+
if (sync) steps.push(sync);
|
|
3406
|
+
const align = result.alignments && result.upstreamCommit ? await alignmentsStep(config, {
|
|
3407
|
+
ref: result.upstreamCommit.hash,
|
|
3408
|
+
open: result.alignments.open,
|
|
3409
|
+
arriving: result.alignments.arrived
|
|
3410
|
+
}) : void 0;
|
|
3411
|
+
if (align) steps.push(align);
|
|
3412
|
+
printNextSteps(steps);
|
|
2870
3413
|
return result;
|
|
2871
3414
|
}
|
|
3415
|
+
function syncStep(result) {
|
|
3416
|
+
if (!result.upstreamCommitCount) return void 0;
|
|
3417
|
+
const { behind, diverged, renamed } = result.summary;
|
|
3418
|
+
const changes = [
|
|
3419
|
+
behind > 0 ? `${behind} update` : "",
|
|
3420
|
+
diverged > 0 ? `${diverged} merge` : "",
|
|
3421
|
+
renamed > 0 ? `${renamed} rename` : ""
|
|
3422
|
+
].filter(Boolean);
|
|
3423
|
+
return {
|
|
3424
|
+
label: "upgrade",
|
|
3425
|
+
line: "pnpm cella upgrade",
|
|
3426
|
+
details: [
|
|
3427
|
+
colors_default.dim(
|
|
3428
|
+
`commits on a fresh branch, a rerun opens the PR \xB7 ${changes.length > 0 ? changes.join(", ") : "no file changes"}`
|
|
3429
|
+
)
|
|
3430
|
+
]
|
|
3431
|
+
};
|
|
3432
|
+
}
|
|
2872
3433
|
|
|
2873
3434
|
// src/services/audit.ts
|
|
2874
3435
|
import { readFileSync as readFileSync4 } from "fs";
|
|
2875
|
-
import { basename as
|
|
3436
|
+
import { basename as basename5, join as join13, relative as relative3 } from "path";
|
|
2876
3437
|
import { checkbox, Separator } from "@inquirer/prompts";
|
|
2877
3438
|
|
|
2878
3439
|
// src/utils/audit-utils.ts
|
|
@@ -2980,14 +3541,14 @@ function formatDependents(dependents) {
|
|
|
2980
3541
|
|
|
2981
3542
|
// src/utils/local-tools.ts
|
|
2982
3543
|
import { execFile as execFile3 } from "child_process";
|
|
2983
|
-
import { existsSync as
|
|
3544
|
+
import { existsSync as existsSync8, realpathSync as realpathSync2 } from "fs";
|
|
2984
3545
|
import { tmpdir } from "os";
|
|
2985
|
-
import { delimiter, dirname as
|
|
3546
|
+
import { delimiter, dirname as dirname7, join as join11, sep as sep2 } from "path";
|
|
2986
3547
|
import { promisify as promisify3 } from "util";
|
|
2987
3548
|
|
|
2988
3549
|
// src/utils/runtimes.ts
|
|
2989
3550
|
import { readFileSync as readFileSync2 } from "fs";
|
|
2990
|
-
import { basename as
|
|
3551
|
+
import { basename as basename4, dirname as dirname6, join as join10 } from "path";
|
|
2991
3552
|
var DAY = 24 * 60 * 60 * 1e3;
|
|
2992
3553
|
var FROZEN_AFTER = 180 * DAY;
|
|
2993
3554
|
var RECENT_PUSH = 30 * DAY;
|
|
@@ -2995,7 +3556,7 @@ var EXACT_VERSION = /^\d+\.\d+\.\d+$/;
|
|
|
2995
3556
|
var FROM_LINE = /^\s*FROM\s+(?:--platform=\S+\s+)?(\S+)(?:\s+AS\s+(\S+))?\s*$/i;
|
|
2996
3557
|
var NODE_ARG_TAG = /^(?:docker\.io\/)?(?:library\/)?node:\$\{?\w+\}?([\w.-]*)$/;
|
|
2997
3558
|
var MINOR_TAG = /^(\d+(?:\.\d+){1,2})(-[\w.-]+)?$/;
|
|
2998
|
-
var isDockerfile = (path) => /(^|\.)Dockerfile$|^Dockerfile\./i.test(
|
|
3559
|
+
var isDockerfile = (path) => /(^|\.)Dockerfile$|^Dockerfile\./i.test(basename4(path));
|
|
2999
3560
|
function parseDockerfile(file, content) {
|
|
3000
3561
|
const images = [];
|
|
3001
3562
|
const nodeImageSuffixes = [];
|
|
@@ -3233,7 +3794,7 @@ async function missingNodeImages(version, suffixes) {
|
|
|
3233
3794
|
return missing.flat();
|
|
3234
3795
|
}
|
|
3235
3796
|
function pinLocation(file) {
|
|
3236
|
-
const dir =
|
|
3797
|
+
const dir = dirname6(file);
|
|
3237
3798
|
return dir === "." ? file : dir;
|
|
3238
3799
|
}
|
|
3239
3800
|
|
|
@@ -3357,7 +3918,7 @@ var execFileAsync3 = promisify3(execFile3);
|
|
|
3357
3918
|
function findOnPath(name, skip = []) {
|
|
3358
3919
|
for (const dir of (getEnv("PATH") ?? "").split(delimiter)) {
|
|
3359
3920
|
if (!dir || skip.includes(dir) || dir.endsWith(`node_modules${sep2}.bin`)) continue;
|
|
3360
|
-
if (
|
|
3921
|
+
if (existsSync8(join11(dir, name))) return join11(dir, name);
|
|
3361
3922
|
}
|
|
3362
3923
|
return null;
|
|
3363
3924
|
}
|
|
@@ -3385,7 +3946,7 @@ async function readNode(now) {
|
|
|
3385
3946
|
}
|
|
3386
3947
|
async function readPnpm(cwd) {
|
|
3387
3948
|
const running = getEnv("npm_execpath");
|
|
3388
|
-
const file = findOnPath("pnpm", running ? [
|
|
3949
|
+
const file = findOnPath("pnpm", running ? [dirname7(running)] : []);
|
|
3389
3950
|
const version = file && await versionOf(file, tmpdir());
|
|
3390
3951
|
if (!file || !version) return null;
|
|
3391
3952
|
const [published, advisories] = await Promise.all([fetchPublishTimes("pnpm", cwd), fetchPnpmAdvisories(version)]);
|
|
@@ -3556,14 +4117,14 @@ function writeAuditSettings(content, next) {
|
|
|
3556
4117
|
|
|
3557
4118
|
// src/services/audit-security.ts
|
|
3558
4119
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
3559
|
-
import { cpSync, existsSync as
|
|
4120
|
+
import { cpSync, existsSync as existsSync9, mkdirSync as mkdirSync2, mkdtempSync, readFileSync as readFileSync3, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
3560
4121
|
import { tmpdir as tmpdir2 } from "os";
|
|
3561
|
-
import { dirname as
|
|
4122
|
+
import { dirname as dirname8, join as join12, relative as relative2 } from "path";
|
|
3562
4123
|
import { confirm, select as select2 } from "@inquirer/prompts";
|
|
3563
4124
|
var WORKSPACE_FILE = "pnpm-workspace.yaml";
|
|
3564
4125
|
var LOCK_FILE = "pnpm-lock.yaml";
|
|
3565
4126
|
var RESOLUTION_FILES = ["package.json", LOCK_FILE, ".npmrc", ".pnpmfile.cjs"];
|
|
3566
|
-
var readOr = (path, fallback = "") =>
|
|
4127
|
+
var readOr = (path, fallback = "") => existsSync9(path) ? readFileSync3(path, "utf8") : fallback;
|
|
3567
4128
|
var count2 = (n, noun, plural2 = `${noun}s`) => `${n} ${n === 1 ? noun : plural2}`;
|
|
3568
4129
|
var isBreaking = (item) => item.remedy.kind !== "none" && item.remedy.breaking;
|
|
3569
4130
|
var needsDecision = (item) => item.standing === "new" && (item.remedy.kind !== "override" || item.remedy.breaking || item.direct);
|
|
@@ -3577,8 +4138,8 @@ async function createStaging(forkPath, workspaceYaml) {
|
|
|
3577
4138
|
...projects.map((project) => join12(relative2(forkPath, project.path), "package.json"))
|
|
3578
4139
|
];
|
|
3579
4140
|
for (const file of new Set(files)) {
|
|
3580
|
-
if (file.startsWith("..") || !
|
|
3581
|
-
mkdirSync2(
|
|
4141
|
+
if (file.startsWith("..") || !existsSync9(join12(forkPath, file))) continue;
|
|
4142
|
+
mkdirSync2(dirname8(join12(staging, file)), { recursive: true });
|
|
3582
4143
|
cpSync(join12(forkPath, file), join12(staging, file));
|
|
3583
4144
|
}
|
|
3584
4145
|
return staging;
|
|
@@ -3821,7 +4382,7 @@ async function runSecurityPass(forkPath, options) {
|
|
|
3821
4382
|
return true;
|
|
3822
4383
|
} finally {
|
|
3823
4384
|
spinner.stop();
|
|
3824
|
-
|
|
4385
|
+
rmSync2(staging, { recursive: true, force: true });
|
|
3825
4386
|
}
|
|
3826
4387
|
}
|
|
3827
4388
|
function printOpen(modules) {
|
|
@@ -4023,7 +4584,7 @@ function printOutdatedResults(rows, forkPath) {
|
|
|
4023
4584
|
const dependentMap = /* @__PURE__ */ new Map();
|
|
4024
4585
|
for (const pkg of rows) {
|
|
4025
4586
|
for (const { name, location } of pkg.dependentPackages) {
|
|
4026
|
-
const dependentName = name ||
|
|
4587
|
+
const dependentName = name || basename5(location);
|
|
4027
4588
|
const existing = dependentMap.get(dependentName);
|
|
4028
4589
|
if (existing) {
|
|
4029
4590
|
existing.count++;
|
|
@@ -4219,8 +4780,8 @@ import {
|
|
|
4219
4780
|
import { select as select3 } from "@inquirer/prompts";
|
|
4220
4781
|
|
|
4221
4782
|
// src/services/contrib-core.ts
|
|
4222
|
-
import { rmSync as
|
|
4223
|
-
import { dirname as
|
|
4783
|
+
import { rmSync as rmSync3 } from "fs";
|
|
4784
|
+
import { dirname as dirname9, join as join14, resolve as resolve4 } from "path";
|
|
4224
4785
|
async function listFilesAtRef(repoPath, ref) {
|
|
4225
4786
|
const raw = await git(["ls-tree", "-r", "--name-only", ref], repoPath);
|
|
4226
4787
|
return new Set(raw.split("\n").filter(Boolean));
|
|
@@ -4236,10 +4797,10 @@ async function detectContributableFiles(repoPath, baseRef, forkRef, config, fork
|
|
|
4236
4797
|
const baseFiles = await listFilesAtRef(repoPath, baseRef);
|
|
4237
4798
|
const baseDirs = /* @__PURE__ */ new Set(["."]);
|
|
4238
4799
|
for (const f of baseFiles) {
|
|
4239
|
-
let dir =
|
|
4800
|
+
let dir = dirname9(f);
|
|
4240
4801
|
while (dir !== ".") {
|
|
4241
4802
|
baseDirs.add(dir);
|
|
4242
|
-
dir =
|
|
4803
|
+
dir = dirname9(dir);
|
|
4243
4804
|
}
|
|
4244
4805
|
}
|
|
4245
4806
|
const files = [];
|
|
@@ -4252,7 +4813,7 @@ async function detectContributableFiles(repoPath, baseRef, forkRef, config, fork
|
|
|
4252
4813
|
if (inCella && !inFork) {
|
|
4253
4814
|
kind = "deleted";
|
|
4254
4815
|
} else if (!inCella && inFork) {
|
|
4255
|
-
if (!baseDirs.has(
|
|
4816
|
+
if (!baseDirs.has(dirname9(file.path))) continue;
|
|
4256
4817
|
kind = "created";
|
|
4257
4818
|
} else {
|
|
4258
4819
|
kind = "modified";
|
|
@@ -4306,25 +4867,25 @@ ${commitBody}`;
|
|
|
4306
4867
|
await git(["update-ref", `refs/heads/${branch}`, commitHash], repoPath);
|
|
4307
4868
|
return { branch, appliedFiles };
|
|
4308
4869
|
} finally {
|
|
4309
|
-
|
|
4870
|
+
rmSync3(tmpIndex, { force: true });
|
|
4310
4871
|
}
|
|
4311
4872
|
}
|
|
4312
4873
|
|
|
4313
4874
|
// src/services/fork-utils.ts
|
|
4314
|
-
import { existsSync as
|
|
4315
|
-
import { dirname as
|
|
4875
|
+
import { existsSync as existsSync10 } from "fs";
|
|
4876
|
+
import { dirname as dirname10, resolve as resolve5 } from "path";
|
|
4316
4877
|
async function resolveForkBasePath(repoPath) {
|
|
4317
4878
|
const commonDir = await git(["rev-parse", "--git-common-dir"], repoPath, { ignoreErrors: true });
|
|
4318
|
-
return commonDir ?
|
|
4879
|
+
return commonDir ? dirname10(resolve5(repoPath, commonDir)) : repoPath;
|
|
4319
4880
|
}
|
|
4320
4881
|
function validateForkPath(fork, basePath, requireConfig = false) {
|
|
4321
4882
|
const resolvedPath = resolve5(basePath, fork.localPath);
|
|
4322
4883
|
if (!resolvedPath.startsWith(resolve5(basePath, ".."))) {
|
|
4323
4884
|
return { fork, resolvedPath, valid: false, error: "path resolves outside parent directory" };
|
|
4324
4885
|
}
|
|
4325
|
-
if (!
|
|
4326
|
-
if (!
|
|
4327
|
-
if (requireConfig && !
|
|
4886
|
+
if (!existsSync10(resolvedPath)) return { fork, resolvedPath, valid: false, error: "path does not exist" };
|
|
4887
|
+
if (!existsSync10(`${resolvedPath}/.git`)) return { fork, resolvedPath, valid: false, error: "not a git repository" };
|
|
4888
|
+
if (requireConfig && !existsSync10(`${resolvedPath}/${CONFIG_FILE}`)) {
|
|
4328
4889
|
return { fork, resolvedPath, valid: false, error: `missing ${CONFIG_FILE}` };
|
|
4329
4890
|
}
|
|
4330
4891
|
return { fork, resolvedPath, valid: true };
|
|
@@ -4630,10 +5191,10 @@ async function runContributions(config) {
|
|
|
4630
5191
|
|
|
4631
5192
|
// src/services/forks.ts
|
|
4632
5193
|
import { resolve as resolve6 } from "path";
|
|
4633
|
-
import
|
|
5194
|
+
import process10 from "process";
|
|
4634
5195
|
import { Separator as Separator2, select as select5 } from "@inquirer/prompts";
|
|
4635
5196
|
|
|
4636
|
-
// src/services/
|
|
5197
|
+
// src/services/upgrade.ts
|
|
4637
5198
|
import { spawnSync as spawnSync4 } from "child_process";
|
|
4638
5199
|
import { select as select4 } from "@inquirer/prompts";
|
|
4639
5200
|
|
|
@@ -4647,7 +5208,7 @@ function runGh(cwd, args) {
|
|
|
4647
5208
|
const output = `${result.stdout ?? ""}${result.stderr ?? ""}`.trim();
|
|
4648
5209
|
return { ok: result.status === 0, output };
|
|
4649
5210
|
}
|
|
4650
|
-
function listOpenSyncPrs(cwd,
|
|
5211
|
+
function listOpenSyncPrs(cwd, prefixes) {
|
|
4651
5212
|
const { ok, output } = runGh(cwd, [
|
|
4652
5213
|
"pr",
|
|
4653
5214
|
"list",
|
|
@@ -4665,7 +5226,29 @@ function listOpenSyncPrs(cwd, prefix) {
|
|
|
4665
5226
|
} catch {
|
|
4666
5227
|
return [];
|
|
4667
5228
|
}
|
|
4668
|
-
return parsed.filter((pr) => pr.headRefName.startsWith(`${prefix}/`)).sort((a, b) => b.number - a.number);
|
|
5229
|
+
return parsed.filter((pr) => prefixes.some((prefix) => pr.headRefName.startsWith(`${prefix}/`))).sort((a, b) => b.number - a.number);
|
|
5230
|
+
}
|
|
5231
|
+
function createPr(cwd, repoSlug, pr) {
|
|
5232
|
+
const repo = repoSlug ?? "{owner}/{repo}";
|
|
5233
|
+
const fields = ["title", "head", "base", "body"];
|
|
5234
|
+
const created = runGh(cwd, [
|
|
5235
|
+
"api",
|
|
5236
|
+
`repos/${repo}/pulls`,
|
|
5237
|
+
"-X",
|
|
5238
|
+
"POST",
|
|
5239
|
+
...fields.flatMap((field) => ["-f", `${field}=${pr[field]}`]),
|
|
5240
|
+
"--jq",
|
|
5241
|
+
".html_url"
|
|
5242
|
+
]);
|
|
5243
|
+
if (created.ok) return { ...created, url: created.output || void 0 };
|
|
5244
|
+
const owner = repoSlug ? repoSlug.split("/")[0] : "{owner}";
|
|
5245
|
+
const existing = runGh(cwd, [
|
|
5246
|
+
"api",
|
|
5247
|
+
`repos/${repo}/pulls?state=open&head=${owner}:${pr.head}`,
|
|
5248
|
+
"--jq",
|
|
5249
|
+
".[0].html_url // empty"
|
|
5250
|
+
]);
|
|
5251
|
+
return { ...created, url: existing.ok && existing.output ? existing.output : void 0 };
|
|
4669
5252
|
}
|
|
4670
5253
|
function mergePrSquash(cwd, ref, options = {}) {
|
|
4671
5254
|
return runGh(cwd, ["pr", "merge", String(ref), "--squash", ...options.deleteBranch ? ["--delete-branch"] : []]);
|
|
@@ -4675,11 +5258,11 @@ function closePr(cwd, ref) {
|
|
|
4675
5258
|
}
|
|
4676
5259
|
|
|
4677
5260
|
// src/services/packages.ts
|
|
4678
|
-
import { existsSync as
|
|
5261
|
+
import { existsSync as existsSync12, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "fs";
|
|
4679
5262
|
import { join as join16 } from "path";
|
|
4680
5263
|
|
|
4681
5264
|
// src/services/package-usage.ts
|
|
4682
|
-
import { existsSync as
|
|
5265
|
+
import { existsSync as existsSync11, readFileSync as readFileSync5, statSync } from "fs";
|
|
4683
5266
|
import { join as join15 } from "path";
|
|
4684
5267
|
var MAX_FILE_BYTES = 1024 * 1024;
|
|
4685
5268
|
function escapeRegExp(value) {
|
|
@@ -4720,7 +5303,7 @@ function binNames(forkPath, location, name) {
|
|
|
4720
5303
|
const bareName = name.split("/").pop();
|
|
4721
5304
|
for (const dir of [join15(forkPath, location), forkPath]) {
|
|
4722
5305
|
const manifest = join15(dir, "node_modules", name, "package.json");
|
|
4723
|
-
if (!
|
|
5306
|
+
if (!existsSync11(manifest)) continue;
|
|
4724
5307
|
try {
|
|
4725
5308
|
const { bin } = JSON.parse(readFileSync5(manifest, "utf8"));
|
|
4726
5309
|
if (typeof bin === "string") return [bareName];
|
|
@@ -4901,7 +5484,7 @@ function setTopLevelKey(pkg, key, value) {
|
|
|
4901
5484
|
Object.assign(pkg, Object.fromEntries(entries));
|
|
4902
5485
|
}
|
|
4903
5486
|
function readPackageJson(filePath) {
|
|
4904
|
-
if (!
|
|
5487
|
+
if (!existsSync12(filePath)) return null;
|
|
4905
5488
|
try {
|
|
4906
5489
|
return JSON.parse(readFileSync6(filePath, "utf8"));
|
|
4907
5490
|
} catch (error) {
|
|
@@ -4941,7 +5524,7 @@ async function syncPackageJson(config, baseRef, usage, relativePath, keysToSync)
|
|
|
4941
5524
|
const basePkg = baseRef ? (await readPackageJsonAtRef(forkPath, baseRef, relativePath))?.data : void 0;
|
|
4942
5525
|
if (!upstream) return { updated: false, changes, kept };
|
|
4943
5526
|
if (!forkPkg) {
|
|
4944
|
-
const workspaceArrived = relativePath !== "" &&
|
|
5527
|
+
const workspaceArrived = relativePath !== "" && existsSync12(join16(forkPath, relativePath));
|
|
4945
5528
|
if (basePkg || !workspaceArrived || isIgnored(pkgRelPath, config)) return { updated: false, changes, kept };
|
|
4946
5529
|
writeFileSync3(pkgPath, upstream.raw.endsWith("\n") ? upstream.raw : `${upstream.raw}
|
|
4947
5530
|
`, "utf8");
|
|
@@ -5013,8 +5596,8 @@ async function syncPackageJson(config, baseRef, usage, relativePath, keysToSync)
|
|
|
5013
5596
|
return { updated, changes, kept };
|
|
5014
5597
|
}
|
|
5015
5598
|
async function runPackages(config, options = {}) {
|
|
5016
|
-
createSpinner("
|
|
5017
|
-
const keysToSync = config.settings.
|
|
5599
|
+
createSpinner("merging package.json files...");
|
|
5600
|
+
const keysToSync = config.settings.packageJsonKeys || DEFAULT_PACKAGE_JSON_KEYS;
|
|
5018
5601
|
const conflictedSet = new Set(options.conflictedFiles ?? []);
|
|
5019
5602
|
const baseRef = await resolveMergeBase(config.forkPath, config.upstreamRef);
|
|
5020
5603
|
const usage = baseRef ? await loadForkUsage(config.forkPath, config.upstreamRef, baseRef) : null;
|
|
@@ -5027,12 +5610,15 @@ async function runPackages(config, options = {}) {
|
|
|
5027
5610
|
skipped.push(pkgRelPath);
|
|
5028
5611
|
continue;
|
|
5029
5612
|
}
|
|
5030
|
-
spinnerText(`
|
|
5613
|
+
spinnerText(`merging ${location || "root"}/package.json...`);
|
|
5031
5614
|
const { updated, changes, kept: kept2 } = await syncPackageJson(config, baseRef, usage, location, keysToSync);
|
|
5032
5615
|
if (updated || kept2.length > 0) reports.push({ path: pkgRelPath, changes, kept: kept2 });
|
|
5033
5616
|
}
|
|
5034
5617
|
const changedCount = reports.filter(({ changes }) => changes.length > 0).length;
|
|
5035
|
-
spinnerSuccess(
|
|
5618
|
+
spinnerSuccess(
|
|
5619
|
+
"package.json merge complete",
|
|
5620
|
+
`${plural(changedCount, "package.json was", "package.jsons were")} changed`
|
|
5621
|
+
);
|
|
5036
5622
|
for (const { path, changes } of reports) {
|
|
5037
5623
|
if (changes.length === 0) continue;
|
|
5038
5624
|
console.info(` ${path}`);
|
|
@@ -5042,7 +5628,7 @@ async function runPackages(config, options = {}) {
|
|
|
5042
5628
|
const them = kept.length === 1 ? "it" : "them";
|
|
5043
5629
|
printWarnList(`kept ${plural(kept.length, "entry", "entries")} upstream dropped, the fork still uses ${them}:`, kept);
|
|
5044
5630
|
printWarnList(
|
|
5045
|
-
`skipped ${skipped.length} conflicted package.json file(s): resolve the conflicts, commit the merge, then rerun \`pnpm cella
|
|
5631
|
+
`skipped ${skipped.length} conflicted package.json file(s): resolve the conflicts, commit the merge, then rerun \`pnpm cella upgrade\`:`,
|
|
5046
5632
|
skipped
|
|
5047
5633
|
);
|
|
5048
5634
|
}
|
|
@@ -5053,7 +5639,7 @@ function printWarnList(header, entries) {
|
|
|
5053
5639
|
for (const entry of entries) console.info(` ${colors_default.dim("\u2192")} ${entry}`);
|
|
5054
5640
|
}
|
|
5055
5641
|
|
|
5056
|
-
// src/services/
|
|
5642
|
+
// src/services/upgrade.ts
|
|
5057
5643
|
async function resolveCycleStart(forkPath, base, currentBranch) {
|
|
5058
5644
|
if (!await branchExists(forkPath, base)) {
|
|
5059
5645
|
await fetchRemote(forkPath, "origin").catch(() => {
|
|
@@ -5069,7 +5655,7 @@ async function resolveCycleStart(forkPath, base, currentBranch) {
|
|
|
5069
5655
|
if (ahead > 0 && behind > 0) {
|
|
5070
5656
|
throw new Error(
|
|
5071
5657
|
`'${base}' has diverged from '${upstream}' (${ahead} ahead, ${behind} behind).
|
|
5072
|
-
Reconcile '${base}' with '${upstream}' first (e.g. rebase or reset it), then re-run
|
|
5658
|
+
Reconcile '${base}' with '${upstream}' first (e.g. rebase or reset it), then re-run the upgrade.`
|
|
5073
5659
|
);
|
|
5074
5660
|
}
|
|
5075
5661
|
if (behind > 0) {
|
|
@@ -5098,7 +5684,7 @@ async function setupTemporarySyncBranch(config) {
|
|
|
5098
5684
|
console.info(colors_default.dim(`updating '${base}'...`));
|
|
5099
5685
|
const startPoint = await resolveCycleStart(forkPath, base, startBranch);
|
|
5100
5686
|
const temporaryBranch = buildTemporarySyncBranch();
|
|
5101
|
-
console.info(colors_default.dim(`creating temporary
|
|
5687
|
+
console.info(colors_default.dim(`creating temporary upgrade branch '${temporaryBranch}' from '${startPoint}'...`));
|
|
5102
5688
|
console.info();
|
|
5103
5689
|
await createBranchFrom(forkPath, temporaryBranch, startPoint);
|
|
5104
5690
|
return { temporaryBranch, base, startBranch, startCommit };
|
|
@@ -5122,7 +5708,7 @@ async function returnToBase(forkPath, base) {
|
|
|
5122
5708
|
return `back on '${base}'`;
|
|
5123
5709
|
}
|
|
5124
5710
|
async function runSync(config, options) {
|
|
5125
|
-
createSpinner("starting
|
|
5711
|
+
createSpinner("starting upgrade...");
|
|
5126
5712
|
let result;
|
|
5127
5713
|
try {
|
|
5128
5714
|
result = await runEngineWithSpinner(config, true);
|
|
@@ -5137,7 +5723,7 @@ async function runSync(config, options) {
|
|
|
5137
5723
|
if (result.success) {
|
|
5138
5724
|
spinnerSuccess();
|
|
5139
5725
|
} else {
|
|
5140
|
-
spinnerFail("
|
|
5726
|
+
spinnerFail("upgrade merge completed with conflicts");
|
|
5141
5727
|
}
|
|
5142
5728
|
printEngineReports(result, "merge summary");
|
|
5143
5729
|
if (config.unpinned) {
|
|
@@ -5147,49 +5733,89 @@ async function runSync(config, options) {
|
|
|
5147
5733
|
const staged = result.files.some((file) => SYNC_APPLIED_STATUSES.includes(file.status));
|
|
5148
5734
|
const stagedBranch = options?.stagedBranch && result.conflicts.length === 0 && staged ? options.stagedBranch : void 0;
|
|
5149
5735
|
printSyncComplete(result, { stagedBranch });
|
|
5150
|
-
await reportReviewPage(config.forkPath, result, "
|
|
5151
|
-
|
|
5152
|
-
|
|
5736
|
+
const reviewPage = await reportReviewPage(config.forkPath, result, "upgrade");
|
|
5737
|
+
return { ...result, reviewPage };
|
|
5738
|
+
}
|
|
5739
|
+
var CONFLICTS_LISTED = 10;
|
|
5740
|
+
async function syncAlignmentsStep(config, run) {
|
|
5741
|
+
if (run?.alignments && run.upstreamCommit) {
|
|
5742
|
+
const { open, arrived } = run.alignments;
|
|
5743
|
+
return alignmentsStep(config, { ref: run.upstreamCommit.hash, open, arrived });
|
|
5744
|
+
}
|
|
5745
|
+
const view = await readOpenAlignments(config);
|
|
5746
|
+
return view ? alignmentsStep(config, view) : void 0;
|
|
5747
|
+
}
|
|
5748
|
+
function syncReviewStep(config, run) {
|
|
5749
|
+
const page = run ? run.reviewPage : existingSyncReviewPage(config.forkPath);
|
|
5750
|
+
return page ? reviewStep(page) : void 0;
|
|
5751
|
+
}
|
|
5752
|
+
async function conflictSteps(config, conflicts, run) {
|
|
5753
|
+
const listed = conflicts.slice(0, CONFLICTS_LISTED).map((file) => colors_default.dim(file));
|
|
5754
|
+
if (conflicts.length > CONFLICTS_LISTED) listed.push(colors_default.dim(`+ ${conflicts.length - CONFLICTS_LISTED} more`));
|
|
5755
|
+
const steps = [
|
|
5756
|
+
{
|
|
5757
|
+
label: "resolve",
|
|
5758
|
+
line: `${plural(conflicts.length, "conflict")} in your editor, then stage them`,
|
|
5759
|
+
details: listed
|
|
5760
|
+
},
|
|
5761
|
+
{
|
|
5762
|
+
label: "finish",
|
|
5763
|
+
line: "pnpm cella upgrade",
|
|
5764
|
+
details: [
|
|
5765
|
+
colors_default.dim("commits the upgrade and stops for drift triage"),
|
|
5766
|
+
colors_default.dim("let the rerun commit: a manual `git commit` records a merge commit that bloats the PR")
|
|
5767
|
+
]
|
|
5768
|
+
},
|
|
5769
|
+
syncReviewStep(config, run),
|
|
5770
|
+
await syncAlignmentsStep(config, run)
|
|
5771
|
+
];
|
|
5772
|
+
return steps.filter((step) => !!step);
|
|
5153
5773
|
}
|
|
5154
|
-
var
|
|
5774
|
+
var UPGRADE_PR_TITLE = "chore: upgrade cella";
|
|
5155
5775
|
async function buildSyncCommitMessage(forkPath, upstreamRef) {
|
|
5156
5776
|
const shortSha = await getShortSha(forkPath, upstreamRef).catch(() => "");
|
|
5157
|
-
if (!shortSha) return
|
|
5777
|
+
if (!shortSha) return UPGRADE_PR_TITLE;
|
|
5158
5778
|
const manifest = await readSyncManifest(forkPath);
|
|
5159
5779
|
const version = manifest?.upstream.release ?? await readPackageVersionAtRef(forkPath, upstreamRef).catch(() => null);
|
|
5160
|
-
return version ? `${
|
|
5780
|
+
return version ? `${UPGRADE_PR_TITLE} to v${version.replace(/^v/, "")} (${shortSha})` : `${UPGRADE_PR_TITLE} to ${shortSha}`;
|
|
5161
5781
|
}
|
|
5162
5782
|
function qualifyPrRefs(subject, repoSlug) {
|
|
5163
5783
|
if (!repoSlug) return subject;
|
|
5164
5784
|
return subject.replace(/(^|[^\w/])#(\d+)\b/g, `$1${repoSlug}#$2`);
|
|
5165
5785
|
}
|
|
5166
5786
|
function buildSyncPrBody(input) {
|
|
5167
|
-
const { repoSlug, version, fromSha, toSha, commits, totalCount,
|
|
5787
|
+
const { repoSlug, version, fromSha, toSha, commits, totalCount, alignments } = input;
|
|
5168
5788
|
const githubUrl = repoSlug ? `https://github.com/${repoSlug}` : void 0;
|
|
5169
5789
|
const short = (sha) => sha.slice(0, 7);
|
|
5170
5790
|
const commitRef = (sha) => githubUrl ? `[\`${short(sha)}\`](${githubUrl}/commit/${sha})` : `\`${short(sha)}\``;
|
|
5171
5791
|
const upstreamName = githubUrl ? `[${repoSlug}](${githubUrl})` : "cella";
|
|
5172
5792
|
const versionSuffix = version ? ` (v${version.replace(/^v/, "")})` : "";
|
|
5173
|
-
const lines = [`
|
|
5793
|
+
const lines = [`Upgrades ${upstreamName} to ${commitRef(toSha)}${versionSuffix}.`];
|
|
5174
5794
|
if (totalCount > 0 && fromSha) {
|
|
5175
5795
|
const compare = githubUrl ? ` ([compare](${githubUrl}/compare/${short(fromSha)}...${short(toSha)}))` : "";
|
|
5176
|
-
lines.push(
|
|
5796
|
+
lines.push(
|
|
5797
|
+
"",
|
|
5798
|
+
`**${totalCount} upstream commit${totalCount === 1 ? "" : "s"} since the last upgrade**${compare}:`,
|
|
5799
|
+
""
|
|
5800
|
+
);
|
|
5177
5801
|
if (totalCount > commits.length) lines.push(`- \u2026${totalCount - commits.length} earlier commit(s) not shown`);
|
|
5178
5802
|
for (const commit of commits) {
|
|
5179
5803
|
lines.push(`- ${commitRef(commit.hash)} ${qualifyPrRefs(commit.message, repoSlug)}`);
|
|
5180
5804
|
}
|
|
5181
5805
|
}
|
|
5182
|
-
if (
|
|
5183
|
-
const { total, open } =
|
|
5184
|
-
if (open.length === 0) lines.push("", `**
|
|
5806
|
+
if (alignments && alignments.total > 0) {
|
|
5807
|
+
const { total, open } = alignments;
|
|
5808
|
+
if (open.length === 0) lines.push("", `**Alignments**: all ${total} handled.`);
|
|
5185
5809
|
else {
|
|
5186
5810
|
lines.push(
|
|
5187
5811
|
"",
|
|
5188
|
-
`**
|
|
5812
|
+
`**Alignments**: ${Math.max(0, total - open.length)} of ${total} handled, open (\`pnpm cella align\`):`,
|
|
5189
5813
|
""
|
|
5190
5814
|
);
|
|
5191
|
-
for (const
|
|
5192
|
-
lines.push(
|
|
5815
|
+
for (const alignment of open)
|
|
5816
|
+
lines.push(
|
|
5817
|
+
`- ${alignment.url ? `[${alignment.title}](${alignment.url})` : alignment.title} (\`${alignment.id}\`)`
|
|
5818
|
+
);
|
|
5193
5819
|
}
|
|
5194
5820
|
}
|
|
5195
5821
|
return `${lines.join("\n")}
|
|
@@ -5203,22 +5829,30 @@ async function buildSyncPrBodyForBranch(forkPath, base) {
|
|
|
5203
5829
|
const version = manifest.upstream.release ?? await readPackageVersionAtRef(forkPath, toSha).catch(() => null);
|
|
5204
5830
|
const totalCount = fromSha ? await countCommitsBetween(forkPath, fromSha, toSha).catch(() => 0) : 0;
|
|
5205
5831
|
const commits = fromSha && totalCount > 0 ? await listCommitsBetween(forkPath, fromSha, toSha, COMMIT_LIST_MAX) : [];
|
|
5206
|
-
const
|
|
5207
|
-
return buildSyncPrBody({
|
|
5832
|
+
const alignments = await readPrBodyAlignments(forkPath, toSha, manifest.upstream.repo).catch(() => void 0);
|
|
5833
|
+
return buildSyncPrBody({
|
|
5834
|
+
repoSlug: manifest.upstream.repo,
|
|
5835
|
+
version,
|
|
5836
|
+
fromSha,
|
|
5837
|
+
toSha,
|
|
5838
|
+
commits,
|
|
5839
|
+
totalCount,
|
|
5840
|
+
alignments
|
|
5841
|
+
});
|
|
5208
5842
|
}
|
|
5209
|
-
async function
|
|
5843
|
+
async function readPrBodyAlignments(forkPath, toSha, repoSlug) {
|
|
5210
5844
|
const githubUrl = repoSlug ? `https://github.com/${repoSlug}` : void 0;
|
|
5211
|
-
const [ids, pending] = await Promise.all([
|
|
5845
|
+
const [ids, pending] = await Promise.all([listAlignmentIds(forkPath, toSha), readPending(forkPath)]);
|
|
5212
5846
|
const open = await Promise.all(
|
|
5213
5847
|
pending.map(async (id) => ({
|
|
5214
5848
|
id,
|
|
5215
|
-
title: (await
|
|
5216
|
-
url:
|
|
5849
|
+
title: (await readAlignment(forkPath, toSha, id))?.title ?? id,
|
|
5850
|
+
url: alignmentUrl(githubUrl, toSha, id)
|
|
5217
5851
|
}))
|
|
5218
5852
|
);
|
|
5219
5853
|
return { total: ids.length, open };
|
|
5220
5854
|
}
|
|
5221
|
-
function printPrCreateStep(branch, base, title =
|
|
5855
|
+
function printPrCreateStep(branch, base, title = UPGRADE_PR_TITLE) {
|
|
5222
5856
|
console.info(colors_default.dim(` gh pr create --base ${base} --head ${branch} --title "${title}" --fill`));
|
|
5223
5857
|
}
|
|
5224
5858
|
async function flattenSyncBranch(forkPath, branch, base) {
|
|
@@ -5242,7 +5876,7 @@ async function shipSyncBranch(config, branch) {
|
|
|
5242
5876
|
let prUrl;
|
|
5243
5877
|
let prOpened = false;
|
|
5244
5878
|
const upstreamCommit = (await readSyncManifest(forkPath))?.upstream.commit;
|
|
5245
|
-
const prTitle = upstreamCommit ? await buildSyncCommitMessage(forkPath, upstreamCommit) :
|
|
5879
|
+
const prTitle = upstreamCommit ? await buildSyncCommitMessage(forkPath, upstreamCommit) : UPGRADE_PR_TITLE;
|
|
5246
5880
|
console.info(colors_default.dim(`pushing '${branch}' to origin...`));
|
|
5247
5881
|
try {
|
|
5248
5882
|
await pushBranch(forkPath, "origin", branch, { forceWithLease: flattened });
|
|
@@ -5254,16 +5888,11 @@ async function shipSyncBranch(config, branch) {
|
|
|
5254
5888
|
}
|
|
5255
5889
|
if (ghAvailable()) {
|
|
5256
5890
|
console.info(colors_default.dim("opening a pull request..."));
|
|
5257
|
-
const prBody = await buildSyncPrBodyForBranch(forkPath, cutBase);
|
|
5258
|
-
const
|
|
5259
|
-
const pr =
|
|
5260
|
-
|
|
5261
|
-
|
|
5262
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
5263
|
-
});
|
|
5264
|
-
prUrl = `${pr.stdout ?? ""}
|
|
5265
|
-
${pr.stderr ?? ""}`.match(/https?:\/\/\S+/)?.[0];
|
|
5266
|
-
prOpened = pr.status === 0;
|
|
5891
|
+
const prBody = await buildSyncPrBodyForBranch(forkPath, cutBase) ?? "";
|
|
5892
|
+
const repoSlug = (await readOriginGitHubUrl(forkPath))?.replace("https://github.com/", "") ?? null;
|
|
5893
|
+
const pr = createPr(forkPath, repoSlug, { base, head: branch, title: prTitle, body: prBody });
|
|
5894
|
+
prUrl = pr.url;
|
|
5895
|
+
prOpened = pr.ok;
|
|
5267
5896
|
if (!prOpened) {
|
|
5268
5897
|
console.info(colors_default.yellow("could not open the PR automatically (it may already exist). open it with:"));
|
|
5269
5898
|
if (prUrl) console.info(colors_default.dim(` ${prUrl}`));
|
|
@@ -5276,13 +5905,21 @@ ${pr.stderr ?? ""}`.match(/https?:\/\/\S+/)?.[0];
|
|
|
5276
5905
|
const position = await returnToBase(forkPath, base);
|
|
5277
5906
|
console.info();
|
|
5278
5907
|
if (prUrl) {
|
|
5279
|
-
console.info(`${checkMark}
|
|
5908
|
+
console.info(`${checkMark} upgrade pull request ${prOpened ? "opened" : "ready"}`);
|
|
5280
5909
|
console.info(colors_default.dim(` ${prUrl} \xB7 branch pushed, ${position}`));
|
|
5281
5910
|
} else {
|
|
5282
|
-
console.info(`${checkMark}
|
|
5911
|
+
console.info(`${checkMark} upgrade branch pushed`);
|
|
5283
5912
|
console.info(colors_default.dim(` '${branch}' is on origin, ${position}`));
|
|
5284
5913
|
}
|
|
5285
|
-
|
|
5914
|
+
const steps = [
|
|
5915
|
+
prUrl ? {
|
|
5916
|
+
label: "merge",
|
|
5917
|
+
line: hyperlink(colors_default.cyan(prUrl), prUrl),
|
|
5918
|
+
details: [colors_default.dim(`squash-merge it into '${base}'; the next upgrade offers to while it is open`)]
|
|
5919
|
+
} : void 0,
|
|
5920
|
+
await syncAlignmentsStep(config)
|
|
5921
|
+
];
|
|
5922
|
+
printNextSteps(steps.filter((step) => !!step));
|
|
5286
5923
|
}
|
|
5287
5924
|
function finalizeWorkspace(forkPath) {
|
|
5288
5925
|
for (const args of [["install"], ["check"]]) {
|
|
@@ -5318,43 +5955,56 @@ async function runSyncCycle(config) {
|
|
|
5318
5955
|
}
|
|
5319
5956
|
throw error;
|
|
5320
5957
|
}
|
|
5321
|
-
if (config.settings.
|
|
5958
|
+
if (config.settings.mergePackageJson !== false) {
|
|
5322
5959
|
await runPackages(config, { conflictedFiles: result.conflicts });
|
|
5323
5960
|
}
|
|
5324
|
-
if (result.conflicts.length > 0) return { status: "conflicts", branch };
|
|
5961
|
+
if (result.conflicts.length > 0) return { status: "conflicts", branch, result };
|
|
5325
5962
|
if (!await mergeInProgress(forkPath)) {
|
|
5326
5963
|
await discardTemporarySyncBranch(forkPath, branch);
|
|
5327
|
-
return { status: "noop" };
|
|
5964
|
+
return { status: "noop", result };
|
|
5328
5965
|
}
|
|
5329
|
-
return { status: "staged", branch };
|
|
5966
|
+
return { status: "staged", branch, result };
|
|
5330
5967
|
}
|
|
5331
|
-
async function commitSyncMerge(config, branch) {
|
|
5968
|
+
async function commitSyncMerge(config, branch, run) {
|
|
5332
5969
|
const { forkPath } = config;
|
|
5333
5970
|
const conflicts = await getConflictedFiles(forkPath);
|
|
5334
5971
|
console.info();
|
|
5335
5972
|
if (conflicts.length > 0) {
|
|
5336
|
-
|
|
5337
|
-
|
|
5338
|
-
console.info(colors_default.dim("resolve and stage them, then re-run `pnpm cella sync` to finish (it commits for you)."));
|
|
5973
|
+
printStep("conflicts", `${plural(conflicts.length, "file")} still conflict on '${branch}'`, warningMark);
|
|
5974
|
+
printNextSteps(await conflictSteps(config, conflicts, run));
|
|
5339
5975
|
return;
|
|
5340
5976
|
}
|
|
5341
5977
|
await stageAll(forkPath);
|
|
5342
5978
|
if (!finalizeWorkspace(forkPath)) {
|
|
5343
5979
|
console.info();
|
|
5344
|
-
|
|
5345
|
-
|
|
5346
|
-
|
|
5980
|
+
printStep("checks", "`pnpm install` or `pnpm check` failed", warningMark);
|
|
5981
|
+
printNextSteps([
|
|
5982
|
+
{ label: "fix", line: "what failed above", details: [colors_default.dim(`the merge stays staged on '${branch}'`)] },
|
|
5983
|
+
{
|
|
5984
|
+
label: "finish",
|
|
5985
|
+
line: "pnpm cella upgrade",
|
|
5986
|
+
details: [colors_default.dim("commits the upgrade and stops for drift triage")]
|
|
5987
|
+
}
|
|
5988
|
+
]);
|
|
5347
5989
|
return;
|
|
5348
5990
|
}
|
|
5349
5991
|
const message = await buildSyncCommitMessage(forkPath, "MERGE_HEAD");
|
|
5350
5992
|
await stageAll(forkPath);
|
|
5351
5993
|
await commitSquash(forkPath, message);
|
|
5352
5994
|
console.info();
|
|
5353
|
-
|
|
5354
|
-
|
|
5355
|
-
|
|
5356
|
-
|
|
5357
|
-
|
|
5995
|
+
printStep("committed", `${message}
|
|
5996
|
+
${colors_default.dim(`on '${branch}', not pushed`)}`);
|
|
5997
|
+
const steps = [
|
|
5998
|
+
syncReviewStep(config, run),
|
|
5999
|
+
{
|
|
6000
|
+
label: "triage",
|
|
6001
|
+
line: "pnpm cella analyze",
|
|
6002
|
+
details: [colors_default.dim("drift triage: commit any follow-ups on this branch")]
|
|
6003
|
+
},
|
|
6004
|
+
await syncAlignmentsStep(config, run),
|
|
6005
|
+
{ label: "ship", line: "pnpm cella upgrade", details: [colors_default.dim("pushes the branch and opens the squash-merge PR")] }
|
|
6006
|
+
];
|
|
6007
|
+
printNextSteps(steps.filter((step) => !!step));
|
|
5358
6008
|
}
|
|
5359
6009
|
async function mergeOpenSyncPrs(config, open) {
|
|
5360
6010
|
const { forkPath, settings } = config;
|
|
@@ -5365,7 +6015,7 @@ async function mergeOpenSyncPrs(config, open) {
|
|
|
5365
6015
|
const merged = mergePrSquash(forkPath, newest.number, { deleteBranch: true });
|
|
5366
6016
|
if (!merged.ok) {
|
|
5367
6017
|
console.info();
|
|
5368
|
-
console.info(colors_default.yellow(`could not merge #${newest.number}: resolve it first, then re-run \`pnpm cella
|
|
6018
|
+
console.info(colors_default.yellow(`could not merge #${newest.number}: resolve it first, then re-run \`pnpm cella upgrade\`:`));
|
|
5369
6019
|
if (merged.output) console.info(colors_default.dim(merged.output.replace(/^/gm, " ")));
|
|
5370
6020
|
console.info(colors_default.dim(` ${newest.url}`));
|
|
5371
6021
|
return "cancel";
|
|
@@ -5382,25 +6032,27 @@ async function mergeOpenSyncPrs(config, open) {
|
|
|
5382
6032
|
async function guardAgainstOpenSyncPr(config) {
|
|
5383
6033
|
const { forkPath } = config;
|
|
5384
6034
|
if (!ghAvailable() || !process.stdout.isTTY) return "continue";
|
|
5385
|
-
const open = listOpenSyncPrs(forkPath,
|
|
6035
|
+
const open = listOpenSyncPrs(forkPath, TEMPORARY_BRANCH_PREFIXES);
|
|
5386
6036
|
if (open.length === 0) return "continue";
|
|
5387
6037
|
const many = open.length > 1;
|
|
5388
6038
|
console.info();
|
|
5389
6039
|
console.info(
|
|
5390
|
-
`${warningMark} ${colors_default.yellow(`${many ? `${open.length} earlier
|
|
6040
|
+
`${warningMark} ${colors_default.yellow(`${many ? `${open.length} earlier upgrade PRs are` : "an earlier upgrade PR is"} still open and unmerged:`)}`
|
|
5391
6041
|
);
|
|
5392
6042
|
for (const pr of open) {
|
|
5393
6043
|
console.info(colors_default.dim(` #${pr.number} ${pr.title}`));
|
|
5394
6044
|
console.info(colors_default.dim(` ${pr.url}`));
|
|
5395
6045
|
}
|
|
5396
|
-
console.info(
|
|
6046
|
+
console.info(
|
|
6047
|
+
colors_default.dim(` starting a new upgrade now produces a PR that re-includes ${many ? "these" : "those"} changes.`)
|
|
6048
|
+
);
|
|
5397
6049
|
console.info();
|
|
5398
6050
|
const choice = await select4({
|
|
5399
6051
|
message: "how do you want to proceed?",
|
|
5400
6052
|
choices: [
|
|
5401
6053
|
{
|
|
5402
6054
|
value: "merge",
|
|
5403
|
-
name: `merge ${many ? "them" : `#${open[0].number}`} first, then
|
|
6055
|
+
name: `merge ${many ? "them" : `#${open[0].number}`} first, then upgrade ${colors_default.dim("(recommended)")}`
|
|
5404
6056
|
},
|
|
5405
6057
|
{ value: "continue", name: `continue anyway ${colors_default.dim("(new PR will re-include the unmerged changes)")}` },
|
|
5406
6058
|
{ value: "cancel", name: colors_default.red("cancel") }
|
|
@@ -5411,7 +6063,7 @@ async function guardAgainstOpenSyncPr(config) {
|
|
|
5411
6063
|
if (choice === "continue") return "continue";
|
|
5412
6064
|
return mergeOpenSyncPrs(config, open);
|
|
5413
6065
|
}
|
|
5414
|
-
async function
|
|
6066
|
+
async function runUpgradeCommand(config) {
|
|
5415
6067
|
const { forkPath } = config;
|
|
5416
6068
|
const currentBranch = await getCurrentBranch(forkPath);
|
|
5417
6069
|
const onSyncBranch = isTemporarySyncBranch(currentBranch);
|
|
@@ -5424,7 +6076,7 @@ async function runSyncCommand(config) {
|
|
|
5424
6076
|
console.info();
|
|
5425
6077
|
console.info(
|
|
5426
6078
|
colors_default.yellow(
|
|
5427
|
-
`
|
|
6079
|
+
`upgrade branch '${currentBranch}' has uncommitted changes.
|
|
5428
6080
|
commit them first (\`git commit --amend --no-edit\` or a new commit).`
|
|
5429
6081
|
)
|
|
5430
6082
|
);
|
|
@@ -5434,29 +6086,30 @@ commit them first (\`git commit --amend --no-edit\` or a new commit).`
|
|
|
5434
6086
|
return;
|
|
5435
6087
|
}
|
|
5436
6088
|
if (!await isClean(forkPath)) {
|
|
5437
|
-
throw new Error("working directory has uncommitted changes. please commit or stash before
|
|
6089
|
+
throw new Error("working directory has uncommitted changes. please commit or stash before upgrading.");
|
|
5438
6090
|
}
|
|
5439
6091
|
if (await guardAgainstOpenSyncPr(config) === "cancel") return;
|
|
5440
6092
|
const outcome = await runSyncCycle(config);
|
|
5441
|
-
console.info();
|
|
5442
6093
|
if (outcome.status === "noop") {
|
|
6094
|
+
console.info();
|
|
5443
6095
|
if (outcome.behind) {
|
|
5444
|
-
console.info(colors_default.green("nothing to
|
|
6096
|
+
console.info(colors_default.green("nothing to upgrade: the last upgrade already went past this upstream point."));
|
|
5445
6097
|
for (const line of outcome.behind.split("\n")) console.info(colors_default.dim(` ${line}`));
|
|
5446
6098
|
} else {
|
|
5447
|
-
console.info(colors_default.green("already up to date with upstream, nothing to
|
|
6099
|
+
console.info(colors_default.green("already up to date with upstream, nothing to upgrade."));
|
|
5448
6100
|
}
|
|
6101
|
+
const align = await syncAlignmentsStep(config, outcome.result);
|
|
6102
|
+
if (align) printNextSteps([align]);
|
|
5449
6103
|
return;
|
|
5450
6104
|
}
|
|
5451
6105
|
const { temporaryBranch } = outcome.branch;
|
|
5452
6106
|
if (outcome.status === "conflicts") {
|
|
5453
|
-
|
|
5454
|
-
|
|
5455
|
-
|
|
5456
|
-
console.info(colors_default.dim(" let the rerun commit: a manual `git commit` records a merge commit that bloats the PR."));
|
|
6107
|
+
const conflicts = await getConflictedFiles(forkPath);
|
|
6108
|
+
printStep("conflicts", `${plural(conflicts.length, "file")} on '${temporaryBranch}'`, warningMark);
|
|
6109
|
+
printNextSteps(await conflictSteps(config, conflicts, outcome.result));
|
|
5457
6110
|
return;
|
|
5458
6111
|
}
|
|
5459
|
-
await commitSyncMerge(config, temporaryBranch);
|
|
6112
|
+
await commitSyncMerge(config, temporaryBranch, outcome.result);
|
|
5460
6113
|
}
|
|
5461
6114
|
|
|
5462
6115
|
// src/services/forks.ts
|
|
@@ -5481,7 +6134,7 @@ function formatForkChoice(name, status) {
|
|
|
5481
6134
|
syncPart = colors_default.dim(`${status.lastSync.date}`);
|
|
5482
6135
|
if (msg) syncPart += colors_default.dim(` '${msg}'`);
|
|
5483
6136
|
} else {
|
|
5484
|
-
syncPart = colors_default.dim("never
|
|
6137
|
+
syncPart = colors_default.dim("never upgraded");
|
|
5485
6138
|
}
|
|
5486
6139
|
const dirtyPart = status.dirty > 0 ? colors_default.yellow(`${status.dirty} uncommitted`) : "";
|
|
5487
6140
|
const parts = [name, colors_default.dim(`[${status.branch}]`), syncPart];
|
|
@@ -5513,15 +6166,15 @@ async function buildForkChoices(forks, basePath) {
|
|
|
5513
6166
|
}
|
|
5514
6167
|
async function syncFork(config, fork, forkPath) {
|
|
5515
6168
|
console.info();
|
|
5516
|
-
console.info(colors_default.cyan(`
|
|
6169
|
+
console.info(colors_default.cyan(`upgrading ${fork.name}...`));
|
|
5517
6170
|
console.info(colors_default.dim(`path: ${forkPath}`));
|
|
5518
6171
|
console.info();
|
|
5519
6172
|
const forkConfig = await loadConfig(forkPath);
|
|
5520
|
-
await
|
|
6173
|
+
await runUpgradeCommand({
|
|
5521
6174
|
...forkConfig,
|
|
5522
6175
|
forkPath,
|
|
5523
6176
|
upstreamRef: resolveUpstream(forkConfig.settings).branchRef,
|
|
5524
|
-
service: "
|
|
6177
|
+
service: "upgrade",
|
|
5525
6178
|
list: false,
|
|
5526
6179
|
json: false,
|
|
5527
6180
|
verbose: false,
|
|
@@ -5548,12 +6201,12 @@ async function runForks(config) {
|
|
|
5548
6201
|
const choices = await buildForkChoices(forks, forkBasePath);
|
|
5549
6202
|
const forkChoices = [...choices, new Separator2(MENU_DIVIDER), { value: "_exit", name: colors_default.dim("exit") }];
|
|
5550
6203
|
const selectedPath = await select5({
|
|
5551
|
-
message: "select fork to
|
|
6204
|
+
message: "select fork to upgrade:",
|
|
5552
6205
|
choices: forkChoices,
|
|
5553
6206
|
loop: false
|
|
5554
6207
|
});
|
|
5555
6208
|
if (selectedPath === "_exit") {
|
|
5556
|
-
|
|
6209
|
+
process10.exit(0);
|
|
5557
6210
|
}
|
|
5558
6211
|
const resolvedForkPath = resolve6(forkBasePath, selectedPath);
|
|
5559
6212
|
const selectedFork = forks.find((f) => f.localPath === selectedPath);
|
|
@@ -5571,7 +6224,7 @@ import { readFile as readFile3 } from "fs/promises";
|
|
|
5571
6224
|
import { join as join18 } from "path";
|
|
5572
6225
|
|
|
5573
6226
|
// src/utils/coverage-utils.ts
|
|
5574
|
-
import { existsSync as
|
|
6227
|
+
import { existsSync as existsSync13, readFileSync as readFileSync7, statSync as statSync2 } from "fs";
|
|
5575
6228
|
import { join as join17, relative as relative4, sep as sep3 } from "path";
|
|
5576
6229
|
var STALE_THRESHOLD_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
5577
6230
|
function colorPct(pct) {
|
|
@@ -5594,7 +6247,7 @@ function printCoverageSummary(forkPath) {
|
|
|
5594
6247
|
let summary;
|
|
5595
6248
|
let mtimeMs = 0;
|
|
5596
6249
|
let problem = "no coverage data found";
|
|
5597
|
-
if (
|
|
6250
|
+
if (existsSync13(file)) {
|
|
5598
6251
|
try {
|
|
5599
6252
|
mtimeMs = statSync2(file).mtimeMs;
|
|
5600
6253
|
summary = JSON.parse(readFileSync7(file, "utf8"));
|
|
@@ -5994,26 +6647,26 @@ function getForkPath() {
|
|
|
5994
6647
|
const envPath = getEnv("CELLA_FORK_PATH");
|
|
5995
6648
|
if (envPath) {
|
|
5996
6649
|
const resolved = resolve7(envPath);
|
|
5997
|
-
if (!
|
|
6650
|
+
if (!existsSync14(resolved) || !statSync3(resolved).isDirectory()) {
|
|
5998
6651
|
throw new Error(`CELLA_FORK_PATH is not a valid directory: ${resolved}`);
|
|
5999
6652
|
}
|
|
6000
6653
|
return resolved;
|
|
6001
6654
|
}
|
|
6002
|
-
let cwd =
|
|
6655
|
+
let cwd = process11.cwd();
|
|
6003
6656
|
if (cwd.endsWith("/cli/cella") || cwd.endsWith("\\cli\\cella")) {
|
|
6004
6657
|
cwd = resolve7(cwd, "../..");
|
|
6005
6658
|
}
|
|
6006
6659
|
return cwd;
|
|
6007
6660
|
}
|
|
6008
6661
|
function preflight(forkPath) {
|
|
6009
|
-
if (!
|
|
6662
|
+
if (!existsSync14(join19(forkPath, ".git"))) {
|
|
6010
6663
|
throw new Error(`not a git repository: ${forkPath}`);
|
|
6011
6664
|
}
|
|
6012
6665
|
}
|
|
6013
6666
|
var services = {
|
|
6014
6667
|
analyze: runAnalyze,
|
|
6015
|
-
|
|
6016
|
-
|
|
6668
|
+
upgrade: runUpgradeCommand,
|
|
6669
|
+
align: runAlign,
|
|
6017
6670
|
audit: runAudit,
|
|
6018
6671
|
forks: runForks,
|
|
6019
6672
|
contributions: runContributions,
|
|
@@ -6024,7 +6677,7 @@ var services = {
|
|
|
6024
6677
|
}
|
|
6025
6678
|
};
|
|
6026
6679
|
function isHelpOrVersionRequest() {
|
|
6027
|
-
return
|
|
6680
|
+
return process11.argv.slice(2).some((arg) => arg === "--help" || arg === "-h" || arg === "--version" || arg === "-v") || process11.argv[2] === "help";
|
|
6028
6681
|
}
|
|
6029
6682
|
async function main() {
|
|
6030
6683
|
try {
|
|
@@ -6043,7 +6696,7 @@ async function main() {
|
|
|
6043
6696
|
} catch (error) {
|
|
6044
6697
|
console.error();
|
|
6045
6698
|
console.error(`${colors_default.red("\u2717")} ${errorMessage(error)}`);
|
|
6046
|
-
|
|
6699
|
+
process11.exit(1);
|
|
6047
6700
|
}
|
|
6048
6701
|
}
|
|
6049
6702
|
main();
|