@cellajs/cli 0.4.2 → 0.5.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 +98 -40
- package/dist/config.js.map +1 -1
- package/dist/index.js +1327 -719
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
|
@@ -213,13 +213,13 @@ ${z2.prettifyError(result.error)}`);
|
|
|
213
213
|
}
|
|
214
214
|
|
|
215
215
|
// src/utils/display.ts
|
|
216
|
-
import { resolve as
|
|
217
|
-
import
|
|
216
|
+
import { resolve as resolve3 } from "path";
|
|
217
|
+
import process5 from "process";
|
|
218
218
|
|
|
219
219
|
// package.json
|
|
220
220
|
var package_default = {
|
|
221
221
|
name: "@cellajs/cli",
|
|
222
|
-
version: "0.
|
|
222
|
+
version: "0.5.0",
|
|
223
223
|
private: false,
|
|
224
224
|
license: "MIT",
|
|
225
225
|
description: "CLI for managing your monorepo and syncing with upstream Cella.",
|
|
@@ -334,377 +334,19 @@ var package_default = {
|
|
|
334
334
|
200
|
|
335
335
|
],
|
|
336
336
|
"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";
|
|
337
|
+
0,
|
|
338
|
+
"always",
|
|
339
|
+
200
|
|
340
|
+
]
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
};
|
|
702
344
|
|
|
703
345
|
// src/utils/git.ts
|
|
704
346
|
import { execFile } from "child_process";
|
|
705
347
|
import { existsSync as existsSync3 } from "fs";
|
|
706
348
|
import { mkdir as mkdir2, readdir, rmdir, unlink } from "fs/promises";
|
|
707
|
-
import { dirname as dirname2, join as join4, resolve as
|
|
349
|
+
import { dirname as dirname2, join as join4, resolve as resolve2 } from "path";
|
|
708
350
|
import { promisify } from "util";
|
|
709
351
|
|
|
710
352
|
// src/utils/manifest.ts
|
|
@@ -840,7 +482,7 @@ async function commitSquash(cwd, message) {
|
|
|
840
482
|
await git(["commit", "-m", message], cwd, { skipEditor: true });
|
|
841
483
|
}
|
|
842
484
|
async function gitPath(cwd, file) {
|
|
843
|
-
return
|
|
485
|
+
return resolve2(cwd, await git(["rev-parse", "--git-path", file], cwd));
|
|
844
486
|
}
|
|
845
487
|
async function stageAll(cwd) {
|
|
846
488
|
await git(["add", "-A"], cwd);
|
|
@@ -848,6 +490,10 @@ async function stageAll(cwd) {
|
|
|
848
490
|
async function mergeInProgress(cwd) {
|
|
849
491
|
return await revParse(cwd, "MERGE_HEAD") !== null;
|
|
850
492
|
}
|
|
493
|
+
function splitPrNumber(subject) {
|
|
494
|
+
const match = /\s*\(#(\d+)\)\s*$/.exec(subject);
|
|
495
|
+
return match ? { subject: subject.slice(0, match.index), pr: Number(match[1]) } : { subject };
|
|
496
|
+
}
|
|
851
497
|
async function getShortSha(cwd, ref) {
|
|
852
498
|
return git(["rev-parse", "--short", ref], cwd);
|
|
853
499
|
}
|
|
@@ -920,16 +566,16 @@ async function countCommitsBetween(cwd, fromRef, toRef) {
|
|
|
920
566
|
return Number.parseInt(output.trim(), 10) || 0;
|
|
921
567
|
}
|
|
922
568
|
async function listCommitsBetween(cwd, fromRef, toRef, limit) {
|
|
923
|
-
const args = ["log", "--format=%H%x1f%s%x1f%ar", "--reverse", `${fromRef}..${toRef}`];
|
|
569
|
+
const args = ["log", "--format=%H%x1f%s%x1f%ar%x1f%ct", "--reverse", `${fromRef}..${toRef}`];
|
|
924
570
|
if (limit) args.push(`-n${limit}`);
|
|
925
571
|
const output = await git(args, cwd, { ignoreErrors: true });
|
|
926
572
|
if (!output) return [];
|
|
927
573
|
const commits = [];
|
|
928
574
|
for (const line of output.split("\n")) {
|
|
929
575
|
if (!line) continue;
|
|
930
|
-
const [hash, message, date] = line.split("");
|
|
576
|
+
const [hash, message, date, timestamp] = line.split("");
|
|
931
577
|
if (!hash || !message || !date) continue;
|
|
932
|
-
commits.push({ hash, message, date });
|
|
578
|
+
commits.push({ hash, message, date, timestamp: Number(timestamp) || void 0 });
|
|
933
579
|
}
|
|
934
580
|
return commits;
|
|
935
581
|
}
|
|
@@ -1227,40 +873,434 @@ async function getEffectiveMergeBase(cwd, headRef, upstreamRef) {
|
|
|
1227
873
|
if (!recorded) {
|
|
1228
874
|
return { base: gitBase, isStale: false, storedRef: null };
|
|
1229
875
|
}
|
|
1230
|
-
const recordedIsNewer = await isAncestor(cwd, gitBase, recorded);
|
|
1231
|
-
if (recordedIsNewer && recorded !== gitBase) {
|
|
1232
|
-
return { base: recorded, isStale: true, storedRef: recorded };
|
|
876
|
+
const recordedIsNewer = await isAncestor(cwd, gitBase, recorded);
|
|
877
|
+
if (recordedIsNewer && recorded !== gitBase) {
|
|
878
|
+
return { base: recorded, isStale: true, storedRef: recorded };
|
|
879
|
+
}
|
|
880
|
+
return { base: gitBase, isStale: false, storedRef: recorded };
|
|
881
|
+
}
|
|
882
|
+
async function withTemporarySyncBaseGraft(cwd, headRef, baseSha, fn) {
|
|
883
|
+
const headSha = await revParse(cwd, headRef);
|
|
884
|
+
if (!headSha || !await commitObjectExists(cwd, baseSha)) return fn();
|
|
885
|
+
if (await isAncestor(cwd, baseSha, headSha)) return fn();
|
|
886
|
+
const existingReplacement = await git(["replace", "-l", headSha], cwd, { ignoreErrors: true });
|
|
887
|
+
if (existingReplacement) return fn();
|
|
888
|
+
const parentsRaw = await git(["log", "-1", "--format=%P", headSha], cwd, { ignoreErrors: true });
|
|
889
|
+
const parents = parentsRaw ? parentsRaw.split(/\s+/) : [];
|
|
890
|
+
await git(["replace", "-f", "--graft", headSha, ...parents, baseSha], cwd);
|
|
891
|
+
try {
|
|
892
|
+
return await fn();
|
|
893
|
+
} finally {
|
|
894
|
+
await git(["replace", "-d", headSha], cwd, { ignoreErrors: true });
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
// src/utils/layout.ts
|
|
899
|
+
import process4 from "process";
|
|
900
|
+
import { stripVTControlCharacters } from "util";
|
|
901
|
+
function terminalColumns() {
|
|
902
|
+
const columns = process4.stdout.isTTY ? process4.stdout.columns : void 0;
|
|
903
|
+
return Math.max(60, columns || 80);
|
|
904
|
+
}
|
|
905
|
+
function visibleWidth(text) {
|
|
906
|
+
return stripVTControlCharacters(text).length;
|
|
907
|
+
}
|
|
908
|
+
function padVisible(text, width, align = "start") {
|
|
909
|
+
const padding = " ".repeat(Math.max(0, width - visibleWidth(text)));
|
|
910
|
+
return align === "start" ? `${text}${padding}` : `${padding}${text}`;
|
|
911
|
+
}
|
|
912
|
+
function fitText(text, width) {
|
|
913
|
+
if (text.length <= width) return text;
|
|
914
|
+
return `${text.slice(0, Math.max(0, width - 1)).trimEnd()}\u2026`;
|
|
915
|
+
}
|
|
916
|
+
var AGE_STEPS = [
|
|
917
|
+
[90 * 60, 60, "m"],
|
|
918
|
+
[36 * 3600, 3600, "h"],
|
|
919
|
+
[14 * 86400, 86400, "d"],
|
|
920
|
+
[70 * 86400, 7 * 86400, "w"],
|
|
921
|
+
[365 * 86400, 30 * 86400, "mo"]
|
|
922
|
+
];
|
|
923
|
+
function shortAge(epochSeconds, nowMs = Date.now()) {
|
|
924
|
+
const seconds = Math.max(0, Math.round(nowMs / 1e3 - epochSeconds));
|
|
925
|
+
if (seconds < 60) return "now";
|
|
926
|
+
for (const [below, unit, suffix] of AGE_STEPS) {
|
|
927
|
+
if (seconds < below) return `${Math.max(1, Math.round(seconds / unit))}${suffix}`;
|
|
928
|
+
}
|
|
929
|
+
return `${Math.floor(seconds / (365 * 86400))}y`;
|
|
930
|
+
}
|
|
931
|
+
function compactCount(count3) {
|
|
932
|
+
if (count3 < 1e4) return String(count3);
|
|
933
|
+
if (count3 < 1e6) return `${(count3 / 1e3).toFixed(1)}k`;
|
|
934
|
+
return `${(count3 / 1e6).toFixed(1)}M`;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
// src/utils/display.ts
|
|
938
|
+
var VERSION = package_default.version;
|
|
939
|
+
var DIVIDER = "\u2500".repeat(60);
|
|
940
|
+
var MENU_DIVIDER = "\u2500".repeat(40);
|
|
941
|
+
var COMMIT_LIST_MAX = 50;
|
|
942
|
+
var COMMIT_STEP_MAX = 10;
|
|
943
|
+
var STEP_LABEL_WIDTH = 9;
|
|
944
|
+
var STEP_INDENT = 2 + STEP_LABEL_WIDTH + 2;
|
|
945
|
+
var warningMark = colors_default.yellow("\u26A0");
|
|
946
|
+
var checkMark = colors_default.green("\u2713");
|
|
947
|
+
var activeSpinner = null;
|
|
948
|
+
var jsonMode = false;
|
|
949
|
+
function setJsonMode(enabled) {
|
|
950
|
+
jsonMode = enabled;
|
|
951
|
+
if (!enabled) return;
|
|
952
|
+
const toStderr = (...args) => {
|
|
953
|
+
process5.stderr.write(`${args.map((a) => typeof a === "string" ? a : String(a)).join(" ")}
|
|
954
|
+
`);
|
|
955
|
+
};
|
|
956
|
+
console.info = toStderr;
|
|
957
|
+
console.warn = toStderr;
|
|
958
|
+
}
|
|
959
|
+
function writeStdout(text) {
|
|
960
|
+
process5.stdout.write(text.endsWith("\n") ? text : `${text}
|
|
961
|
+
`);
|
|
962
|
+
}
|
|
963
|
+
function printHeader() {
|
|
964
|
+
const left = "\u29C8 cella cli";
|
|
965
|
+
const version = ` \xB7 v${VERSION}`;
|
|
966
|
+
const right = "cellajs.com";
|
|
967
|
+
const padding = Math.max(1, DIVIDER.length - `${left}${version}${right}`.length);
|
|
968
|
+
console.info();
|
|
969
|
+
console.info(`${colors_default.cyan(left)}${colors_default.dim(version)}${colors_default.cyan(`${" ".repeat(padding)}${right}`)}`);
|
|
970
|
+
console.info(DIVIDER);
|
|
971
|
+
console.info();
|
|
972
|
+
}
|
|
973
|
+
function createSpinner(text) {
|
|
974
|
+
const enabled = !jsonMode && !getEnv("VITEST") && getEnv("NODE_ENV") !== "test" && !!process5.stdout.isTTY;
|
|
975
|
+
const frames = ["-", "\\", "|", "/"];
|
|
976
|
+
let frame = 0;
|
|
977
|
+
let timer;
|
|
978
|
+
const spinner = {
|
|
979
|
+
text,
|
|
980
|
+
stop() {
|
|
981
|
+
clearInterval(timer);
|
|
982
|
+
if (enabled) process5.stdout.write("\r\x1B[2K");
|
|
983
|
+
}
|
|
984
|
+
};
|
|
985
|
+
const render = () => process5.stdout.write(`\r${colors_default.cyan(frames[frame++ % frames.length])} ${spinner.text}`);
|
|
986
|
+
if (enabled) {
|
|
987
|
+
render();
|
|
988
|
+
timer = setInterval(render, 80);
|
|
989
|
+
}
|
|
990
|
+
activeSpinner = spinner;
|
|
991
|
+
return spinner;
|
|
992
|
+
}
|
|
993
|
+
function printStep(label, detail, mark = checkMark) {
|
|
994
|
+
stopSpinner();
|
|
995
|
+
const [first = "", ...rest] = (detail ?? "").split("\n");
|
|
996
|
+
console.info(`${mark} ${colors_default.bold(label.padEnd(STEP_LABEL_WIDTH))} ${first}`.trimEnd());
|
|
997
|
+
for (const line of rest) console.info(line ? `${" ".repeat(STEP_INDENT)}${line}` : "");
|
|
998
|
+
}
|
|
999
|
+
function spinnerSuccess(message, detail) {
|
|
1000
|
+
stopSpinner();
|
|
1001
|
+
if (!message) return;
|
|
1002
|
+
console.info(`${checkMark} ${message}`);
|
|
1003
|
+
if (detail) {
|
|
1004
|
+
console.info(` ${colors_default.dim(detail)}`);
|
|
1005
|
+
console.info();
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
function spinnerFail(message) {
|
|
1009
|
+
stopSpinner();
|
|
1010
|
+
console.info(`${colors_default.red("\u2717")} ${message}`);
|
|
1011
|
+
}
|
|
1012
|
+
function spinnerText(text) {
|
|
1013
|
+
if (activeSpinner) {
|
|
1014
|
+
activeSpinner.text = text;
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
function stopSpinner() {
|
|
1018
|
+
if (activeSpinner) {
|
|
1019
|
+
activeSpinner.stop();
|
|
1020
|
+
activeSpinner = null;
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
function hyperlink(label, url) {
|
|
1024
|
+
if (!url) return label;
|
|
1025
|
+
const safeUrl = url.replace(/[\x00-\x1f\x7f]/g, "");
|
|
1026
|
+
return `\x1B]8;;${safeUrl}\x07${label}\x1B]8;;\x07`;
|
|
1027
|
+
}
|
|
1028
|
+
function styleSubject(subject) {
|
|
1029
|
+
if (/^chore(\([^)]*\))?: release\b/.test(subject)) return colors_default.dim(subject);
|
|
1030
|
+
const breaking = /^([a-z]+(\([^)]*\))?!:)/.exec(subject);
|
|
1031
|
+
return breaking ? `${colors_default.yellow(breaking[1])}${subject.slice(breaking[1].length)}` : subject;
|
|
1032
|
+
}
|
|
1033
|
+
function formatCheckoutStep(checkout) {
|
|
1034
|
+
const { forkPath, branch, headSha, changeCount, upstream } = checkout;
|
|
1035
|
+
const parts = [
|
|
1036
|
+
changeCount === 0 ? "clean" : `${plural(changeCount, "uncommitted change")}, not included in the analysis`
|
|
1037
|
+
];
|
|
1038
|
+
if (upstream.upstream && (upstream.ahead > 0 || upstream.behind > 0)) {
|
|
1039
|
+
parts.push(`${upstream.ahead} ahead, ${upstream.behind} behind ${upstream.upstream}`);
|
|
1040
|
+
}
|
|
1041
|
+
const line = `${branch} @ ${headSha}${colors_default.dim(` \xB7 ${parts.join(" \xB7 ")}`)}`;
|
|
1042
|
+
return resolve3(forkPath) === resolve3(process5.cwd()) ? line : `${line}
|
|
1043
|
+
${colors_default.dim(forkPath)}`;
|
|
1044
|
+
}
|
|
1045
|
+
function formatUpstreamStep(upstream) {
|
|
1046
|
+
const { label, sha, baseSha, pinned, commitCount, commits, githubUrl } = upstream;
|
|
1047
|
+
const since = commitCount === 0 ? "no new commits since the last sync" : `${plural(commitCount, "new commit")} since the last sync at ${baseSha}`;
|
|
1048
|
+
return [
|
|
1049
|
+
`${label} @ ${sha}${colors_default.dim(`${pinned ? " \xB7 --ref" : ""} \xB7 ${since}`)}`,
|
|
1050
|
+
...formatUpstreamCommits(commitCount, commits, githubUrl)
|
|
1051
|
+
].join("\n");
|
|
1052
|
+
}
|
|
1053
|
+
function formatUpstreamCommits(commitCount, commits, upstreamGitHubUrl, options = {}) {
|
|
1054
|
+
const { columns = terminalColumns(), max = COMMIT_STEP_MAX, nowMs } = options;
|
|
1055
|
+
const shown = commits.slice(-max).reverse();
|
|
1056
|
+
const rows = shown.map((commit) => ({ commit, ...splitPrNumber(commit.message) }));
|
|
1057
|
+
const prWidth = Math.max(0, ...rows.map((row) => row.pr ? `#${row.pr}`.length : 0));
|
|
1058
|
+
const ages = rows.map((row) => row.commit.timestamp ? shortAge(row.commit.timestamp, nowMs) : "");
|
|
1059
|
+
const ageWidth = Math.max(0, ...ages.map((age) => age.length));
|
|
1060
|
+
const subjectWidth = Math.max(
|
|
1061
|
+
20,
|
|
1062
|
+
columns - STEP_INDENT - 7 - 2 - (prWidth ? prWidth + 2 : 0) - (ageWidth ? ageWidth + 2 : 0)
|
|
1063
|
+
);
|
|
1064
|
+
const lines = rows.map((row, index) => {
|
|
1065
|
+
const hash = hyperlink(
|
|
1066
|
+
row.commit.hash.slice(0, 7),
|
|
1067
|
+
upstreamGitHubUrl && `${upstreamGitHubUrl}/commit/${row.commit.hash}`
|
|
1068
|
+
);
|
|
1069
|
+
const subject = fitText(row.subject, subjectWidth);
|
|
1070
|
+
let line = `${colors_default.dim(hash)} ${padVisible(styleSubject(subject), subjectWidth)}`;
|
|
1071
|
+
if (prWidth) {
|
|
1072
|
+
const pr = row.pr ? hyperlink(`#${row.pr}`, upstreamGitHubUrl && `${upstreamGitHubUrl}/pull/${row.pr}`) : "";
|
|
1073
|
+
line += ` ${colors_default.dim(padVisible(pr, prWidth))}`;
|
|
1074
|
+
}
|
|
1075
|
+
if (ageWidth) line += ` ${colors_default.dim(ages[index].padStart(ageWidth))}`;
|
|
1076
|
+
return line.trimEnd();
|
|
1077
|
+
});
|
|
1078
|
+
if (commitCount > shown.length) lines.push(colors_default.dim(`+ ${commitCount - shown.length} earlier`));
|
|
1079
|
+
return lines;
|
|
1080
|
+
}
|
|
1081
|
+
function printNextSteps(steps) {
|
|
1082
|
+
if (steps.length === 0) return;
|
|
1083
|
+
printSectionHeader(colors_default.cyan("next steps"));
|
|
1084
|
+
for (const [index, step] of steps.entries()) {
|
|
1085
|
+
const label = step.label.padEnd(STEP_INDENT - 5);
|
|
1086
|
+
console.info(` ${colors_default.dim(String(index + 1))} ${colors_default.bold(label)} ${step.line}`.trimEnd());
|
|
1087
|
+
for (const detail of step.details ?? []) console.info(`${" ".repeat(STEP_INDENT)}${detail}`);
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
function printSectionHeader(title) {
|
|
1091
|
+
console.info();
|
|
1092
|
+
console.info(title);
|
|
1093
|
+
console.info(DIVIDER);
|
|
1094
|
+
console.info();
|
|
1095
|
+
}
|
|
1096
|
+
function formatMergeInProgressDetail(conflictCount, autoMergedFiles, linkOptions, maxFiles = 100) {
|
|
1097
|
+
const lines = [`${plural(conflictCount, "conflict")} to resolve in your editor`];
|
|
1098
|
+
if (autoMergedFiles.length === 0) {
|
|
1099
|
+
return lines.join("\n");
|
|
1100
|
+
}
|
|
1101
|
+
lines.push("");
|
|
1102
|
+
lines.push(` auto-merged files (${autoMergedFiles.length}):`);
|
|
1103
|
+
const { forkPath } = linkOptions;
|
|
1104
|
+
const shown = autoMergedFiles.slice(0, maxFiles);
|
|
1105
|
+
for (const filePath of shown) {
|
|
1106
|
+
lines.push(` - ${forkPath ? hyperlink(filePath, `vscode://file${resolve3(forkPath, filePath)}`) : filePath}`);
|
|
1107
|
+
}
|
|
1108
|
+
if (autoMergedFiles.length > shown.length) {
|
|
1109
|
+
lines.push(` ... + ${autoMergedFiles.length - shown.length} more`);
|
|
1110
|
+
}
|
|
1111
|
+
return lines.join("\n");
|
|
1112
|
+
}
|
|
1113
|
+
var statusConfig = {
|
|
1114
|
+
behind: { icon: colors_default.cyan("\u2193"), label: "behind", color: colors_default.cyan, description: "upstream changed, will sync" },
|
|
1115
|
+
diverged: { icon: colors_default.magenta("\u21C5"), label: "diverged", color: colors_default.magenta, description: "both changed, will merge" },
|
|
1116
|
+
drifted: { icon: colors_default.yellow("!"), label: "drifted", color: colors_default.yellow, description: "fork changed (at risk)" },
|
|
1117
|
+
ahead: { icon: colors_default.blue("\u2191"), label: "ahead", color: colors_default.blue, description: "fork changed (protected)" },
|
|
1118
|
+
local: { icon: colors_default.green("+"), label: "local", color: colors_default.green, description: "local file in cella folders" },
|
|
1119
|
+
pinned: { icon: colors_default.green("\u2A00"), label: "pinned", color: colors_default.green, description: "both changed, fork wins" },
|
|
1120
|
+
ignored: { icon: colors_default.gray("\u2A02"), label: "ignored", color: colors_default.gray },
|
|
1121
|
+
identical: { icon: colors_default.gray("\u2713"), label: "identical", color: colors_default.gray, description: "no changes" },
|
|
1122
|
+
deleted: { icon: colors_default.red("\u2717"), label: "deleted", color: colors_default.red },
|
|
1123
|
+
renamed: { icon: colors_default.blue("\u2192"), label: "renamed", color: colors_default.blue }
|
|
1124
|
+
};
|
|
1125
|
+
var summaryStatusConfig = {
|
|
1126
|
+
...statusConfig,
|
|
1127
|
+
managed: {
|
|
1128
|
+
icon: colors_default.cyan("\u25C7"),
|
|
1129
|
+
label: "managed",
|
|
1130
|
+
color: colors_default.cyan,
|
|
1131
|
+
description: "package.jsons & cella/cella.config.ts handled separately"
|
|
1132
|
+
},
|
|
1133
|
+
ignored: { ...statusConfig.ignored, description: "protected by ignored config" }
|
|
1134
|
+
};
|
|
1135
|
+
function printSummary(summary, title = "summary") {
|
|
1136
|
+
printSectionHeader(colors_default.cyan(title));
|
|
1137
|
+
const maxCount = Math.max(...Object.values(summary));
|
|
1138
|
+
const countWidth = String(maxCount).length + 1;
|
|
1139
|
+
const groups = [
|
|
1140
|
+
["managed", "ignored"],
|
|
1141
|
+
["identical"],
|
|
1142
|
+
["ahead", "local", "drifted"],
|
|
1143
|
+
["diverged", "pinned"],
|
|
1144
|
+
["behind"]
|
|
1145
|
+
];
|
|
1146
|
+
for (const group of groups) {
|
|
1147
|
+
if (group !== groups[0]) console.info();
|
|
1148
|
+
for (const status of group) {
|
|
1149
|
+
const { icon, label, color, description } = summaryStatusConfig[status];
|
|
1150
|
+
const countStr = color(String(summary[status]).padStart(countWidth));
|
|
1151
|
+
console.info(` ${icon} ${countStr} ${label.padEnd(15)}${description ? colors_default.dim(description) : ""}`);
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
function filesWithStatus(files, status) {
|
|
1156
|
+
return files.filter((f) => f.status === status && !isManagedFile(f.path));
|
|
1157
|
+
}
|
|
1158
|
+
function findProtectedBehind(files) {
|
|
1159
|
+
return files.filter((file) => file.upstreamChanged && !isManagedFile(file.path) && !isGeneratedFile(file.path));
|
|
1160
|
+
}
|
|
1161
|
+
function findProtectedBehindUpstream(result) {
|
|
1162
|
+
const ignoredOnly = new Set((result.ignoredUpstreamChanges ?? []).flatMap((group) => group.paths));
|
|
1163
|
+
return [...findProtectedBehind(result.files), ...result.files.filter((file) => ignoredOnly.has(file.path))];
|
|
1164
|
+
}
|
|
1165
|
+
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.";
|
|
1166
|
+
function protectionIcon(file) {
|
|
1167
|
+
return file.isIgnored ? statusConfig.ignored.icon : statusConfig.pinned.icon;
|
|
1168
|
+
}
|
|
1169
|
+
function formatUpstreamChangedLines(file) {
|
|
1170
|
+
if (file.upstreamChangedLines === void 0) return "";
|
|
1171
|
+
return colors_default.yellow(` \xB7 ${plural(file.upstreamChangedLines, "line")} changed upstream`);
|
|
1172
|
+
}
|
|
1173
|
+
function printSyncComplete(result, options = {}) {
|
|
1174
|
+
const updated = result.files.filter((file) => file.status === "behind" || file.status === "diverged").length;
|
|
1175
|
+
const diverged = result.files.filter((file) => file.status === "diverged").length;
|
|
1176
|
+
const merged = result.autoMergedFiles?.length ?? Math.max(0, diverged - result.conflicts.length);
|
|
1177
|
+
const conflicts = result.conflicts.length;
|
|
1178
|
+
const counts = `${plural(updated, "file")} updated, ${merged} auto-merged, ${plural(conflicts, "conflict")}`;
|
|
1179
|
+
console.info();
|
|
1180
|
+
if (options.stagedBranch) {
|
|
1181
|
+
console.info(`${checkMark} sync merge staged on '${options.stagedBranch}'`);
|
|
1182
|
+
console.info(colors_default.dim(` ${counts}. committing next...`));
|
|
1183
|
+
} else if (conflicts > 0) {
|
|
1184
|
+
console.info(`${warningMark} sync merge staged with conflicts`);
|
|
1185
|
+
console.info(colors_default.dim(` ${counts}`));
|
|
1186
|
+
} else {
|
|
1187
|
+
console.info(`${checkMark} sync complete`);
|
|
1188
|
+
console.info(colors_default.dim(` ${counts}`));
|
|
1189
|
+
}
|
|
1190
|
+
printProtectedConflicts(result);
|
|
1191
|
+
console.info();
|
|
1192
|
+
}
|
|
1193
|
+
function printProtectedConflicts(result) {
|
|
1194
|
+
const paths = result.protectedConflicts ?? [];
|
|
1195
|
+
if (paths.length === 0) return;
|
|
1196
|
+
const byPath2 = new Map(result.files.map((file) => [file.path, file]));
|
|
1197
|
+
console.info();
|
|
1198
|
+
console.info(
|
|
1199
|
+
`${warningMark} ${colors_default.yellow(
|
|
1200
|
+
`${plural(paths.length, "protected file kept", "protected files kept")} the fork version, dropping upstream changes:`
|
|
1201
|
+
)}`
|
|
1202
|
+
);
|
|
1203
|
+
for (const path of paths) {
|
|
1204
|
+
const file = byPath2.get(path);
|
|
1205
|
+
const icon = file ? protectionIcon(file) : statusConfig.pinned.icon;
|
|
1206
|
+
console.info(` ${icon} ${path}${file ? formatUpstreamChangedLines(file) : ""}`);
|
|
1207
|
+
}
|
|
1208
|
+
console.info(colors_default.dim(` ${PROTECTED_BEHIND_HINT}`));
|
|
1209
|
+
}
|
|
1210
|
+
function plural(count3, singular, pluralForm = `${singular}s`) {
|
|
1211
|
+
return `${count3} ${count3 === 1 ? singular : pluralForm}`;
|
|
1212
|
+
}
|
|
1213
|
+
function printIgnoredUpstreamChanges(result) {
|
|
1214
|
+
const groups = result.ignoredUpstreamChanges ?? [];
|
|
1215
|
+
if (groups.length === 0) return;
|
|
1216
|
+
const total = groups.reduce((count3, group) => count3 + group.paths.length, 0);
|
|
1217
|
+
console.info();
|
|
1218
|
+
console.info(
|
|
1219
|
+
`${warningMark} ${colors_default.yellow(`ignored paths changed upstream \xB7 ${plural(total, "file")} under ${plural(groups.length, "entry", "entries")}`)}`
|
|
1220
|
+
);
|
|
1221
|
+
for (const group of groups) {
|
|
1222
|
+
const counts = [plural(group.paths.length, "file")];
|
|
1223
|
+
if (group.added > 0) counts.push(`${group.added} new`);
|
|
1224
|
+
if (group.deleted > 0) counts.push(`${group.deleted} deleted`);
|
|
1225
|
+
console.info(` ${statusConfig.ignored.icon} ${group.entry} ${colors_default.dim(`\xB7 ${counts.join(", ")}`)}`);
|
|
1226
|
+
}
|
|
1227
|
+
console.info(colors_default.dim(" the fork left these untouched and ignored paths never sync: review and adopt what you need."));
|
|
1228
|
+
}
|
|
1229
|
+
function printUpstreamOverrideChanges(result) {
|
|
1230
|
+
const report = result.upstreamOverrides;
|
|
1231
|
+
if (!report) return;
|
|
1232
|
+
console.info();
|
|
1233
|
+
if (report.kind === "unreadable") {
|
|
1234
|
+
const side = report.side === "base" ? "the last sync point" : "the incoming ref";
|
|
1235
|
+
console.info(
|
|
1236
|
+
colors_default.dim(`upstream sync config not compared: upstream ${CONFIG_FILE} is missing or unreadable at ${side}`)
|
|
1237
|
+
);
|
|
1238
|
+
return;
|
|
1239
|
+
}
|
|
1240
|
+
const lines = [];
|
|
1241
|
+
for (const list of ["pinned", "ignored", "packageJsonSync"]) {
|
|
1242
|
+
for (const entry of report[list].added) lines.push(` ${colors_default.green("+")} ${list}: ${entry}`);
|
|
1243
|
+
for (const entry of report[list].removed) {
|
|
1244
|
+
lines.push(` ${colors_default.red("\u2212")} ${list}: ${entry}${colors_default.dim(" \xB7 dropped upstream, still in your config")}`);
|
|
1245
|
+
}
|
|
1233
1246
|
}
|
|
1234
|
-
|
|
1247
|
+
console.info(
|
|
1248
|
+
`${warningMark} ${colors_default.yellow(`upstream changed its sync config \xB7 ${plural(lines.length, "entry", "entries")} to review`)}`
|
|
1249
|
+
);
|
|
1250
|
+
for (const line of lines) console.info(line);
|
|
1251
|
+
console.info(colors_default.dim(` ${CONFIG_FILE} never syncs: add or drop these by hand where they fit your app.`));
|
|
1235
1252
|
}
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
const
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1253
|
+
function findMaskingPins(files) {
|
|
1254
|
+
return files.filter(
|
|
1255
|
+
(file) => file.isPinned && !isManagedFile(file.path) && file.status === "behind" && file.existsInFork && file.existsInUpstream
|
|
1256
|
+
);
|
|
1257
|
+
}
|
|
1258
|
+
function printMaskingPinWarning(files) {
|
|
1259
|
+
const masking = findMaskingPins(files);
|
|
1260
|
+
if (masking.length === 0) return;
|
|
1261
|
+
console.info();
|
|
1262
|
+
console.info(
|
|
1263
|
+
`${warningMark} ${colors_default.yellow(
|
|
1264
|
+
`${plural(masking.length, "pinned file matches", "pinned files match")} the previous upstream but changed upstream:`
|
|
1265
|
+
)}`
|
|
1266
|
+
);
|
|
1267
|
+
console.info(colors_default.yellow(" the pin keeps the old fork copy and silently drops those upstream changes:"));
|
|
1268
|
+
for (const file of masking) {
|
|
1269
|
+
console.info(colors_default.dim(` \u2A00 ${file.path}`));
|
|
1249
1270
|
}
|
|
1271
|
+
console.info(
|
|
1272
|
+
colors_default.yellow(" if the fork never customized these, unpin them in cella/cella.config.ts to take upstream.")
|
|
1273
|
+
);
|
|
1274
|
+
console.info();
|
|
1250
1275
|
}
|
|
1276
|
+
function printEngineReports(result, title) {
|
|
1277
|
+
printSummary(result.summary, title);
|
|
1278
|
+
printIgnoredUpstreamChanges(result);
|
|
1279
|
+
printUpstreamOverrideChanges(result);
|
|
1280
|
+
printMaskingPinWarning(result.files);
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
// src/utils/overrides.ts
|
|
1284
|
+
import { existsSync as existsSync5 } from "fs";
|
|
1285
|
+
import { join as join6 } from "path";
|
|
1251
1286
|
|
|
1252
|
-
// src/utils/
|
|
1287
|
+
// src/utils/alignments.ts
|
|
1253
1288
|
import { existsSync as existsSync4 } from "fs";
|
|
1254
1289
|
import { mkdir as mkdir3, readFile as readFile2, rm, writeFile as writeFile2 } from "fs/promises";
|
|
1255
1290
|
import { dirname as dirname3, join as join5 } from "path";
|
|
1256
|
-
var
|
|
1257
|
-
var
|
|
1258
|
-
var
|
|
1291
|
+
var ALIGNMENTS_DIR = "cella/alignments";
|
|
1292
|
+
var UPSTREAM_ONLY_DIRS = [ALIGNMENTS_DIR, "cella/migrations"];
|
|
1293
|
+
var PENDING_FILE = "cella/cella.alignments.json";
|
|
1294
|
+
var ALIGNMENT_ID = /^\d{8}T\d{4}-[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
1295
|
+
var CAUGHT_BY = ["compiler", "tests", "nothing"];
|
|
1259
1296
|
function isCodemodFile(name) {
|
|
1260
1297
|
return name.endsWith(".ts") && !name.endsWith(".test.ts");
|
|
1261
1298
|
}
|
|
1299
|
+
function splitList(value) {
|
|
1300
|
+
return (value ?? "").split(",").map((item) => item.trim()).filter(Boolean);
|
|
1301
|
+
}
|
|
1262
1302
|
function isUpstreamOnly(filePath) {
|
|
1263
|
-
return filePath ===
|
|
1303
|
+
return UPSTREAM_ONLY_DIRS.some((dir) => filePath === dir || filePath.startsWith(`${dir}/`));
|
|
1264
1304
|
}
|
|
1265
1305
|
function bodyLines(lines) {
|
|
1266
1306
|
let inComment = false;
|
|
@@ -1270,7 +1310,7 @@ function bodyLines(lines) {
|
|
|
1270
1310
|
return false;
|
|
1271
1311
|
});
|
|
1272
1312
|
}
|
|
1273
|
-
function
|
|
1313
|
+
function parseAlignment(id, source, fileNames) {
|
|
1274
1314
|
const lines = source.replace(/\r\n/g, "\n").split("\n");
|
|
1275
1315
|
const close = lines[0] === "---" ? lines.indexOf("---", 1) : -1;
|
|
1276
1316
|
const meta = /* @__PURE__ */ new Map();
|
|
@@ -1282,31 +1322,74 @@ function parseNote(id, source, fileNames) {
|
|
|
1282
1322
|
const blocks = body.split(/\n\s*\n/).map((block) => block.trim()).filter(Boolean);
|
|
1283
1323
|
const title = /^# (.+)$/.exec(blocks[0] ?? "")?.[1].trim();
|
|
1284
1324
|
const summary = title && blocks[1] && !/^(#|>|[-*|]|```)/.test(blocks[1]) ? blocks[1].replace(/\s*\n\s*/g, " ") : "";
|
|
1325
|
+
const caughtBy = CAUGHT_BY.find((value) => value === meta.get("caughtBy")) ?? null;
|
|
1285
1326
|
return {
|
|
1286
1327
|
id,
|
|
1287
1328
|
title: title || id,
|
|
1288
1329
|
summary,
|
|
1289
1330
|
syncBreaking: meta.get("syncBreaking") === "true",
|
|
1290
1331
|
clientCacheBump: meta.get("clientCacheBump") === "true",
|
|
1291
|
-
|
|
1332
|
+
caughtBy,
|
|
1333
|
+
generate: meta.get("generate") === "true",
|
|
1334
|
+
grep: splitList(meta.get("grep")),
|
|
1335
|
+
roots: splitList(meta.get("roots")),
|
|
1292
1336
|
codemod: fileNames.find(isCodemodFile) ?? null
|
|
1293
1337
|
};
|
|
1294
1338
|
}
|
|
1295
|
-
async function
|
|
1296
|
-
const out = await git(["ls-tree", "--name-only", `${ref}:${
|
|
1297
|
-
return out.split("\n").filter((name) =>
|
|
1339
|
+
async function listAlignmentIds(cwd, ref) {
|
|
1340
|
+
const out = await git(["ls-tree", "--name-only", `${ref}:${ALIGNMENTS_DIR}`], cwd, { ignoreErrors: true });
|
|
1341
|
+
return out.split("\n").filter((name) => ALIGNMENT_ID.test(name)).sort();
|
|
1298
1342
|
}
|
|
1299
|
-
async function
|
|
1300
|
-
const source = await git(["show", `${ref}:${
|
|
1343
|
+
async function readAlignment(cwd, ref, id) {
|
|
1344
|
+
const source = await git(["show", `${ref}:${ALIGNMENTS_DIR}/${id}/README.md`], cwd, { ignoreErrors: true });
|
|
1301
1345
|
if (!source) return null;
|
|
1302
|
-
const files = await git(["ls-tree", "--name-only", `${ref}:${
|
|
1303
|
-
return
|
|
1346
|
+
const files = await git(["ls-tree", "--name-only", `${ref}:${ALIGNMENTS_DIR}/${id}`], cwd, { ignoreErrors: true });
|
|
1347
|
+
return parseAlignment(id, source, files.split("\n").filter(Boolean));
|
|
1304
1348
|
}
|
|
1305
|
-
async function
|
|
1306
|
-
const [before, after] = await Promise.all([
|
|
1349
|
+
async function arrivedAlignmentIds(cwd, from, to) {
|
|
1350
|
+
const [before, after] = await Promise.all([listAlignmentIds(cwd, from), listAlignmentIds(cwd, to)]);
|
|
1307
1351
|
const known = new Set(before);
|
|
1308
1352
|
return after.filter((id) => !known.has(id));
|
|
1309
1353
|
}
|
|
1354
|
+
async function readAlignmentSource(cwd, ref, id) {
|
|
1355
|
+
const out = await git(
|
|
1356
|
+
["log", "-1", "--diff-filter=A", "--format=%H%x09%s", ref, "--", `${ALIGNMENTS_DIR}/${id}/README.md`],
|
|
1357
|
+
cwd,
|
|
1358
|
+
{ ignoreErrors: true }
|
|
1359
|
+
);
|
|
1360
|
+
if (!out) return null;
|
|
1361
|
+
const [commit, subject = ""] = out.split(" ");
|
|
1362
|
+
const { pr = null } = splitPrNumber(subject);
|
|
1363
|
+
const nulSeparated = (text) => text.split("\0").filter(Boolean);
|
|
1364
|
+
const [changed, added] = await Promise.all([
|
|
1365
|
+
git(["diff-tree", "--root", "--no-commit-id", "--name-only", "--no-renames", "-r", "-z", commit], cwd),
|
|
1366
|
+
git(
|
|
1367
|
+
[
|
|
1368
|
+
"diff-tree",
|
|
1369
|
+
"--root",
|
|
1370
|
+
"--no-commit-id",
|
|
1371
|
+
"--name-only",
|
|
1372
|
+
"--diff-filter=A",
|
|
1373
|
+
"-r",
|
|
1374
|
+
"-z",
|
|
1375
|
+
commit,
|
|
1376
|
+
"--",
|
|
1377
|
+
ALIGNMENTS_DIR
|
|
1378
|
+
],
|
|
1379
|
+
cwd
|
|
1380
|
+
)
|
|
1381
|
+
]);
|
|
1382
|
+
const readmeId = new RegExp(`^${ALIGNMENTS_DIR}/([^/]+)/README\\.md$`);
|
|
1383
|
+
const sharedCommit = nulSeparated(added).map((path) => readmeId.exec(path)?.[1]).filter((other) => !!other && other !== id && ALIGNMENT_ID.test(other)).sort();
|
|
1384
|
+
return { commit, pr, sharedCommit, changedFiles: nulSeparated(changed) };
|
|
1385
|
+
}
|
|
1386
|
+
async function grepAlignmentTerms(cwd, terms, inFiles) {
|
|
1387
|
+
if (terms.length === 0) return [];
|
|
1388
|
+
const out = await git(["grep", "-n", "-I", "-w", "-F", "-z", ...terms.flatMap((term) => ["-e", term]), "--"], cwd, {
|
|
1389
|
+
ignoreErrors: true
|
|
1390
|
+
});
|
|
1391
|
+
return out.split("\n").map((match) => match.split("\0")).filter(([file, line]) => file && line && inFiles(file)).map(([file, line]) => ({ file, line: Number(line) }));
|
|
1392
|
+
}
|
|
1310
1393
|
async function readPending(cwd) {
|
|
1311
1394
|
const path = join5(cwd, PENDING_FILE);
|
|
1312
1395
|
if (!existsSync4(path)) return [];
|
|
@@ -1324,14 +1407,13 @@ async function writePending(cwd, ids) {
|
|
|
1324
1407
|
await writeFile2(path, `${JSON.stringify({ pending }, null, 2)}
|
|
1325
1408
|
`, "utf8");
|
|
1326
1409
|
}
|
|
1327
|
-
function
|
|
1328
|
-
return upstreamGitHubUrl ? `${upstreamGitHubUrl}/blob/${commit}/${
|
|
1410
|
+
function alignmentUrl(upstreamGitHubUrl, commit, id) {
|
|
1411
|
+
return upstreamGitHubUrl ? `${upstreamGitHubUrl}/blob/${commit}/${ALIGNMENTS_DIR}/${id}/README.md` : void 0;
|
|
1329
1412
|
}
|
|
1330
|
-
function
|
|
1413
|
+
function formatAlignmentsLine(total, open) {
|
|
1331
1414
|
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 ")}`;
|
|
1415
|
+
if (open > 0) parts.push(`${open} open: pnpm cella align`);
|
|
1416
|
+
return `alignments: ${parts.join(" \xB7 ")}`;
|
|
1335
1417
|
}
|
|
1336
1418
|
|
|
1337
1419
|
// src/utils/overrides.ts
|
|
@@ -1455,6 +1537,7 @@ var serviceDefinitions = [
|
|
|
1455
1537
|
["--list", "non-interactive output for tooling (one file per line)"],
|
|
1456
1538
|
jsonOption,
|
|
1457
1539
|
["--scope <scope>", "analyze scope for --list/--json: all|risk|protected"],
|
|
1540
|
+
["--all", "list every file instead of collapsing large groups"],
|
|
1458
1541
|
trackOption,
|
|
1459
1542
|
refOption,
|
|
1460
1543
|
unpinnedOption
|
|
@@ -1473,19 +1556,19 @@ var serviceDefinitions = [
|
|
|
1473
1556
|
includeInMenu: (context) => !context.isUpstreamRepo
|
|
1474
1557
|
},
|
|
1475
1558
|
{
|
|
1476
|
-
name: "
|
|
1477
|
-
description: "list the upstream
|
|
1559
|
+
name: "align",
|
|
1560
|
+
description: "list the upstream alignments this app has not handled yet",
|
|
1478
1561
|
options: [
|
|
1479
|
-
["--all", "list every upstream
|
|
1562
|
+
["--all", "list every upstream alignment, handled or not"],
|
|
1480
1563
|
jsonOption,
|
|
1481
|
-
["--show <id>", "print one
|
|
1482
|
-
["--extract <id>", "write one
|
|
1483
|
-
["--run <id>", "run one
|
|
1484
|
-
["--mark <ids...>", "record
|
|
1564
|
+
["--show <id>", "print one alignment's README and what git tells about it"],
|
|
1565
|
+
["--extract <id>", "write one alignment's folder under node_modules/.cache to run its codemod"],
|
|
1566
|
+
["--run <id>", "run one alignment's codemod; files identical to upstream stay as they are"],
|
|
1567
|
+
["--mark <ids...>", "record alignments as handled"]
|
|
1485
1568
|
],
|
|
1486
1569
|
operands: [
|
|
1487
1570
|
"[codemodArgs...]",
|
|
1488
|
-
"with --run, after `--`: arguments for the codemod (default: inventory and the
|
|
1571
|
+
"with --run, after `--`: arguments for the codemod (default: inventory and the alignment's roots)"
|
|
1489
1572
|
],
|
|
1490
1573
|
includeInMenu: (context) => !context.isUpstreamRepo
|
|
1491
1574
|
},
|
|
@@ -1560,8 +1643,8 @@ function buildProgram(setSelection) {
|
|
|
1560
1643
|
" $ cella analyze --json --scope risk",
|
|
1561
1644
|
" $ cella sync --track branch",
|
|
1562
1645
|
" $ cella sync --ref 4f7d87c",
|
|
1563
|
-
" $ cella
|
|
1564
|
-
" $ cella
|
|
1646
|
+
" $ cella align --run 20261001T2116-tailwind-class-conventions -- rewrite frontend/src",
|
|
1647
|
+
" $ cella align --mark 20261002T0614-config-switch",
|
|
1565
1648
|
" $ cella audit --list",
|
|
1566
1649
|
" $ cella stats --since origin/main --md",
|
|
1567
1650
|
" $ cella contributions --fork raak --json"
|
|
@@ -1592,7 +1675,7 @@ async function promptForService(userConfig, forkPath) {
|
|
|
1592
1675
|
if (selected === "exit") {
|
|
1593
1676
|
console.info(colors_default.dim("exiting..."));
|
|
1594
1677
|
console.info();
|
|
1595
|
-
|
|
1678
|
+
process6.exit(0);
|
|
1596
1679
|
}
|
|
1597
1680
|
console.info();
|
|
1598
1681
|
return selected;
|
|
@@ -1601,7 +1684,7 @@ function buildRuntimeConfig(userConfig, forkPath, selection, upstreamRef) {
|
|
|
1601
1684
|
return { ...userConfig, forkPath, upstreamRef, service: selection.service ?? "analyze", ...selection.options };
|
|
1602
1685
|
}
|
|
1603
1686
|
async function parseCli(userConfig, forkPath) {
|
|
1604
|
-
const selection = parseCommandLine(
|
|
1687
|
+
const selection = parseCommandLine(process6.argv);
|
|
1605
1688
|
const machineList = selection.options.list && selection.service !== "audit";
|
|
1606
1689
|
if (selection.options.json || selection.options.md || machineList) setJsonMode(true);
|
|
1607
1690
|
printHeader();
|
|
@@ -1615,6 +1698,12 @@ async function parseCli(userConfig, forkPath) {
|
|
|
1615
1698
|
return buildRuntimeConfig(userConfig, forkPath, selection, branchRef);
|
|
1616
1699
|
}
|
|
1617
1700
|
|
|
1701
|
+
// src/services/align.ts
|
|
1702
|
+
import { execFileSync, spawnSync } from "child_process";
|
|
1703
|
+
import { mkdir as mkdir4, writeFile as writeFile3 } from "fs/promises";
|
|
1704
|
+
import { dirname as dirname4, join as join8 } from "path";
|
|
1705
|
+
import process7 from "process";
|
|
1706
|
+
|
|
1618
1707
|
// src/services/merge-engine.ts
|
|
1619
1708
|
import { existsSync as existsSync6 } from "fs";
|
|
1620
1709
|
import { rm as rm2 } from "fs/promises";
|
|
@@ -1936,7 +2025,9 @@ async function analyzeRefs(repoPath, localRef, incomingRef, mergeBaseRef, predic
|
|
|
1936
2025
|
const upstreamOnly = fileIsIgnored && isReportableIgnored(filePath) && upstreamHash !== baseHash && forkHash === baseHash;
|
|
1937
2026
|
push(status, { upstreamChanged: upstreamChanged || void 0, upstreamOnly: upstreamOnly || void 0 });
|
|
1938
2027
|
}
|
|
1939
|
-
const flagged = analyzedFiles.filter(
|
|
2028
|
+
const flagged = analyzedFiles.filter(
|
|
2029
|
+
(file) => file.upstreamChanged && !renamedProtected.has(file.path) || file.upstreamOnly
|
|
2030
|
+
);
|
|
1940
2031
|
if (flagged.length > 0) {
|
|
1941
2032
|
const stat = await getDiffStat(
|
|
1942
2033
|
repoPath,
|
|
@@ -1983,7 +2074,7 @@ async function enrichChangeInfo(repoPath, files, mergeBaseRef, localRef, incomin
|
|
|
1983
2074
|
setChanged(file, forkInfo.get(file.path));
|
|
1984
2075
|
} else if (file.status === "behind") {
|
|
1985
2076
|
setChanged(file, upstreamInfo.get(file.path));
|
|
1986
|
-
} else if (file.status === "diverged" || file.status === "pinned" || file.upstreamChanged) {
|
|
2077
|
+
} else if (file.status === "diverged" || file.status === "pinned" || file.upstreamChanged || file.upstreamOnly) {
|
|
1987
2078
|
setChanged(file, forkInfo.get(file.path));
|
|
1988
2079
|
const info = upstreamInfo.get(file.path);
|
|
1989
2080
|
if (info) {
|
|
@@ -2015,6 +2106,10 @@ function getGitHubBaseUrl(remoteUrl) {
|
|
|
2015
2106
|
const match = remoteUrl.match(/github\.com[:/]([^/]+)\/([^/]+?)(?:\.git)?$/);
|
|
2016
2107
|
return match ? `https://github.com/${match[1]}/${match[2]}` : null;
|
|
2017
2108
|
}
|
|
2109
|
+
async function readOriginGitHubUrl(forkPath) {
|
|
2110
|
+
const origin = await git(["remote", "get-url", "origin"], forkPath, { ignoreErrors: true });
|
|
2111
|
+
return getGitHubBaseUrl(origin.trim()) ?? void 0;
|
|
2112
|
+
}
|
|
2018
2113
|
function isProtectedFile(file) {
|
|
2019
2114
|
return file.isIgnored || file.isPinned;
|
|
2020
2115
|
}
|
|
@@ -2150,11 +2245,6 @@ function calculateSummary(files) {
|
|
|
2150
2245
|
}
|
|
2151
2246
|
return summary;
|
|
2152
2247
|
}
|
|
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
2248
|
async function resolvePinnedRef(forkPath, config, remoteName, branchRef, ref) {
|
|
2159
2249
|
const resolved = await resolveUpstreamCommit(forkPath, remoteName, ref);
|
|
2160
2250
|
if (!resolved) {
|
|
@@ -2178,7 +2268,7 @@ async function prepareUpstream(config, onProgress, onStep) {
|
|
|
2178
2268
|
getWorkingTreeChangeCount(forkPath),
|
|
2179
2269
|
getUpstreamStatus(forkPath)
|
|
2180
2270
|
]);
|
|
2181
|
-
onStep?.("
|
|
2271
|
+
onStep?.("checkout", formatCheckoutStep({ forkPath, branch, headSha, changeCount, upstream: upstreamStatus }));
|
|
2182
2272
|
onProgress?.("setting up upstream remote...");
|
|
2183
2273
|
const { track: configTrack, branchRef } = resolveUpstream(config.settings);
|
|
2184
2274
|
const remoteName = DEFAULT_UPSTREAM_REMOTE;
|
|
@@ -2206,7 +2296,6 @@ async function prepareUpstream(config, onProgress, onStep) {
|
|
|
2206
2296
|
}
|
|
2207
2297
|
const upstreamLabel = releaseTag ?? (config.ref ? `--ref ${config.ref}` : upstreamRef);
|
|
2208
2298
|
config.upstreamRef = upstreamRef;
|
|
2209
|
-
onStep?.("remote configured", `${upstreamLabel} \u2192 ${config.settings.upstreamUrl}`);
|
|
2210
2299
|
const cliRange = await readUpstreamCliRange(forkPath, upstreamRef);
|
|
2211
2300
|
const cliMismatch = cliRange ? cliVersionMismatch(VERSION, cliRange) : null;
|
|
2212
2301
|
if (cliMismatch) throw new Error(cliMismatch);
|
|
@@ -2217,20 +2306,35 @@ async function prepareUpstream(config, onProgress, onStep) {
|
|
|
2217
2306
|
const mergeRef = config.unpinned ? await getMergeBase(forkPath, "HEAD", upstreamRef) : mergeBase;
|
|
2218
2307
|
const upstreamCommit = await getCommitInfo(forkPath, upstreamRef);
|
|
2219
2308
|
if (upstreamCommit.hash !== mergeBase && await isAncestor(forkPath, upstreamCommit.hash, mergeBase)) {
|
|
2220
|
-
const [
|
|
2309
|
+
const [upstreamShort2, syncShort] = await Promise.all([
|
|
2221
2310
|
getShortSha(forkPath, upstreamCommit.hash),
|
|
2222
2311
|
getShortSha(forkPath, mergeBase)
|
|
2223
2312
|
]);
|
|
2224
|
-
const
|
|
2313
|
+
const hint2 = config.ref ? "Pick a newer --ref." : releaseTag ? `Nothing to sync until a release past ${syncShort}; to follow the tip instead, run with --track branch.` : `Nothing to sync until '${branchRef}' moves past ${syncShort}.`;
|
|
2225
2314
|
throw new BehindSyncPointError(
|
|
2226
|
-
`upstream ${upstreamLabel} (${
|
|
2227
|
-
${
|
|
2315
|
+
`upstream ${upstreamLabel} (${upstreamShort2}) is behind the last sync point ${syncShort}: the fork already has it, and syncing it would revert the upstream changes after it.
|
|
2316
|
+
${hint2}`
|
|
2228
2317
|
);
|
|
2229
2318
|
}
|
|
2230
2319
|
const commitCount = await countCommitsBetween(forkPath, mergeBase, upstreamRef);
|
|
2231
|
-
const upstreamCommits = commitCount > 0 ? await listCommitsBetween(forkPath, mergeBase, upstreamRef,
|
|
2232
|
-
|
|
2233
|
-
|
|
2320
|
+
const upstreamCommits = commitCount > 0 ? await listCommitsBetween(forkPath, mergeBase, upstreamRef, COMMIT_STEP_MAX) : [];
|
|
2321
|
+
const [upstreamShort, baseShort] = await Promise.all([
|
|
2322
|
+
getShortSha(forkPath, upstreamCommit.hash),
|
|
2323
|
+
getShortSha(forkPath, mergeBase)
|
|
2324
|
+
]);
|
|
2325
|
+
onStep?.(
|
|
2326
|
+
"upstream",
|
|
2327
|
+
formatUpstreamStep({
|
|
2328
|
+
label: releaseTag ?? branchRef.replace(`${remoteName}/`, ""),
|
|
2329
|
+
sha: upstreamShort,
|
|
2330
|
+
baseSha: baseShort,
|
|
2331
|
+
pinned: !!config.ref,
|
|
2332
|
+
commitCount,
|
|
2333
|
+
commits: upstreamCommits,
|
|
2334
|
+
githubUrl: upstreamGitHubUrl
|
|
2335
|
+
})
|
|
2336
|
+
);
|
|
2337
|
+
return { upstreamRef, releaseTag, mergeBase, mergeRef, upstreamGitHubUrl, upstreamCommit, commitCount };
|
|
2234
2338
|
}
|
|
2235
2339
|
async function assertSyncConfigFollowed(config, ctx) {
|
|
2236
2340
|
if (config.keepConfig) return;
|
|
@@ -2245,7 +2349,8 @@ function resultMeta(config, ctx) {
|
|
|
2245
2349
|
upstreamRef: ctx.upstreamRef,
|
|
2246
2350
|
upstreamTag: ctx.releaseTag,
|
|
2247
2351
|
upstreamGitHubUrl: ctx.upstreamGitHubUrl,
|
|
2248
|
-
upstreamCommit: ctx.upstreamCommit
|
|
2352
|
+
upstreamCommit: ctx.upstreamCommit,
|
|
2353
|
+
upstreamCommitCount: ctx.commitCount
|
|
2249
2354
|
};
|
|
2250
2355
|
}
|
|
2251
2356
|
async function upstreamOnlyReports(config, ctx, files) {
|
|
@@ -2266,33 +2371,40 @@ async function upstreamOnlyReports(config, ctx, files) {
|
|
|
2266
2371
|
return {};
|
|
2267
2372
|
}
|
|
2268
2373
|
}
|
|
2269
|
-
|
|
2374
|
+
function pendingAfterSync(alignments) {
|
|
2375
|
+
return [.../* @__PURE__ */ new Set([...alignments.pending, ...alignments.arrived])];
|
|
2376
|
+
}
|
|
2377
|
+
async function readAlignmentsInRange(config, ctx) {
|
|
2270
2378
|
const { forkPath } = config;
|
|
2271
2379
|
try {
|
|
2272
2380
|
const [total, arrived, pending] = await Promise.all([
|
|
2273
|
-
|
|
2274
|
-
|
|
2381
|
+
listAlignmentIds(forkPath, ctx.upstreamRef),
|
|
2382
|
+
arrivedAlignmentIds(forkPath, ctx.mergeBase, ctx.upstreamRef),
|
|
2275
2383
|
readPending(forkPath)
|
|
2276
2384
|
]);
|
|
2277
|
-
return { total: total.length, arrived, pending
|
|
2385
|
+
return { total: total.length, arrived, pending };
|
|
2278
2386
|
} catch {
|
|
2279
2387
|
return void 0;
|
|
2280
2388
|
}
|
|
2281
2389
|
}
|
|
2282
|
-
function
|
|
2283
|
-
|
|
2390
|
+
function alignmentsResult(alignments, options) {
|
|
2391
|
+
if (!alignments) return {};
|
|
2392
|
+
const open = options.dryRun ? alignments.pending : pendingAfterSync(alignments);
|
|
2393
|
+
return { alignments: { total: alignments.total, arrived: alignments.arrived, open } };
|
|
2284
2394
|
}
|
|
2285
2395
|
async function removeUpstreamOnlyCopy(forkPath, onStep) {
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2396
|
+
for (const dir of UPSTREAM_ONLY_DIRS) {
|
|
2397
|
+
if (!existsSync6(join7(forkPath, dir))) continue;
|
|
2398
|
+
await git(["rm", "-r", "-q", "--ignore-unmatch", "--", dir], forkPath);
|
|
2399
|
+
await rm2(join7(forkPath, dir), { recursive: true, force: true });
|
|
2400
|
+
onStep?.("alignments", `removed ${dir}/: alignments stay upstream, list them with pnpm cella align`);
|
|
2401
|
+
}
|
|
2290
2402
|
}
|
|
2291
2403
|
async function runSyncMerge(config, ctx, onProgress, onStep) {
|
|
2292
2404
|
const { forkPath } = config;
|
|
2293
2405
|
const { upstreamRef, releaseTag, mergeRef, upstreamGitHubUrl, upstreamCommit } = ctx;
|
|
2294
2406
|
await assertSyncConfigFollowed(config, ctx);
|
|
2295
|
-
const
|
|
2407
|
+
const alignments = await readAlignmentsInRange(config, ctx);
|
|
2296
2408
|
const { remainingConflicts, analyzedFiles, autoMergedFiles, protectedConflicts } = await applyDirectMerge(
|
|
2297
2409
|
forkPath,
|
|
2298
2410
|
upstreamRef,
|
|
@@ -2317,18 +2429,15 @@ async function runSyncMerge(config, ctx, onProgress, onStep) {
|
|
|
2317
2429
|
await storeLastSyncRef(forkPath, upstreamCommit.hash);
|
|
2318
2430
|
await writeSyncManifest(forkPath, syncManifest);
|
|
2319
2431
|
await stagePath(forkPath, MANIFEST_FILE);
|
|
2320
|
-
if (
|
|
2321
|
-
await writePending(forkPath,
|
|
2432
|
+
if (alignments) {
|
|
2433
|
+
await writePending(forkPath, pendingAfterSync(alignments));
|
|
2322
2434
|
await stagePath(forkPath, PENDING_FILE);
|
|
2323
2435
|
}
|
|
2324
2436
|
await removeUpstreamOnlyCopy(forkPath, onStep);
|
|
2325
2437
|
};
|
|
2326
2438
|
if (remainingConflicts.length > 0) {
|
|
2327
2439
|
await recordSyncPoint();
|
|
2328
|
-
onStep?.(
|
|
2329
|
-
"merge in progress",
|
|
2330
|
-
formatMergeInProgressDetail(remainingConflicts.length, autoMergedFiles, { forkPath }, 100)
|
|
2331
|
-
);
|
|
2440
|
+
onStep?.("merged", formatMergeInProgressDetail(remainingConflicts.length, autoMergedFiles, { forkPath }, 100));
|
|
2332
2441
|
} else if (totalResolved > 0) {
|
|
2333
2442
|
const label = synced > 0 ? `${synced} files from upstream` : "upstream changes";
|
|
2334
2443
|
await recordSyncPoint();
|
|
@@ -2346,12 +2455,12 @@ async function runSyncMerge(config, ctx, onProgress, onStep) {
|
|
|
2346
2455
|
protectedConflicts,
|
|
2347
2456
|
...resultMeta(config, ctx),
|
|
2348
2457
|
...await upstreamOnlyReports(config, ctx, analyzedFiles),
|
|
2349
|
-
...
|
|
2458
|
+
...alignmentsResult(alignments, { dryRun: false })
|
|
2350
2459
|
};
|
|
2351
2460
|
}
|
|
2352
2461
|
async function runAnalyzePreview(config, ctx, onProgress, onStep) {
|
|
2353
2462
|
const { forkPath } = config;
|
|
2354
|
-
const
|
|
2463
|
+
const alignments = await readAlignmentsInRange(config, ctx);
|
|
2355
2464
|
onProgress?.("analyzing files...");
|
|
2356
2465
|
const analyzedFiles = await analyzeRefs(
|
|
2357
2466
|
forkPath,
|
|
@@ -2364,7 +2473,7 @@ async function runAnalyzePreview(config, ctx, onProgress, onStep) {
|
|
|
2364
2473
|
await enrichChangeInfo(forkPath, analyzedFiles, ctx.mergeBase, "HEAD", ctx.upstreamRef);
|
|
2365
2474
|
const protectedConflicts = analyzedFiles.filter((f) => f.upstreamChanged).map((f) => f.path);
|
|
2366
2475
|
const reports = await upstreamOnlyReports(config, ctx, analyzedFiles);
|
|
2367
|
-
onStep?.("
|
|
2476
|
+
onStep?.("analyzed", `${plural(analyzedFiles.length, "file")}${colors_default.dim(" \xB7 dry run, nothing changed")}`);
|
|
2368
2477
|
return {
|
|
2369
2478
|
success: true,
|
|
2370
2479
|
files: analyzedFiles,
|
|
@@ -2374,25 +2483,23 @@ async function runAnalyzePreview(config, ctx, onProgress, onStep) {
|
|
|
2374
2483
|
protectedConflicts,
|
|
2375
2484
|
...resultMeta(config, ctx),
|
|
2376
2485
|
...reports,
|
|
2377
|
-
...
|
|
2486
|
+
...alignmentsResult(alignments, { dryRun: true })
|
|
2378
2487
|
};
|
|
2379
2488
|
}
|
|
2380
2489
|
async function runEngineWithSpinner(config, apply) {
|
|
2381
2490
|
const onProgress = (message) => spinnerText(message);
|
|
2382
2491
|
const onStep = (label, detail) => {
|
|
2383
|
-
|
|
2492
|
+
printStep(label, detail);
|
|
2384
2493
|
createSpinner("...");
|
|
2385
2494
|
};
|
|
2386
2495
|
const ctx = await prepareUpstream(config, onProgress, onStep);
|
|
2387
2496
|
return apply ? runSyncMerge(config, ctx, onProgress, onStep) : runAnalyzePreview(config, ctx, onProgress, onStep);
|
|
2388
2497
|
}
|
|
2389
2498
|
|
|
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";
|
|
2499
|
+
// src/services/align.ts
|
|
2500
|
+
var EXTRACT_DIR = "node_modules/.cache/cella/alignments";
|
|
2395
2501
|
var RESTORED_LIST_MAX = 20;
|
|
2502
|
+
var SHOW_LIST_MAX = 30;
|
|
2396
2503
|
async function ensureUpstreamCommit(config, sha) {
|
|
2397
2504
|
const { forkPath } = config;
|
|
2398
2505
|
const present = async () => !!await revParse(forkPath, `${sha}^{commit}`);
|
|
@@ -2402,60 +2509,158 @@ async function ensureUpstreamCommit(config, sha) {
|
|
|
2402
2509
|
if (!await present())
|
|
2403
2510
|
throw new Error(`upstream commit ${sha.slice(0, 7)} is not reachable from '${DEFAULT_UPSTREAM_REMOTE}'`);
|
|
2404
2511
|
}
|
|
2405
|
-
async function
|
|
2512
|
+
async function resolveAlignmentsRef(config) {
|
|
2406
2513
|
const { forkPath } = config;
|
|
2407
2514
|
const ref = await getStoredSyncRef(forkPath) ?? await readManifestBaseAtRef(forkPath, "HEAD");
|
|
2408
2515
|
if (!ref) return null;
|
|
2409
2516
|
await ensureUpstreamCommit(config, ref);
|
|
2410
2517
|
return ref;
|
|
2411
2518
|
}
|
|
2412
|
-
async function
|
|
2519
|
+
async function readOpenAlignments(config) {
|
|
2413
2520
|
try {
|
|
2414
|
-
const ref = await
|
|
2521
|
+
const ref = await resolveAlignmentsRef(config);
|
|
2415
2522
|
if (!ref) return null;
|
|
2416
|
-
|
|
2417
|
-
return ids.length > 0 || pending.length > 0 ? { total: ids.length, open: pending.length } : null;
|
|
2523
|
+
return { ref, open: await readPending(config.forkPath) };
|
|
2418
2524
|
} catch {
|
|
2419
2525
|
return null;
|
|
2420
2526
|
}
|
|
2421
2527
|
}
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
const
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2528
|
+
var STEP_ROWS_MAX = 5;
|
|
2529
|
+
function alignmentTags(alignment) {
|
|
2530
|
+
return [
|
|
2531
|
+
alignment.codemod ? "codemod" : "manual",
|
|
2532
|
+
alignment.syncBreaking ? "sync-breaking" : "not sync-breaking",
|
|
2533
|
+
alignment.caughtBy ? `caught by ${alignment.caughtBy}` : null
|
|
2534
|
+
].filter(Boolean).join(" \xB7 ");
|
|
2535
|
+
}
|
|
2536
|
+
async function alignmentsStep(config, view) {
|
|
2537
|
+
const arriving = view.arriving ?? [];
|
|
2538
|
+
const open = view.open.filter((id) => !arriving.includes(id));
|
|
2539
|
+
const arrived = (view.arrived ?? []).filter((id) => open.includes(id));
|
|
2540
|
+
if (arriving.length === 0 && open.length === 0) return void 0;
|
|
2541
|
+
const line = arriving.length > 0 ? `${plural(arriving.length, "alignment")} ${arriving.length === 1 ? "arrives" : "arrive"} with this sync${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 sync`) : ""}`;
|
|
2542
|
+
const githubUrl = getGitHubBaseUrl(config.settings.upstreamUrl) ?? void 0;
|
|
2543
|
+
const ids = [...arriving, ...open];
|
|
2544
|
+
const details = [];
|
|
2545
|
+
for (const id of ids.slice(0, STEP_ROWS_MAX)) {
|
|
2546
|
+
const alignment = await readAlignment(config.forkPath, view.ref, id).catch(() => null);
|
|
2547
|
+
const title = hyperlink(alignment?.title ?? id, alignmentUrl(githubUrl, view.ref, id));
|
|
2548
|
+
details.push(`${colors_default.cyan("\u25CB")} ${title}${alignment ? colors_default.dim(` \xB7 ${alignmentTags(alignment)}`) : ""}`);
|
|
2549
|
+
if (!process7.stdout.isTTY) details.push(colors_default.dim(` id ${id}`));
|
|
2550
|
+
}
|
|
2551
|
+
if (ids.length > STEP_ROWS_MAX) details.push(colors_default.dim(`+ ${ids.length - STEP_ROWS_MAX} more`));
|
|
2552
|
+
details.push(`${arriving.length > 0 ? colors_default.dim("after the sync: ") : ""}pnpm cella align`);
|
|
2553
|
+
return { label: "align", line, details };
|
|
2554
|
+
}
|
|
2555
|
+
async function readForkView(config, ref) {
|
|
2556
|
+
const { pinned = [], ignored = [] } = config.overrides ?? {};
|
|
2557
|
+
const counts = (path) => !isManagedFile(path) && !isGeneratedFile(path) && !isUpstreamOnly(path);
|
|
2558
|
+
const out = await git(["diff", "--name-only", "--no-renames", "-z", ref, "--"], config.forkPath);
|
|
2559
|
+
return {
|
|
2560
|
+
githubUrl: getGitHubBaseUrl(config.settings.upstreamUrl) ?? void 0,
|
|
2561
|
+
owns: (path) => counts(path) && isUnderAnyFolder(path, [...pinned, ...ignored]),
|
|
2562
|
+
changed: new Set(out.split("\0").filter((path) => path && counts(path)))
|
|
2563
|
+
};
|
|
2564
|
+
}
|
|
2565
|
+
async function describeAlignment(config, view, ref, alignment, open) {
|
|
2566
|
+
const { forkPath } = config;
|
|
2567
|
+
const [source, grepHits] = await Promise.all([
|
|
2568
|
+
readAlignmentSource(forkPath, ref, alignment.id),
|
|
2569
|
+
grepAlignmentTerms(forkPath, alignment.grep, (path) => view.changed.has(path))
|
|
2570
|
+
]);
|
|
2571
|
+
const pr = source?.pr ?? null;
|
|
2572
|
+
return {
|
|
2573
|
+
...alignment,
|
|
2574
|
+
kind: alignment.codemod ? "codemod" : "manual",
|
|
2575
|
+
open,
|
|
2576
|
+
url: alignmentUrl(view.githubUrl, ref, alignment.id) ?? null,
|
|
2577
|
+
commit: source?.commit ?? null,
|
|
2578
|
+
pr,
|
|
2579
|
+
prUrl: pr !== null && view.githubUrl ? `${view.githubUrl}/pull/${pr}` : null,
|
|
2580
|
+
sharedCommit: source?.sharedCommit ?? [],
|
|
2581
|
+
yourFiles: (source?.changedFiles ?? []).filter((path) => view.owns(path) || view.changed.has(path)),
|
|
2582
|
+
grepHits
|
|
2583
|
+
};
|
|
2584
|
+
}
|
|
2585
|
+
function grepSummary(alignment) {
|
|
2586
|
+
if (alignment.grep.length === 0) return null;
|
|
2587
|
+
const hits = alignment.grepHits.length;
|
|
2588
|
+
if (hits === 0) return "grep: no app hits";
|
|
2589
|
+
const files = new Set(alignment.grepHits.map((hit) => hit.file)).size;
|
|
2590
|
+
return `grep: ${hits} hit${hits === 1 ? "" : "s"} in ${files} app file${files === 1 ? "" : "s"}`;
|
|
2436
2591
|
}
|
|
2437
|
-
function
|
|
2592
|
+
function printAlignment(index, alignment) {
|
|
2438
2593
|
const tags = [
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2594
|
+
alignment.codemod ? "codemod" : "manual",
|
|
2595
|
+
alignment.syncBreaking ? "sync-breaking" : null,
|
|
2596
|
+
alignment.clientCacheBump ? "cache-bump" : null,
|
|
2597
|
+
alignment.caughtBy ? `caught by: ${alignment.caughtBy}` : null,
|
|
2598
|
+
alignment.generate ? "generate" : null
|
|
2442
2599
|
].filter(Boolean).join(", ");
|
|
2443
|
-
|
|
2444
|
-
console.info(colors_default.dim(`
|
|
2445
|
-
|
|
2446
|
-
console.info(
|
|
2447
|
-
if (
|
|
2448
|
-
|
|
2600
|
+
const grep = grepSummary(alignment);
|
|
2601
|
+
console.info(`${index}. ${alignment.title} ${colors_default.dim(`[${tags}]`)}`);
|
|
2602
|
+
console.info(colors_default.dim(` id: ${alignment.id}`));
|
|
2603
|
+
if (alignment.summary) console.info(` ${alignment.summary}`);
|
|
2604
|
+
if (alignment.pr !== null) {
|
|
2605
|
+
console.info(colors_default.dim(` added in #${alignment.pr}${alignment.prUrl ? ` ${alignment.prUrl}` : ""}`));
|
|
2606
|
+
}
|
|
2607
|
+
if (grep) console.info(colors_default.dim(` ${grep}`));
|
|
2608
|
+
console.info(colors_default.dim(` read: pnpm cella align --show ${alignment.id}`));
|
|
2609
|
+
if (alignment.url) console.info(colors_default.dim(` ${alignment.url}`));
|
|
2610
|
+
if (alignment.codemod) console.info(colors_default.dim(` codemod: pnpm cella align --run ${alignment.id}`));
|
|
2449
2611
|
console.info();
|
|
2450
2612
|
}
|
|
2451
|
-
|
|
2613
|
+
function printShowBlock(title, items) {
|
|
2614
|
+
if (items.length === 0) return;
|
|
2615
|
+
console.info(colors_default.dim(`${title} (${items.length}):`));
|
|
2616
|
+
for (const item of items.slice(0, SHOW_LIST_MAX)) console.info(colors_default.dim(` ${item}`));
|
|
2617
|
+
if (items.length > SHOW_LIST_MAX) console.info(colors_default.dim(` ... + ${items.length - SHOW_LIST_MAX} more`));
|
|
2618
|
+
}
|
|
2619
|
+
async function showAlignment(config, ref, pending, id) {
|
|
2620
|
+
const { forkPath } = config;
|
|
2621
|
+
const upstreamBranch = `${DEFAULT_UPSTREAM_REMOTE}/${config.settings.upstreamBranch}`;
|
|
2622
|
+
let readRef = ref;
|
|
2623
|
+
let alignment = await readAlignment(forkPath, ref, id);
|
|
2624
|
+
if (!alignment && await revParse(forkPath, upstreamBranch)) {
|
|
2625
|
+
alignment = await readAlignment(forkPath, upstreamBranch, id);
|
|
2626
|
+
readRef = upstreamBranch;
|
|
2627
|
+
}
|
|
2628
|
+
if (!alignment) throw new Error(`no alignment '${id}' at upstream ${ref.slice(0, 7)} or ${upstreamBranch}`);
|
|
2629
|
+
const source = await git(["show", `${readRef}:${ALIGNMENTS_DIR}/${id}/README.md`], forkPath);
|
|
2630
|
+
const described = await describeAlignment(
|
|
2631
|
+
config,
|
|
2632
|
+
await readForkView(config, ref),
|
|
2633
|
+
readRef,
|
|
2634
|
+
alignment,
|
|
2635
|
+
pending.includes(id)
|
|
2636
|
+
);
|
|
2637
|
+
if (readRef !== ref) console.info(colors_default.dim(`not at the sync point ${ref.slice(0, 7)}: read at ${upstreamBranch}`));
|
|
2638
|
+
if (described.url) console.info(colors_default.dim(described.url));
|
|
2639
|
+
console.info(source);
|
|
2640
|
+
console.info();
|
|
2641
|
+
if (described.commit) {
|
|
2642
|
+
const pr = described.pr !== null ? `#${described.pr} ` : "";
|
|
2643
|
+
console.info(
|
|
2644
|
+
colors_default.dim(`added in ${pr}(${described.commit.slice(0, 9)})${described.prUrl ? ` ${described.prUrl}` : ""}`)
|
|
2645
|
+
);
|
|
2646
|
+
}
|
|
2647
|
+
printShowBlock("added in the same commit", described.sharedCommit);
|
|
2648
|
+
printShowBlock("files of that commit the app pins, ignores or changed", described.yourFiles);
|
|
2649
|
+
const grep = grepSummary(described);
|
|
2650
|
+
if (grep) console.info(colors_default.dim(`${grep} (${described.grep.join(", ")})`));
|
|
2651
|
+
printShowBlock(
|
|
2652
|
+
"grep hits",
|
|
2653
|
+
described.grepHits.map((hit) => `${hit.file}:${hit.line}`)
|
|
2654
|
+
);
|
|
2655
|
+
}
|
|
2656
|
+
async function writeAlignmentFolder(config, ref, id) {
|
|
2452
2657
|
const { forkPath } = config;
|
|
2453
|
-
const
|
|
2454
|
-
if (!
|
|
2455
|
-
const files = (await git(["ls-tree", "-r", "--name-only", `${ref}:${
|
|
2658
|
+
const alignment = await readAlignment(forkPath, ref, id);
|
|
2659
|
+
if (!alignment) throw new Error(`no alignment '${id}' at upstream ${ref.slice(0, 7)}`);
|
|
2660
|
+
const files = (await git(["ls-tree", "-r", "--name-only", `${ref}:${ALIGNMENTS_DIR}/${id}`], forkPath)).split("\n").filter(Boolean);
|
|
2456
2661
|
const target = join8(EXTRACT_DIR, id);
|
|
2457
2662
|
for (const file of files) {
|
|
2458
|
-
const content = execFileSync("git", ["show", `${ref}:${
|
|
2663
|
+
const content = execFileSync("git", ["show", `${ref}:${ALIGNMENTS_DIR}/${id}/${file}`], {
|
|
2459
2664
|
cwd: forkPath,
|
|
2460
2665
|
maxBuffer: MAX_BUFFER
|
|
2461
2666
|
});
|
|
@@ -2463,27 +2668,27 @@ async function writeNoteFolder(config, ref, id) {
|
|
|
2463
2668
|
await mkdir4(dirname4(path), { recursive: true });
|
|
2464
2669
|
await writeFile3(path, content);
|
|
2465
2670
|
}
|
|
2466
|
-
return {
|
|
2671
|
+
return { alignment, target, files };
|
|
2467
2672
|
}
|
|
2468
|
-
async function
|
|
2469
|
-
const {
|
|
2673
|
+
async function extractAlignment(config, ref, id) {
|
|
2674
|
+
const { alignment, target, files } = await writeAlignmentFolder(config, ref, id);
|
|
2470
2675
|
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
|
|
2676
|
+
console.info(colors_default.dim(` the README's ${ALIGNMENTS_DIR}/${id}/ paths are this folder.`));
|
|
2677
|
+
if (alignment.codemod) {
|
|
2678
|
+
console.info(colors_default.dim(` pnpm exec tsx ${target}/${alignment.codemod} inventory ${alignment.roots.join(" ")}`));
|
|
2679
|
+
console.info(colors_default.dim(` or, keeping files identical to upstream as they are: pnpm cella align --run ${id}`));
|
|
2475
2680
|
}
|
|
2476
2681
|
}
|
|
2477
|
-
async function
|
|
2682
|
+
async function runAlignment(config, ref, id) {
|
|
2478
2683
|
const { forkPath } = config;
|
|
2479
|
-
const {
|
|
2480
|
-
if (!
|
|
2684
|
+
const { alignment, target } = await writeAlignmentFolder(config, ref, id);
|
|
2685
|
+
if (!alignment.codemod) {
|
|
2481
2686
|
throw new Error(
|
|
2482
|
-
`
|
|
2687
|
+
`alignment '${id}' ships no codemod script: its steps are manual, read them with \`pnpm cella align --show ${id}\`.`
|
|
2483
2688
|
);
|
|
2484
2689
|
}
|
|
2485
|
-
const script = join8(target,
|
|
2486
|
-
const args = config.runArgs?.length ? config.runArgs : ["inventory", ...
|
|
2690
|
+
const script = join8(target, alignment.codemod);
|
|
2691
|
+
const args = config.runArgs?.length ? config.runArgs : ["inventory", ...alignment.roots];
|
|
2487
2692
|
const upstreamCommit = await readManifestBase(forkPath) ?? ref;
|
|
2488
2693
|
await ensureUpstreamCommit(config, upstreamCommit);
|
|
2489
2694
|
const identical = await listIdenticalToRef(forkPath, upstreamCommit);
|
|
@@ -2508,92 +2713,335 @@ async function runNote(config, ref, id) {
|
|
|
2508
2713
|
}
|
|
2509
2714
|
if (child.status !== 0) throw new Error(`the codemod exited with code ${child.status ?? "unknown"}`);
|
|
2510
2715
|
if (!config.runArgs?.length) {
|
|
2511
|
-
const apply = ["rewrite", ...
|
|
2512
|
-
console.info(colors_default.dim(`report only. to apply: pnpm cella
|
|
2716
|
+
const apply = ["rewrite", ...alignment.roots].join(" ");
|
|
2717
|
+
console.info(colors_default.dim(`report only. to apply: pnpm cella align --run ${id} -- ${apply}`));
|
|
2513
2718
|
}
|
|
2514
2719
|
}
|
|
2515
|
-
async function
|
|
2720
|
+
async function markAlignments(config, ref, pending, ids) {
|
|
2516
2721
|
const open = new Set(pending);
|
|
2517
2722
|
const unknown = ids.filter((id) => !open.has(id));
|
|
2518
2723
|
for (const id of ids) open.delete(id);
|
|
2519
2724
|
await writePending(config.forkPath, [...open]);
|
|
2520
2725
|
const marked = ids.length - unknown.length;
|
|
2521
|
-
if (marked > 0) console.info(`${checkMark} recorded ${marked}
|
|
2726
|
+
if (marked > 0) console.info(`${checkMark} recorded ${marked} alignment(s) as handled`);
|
|
2522
2727
|
if (unknown.length > 0) console.info(colors_default.dim(`not pending, left as is: ${unknown.join(", ")}`));
|
|
2523
|
-
console.info(colors_default.dim(
|
|
2728
|
+
console.info(colors_default.dim(formatAlignmentsLine((await listAlignmentIds(config.forkPath, ref)).length, open.size)));
|
|
2524
2729
|
}
|
|
2525
|
-
async function
|
|
2730
|
+
async function runAlign(config) {
|
|
2526
2731
|
const { forkPath } = config;
|
|
2527
|
-
const ref = await
|
|
2732
|
+
const ref = await resolveAlignmentsRef(config);
|
|
2528
2733
|
if (!ref) {
|
|
2529
|
-
console.info(colors_default.dim("no sync point recorded yet:
|
|
2734
|
+
console.info(colors_default.dim("no sync point recorded yet: alignments start with the first `pnpm cella sync`."));
|
|
2530
2735
|
return;
|
|
2531
2736
|
}
|
|
2532
2737
|
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 ?? {
|
|
2738
|
+
if (config.mark?.length) return markAlignments(config, ref, pending, config.mark);
|
|
2739
|
+
if (config.run) return runAlignment(config, ref, config.run);
|
|
2740
|
+
if (config.extract) return extractAlignment(config, ref, config.extract);
|
|
2741
|
+
if (config.show) return showAlignment(config, ref, pending, config.show);
|
|
2742
|
+
const ids = config.all ? await listAlignmentIds(forkPath, ref) : pending;
|
|
2743
|
+
const read = await Promise.all(ids.map((id) => readAlignment(forkPath, ref, id)));
|
|
2744
|
+
const alignments = read.map(
|
|
2745
|
+
(alignment, i) => alignment ?? {
|
|
2551
2746
|
id: ids[i],
|
|
2552
2747
|
title: `${ids[i]} (no longer upstream)`,
|
|
2553
2748
|
summary: "",
|
|
2554
2749
|
syncBreaking: false,
|
|
2555
2750
|
clientCacheBump: false,
|
|
2751
|
+
caughtBy: null,
|
|
2752
|
+
generate: false,
|
|
2753
|
+
grep: [],
|
|
2556
2754
|
roots: [],
|
|
2557
2755
|
codemod: null
|
|
2558
2756
|
}
|
|
2559
2757
|
);
|
|
2560
|
-
const total = (await
|
|
2758
|
+
const total = (await listAlignmentIds(forkPath, ref)).length;
|
|
2759
|
+
const open = new Set(pending);
|
|
2760
|
+
const view = await readForkView(config, ref);
|
|
2761
|
+
const described = await Promise.all(
|
|
2762
|
+
alignments.map((alignment) => describeAlignment(config, view, ref, alignment, open.has(alignment.id)))
|
|
2763
|
+
);
|
|
2561
2764
|
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));
|
|
2765
|
+
writeStdout(JSON.stringify({ upstreamCommit: ref, total, open: pending.length, alignments: described }, null, 2));
|
|
2575
2766
|
return;
|
|
2576
2767
|
}
|
|
2577
|
-
console.info(colors_default.dim(
|
|
2578
|
-
if (
|
|
2768
|
+
console.info(colors_default.dim(formatAlignmentsLine(total, pending.length)));
|
|
2769
|
+
if (described.length === 0) return;
|
|
2579
2770
|
console.info();
|
|
2580
|
-
for (const [i,
|
|
2771
|
+
for (const [i, alignment] of described.entries()) printAlignment(i + 1, alignment);
|
|
2772
|
+
const schema = described.filter((alignment) => alignment.open && alignment.generate);
|
|
2773
|
+
if (schema.length > 1) {
|
|
2774
|
+
console.info(
|
|
2775
|
+
colors_default.dim(
|
|
2776
|
+
`${schema.length} open alignments change the schema: work their code steps first, then run pnpm generate once.`
|
|
2777
|
+
)
|
|
2778
|
+
);
|
|
2779
|
+
}
|
|
2581
2780
|
if (pending.length > 0) {
|
|
2582
|
-
console.info(colors_default.dim("work each
|
|
2583
|
-
console.info(colors_default.dim(` pnpm cella
|
|
2781
|
+
console.info(colors_default.dim("work each alignment's README, check with `pnpm check`, then record it:"));
|
|
2782
|
+
console.info(colors_default.dim(` pnpm cella align --mark ${pending.join(" ")}`));
|
|
2584
2783
|
}
|
|
2585
2784
|
}
|
|
2586
2785
|
|
|
2587
|
-
// src/services/
|
|
2786
|
+
// src/services/analyze-report.ts
|
|
2588
2787
|
import { basename as basename2 } from "path";
|
|
2589
|
-
|
|
2788
|
+
var BEHIND_FLAT_MAX = 15;
|
|
2789
|
+
var AHEAD_FLAT_MAX = 8;
|
|
2790
|
+
var ABSENT_SHOWN_MIN = 10;
|
|
2791
|
+
var ENTRY_COLLAPSE_MIN = 5;
|
|
2792
|
+
var SECTION_ROWS_MAX = 100;
|
|
2793
|
+
var UPSTREAM_CHANGE = { fields: "changed", repo: "upstream" };
|
|
2794
|
+
var UPSTREAM_SIDE = { fields: "upstream", repo: "upstream" };
|
|
2795
|
+
var FORK_SIDE = { fields: "changed", repo: "fork" };
|
|
2796
|
+
function cell(value, width, color, align) {
|
|
2797
|
+
if (!value) return " ".repeat(width);
|
|
2798
|
+
return padVisible(color ? color(value) : value, width, align === "end" ? "end" : "start");
|
|
2799
|
+
}
|
|
2800
|
+
function formatRows(rows, caption) {
|
|
2801
|
+
const iconWidth = rows.some((row) => row.icon) ? 2 : 0;
|
|
2802
|
+
const labelWidth = Math.max(...rows.map((row) => row.label.length + (row.note ? row.note.length + 1 : 0)));
|
|
2803
|
+
const countWidth = Math.max(0, ...rows.map((row) => row.count?.length ?? 0));
|
|
2804
|
+
const tagWidth = Math.max(0, ...rows.map((row) => row.tag?.length ?? 0));
|
|
2805
|
+
const ageWidth = Math.max(0, ...rows.map((row) => row.age?.length ?? 0));
|
|
2806
|
+
const lines = [];
|
|
2807
|
+
if (caption && countWidth > 0) lines.push(colors_default.dim(caption.padStart(2 + iconWidth + labelWidth + 2 + countWidth)));
|
|
2808
|
+
for (const row of rows) {
|
|
2809
|
+
const icon = iconWidth ? cell(row.icon, iconWidth) : "";
|
|
2810
|
+
const label = `${hyperlink(row.label, row.link)}${row.note ? ` ${colors_default.dim(row.note)}` : ""}`;
|
|
2811
|
+
const cells = [` ${icon}${padVisible(label, labelWidth)}`];
|
|
2812
|
+
if (countWidth) cells.push(cell(row.count, countWidth, row.countColor, "end"));
|
|
2813
|
+
if (tagWidth) cells.push(cell(row.tag, tagWidth, row.tagColor));
|
|
2814
|
+
if (ageWidth) cells.push(cell(row.age, ageWidth, colors_default.dim, "end"));
|
|
2815
|
+
lines.push(cells.join(" ").trimEnd());
|
|
2816
|
+
}
|
|
2817
|
+
return lines;
|
|
2818
|
+
}
|
|
2819
|
+
function formatCappedRows(rows, caption) {
|
|
2820
|
+
if (rows.length <= SECTION_ROWS_MAX) return formatRows(rows, caption);
|
|
2821
|
+
return [
|
|
2822
|
+
...formatRows(rows.slice(0, SECTION_ROWS_MAX), caption),
|
|
2823
|
+
colors_default.dim(` + ${rows.length - SECTION_ROWS_MAX} more`)
|
|
2824
|
+
];
|
|
2825
|
+
}
|
|
2826
|
+
function hint(text, command = "") {
|
|
2827
|
+
return ` ${colors_default.dim(text)}${command}`;
|
|
2828
|
+
}
|
|
2829
|
+
function printSection(title, count3, body, hints = []) {
|
|
2830
|
+
printSectionHeader(`${title} ${colors_default.dim(`\xB7 ${plural(count3, "file")}`)}`);
|
|
2831
|
+
for (const line of body) console.info(line);
|
|
2832
|
+
if (hints.length === 0) return;
|
|
2833
|
+
console.info();
|
|
2834
|
+
for (const line of hints) console.info(line);
|
|
2835
|
+
}
|
|
2836
|
+
function byPath(a, b) {
|
|
2837
|
+
return a.path.localeCompare(b.path);
|
|
2838
|
+
}
|
|
2839
|
+
function fileLink(file, side, links) {
|
|
2840
|
+
const { fileLinkMode = "commit", upstreamGitHubUrl, upstreamSha, forkGitHubUrl } = links;
|
|
2841
|
+
if (fileLinkMode === "file") {
|
|
2842
|
+
if (!upstreamGitHubUrl || !upstreamSha || !file.existsInUpstream) return void 0;
|
|
2843
|
+
return `${upstreamGitHubUrl}/blob/${upstreamSha}/${file.path}`;
|
|
2844
|
+
}
|
|
2845
|
+
const commit = side.fields === "upstream" ? file.upstreamCommit : file.changedCommit;
|
|
2846
|
+
const repo = side.repo === "upstream" ? upstreamGitHubUrl : forkGitHubUrl;
|
|
2847
|
+
return commit && repo ? `${repo}/commit/${commit}` : void 0;
|
|
2848
|
+
}
|
|
2849
|
+
function changeTimestamp(file, side) {
|
|
2850
|
+
return side.fields === "upstream" ? file.upstreamChangedTs : file.changedTs;
|
|
2851
|
+
}
|
|
2852
|
+
function changeKind(file) {
|
|
2853
|
+
if (!file.existsInFork) return "new";
|
|
2854
|
+
if (!file.existsInUpstream) return "deleted";
|
|
2855
|
+
return "changed";
|
|
2856
|
+
}
|
|
2857
|
+
var KIND_TAG = { new: { tag: "new", color: colors_default.green }, deleted: { tag: "deleted", color: colors_default.red } };
|
|
2858
|
+
function fileRow(file, side, options, extra = {}) {
|
|
2859
|
+
const timestamp = changeTimestamp(file, side);
|
|
2860
|
+
const kind = changeKind(file);
|
|
2861
|
+
return {
|
|
2862
|
+
label: file.path,
|
|
2863
|
+
link: fileLink(file, side, options.links),
|
|
2864
|
+
age: timestamp ? shortAge(timestamp, options.nowMs) : void 0,
|
|
2865
|
+
...kind === "changed" ? {} : { tag: KIND_TAG[kind].tag, tagColor: KIND_TAG[kind].color },
|
|
2866
|
+
...extra
|
|
2867
|
+
};
|
|
2868
|
+
}
|
|
2869
|
+
function commonFolder(paths) {
|
|
2870
|
+
const folders = paths.map((path) => path.split("/").slice(0, -1));
|
|
2871
|
+
const [first] = folders;
|
|
2872
|
+
let depth = 0;
|
|
2873
|
+
while (depth < first.length && folders.every((parts) => parts[depth] === first[depth])) depth++;
|
|
2874
|
+
return first.slice(0, depth).join("/");
|
|
2875
|
+
}
|
|
2876
|
+
function formatFolderRows(files, columns) {
|
|
2877
|
+
const groups = /* @__PURE__ */ new Map();
|
|
2878
|
+
for (const file of files) {
|
|
2879
|
+
const top = file.path.includes("/") ? file.path.split("/")[0] : file.path;
|
|
2880
|
+
groups.set(top, [...groups.get(top) ?? [], file]);
|
|
2881
|
+
}
|
|
2882
|
+
const rows = [...groups.values()].map((group) => ({
|
|
2883
|
+
label: group.length === 1 ? group[0].path : `${commonFolder(group.map((file) => file.path))}/`,
|
|
2884
|
+
files: group
|
|
2885
|
+
})).sort((a, b) => b.files.length - a.files.length || a.label.localeCompare(b.label));
|
|
2886
|
+
const labelWidth = Math.max(...rows.map((row) => row.label.length));
|
|
2887
|
+
const countWidth = Math.max(...rows.map((row) => String(row.files.length).length));
|
|
2888
|
+
const order = { new: 0, deleted: 1, changed: 2 };
|
|
2889
|
+
return rows.map((row) => {
|
|
2890
|
+
let line = ` ${row.label.padEnd(labelWidth)} ${colors_default.cyan(String(row.files.length).padStart(countWidth))}`;
|
|
2891
|
+
if (row.files.length === 1) return line.trimEnd();
|
|
2892
|
+
const seen = /* @__PURE__ */ new Map();
|
|
2893
|
+
for (const file of row.files) seen.set(basename2(file.path), (seen.get(basename2(file.path)) ?? 0) + 1);
|
|
2894
|
+
const names = row.files.map((file) => ({
|
|
2895
|
+
kind: changeKind(file),
|
|
2896
|
+
name: (seen.get(basename2(file.path)) ?? 0) > 1 ? file.path.slice(row.label.length) : basename2(file.path)
|
|
2897
|
+
})).sort((a, b) => order[a.kind] - order[b.kind] || a.name.localeCompare(b.name));
|
|
2898
|
+
let width = visibleWidth(line);
|
|
2899
|
+
for (const { kind, name } of names) {
|
|
2900
|
+
const text = kind === "new" ? `+${name}` : kind === "deleted" ? `\u2212${name}` : name;
|
|
2901
|
+
if (width + 2 + text.length > columns - 2) {
|
|
2902
|
+
line += colors_default.dim(" \u2026");
|
|
2903
|
+
break;
|
|
2904
|
+
}
|
|
2905
|
+
line += ` ${kind === "new" ? colors_default.green(text) : kind === "deleted" ? colors_default.red(text) : colors_default.dim(text)}`;
|
|
2906
|
+
width += 2 + text.length;
|
|
2907
|
+
}
|
|
2908
|
+
return line;
|
|
2909
|
+
});
|
|
2910
|
+
}
|
|
2911
|
+
function printBehind(files, options, columns) {
|
|
2912
|
+
const behind = filesWithStatus(files, "behind").sort(byPath);
|
|
2913
|
+
if (behind.length === 0) return;
|
|
2914
|
+
const title = colors_default.cyan("\u2193 behind on upstream");
|
|
2915
|
+
if (options.all || behind.length <= BEHIND_FLAT_MAX) {
|
|
2916
|
+
printSection(title, behind.length, formatCappedRows(behind.map((file) => fileRow(file, UPSTREAM_CHANGE, options))));
|
|
2917
|
+
return;
|
|
2918
|
+
}
|
|
2919
|
+
printSection(title, behind.length, formatFolderRows(behind, columns), [
|
|
2920
|
+
hint("every path: ", "pnpm cella analyze --all")
|
|
2921
|
+
]);
|
|
2922
|
+
}
|
|
2923
|
+
function printFlat(files, title, side, options) {
|
|
2924
|
+
if (files.length === 0) return;
|
|
2925
|
+
const rows = [...files].sort(byPath).map((file) => fileRow(file, side, options));
|
|
2926
|
+
printSection(title, files.length, formatCappedRows(rows));
|
|
2927
|
+
}
|
|
2928
|
+
function absentLines(file) {
|
|
2929
|
+
return file.upstreamLinesAbsent ?? 0;
|
|
2930
|
+
}
|
|
2931
|
+
function describeAbsent(files) {
|
|
2932
|
+
const counts = files.map(absentLines);
|
|
2933
|
+
const missing = counts.filter((count3) => count3 > 0);
|
|
2934
|
+
const parts = [];
|
|
2935
|
+
if (missing.length > 0) {
|
|
2936
|
+
const [low, high] = [Math.min(...missing), Math.max(...missing)];
|
|
2937
|
+
parts.push(`${missing.length} with ${low === high ? plural(low, "line") : `${low}\u2013${high} lines`} absent`);
|
|
2938
|
+
}
|
|
2939
|
+
if (missing.length < counts.length) parts.push(`${counts.length - missing.length} with none`);
|
|
2940
|
+
return parts.join(", ");
|
|
2941
|
+
}
|
|
2942
|
+
function printAhead(files, options) {
|
|
2943
|
+
const ahead = filesWithStatus(files, "ahead").sort((a, b) => absentLines(b) - absentLines(a) || byPath(a, b));
|
|
2944
|
+
if (ahead.length === 0) return;
|
|
2945
|
+
let shown = ahead;
|
|
2946
|
+
if (!options.all && ahead.length > AHEAD_FLAT_MAX) {
|
|
2947
|
+
const many = ahead.filter((file) => absentLines(file) >= ABSENT_SHOWN_MIN);
|
|
2948
|
+
shown = many.length > 0 ? many : ahead.slice(0, 5);
|
|
2949
|
+
}
|
|
2950
|
+
const rest = ahead.slice(shown.length);
|
|
2951
|
+
const rows = shown.map(
|
|
2952
|
+
(file) => fileRow(file, FORK_SIDE, options, {
|
|
2953
|
+
count: absentLines(file) > 0 ? String(absentLines(file)) : void 0,
|
|
2954
|
+
countColor: colors_default.yellow
|
|
2955
|
+
})
|
|
2956
|
+
);
|
|
2957
|
+
const body = formatCappedRows(rows, "upstream lines absent");
|
|
2958
|
+
if (rest.length > 0) body.push(colors_default.dim(` + ${rest.length} more: ${describeAbsent(rest)}`));
|
|
2959
|
+
const hints = ahead.some((file) => absentLines(file) > 0) ? [hint("what a merge would bring in: ", "pnpm cella analyze --unpinned")] : [];
|
|
2960
|
+
printSection(colors_default.blue("\u2191 protected in fork"), ahead.length, body, hints);
|
|
2961
|
+
}
|
|
2962
|
+
function entryFinder(overrides) {
|
|
2963
|
+
const entries = [...overrides?.ignored ?? [], ...overrides?.pinned ?? []].map(normalizeEntry).filter(Boolean).sort((a, b) => b.length - a.length);
|
|
2964
|
+
return (path) => entries.find((entry) => isUnderAnyFolder(path, [entry]));
|
|
2965
|
+
}
|
|
2966
|
+
var KIND_ORDER = { new: 0, changed: 1, deleted: 2 };
|
|
2967
|
+
function protectedRow(items, options) {
|
|
2968
|
+
const [{ file, kind, entry }] = items;
|
|
2969
|
+
const lines = items.map((item) => item.file.upstreamChangedLines);
|
|
2970
|
+
const total = lines.every((count3) => count3 !== void 0) ? lines.reduce((sum, count3) => sum + (count3 ?? 0), 0) : void 0;
|
|
2971
|
+
const sign = kind === "new" ? "+" : kind === "deleted" ? "\u2212" : "";
|
|
2972
|
+
const newest = Math.max(0, ...items.map((item) => changeTimestamp(item.file, UPSTREAM_SIDE) ?? 0));
|
|
2973
|
+
const row = {
|
|
2974
|
+
label: file.path,
|
|
2975
|
+
link: fileLink(file, UPSTREAM_SIDE, options.links),
|
|
2976
|
+
icon: protectionIcon(file),
|
|
2977
|
+
count: total === void 0 ? void 0 : `${sign}${compactCount(total)}`,
|
|
2978
|
+
countColor: kind === "new" ? colors_default.green : kind === "deleted" ? colors_default.red : colors_default.yellow,
|
|
2979
|
+
...kind === "changed" ? {} : { tag: KIND_TAG[kind].tag, tagColor: KIND_TAG[kind].color },
|
|
2980
|
+
age: newest ? shortAge(newest, options.nowMs) : void 0
|
|
2981
|
+
};
|
|
2982
|
+
if (items.length === 1 || !entry) return row;
|
|
2983
|
+
const names = new Set(items.map((item) => basename2(item.file.path)));
|
|
2984
|
+
const { upstreamGitHubUrl, upstreamSha } = options.links;
|
|
2985
|
+
return {
|
|
2986
|
+
...row,
|
|
2987
|
+
label: `${entry}/`,
|
|
2988
|
+
note: names.size === 1 ? `\xB7 ${items.length} \xD7 ${[...names][0]}` : `\xB7 ${items.length} files`,
|
|
2989
|
+
link: upstreamGitHubUrl && upstreamSha ? `${upstreamGitHubUrl}/tree/${upstreamSha}/${entry}` : void 0
|
|
2990
|
+
};
|
|
2991
|
+
}
|
|
2992
|
+
function printProtectedBehind(result, options) {
|
|
2993
|
+
const findEntry = entryFinder(options.overrides);
|
|
2994
|
+
const items = findProtectedBehindUpstream(result).map((file) => ({
|
|
2995
|
+
file,
|
|
2996
|
+
kind: file.upstreamOnly ? changeKind(file) : "changed",
|
|
2997
|
+
entry: findEntry(file.path)
|
|
2998
|
+
}));
|
|
2999
|
+
if (items.length === 0) return;
|
|
3000
|
+
const groups = /* @__PURE__ */ new Map();
|
|
3001
|
+
for (const item of items) {
|
|
3002
|
+
const key = options.all || !item.entry ? item.file.path : `${item.kind}\0${item.entry}`;
|
|
3003
|
+
groups.set(key, [...groups.get(key) ?? [], item]);
|
|
3004
|
+
}
|
|
3005
|
+
const single = (group) => group.length < ENTRY_COLLAPSE_MIN;
|
|
3006
|
+
const lines = (group) => group.reduce((sum, item) => sum + (item.file.upstreamChangedLines ?? 0), 0);
|
|
3007
|
+
const ordered = [...groups.values()].flatMap((group) => single(group) ? group.map((item) => [item]) : [group]).sort(
|
|
3008
|
+
(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)
|
|
3009
|
+
);
|
|
3010
|
+
printSection(
|
|
3011
|
+
`${warningMark} ${colors_default.yellow("protected but behind upstream")}`,
|
|
3012
|
+
items.length,
|
|
3013
|
+
formatCappedRows(
|
|
3014
|
+
ordered.map((group) => protectedRow(group, options)),
|
|
3015
|
+
"lines changed upstream"
|
|
3016
|
+
),
|
|
3017
|
+
[hint("the fork side wins: adopt what you need from the review page")]
|
|
3018
|
+
);
|
|
3019
|
+
}
|
|
3020
|
+
function printAnalysisFileGroups(result, options) {
|
|
3021
|
+
const columns = options.columns ?? terminalColumns();
|
|
3022
|
+
printBehind(result.files, options, columns);
|
|
3023
|
+
printFlat(filesWithStatus(result.files, "diverged"), colors_default.magenta("\u21C5 diverged"), UPSTREAM_SIDE, options);
|
|
3024
|
+
printAhead(result.files, options);
|
|
3025
|
+
printFlat(
|
|
3026
|
+
filesWithStatus(result.files, "drifted"),
|
|
3027
|
+
`${warningMark} ${colors_default.yellow("drifted from upstream")}`,
|
|
3028
|
+
FORK_SIDE,
|
|
3029
|
+
options
|
|
3030
|
+
);
|
|
3031
|
+
printProtectedBehind(result, options);
|
|
3032
|
+
}
|
|
3033
|
+
|
|
3034
|
+
// src/services/review-page.ts
|
|
3035
|
+
import { existsSync as existsSync7, rmSync } from "fs";
|
|
3036
|
+
import { basename as basename3 } from "path";
|
|
3037
|
+
import process9 from "process";
|
|
2590
3038
|
import { pathToFileURL } from "url";
|
|
2591
3039
|
|
|
2592
3040
|
// src/utils/diff.ts
|
|
2593
3041
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
2594
3042
|
import { mkdirSync, writeFileSync } from "fs";
|
|
2595
|
-
import { join as join9 } from "path";
|
|
2596
|
-
import
|
|
3043
|
+
import { dirname as dirname5, join as join9 } from "path";
|
|
3044
|
+
import process8 from "process";
|
|
2597
3045
|
function gitDiffFile(cwd, range, filePath) {
|
|
2598
3046
|
try {
|
|
2599
3047
|
return execFileSync2("git", ["diff", "--no-color", range, "--", filePath], { cwd, maxBuffer: MAX_BUFFER });
|
|
@@ -2628,9 +3076,9 @@ ${prerenderedHTML}
|
|
|
2628
3076
|
for (const file of section.files) files.push(await renderFile(file));
|
|
2629
3077
|
const count3 = section.files.length + (section.omitted?.length ?? 0);
|
|
2630
3078
|
const title = section.title ? `<h2>${escapeHtml(section.title)} <span class="count">\xB7 ${count3} ${count3 === 1 ? "file" : "files"}</span></h2>` : "";
|
|
2631
|
-
const
|
|
3079
|
+
const hint2 = section.hint ? `<p class="hint">${escapeHtml(section.hint)}</p>` : "";
|
|
2632
3080
|
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}${
|
|
3081
|
+
blocks.push(`<section id="section-${index2 + 1}">${title}${hint2}${files.join("\n")}${omitted}</section>`);
|
|
2634
3082
|
}
|
|
2635
3083
|
const titled = sections.filter((section) => section.title);
|
|
2636
3084
|
const index = titled.length > 1 ? `<nav><ol>${sections.map(
|
|
@@ -2694,16 +3142,18 @@ ${blocks.join("\n")}
|
|
|
2694
3142
|
}
|
|
2695
3143
|
async function writeDiffPage(sections, meta, repoPath, name) {
|
|
2696
3144
|
const html = await renderDiffPage(sections, meta);
|
|
2697
|
-
const
|
|
2698
|
-
|
|
2699
|
-
mkdirSync(dir, { recursive: true });
|
|
2700
|
-
const outPath = join9(dir, `${name.replace(/[^\w.-]+/g, "-")}.html`);
|
|
3145
|
+
const outPath = diffPagePath(repoPath, name);
|
|
3146
|
+
mkdirSync(dirname5(outPath), { recursive: true });
|
|
2701
3147
|
writeFileSync(outPath, html);
|
|
2702
3148
|
return outPath;
|
|
2703
3149
|
}
|
|
3150
|
+
function diffPagePath(repoPath, name) {
|
|
3151
|
+
const gitDir = execFileSync2("git", ["rev-parse", "--absolute-git-dir"], { cwd: repoPath, encoding: "utf8" }).trim();
|
|
3152
|
+
return join9(gitDir, "cella-diff", `${name.replace(/[^\w.-]+/g, "-")}.html`);
|
|
3153
|
+
}
|
|
2704
3154
|
function openInBrowser(target) {
|
|
2705
3155
|
try {
|
|
2706
|
-
execFileSync2(
|
|
3156
|
+
execFileSync2(process8.platform === "darwin" ? "open" : "xdg-open", [target], { stdio: "ignore" });
|
|
2707
3157
|
return true;
|
|
2708
3158
|
} catch {
|
|
2709
3159
|
return false;
|
|
@@ -2711,38 +3161,39 @@ function openInBrowser(target) {
|
|
|
2711
3161
|
}
|
|
2712
3162
|
|
|
2713
3163
|
// src/services/review-page.ts
|
|
2714
|
-
var
|
|
3164
|
+
var PAGE_LIMITS = { files: 50, lines: 15e3, patchLines: 2e3 };
|
|
2715
3165
|
var MAX_PATCH_BYTES = 512 * 1024;
|
|
2716
3166
|
function reviewSections(result, command) {
|
|
2717
3167
|
const [base, upstream] = result.upstreamDiffRange?.split("..") ?? [];
|
|
2718
3168
|
if (!base || !upstream) return [];
|
|
2719
3169
|
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,
|
|
3170
|
+
const sections = command === "sync" ? [
|
|
3171
|
+
{
|
|
3172
|
+
title: "protected files kept the fork version",
|
|
3173
|
+
hint: "What upstream changed in these files since the last sync. The fork kept its own version, so none of it arrived: adopt what you need.",
|
|
3174
|
+
range: sinceSync,
|
|
3175
|
+
paths: result.protectedConflicts ?? []
|
|
3176
|
+
},
|
|
2733
3177
|
{
|
|
2734
3178
|
title: "ignored paths changed upstream",
|
|
2735
3179
|
hint: "What upstream changed under ignored paths since the last sync. The fork left these files untouched, and ignored paths never sync.",
|
|
2736
3180
|
range: sinceSync,
|
|
2737
3181
|
paths: (result.ignoredUpstreamChanges ?? []).flatMap((group) => group.paths)
|
|
2738
|
-
}
|
|
3182
|
+
}
|
|
3183
|
+
] : [
|
|
2739
3184
|
{
|
|
2740
|
-
title: "
|
|
2741
|
-
hint: "What upstream changed in these files since the last sync.
|
|
3185
|
+
title: "protected but behind upstream",
|
|
3186
|
+
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.",
|
|
2742
3187
|
range: sinceSync,
|
|
2743
|
-
paths:
|
|
3188
|
+
paths: findProtectedBehindUpstream(result).map((file) => file.path)
|
|
2744
3189
|
}
|
|
2745
3190
|
];
|
|
3191
|
+
sections.push({
|
|
3192
|
+
title: "pinned files that match the previous upstream",
|
|
3193
|
+
hint: "What upstream changed in these files since the last sync. The fork copy still equals the previous upstream, so the pin only drops these changes: unpin to take them.",
|
|
3194
|
+
range: sinceSync,
|
|
3195
|
+
paths: findMaskingPins(result.files).map((file) => file.path)
|
|
3196
|
+
});
|
|
2746
3197
|
if (command === "analyze") {
|
|
2747
3198
|
sections.push(
|
|
2748
3199
|
{
|
|
@@ -2761,55 +3212,100 @@ function reviewSections(result, command) {
|
|
|
2761
3212
|
}
|
|
2762
3213
|
return sections.filter((section) => section.paths.length > 0);
|
|
2763
3214
|
}
|
|
2764
|
-
|
|
3215
|
+
function countLines(patch) {
|
|
3216
|
+
let count3 = 0;
|
|
3217
|
+
for (let at = patch.indexOf(10); at !== -1; at = patch.indexOf(10, at + 1)) count3++;
|
|
3218
|
+
return count3;
|
|
3219
|
+
}
|
|
3220
|
+
async function writeReviewPage(forkPath, sections, meta, limits = PAGE_LIMITS) {
|
|
2765
3221
|
const [base, upstream] = meta.range.split("..");
|
|
3222
|
+
const pageSections = sections.map((section) => ({
|
|
3223
|
+
title: section.title,
|
|
3224
|
+
hint: section.hint,
|
|
3225
|
+
files: [],
|
|
3226
|
+
omitted: []
|
|
3227
|
+
}));
|
|
3228
|
+
const queues = sections.map((section) => [...section.paths]);
|
|
2766
3229
|
let diffs = 0;
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
const
|
|
2770
|
-
|
|
2771
|
-
if (
|
|
2772
|
-
|
|
3230
|
+
let lines = 0;
|
|
3231
|
+
while (queues.some((queue) => queue.length > 0)) {
|
|
3232
|
+
for (const [index, queue] of queues.entries()) {
|
|
3233
|
+
const path = queue.shift();
|
|
3234
|
+
if (path === void 0) continue;
|
|
3235
|
+
const pageSection = pageSections[index];
|
|
3236
|
+
if (diffs >= limits.files || lines >= limits.lines) {
|
|
3237
|
+
pageSection.omitted.push(path);
|
|
2773
3238
|
continue;
|
|
2774
3239
|
}
|
|
2775
|
-
const patch = gitDiffFile(forkPath,
|
|
3240
|
+
const patch = gitDiffFile(forkPath, sections[index].range, path);
|
|
2776
3241
|
if (patch.length === 0) continue;
|
|
2777
|
-
|
|
2778
|
-
|
|
3242
|
+
const patchLines = patch.length > MAX_PATCH_BYTES ? Number.POSITIVE_INFINITY : countLines(patch);
|
|
3243
|
+
if (patchLines > limits.patchLines || lines + patchLines > limits.lines || !patch.includes("\n@@ ")) {
|
|
3244
|
+
pageSection.omitted.push(path);
|
|
2779
3245
|
continue;
|
|
2780
3246
|
}
|
|
2781
3247
|
pageSection.files.push({ path, patch: patch.toString() });
|
|
2782
3248
|
diffs++;
|
|
3249
|
+
lines += patchLines;
|
|
2783
3250
|
}
|
|
2784
|
-
if (pageSection.files.length > 0 || pageSection.omitted?.length) pageSections.push(pageSection);
|
|
2785
3251
|
}
|
|
2786
3252
|
if (diffs === 0) return void 0;
|
|
3253
|
+
const shown = pageSections.filter((section) => section.files.length > 0 || section.omitted.length > 0);
|
|
2787
3254
|
const page = await writeDiffPage(
|
|
2788
|
-
|
|
2789
|
-
{ title: `${meta.command} review`, note: `${
|
|
3255
|
+
shown,
|
|
3256
|
+
{ title: `${meta.command} review`, note: `${basename3(forkPath)} \xB7 last sync ${base} \u2192 upstream ${upstream}` },
|
|
2790
3257
|
forkPath,
|
|
2791
3258
|
`${meta.command}-review`
|
|
2792
3259
|
);
|
|
2793
|
-
|
|
3260
|
+
const listed = shown.reduce((count3, section) => count3 + section.omitted.length, 0);
|
|
3261
|
+
return { path: page, diffs, listed, sections: shown.length };
|
|
2794
3262
|
}
|
|
2795
3263
|
async function reportReviewPage(forkPath, result, command) {
|
|
2796
3264
|
const sections = reviewSections(result, command);
|
|
2797
|
-
if (sections.length === 0 || !result.upstreamDiffRange) return;
|
|
2798
|
-
createSpinner("writing review page...");
|
|
2799
3265
|
let page;
|
|
3266
|
+
if (sections.length > 0 && result.upstreamDiffRange) {
|
|
3267
|
+
createSpinner("writing review page...");
|
|
3268
|
+
try {
|
|
3269
|
+
page = await writeReviewPage(forkPath, sections, { command, range: result.upstreamDiffRange });
|
|
3270
|
+
} catch {
|
|
3271
|
+
page = void 0;
|
|
3272
|
+
}
|
|
3273
|
+
spinnerSuccess();
|
|
3274
|
+
}
|
|
3275
|
+
if (!page) {
|
|
3276
|
+
if (command === "sync") removeSyncReviewPage(forkPath);
|
|
3277
|
+
return void 0;
|
|
3278
|
+
}
|
|
3279
|
+
const opened = command === "sync" && process9.stdout.isTTY === true && openInBrowser(page.path);
|
|
3280
|
+
return { ...page, command, opened };
|
|
3281
|
+
}
|
|
3282
|
+
function removeSyncReviewPage(forkPath) {
|
|
2800
3283
|
try {
|
|
2801
|
-
|
|
3284
|
+
rmSync(diffPagePath(forkPath, "sync-review"), { force: true });
|
|
2802
3285
|
} catch {
|
|
2803
|
-
page = void 0;
|
|
2804
3286
|
}
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
3287
|
+
}
|
|
3288
|
+
function existingSyncReviewPage(forkPath) {
|
|
3289
|
+
try {
|
|
3290
|
+
const path = diffPagePath(forkPath, "sync-review");
|
|
3291
|
+
return existsSync7(path) ? path : void 0;
|
|
3292
|
+
} catch {
|
|
3293
|
+
return void 0;
|
|
3294
|
+
}
|
|
3295
|
+
}
|
|
3296
|
+
function reviewStep(page) {
|
|
3297
|
+
const path = typeof page === "string" ? page : page.path;
|
|
3298
|
+
const url = pathToFileURL(path).href;
|
|
3299
|
+
let detail = colors_default.dim("upstream changes the sync did not bring in, from the merge run");
|
|
3300
|
+
if (typeof page !== "string") {
|
|
3301
|
+
const what = page.command === "sync" ? `${plural(page.diffs, "diff")} of upstream changes that did not arrive` : `${plural(page.diffs, "diff")} in ${plural(page.sections, "section")}`;
|
|
3302
|
+
const notes = [
|
|
3303
|
+
page.listed > 0 ? `${page.listed} listed without a diff` : "",
|
|
3304
|
+
page.opened ? "opened in the browser" : ""
|
|
3305
|
+
].filter(Boolean).map((note) => ` \xB7 ${note}`).join("");
|
|
3306
|
+
detail = `${what}${notes ? colors_default.dim(notes) : ""}`;
|
|
3307
|
+
}
|
|
3308
|
+
return { label: "review", line: hyperlink(colors_default.cyan(url), url), details: [detail] };
|
|
2813
3309
|
}
|
|
2814
3310
|
|
|
2815
3311
|
// src/services/analyze.ts
|
|
@@ -2856,23 +3352,56 @@ async function runAnalyze(config) {
|
|
|
2856
3352
|
}
|
|
2857
3353
|
return result;
|
|
2858
3354
|
}
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
3355
|
+
printAnalysisFileGroups(result, {
|
|
3356
|
+
links: {
|
|
3357
|
+
upstreamGitHubUrl: result.upstreamGitHubUrl,
|
|
3358
|
+
upstreamSha: result.upstreamCommit?.hash,
|
|
3359
|
+
forkGitHubUrl: await readOriginGitHubUrl(config.forkPath),
|
|
3360
|
+
fileLinkMode: config.settings.fileLinkMode,
|
|
3361
|
+
forkPath: config.forkPath
|
|
3362
|
+
},
|
|
3363
|
+
all: config.all,
|
|
3364
|
+
overrides: config.overrides
|
|
3365
|
+
});
|
|
3366
|
+
printUpstreamOverrideChanges(result);
|
|
3367
|
+
printMaskingPinWarning(result.files);
|
|
3368
|
+
printSummary(result.summary, "analysis summary");
|
|
3369
|
+
const steps = [];
|
|
3370
|
+
const review = await reportReviewPage(config.forkPath, result, "analyze");
|
|
3371
|
+
if (review) steps.push(reviewStep(review));
|
|
3372
|
+
const sync = syncStep(result);
|
|
3373
|
+
if (sync) steps.push(sync);
|
|
3374
|
+
const align = result.alignments && result.upstreamCommit ? await alignmentsStep(config, {
|
|
3375
|
+
ref: result.upstreamCommit.hash,
|
|
3376
|
+
open: result.alignments.open,
|
|
3377
|
+
arriving: result.alignments.arrived
|
|
3378
|
+
}) : void 0;
|
|
3379
|
+
if (align) steps.push(align);
|
|
3380
|
+
printNextSteps(steps);
|
|
2870
3381
|
return result;
|
|
2871
3382
|
}
|
|
3383
|
+
function syncStep(result) {
|
|
3384
|
+
if (!result.upstreamCommitCount) return void 0;
|
|
3385
|
+
const { behind, diverged, renamed } = result.summary;
|
|
3386
|
+
const changes = [
|
|
3387
|
+
behind > 0 ? `${behind} update` : "",
|
|
3388
|
+
diverged > 0 ? `${diverged} merge` : "",
|
|
3389
|
+
renamed > 0 ? `${renamed} rename` : ""
|
|
3390
|
+
].filter(Boolean);
|
|
3391
|
+
return {
|
|
3392
|
+
label: "sync",
|
|
3393
|
+
line: "pnpm cella sync",
|
|
3394
|
+
details: [
|
|
3395
|
+
colors_default.dim(
|
|
3396
|
+
`commits on a fresh branch, a rerun opens the PR \xB7 ${changes.length > 0 ? changes.join(", ") : "no synced file changes"}`
|
|
3397
|
+
)
|
|
3398
|
+
]
|
|
3399
|
+
};
|
|
3400
|
+
}
|
|
2872
3401
|
|
|
2873
3402
|
// src/services/audit.ts
|
|
2874
3403
|
import { readFileSync as readFileSync4 } from "fs";
|
|
2875
|
-
import { basename as
|
|
3404
|
+
import { basename as basename5, join as join13, relative as relative3 } from "path";
|
|
2876
3405
|
import { checkbox, Separator } from "@inquirer/prompts";
|
|
2877
3406
|
|
|
2878
3407
|
// src/utils/audit-utils.ts
|
|
@@ -2980,14 +3509,14 @@ function formatDependents(dependents) {
|
|
|
2980
3509
|
|
|
2981
3510
|
// src/utils/local-tools.ts
|
|
2982
3511
|
import { execFile as execFile3 } from "child_process";
|
|
2983
|
-
import { existsSync as
|
|
3512
|
+
import { existsSync as existsSync8, realpathSync as realpathSync2 } from "fs";
|
|
2984
3513
|
import { tmpdir } from "os";
|
|
2985
|
-
import { delimiter, dirname as
|
|
3514
|
+
import { delimiter, dirname as dirname7, join as join11, sep as sep2 } from "path";
|
|
2986
3515
|
import { promisify as promisify3 } from "util";
|
|
2987
3516
|
|
|
2988
3517
|
// src/utils/runtimes.ts
|
|
2989
3518
|
import { readFileSync as readFileSync2 } from "fs";
|
|
2990
|
-
import { basename as
|
|
3519
|
+
import { basename as basename4, dirname as dirname6, join as join10 } from "path";
|
|
2991
3520
|
var DAY = 24 * 60 * 60 * 1e3;
|
|
2992
3521
|
var FROZEN_AFTER = 180 * DAY;
|
|
2993
3522
|
var RECENT_PUSH = 30 * DAY;
|
|
@@ -2995,7 +3524,7 @@ var EXACT_VERSION = /^\d+\.\d+\.\d+$/;
|
|
|
2995
3524
|
var FROM_LINE = /^\s*FROM\s+(?:--platform=\S+\s+)?(\S+)(?:\s+AS\s+(\S+))?\s*$/i;
|
|
2996
3525
|
var NODE_ARG_TAG = /^(?:docker\.io\/)?(?:library\/)?node:\$\{?\w+\}?([\w.-]*)$/;
|
|
2997
3526
|
var MINOR_TAG = /^(\d+(?:\.\d+){1,2})(-[\w.-]+)?$/;
|
|
2998
|
-
var isDockerfile = (path) => /(^|\.)Dockerfile$|^Dockerfile\./i.test(
|
|
3527
|
+
var isDockerfile = (path) => /(^|\.)Dockerfile$|^Dockerfile\./i.test(basename4(path));
|
|
2999
3528
|
function parseDockerfile(file, content) {
|
|
3000
3529
|
const images = [];
|
|
3001
3530
|
const nodeImageSuffixes = [];
|
|
@@ -3233,7 +3762,7 @@ async function missingNodeImages(version, suffixes) {
|
|
|
3233
3762
|
return missing.flat();
|
|
3234
3763
|
}
|
|
3235
3764
|
function pinLocation(file) {
|
|
3236
|
-
const dir =
|
|
3765
|
+
const dir = dirname6(file);
|
|
3237
3766
|
return dir === "." ? file : dir;
|
|
3238
3767
|
}
|
|
3239
3768
|
|
|
@@ -3357,7 +3886,7 @@ var execFileAsync3 = promisify3(execFile3);
|
|
|
3357
3886
|
function findOnPath(name, skip = []) {
|
|
3358
3887
|
for (const dir of (getEnv("PATH") ?? "").split(delimiter)) {
|
|
3359
3888
|
if (!dir || skip.includes(dir) || dir.endsWith(`node_modules${sep2}.bin`)) continue;
|
|
3360
|
-
if (
|
|
3889
|
+
if (existsSync8(join11(dir, name))) return join11(dir, name);
|
|
3361
3890
|
}
|
|
3362
3891
|
return null;
|
|
3363
3892
|
}
|
|
@@ -3385,7 +3914,7 @@ async function readNode(now) {
|
|
|
3385
3914
|
}
|
|
3386
3915
|
async function readPnpm(cwd) {
|
|
3387
3916
|
const running = getEnv("npm_execpath");
|
|
3388
|
-
const file = findOnPath("pnpm", running ? [
|
|
3917
|
+
const file = findOnPath("pnpm", running ? [dirname7(running)] : []);
|
|
3389
3918
|
const version = file && await versionOf(file, tmpdir());
|
|
3390
3919
|
if (!file || !version) return null;
|
|
3391
3920
|
const [published, advisories] = await Promise.all([fetchPublishTimes("pnpm", cwd), fetchPnpmAdvisories(version)]);
|
|
@@ -3556,14 +4085,14 @@ function writeAuditSettings(content, next) {
|
|
|
3556
4085
|
|
|
3557
4086
|
// src/services/audit-security.ts
|
|
3558
4087
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
3559
|
-
import { cpSync, existsSync as
|
|
4088
|
+
import { cpSync, existsSync as existsSync9, mkdirSync as mkdirSync2, mkdtempSync, readFileSync as readFileSync3, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
3560
4089
|
import { tmpdir as tmpdir2 } from "os";
|
|
3561
|
-
import { dirname as
|
|
4090
|
+
import { dirname as dirname8, join as join12, relative as relative2 } from "path";
|
|
3562
4091
|
import { confirm, select as select2 } from "@inquirer/prompts";
|
|
3563
4092
|
var WORKSPACE_FILE = "pnpm-workspace.yaml";
|
|
3564
4093
|
var LOCK_FILE = "pnpm-lock.yaml";
|
|
3565
4094
|
var RESOLUTION_FILES = ["package.json", LOCK_FILE, ".npmrc", ".pnpmfile.cjs"];
|
|
3566
|
-
var readOr = (path, fallback = "") =>
|
|
4095
|
+
var readOr = (path, fallback = "") => existsSync9(path) ? readFileSync3(path, "utf8") : fallback;
|
|
3567
4096
|
var count2 = (n, noun, plural2 = `${noun}s`) => `${n} ${n === 1 ? noun : plural2}`;
|
|
3568
4097
|
var isBreaking = (item) => item.remedy.kind !== "none" && item.remedy.breaking;
|
|
3569
4098
|
var needsDecision = (item) => item.standing === "new" && (item.remedy.kind !== "override" || item.remedy.breaking || item.direct);
|
|
@@ -3577,8 +4106,8 @@ async function createStaging(forkPath, workspaceYaml) {
|
|
|
3577
4106
|
...projects.map((project) => join12(relative2(forkPath, project.path), "package.json"))
|
|
3578
4107
|
];
|
|
3579
4108
|
for (const file of new Set(files)) {
|
|
3580
|
-
if (file.startsWith("..") || !
|
|
3581
|
-
mkdirSync2(
|
|
4109
|
+
if (file.startsWith("..") || !existsSync9(join12(forkPath, file))) continue;
|
|
4110
|
+
mkdirSync2(dirname8(join12(staging, file)), { recursive: true });
|
|
3582
4111
|
cpSync(join12(forkPath, file), join12(staging, file));
|
|
3583
4112
|
}
|
|
3584
4113
|
return staging;
|
|
@@ -3821,7 +4350,7 @@ async function runSecurityPass(forkPath, options) {
|
|
|
3821
4350
|
return true;
|
|
3822
4351
|
} finally {
|
|
3823
4352
|
spinner.stop();
|
|
3824
|
-
|
|
4353
|
+
rmSync2(staging, { recursive: true, force: true });
|
|
3825
4354
|
}
|
|
3826
4355
|
}
|
|
3827
4356
|
function printOpen(modules) {
|
|
@@ -4023,7 +4552,7 @@ function printOutdatedResults(rows, forkPath) {
|
|
|
4023
4552
|
const dependentMap = /* @__PURE__ */ new Map();
|
|
4024
4553
|
for (const pkg of rows) {
|
|
4025
4554
|
for (const { name, location } of pkg.dependentPackages) {
|
|
4026
|
-
const dependentName = name ||
|
|
4555
|
+
const dependentName = name || basename5(location);
|
|
4027
4556
|
const existing = dependentMap.get(dependentName);
|
|
4028
4557
|
if (existing) {
|
|
4029
4558
|
existing.count++;
|
|
@@ -4219,8 +4748,8 @@ import {
|
|
|
4219
4748
|
import { select as select3 } from "@inquirer/prompts";
|
|
4220
4749
|
|
|
4221
4750
|
// src/services/contrib-core.ts
|
|
4222
|
-
import { rmSync as
|
|
4223
|
-
import { dirname as
|
|
4751
|
+
import { rmSync as rmSync3 } from "fs";
|
|
4752
|
+
import { dirname as dirname9, join as join14, resolve as resolve4 } from "path";
|
|
4224
4753
|
async function listFilesAtRef(repoPath, ref) {
|
|
4225
4754
|
const raw = await git(["ls-tree", "-r", "--name-only", ref], repoPath);
|
|
4226
4755
|
return new Set(raw.split("\n").filter(Boolean));
|
|
@@ -4236,10 +4765,10 @@ async function detectContributableFiles(repoPath, baseRef, forkRef, config, fork
|
|
|
4236
4765
|
const baseFiles = await listFilesAtRef(repoPath, baseRef);
|
|
4237
4766
|
const baseDirs = /* @__PURE__ */ new Set(["."]);
|
|
4238
4767
|
for (const f of baseFiles) {
|
|
4239
|
-
let dir =
|
|
4768
|
+
let dir = dirname9(f);
|
|
4240
4769
|
while (dir !== ".") {
|
|
4241
4770
|
baseDirs.add(dir);
|
|
4242
|
-
dir =
|
|
4771
|
+
dir = dirname9(dir);
|
|
4243
4772
|
}
|
|
4244
4773
|
}
|
|
4245
4774
|
const files = [];
|
|
@@ -4252,7 +4781,7 @@ async function detectContributableFiles(repoPath, baseRef, forkRef, config, fork
|
|
|
4252
4781
|
if (inCella && !inFork) {
|
|
4253
4782
|
kind = "deleted";
|
|
4254
4783
|
} else if (!inCella && inFork) {
|
|
4255
|
-
if (!baseDirs.has(
|
|
4784
|
+
if (!baseDirs.has(dirname9(file.path))) continue;
|
|
4256
4785
|
kind = "created";
|
|
4257
4786
|
} else {
|
|
4258
4787
|
kind = "modified";
|
|
@@ -4306,25 +4835,25 @@ ${commitBody}`;
|
|
|
4306
4835
|
await git(["update-ref", `refs/heads/${branch}`, commitHash], repoPath);
|
|
4307
4836
|
return { branch, appliedFiles };
|
|
4308
4837
|
} finally {
|
|
4309
|
-
|
|
4838
|
+
rmSync3(tmpIndex, { force: true });
|
|
4310
4839
|
}
|
|
4311
4840
|
}
|
|
4312
4841
|
|
|
4313
4842
|
// src/services/fork-utils.ts
|
|
4314
|
-
import { existsSync as
|
|
4315
|
-
import { dirname as
|
|
4843
|
+
import { existsSync as existsSync10 } from "fs";
|
|
4844
|
+
import { dirname as dirname10, resolve as resolve5 } from "path";
|
|
4316
4845
|
async function resolveForkBasePath(repoPath) {
|
|
4317
4846
|
const commonDir = await git(["rev-parse", "--git-common-dir"], repoPath, { ignoreErrors: true });
|
|
4318
|
-
return commonDir ?
|
|
4847
|
+
return commonDir ? dirname10(resolve5(repoPath, commonDir)) : repoPath;
|
|
4319
4848
|
}
|
|
4320
4849
|
function validateForkPath(fork, basePath, requireConfig = false) {
|
|
4321
4850
|
const resolvedPath = resolve5(basePath, fork.localPath);
|
|
4322
4851
|
if (!resolvedPath.startsWith(resolve5(basePath, ".."))) {
|
|
4323
4852
|
return { fork, resolvedPath, valid: false, error: "path resolves outside parent directory" };
|
|
4324
4853
|
}
|
|
4325
|
-
if (!
|
|
4326
|
-
if (!
|
|
4327
|
-
if (requireConfig && !
|
|
4854
|
+
if (!existsSync10(resolvedPath)) return { fork, resolvedPath, valid: false, error: "path does not exist" };
|
|
4855
|
+
if (!existsSync10(`${resolvedPath}/.git`)) return { fork, resolvedPath, valid: false, error: "not a git repository" };
|
|
4856
|
+
if (requireConfig && !existsSync10(`${resolvedPath}/${CONFIG_FILE}`)) {
|
|
4328
4857
|
return { fork, resolvedPath, valid: false, error: `missing ${CONFIG_FILE}` };
|
|
4329
4858
|
}
|
|
4330
4859
|
return { fork, resolvedPath, valid: true };
|
|
@@ -4630,7 +5159,7 @@ async function runContributions(config) {
|
|
|
4630
5159
|
|
|
4631
5160
|
// src/services/forks.ts
|
|
4632
5161
|
import { resolve as resolve6 } from "path";
|
|
4633
|
-
import
|
|
5162
|
+
import process10 from "process";
|
|
4634
5163
|
import { Separator as Separator2, select as select5 } from "@inquirer/prompts";
|
|
4635
5164
|
|
|
4636
5165
|
// src/services/sync.ts
|
|
@@ -4667,6 +5196,28 @@ function listOpenSyncPrs(cwd, prefix) {
|
|
|
4667
5196
|
}
|
|
4668
5197
|
return parsed.filter((pr) => pr.headRefName.startsWith(`${prefix}/`)).sort((a, b) => b.number - a.number);
|
|
4669
5198
|
}
|
|
5199
|
+
function createPr(cwd, repoSlug, pr) {
|
|
5200
|
+
const repo = repoSlug ?? "{owner}/{repo}";
|
|
5201
|
+
const fields = ["title", "head", "base", "body"];
|
|
5202
|
+
const created = runGh(cwd, [
|
|
5203
|
+
"api",
|
|
5204
|
+
`repos/${repo}/pulls`,
|
|
5205
|
+
"-X",
|
|
5206
|
+
"POST",
|
|
5207
|
+
...fields.flatMap((field) => ["-f", `${field}=${pr[field]}`]),
|
|
5208
|
+
"--jq",
|
|
5209
|
+
".html_url"
|
|
5210
|
+
]);
|
|
5211
|
+
if (created.ok) return { ...created, url: created.output || void 0 };
|
|
5212
|
+
const owner = repoSlug ? repoSlug.split("/")[0] : "{owner}";
|
|
5213
|
+
const existing = runGh(cwd, [
|
|
5214
|
+
"api",
|
|
5215
|
+
`repos/${repo}/pulls?state=open&head=${owner}:${pr.head}`,
|
|
5216
|
+
"--jq",
|
|
5217
|
+
".[0].html_url // empty"
|
|
5218
|
+
]);
|
|
5219
|
+
return { ...created, url: existing.ok && existing.output ? existing.output : void 0 };
|
|
5220
|
+
}
|
|
4670
5221
|
function mergePrSquash(cwd, ref, options = {}) {
|
|
4671
5222
|
return runGh(cwd, ["pr", "merge", String(ref), "--squash", ...options.deleteBranch ? ["--delete-branch"] : []]);
|
|
4672
5223
|
}
|
|
@@ -4675,11 +5226,11 @@ function closePr(cwd, ref) {
|
|
|
4675
5226
|
}
|
|
4676
5227
|
|
|
4677
5228
|
// src/services/packages.ts
|
|
4678
|
-
import { existsSync as
|
|
5229
|
+
import { existsSync as existsSync12, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "fs";
|
|
4679
5230
|
import { join as join16 } from "path";
|
|
4680
5231
|
|
|
4681
5232
|
// src/services/package-usage.ts
|
|
4682
|
-
import { existsSync as
|
|
5233
|
+
import { existsSync as existsSync11, readFileSync as readFileSync5, statSync } from "fs";
|
|
4683
5234
|
import { join as join15 } from "path";
|
|
4684
5235
|
var MAX_FILE_BYTES = 1024 * 1024;
|
|
4685
5236
|
function escapeRegExp(value) {
|
|
@@ -4720,7 +5271,7 @@ function binNames(forkPath, location, name) {
|
|
|
4720
5271
|
const bareName = name.split("/").pop();
|
|
4721
5272
|
for (const dir of [join15(forkPath, location), forkPath]) {
|
|
4722
5273
|
const manifest = join15(dir, "node_modules", name, "package.json");
|
|
4723
|
-
if (!
|
|
5274
|
+
if (!existsSync11(manifest)) continue;
|
|
4724
5275
|
try {
|
|
4725
5276
|
const { bin } = JSON.parse(readFileSync5(manifest, "utf8"));
|
|
4726
5277
|
if (typeof bin === "string") return [bareName];
|
|
@@ -4901,7 +5452,7 @@ function setTopLevelKey(pkg, key, value) {
|
|
|
4901
5452
|
Object.assign(pkg, Object.fromEntries(entries));
|
|
4902
5453
|
}
|
|
4903
5454
|
function readPackageJson(filePath) {
|
|
4904
|
-
if (!
|
|
5455
|
+
if (!existsSync12(filePath)) return null;
|
|
4905
5456
|
try {
|
|
4906
5457
|
return JSON.parse(readFileSync6(filePath, "utf8"));
|
|
4907
5458
|
} catch (error) {
|
|
@@ -4941,7 +5492,7 @@ async function syncPackageJson(config, baseRef, usage, relativePath, keysToSync)
|
|
|
4941
5492
|
const basePkg = baseRef ? (await readPackageJsonAtRef(forkPath, baseRef, relativePath))?.data : void 0;
|
|
4942
5493
|
if (!upstream) return { updated: false, changes, kept };
|
|
4943
5494
|
if (!forkPkg) {
|
|
4944
|
-
const workspaceArrived = relativePath !== "" &&
|
|
5495
|
+
const workspaceArrived = relativePath !== "" && existsSync12(join16(forkPath, relativePath));
|
|
4945
5496
|
if (basePkg || !workspaceArrived || isIgnored(pkgRelPath, config)) return { updated: false, changes, kept };
|
|
4946
5497
|
writeFileSync3(pkgPath, upstream.raw.endsWith("\n") ? upstream.raw : `${upstream.raw}
|
|
4947
5498
|
`, "utf8");
|
|
@@ -5147,9 +5698,43 @@ async function runSync(config, options) {
|
|
|
5147
5698
|
const staged = result.files.some((file) => SYNC_APPLIED_STATUSES.includes(file.status));
|
|
5148
5699
|
const stagedBranch = options?.stagedBranch && result.conflicts.length === 0 && staged ? options.stagedBranch : void 0;
|
|
5149
5700
|
printSyncComplete(result, { stagedBranch });
|
|
5150
|
-
await reportReviewPage(config.forkPath, result, "sync");
|
|
5151
|
-
|
|
5152
|
-
|
|
5701
|
+
const reviewPage = await reportReviewPage(config.forkPath, result, "sync");
|
|
5702
|
+
return { ...result, reviewPage };
|
|
5703
|
+
}
|
|
5704
|
+
var CONFLICTS_LISTED = 10;
|
|
5705
|
+
async function syncAlignmentsStep(config, run) {
|
|
5706
|
+
if (run?.alignments && run.upstreamCommit) {
|
|
5707
|
+
const { open, arrived } = run.alignments;
|
|
5708
|
+
return alignmentsStep(config, { ref: run.upstreamCommit.hash, open, arrived });
|
|
5709
|
+
}
|
|
5710
|
+
const view = await readOpenAlignments(config);
|
|
5711
|
+
return view ? alignmentsStep(config, view) : void 0;
|
|
5712
|
+
}
|
|
5713
|
+
function syncReviewStep(config, run) {
|
|
5714
|
+
const page = run ? run.reviewPage : existingSyncReviewPage(config.forkPath);
|
|
5715
|
+
return page ? reviewStep(page) : void 0;
|
|
5716
|
+
}
|
|
5717
|
+
async function conflictSteps(config, conflicts, run) {
|
|
5718
|
+
const listed = conflicts.slice(0, CONFLICTS_LISTED).map((file) => colors_default.dim(file));
|
|
5719
|
+
if (conflicts.length > CONFLICTS_LISTED) listed.push(colors_default.dim(`+ ${conflicts.length - CONFLICTS_LISTED} more`));
|
|
5720
|
+
const steps = [
|
|
5721
|
+
{
|
|
5722
|
+
label: "resolve",
|
|
5723
|
+
line: `${plural(conflicts.length, "conflict")} in your editor, then stage them`,
|
|
5724
|
+
details: listed
|
|
5725
|
+
},
|
|
5726
|
+
{
|
|
5727
|
+
label: "finish",
|
|
5728
|
+
line: "pnpm cella sync",
|
|
5729
|
+
details: [
|
|
5730
|
+
colors_default.dim("commits the sync and stops for drift triage"),
|
|
5731
|
+
colors_default.dim("let the rerun commit: a manual `git commit` records a merge commit that bloats the PR")
|
|
5732
|
+
]
|
|
5733
|
+
},
|
|
5734
|
+
syncReviewStep(config, run),
|
|
5735
|
+
await syncAlignmentsStep(config, run)
|
|
5736
|
+
];
|
|
5737
|
+
return steps.filter((step) => !!step);
|
|
5153
5738
|
}
|
|
5154
5739
|
var SYNC_PR_TITLE = "chore: sync upstream cella";
|
|
5155
5740
|
async function buildSyncCommitMessage(forkPath, upstreamRef) {
|
|
@@ -5164,7 +5749,7 @@ function qualifyPrRefs(subject, repoSlug) {
|
|
|
5164
5749
|
return subject.replace(/(^|[^\w/])#(\d+)\b/g, `$1${repoSlug}#$2`);
|
|
5165
5750
|
}
|
|
5166
5751
|
function buildSyncPrBody(input) {
|
|
5167
|
-
const { repoSlug, version, fromSha, toSha, commits, totalCount,
|
|
5752
|
+
const { repoSlug, version, fromSha, toSha, commits, totalCount, alignments } = input;
|
|
5168
5753
|
const githubUrl = repoSlug ? `https://github.com/${repoSlug}` : void 0;
|
|
5169
5754
|
const short = (sha) => sha.slice(0, 7);
|
|
5170
5755
|
const commitRef = (sha) => githubUrl ? `[\`${short(sha)}\`](${githubUrl}/commit/${sha})` : `\`${short(sha)}\``;
|
|
@@ -5179,17 +5764,19 @@ function buildSyncPrBody(input) {
|
|
|
5179
5764
|
lines.push(`- ${commitRef(commit.hash)} ${qualifyPrRefs(commit.message, repoSlug)}`);
|
|
5180
5765
|
}
|
|
5181
5766
|
}
|
|
5182
|
-
if (
|
|
5183
|
-
const { total, open } =
|
|
5184
|
-
if (open.length === 0) lines.push("", `**
|
|
5767
|
+
if (alignments && alignments.total > 0) {
|
|
5768
|
+
const { total, open } = alignments;
|
|
5769
|
+
if (open.length === 0) lines.push("", `**Alignments**: all ${total} handled.`);
|
|
5185
5770
|
else {
|
|
5186
5771
|
lines.push(
|
|
5187
5772
|
"",
|
|
5188
|
-
`**
|
|
5773
|
+
`**Alignments**: ${Math.max(0, total - open.length)} of ${total} handled, open (\`pnpm cella align\`):`,
|
|
5189
5774
|
""
|
|
5190
5775
|
);
|
|
5191
|
-
for (const
|
|
5192
|
-
lines.push(
|
|
5776
|
+
for (const alignment of open)
|
|
5777
|
+
lines.push(
|
|
5778
|
+
`- ${alignment.url ? `[${alignment.title}](${alignment.url})` : alignment.title} (\`${alignment.id}\`)`
|
|
5779
|
+
);
|
|
5193
5780
|
}
|
|
5194
5781
|
}
|
|
5195
5782
|
return `${lines.join("\n")}
|
|
@@ -5203,17 +5790,25 @@ async function buildSyncPrBodyForBranch(forkPath, base) {
|
|
|
5203
5790
|
const version = manifest.upstream.release ?? await readPackageVersionAtRef(forkPath, toSha).catch(() => null);
|
|
5204
5791
|
const totalCount = fromSha ? await countCommitsBetween(forkPath, fromSha, toSha).catch(() => 0) : 0;
|
|
5205
5792
|
const commits = fromSha && totalCount > 0 ? await listCommitsBetween(forkPath, fromSha, toSha, COMMIT_LIST_MAX) : [];
|
|
5206
|
-
const
|
|
5207
|
-
return buildSyncPrBody({
|
|
5793
|
+
const alignments = await readPrBodyAlignments(forkPath, toSha, manifest.upstream.repo).catch(() => void 0);
|
|
5794
|
+
return buildSyncPrBody({
|
|
5795
|
+
repoSlug: manifest.upstream.repo,
|
|
5796
|
+
version,
|
|
5797
|
+
fromSha,
|
|
5798
|
+
toSha,
|
|
5799
|
+
commits,
|
|
5800
|
+
totalCount,
|
|
5801
|
+
alignments
|
|
5802
|
+
});
|
|
5208
5803
|
}
|
|
5209
|
-
async function
|
|
5804
|
+
async function readPrBodyAlignments(forkPath, toSha, repoSlug) {
|
|
5210
5805
|
const githubUrl = repoSlug ? `https://github.com/${repoSlug}` : void 0;
|
|
5211
|
-
const [ids, pending] = await Promise.all([
|
|
5806
|
+
const [ids, pending] = await Promise.all([listAlignmentIds(forkPath, toSha), readPending(forkPath)]);
|
|
5212
5807
|
const open = await Promise.all(
|
|
5213
5808
|
pending.map(async (id) => ({
|
|
5214
5809
|
id,
|
|
5215
|
-
title: (await
|
|
5216
|
-
url:
|
|
5810
|
+
title: (await readAlignment(forkPath, toSha, id))?.title ?? id,
|
|
5811
|
+
url: alignmentUrl(githubUrl, toSha, id)
|
|
5217
5812
|
}))
|
|
5218
5813
|
);
|
|
5219
5814
|
return { total: ids.length, open };
|
|
@@ -5254,16 +5849,11 @@ async function shipSyncBranch(config, branch) {
|
|
|
5254
5849
|
}
|
|
5255
5850
|
if (ghAvailable()) {
|
|
5256
5851
|
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;
|
|
5852
|
+
const prBody = await buildSyncPrBodyForBranch(forkPath, cutBase) ?? "";
|
|
5853
|
+
const repoSlug = (await readOriginGitHubUrl(forkPath))?.replace("https://github.com/", "") ?? null;
|
|
5854
|
+
const pr = createPr(forkPath, repoSlug, { base, head: branch, title: prTitle, body: prBody });
|
|
5855
|
+
prUrl = pr.url;
|
|
5856
|
+
prOpened = pr.ok;
|
|
5267
5857
|
if (!prOpened) {
|
|
5268
5858
|
console.info(colors_default.yellow("could not open the PR automatically (it may already exist). open it with:"));
|
|
5269
5859
|
if (prUrl) console.info(colors_default.dim(` ${prUrl}`));
|
|
@@ -5282,7 +5872,15 @@ ${pr.stderr ?? ""}`.match(/https?:\/\/\S+/)?.[0];
|
|
|
5282
5872
|
console.info(`${checkMark} sync branch pushed`);
|
|
5283
5873
|
console.info(colors_default.dim(` '${branch}' is on origin, ${position}`));
|
|
5284
5874
|
}
|
|
5285
|
-
|
|
5875
|
+
const steps = [
|
|
5876
|
+
prUrl ? {
|
|
5877
|
+
label: "merge",
|
|
5878
|
+
line: hyperlink(colors_default.cyan(prUrl), prUrl),
|
|
5879
|
+
details: [colors_default.dim(`squash-merge it into '${base}'; the next sync offers to while it is open`)]
|
|
5880
|
+
} : void 0,
|
|
5881
|
+
await syncAlignmentsStep(config)
|
|
5882
|
+
];
|
|
5883
|
+
printNextSteps(steps.filter((step) => !!step));
|
|
5286
5884
|
}
|
|
5287
5885
|
function finalizeWorkspace(forkPath) {
|
|
5288
5886
|
for (const args of [["install"], ["check"]]) {
|
|
@@ -5321,40 +5919,49 @@ async function runSyncCycle(config) {
|
|
|
5321
5919
|
if (config.settings.syncWithPackages !== false) {
|
|
5322
5920
|
await runPackages(config, { conflictedFiles: result.conflicts });
|
|
5323
5921
|
}
|
|
5324
|
-
if (result.conflicts.length > 0) return { status: "conflicts", branch };
|
|
5922
|
+
if (result.conflicts.length > 0) return { status: "conflicts", branch, result };
|
|
5325
5923
|
if (!await mergeInProgress(forkPath)) {
|
|
5326
5924
|
await discardTemporarySyncBranch(forkPath, branch);
|
|
5327
|
-
return { status: "noop" };
|
|
5925
|
+
return { status: "noop", result };
|
|
5328
5926
|
}
|
|
5329
|
-
return { status: "staged", branch };
|
|
5927
|
+
return { status: "staged", branch, result };
|
|
5330
5928
|
}
|
|
5331
|
-
async function commitSyncMerge(config, branch) {
|
|
5929
|
+
async function commitSyncMerge(config, branch, run) {
|
|
5332
5930
|
const { forkPath } = config;
|
|
5333
5931
|
const conflicts = await getConflictedFiles(forkPath);
|
|
5334
5932
|
console.info();
|
|
5335
5933
|
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)."));
|
|
5934
|
+
printStep("conflicts", `${plural(conflicts.length, "file")} still conflict on '${branch}'`, warningMark);
|
|
5935
|
+
printNextSteps(await conflictSteps(config, conflicts, run));
|
|
5339
5936
|
return;
|
|
5340
5937
|
}
|
|
5341
5938
|
await stageAll(forkPath);
|
|
5342
5939
|
if (!finalizeWorkspace(forkPath)) {
|
|
5343
5940
|
console.info();
|
|
5344
|
-
|
|
5345
|
-
|
|
5346
|
-
|
|
5941
|
+
printStep("checks", "`pnpm install` or `pnpm check` failed", warningMark);
|
|
5942
|
+
printNextSteps([
|
|
5943
|
+
{ label: "fix", line: "what failed above", details: [colors_default.dim(`the merge stays staged on '${branch}'`)] },
|
|
5944
|
+
{ label: "finish", line: "pnpm cella sync", details: [colors_default.dim("commits the sync and stops for drift triage")] }
|
|
5945
|
+
]);
|
|
5347
5946
|
return;
|
|
5348
5947
|
}
|
|
5349
5948
|
const message = await buildSyncCommitMessage(forkPath, "MERGE_HEAD");
|
|
5350
5949
|
await stageAll(forkPath);
|
|
5351
5950
|
await commitSquash(forkPath, message);
|
|
5352
5951
|
console.info();
|
|
5353
|
-
|
|
5354
|
-
|
|
5355
|
-
|
|
5356
|
-
|
|
5357
|
-
|
|
5952
|
+
printStep("committed", `${message}
|
|
5953
|
+
${colors_default.dim(`on '${branch}', not pushed`)}`);
|
|
5954
|
+
const steps = [
|
|
5955
|
+
syncReviewStep(config, run),
|
|
5956
|
+
{
|
|
5957
|
+
label: "triage",
|
|
5958
|
+
line: "pnpm cella analyze",
|
|
5959
|
+
details: [colors_default.dim("drift triage: commit any follow-ups on this branch")]
|
|
5960
|
+
},
|
|
5961
|
+
await syncAlignmentsStep(config, run),
|
|
5962
|
+
{ label: "ship", line: "pnpm cella sync", details: [colors_default.dim("pushes the branch and opens the squash-merge PR")] }
|
|
5963
|
+
];
|
|
5964
|
+
printNextSteps(steps.filter((step) => !!step));
|
|
5358
5965
|
}
|
|
5359
5966
|
async function mergeOpenSyncPrs(config, open) {
|
|
5360
5967
|
const { forkPath, settings } = config;
|
|
@@ -5438,25 +6045,26 @@ commit them first (\`git commit --amend --no-edit\` or a new commit).`
|
|
|
5438
6045
|
}
|
|
5439
6046
|
if (await guardAgainstOpenSyncPr(config) === "cancel") return;
|
|
5440
6047
|
const outcome = await runSyncCycle(config);
|
|
5441
|
-
console.info();
|
|
5442
6048
|
if (outcome.status === "noop") {
|
|
6049
|
+
console.info();
|
|
5443
6050
|
if (outcome.behind) {
|
|
5444
6051
|
console.info(colors_default.green("nothing to sync: the last sync already went past this upstream point."));
|
|
5445
6052
|
for (const line of outcome.behind.split("\n")) console.info(colors_default.dim(` ${line}`));
|
|
5446
6053
|
} else {
|
|
5447
6054
|
console.info(colors_default.green("already up to date with upstream, nothing to sync."));
|
|
5448
6055
|
}
|
|
6056
|
+
const align = await syncAlignmentsStep(config, outcome.result);
|
|
6057
|
+
if (align) printNextSteps([align]);
|
|
5449
6058
|
return;
|
|
5450
6059
|
}
|
|
5451
6060
|
const { temporaryBranch } = outcome.branch;
|
|
5452
6061
|
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."));
|
|
6062
|
+
const conflicts = await getConflictedFiles(forkPath);
|
|
6063
|
+
printStep("conflicts", `${plural(conflicts.length, "file")} on '${temporaryBranch}'`, warningMark);
|
|
6064
|
+
printNextSteps(await conflictSteps(config, conflicts, outcome.result));
|
|
5457
6065
|
return;
|
|
5458
6066
|
}
|
|
5459
|
-
await commitSyncMerge(config, temporaryBranch);
|
|
6067
|
+
await commitSyncMerge(config, temporaryBranch, outcome.result);
|
|
5460
6068
|
}
|
|
5461
6069
|
|
|
5462
6070
|
// src/services/forks.ts
|
|
@@ -5553,7 +6161,7 @@ async function runForks(config) {
|
|
|
5553
6161
|
loop: false
|
|
5554
6162
|
});
|
|
5555
6163
|
if (selectedPath === "_exit") {
|
|
5556
|
-
|
|
6164
|
+
process10.exit(0);
|
|
5557
6165
|
}
|
|
5558
6166
|
const resolvedForkPath = resolve6(forkBasePath, selectedPath);
|
|
5559
6167
|
const selectedFork = forks.find((f) => f.localPath === selectedPath);
|
|
@@ -5571,7 +6179,7 @@ import { readFile as readFile3 } from "fs/promises";
|
|
|
5571
6179
|
import { join as join18 } from "path";
|
|
5572
6180
|
|
|
5573
6181
|
// src/utils/coverage-utils.ts
|
|
5574
|
-
import { existsSync as
|
|
6182
|
+
import { existsSync as existsSync13, readFileSync as readFileSync7, statSync as statSync2 } from "fs";
|
|
5575
6183
|
import { join as join17, relative as relative4, sep as sep3 } from "path";
|
|
5576
6184
|
var STALE_THRESHOLD_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
5577
6185
|
function colorPct(pct) {
|
|
@@ -5594,7 +6202,7 @@ function printCoverageSummary(forkPath) {
|
|
|
5594
6202
|
let summary;
|
|
5595
6203
|
let mtimeMs = 0;
|
|
5596
6204
|
let problem = "no coverage data found";
|
|
5597
|
-
if (
|
|
6205
|
+
if (existsSync13(file)) {
|
|
5598
6206
|
try {
|
|
5599
6207
|
mtimeMs = statSync2(file).mtimeMs;
|
|
5600
6208
|
summary = JSON.parse(readFileSync7(file, "utf8"));
|
|
@@ -5994,26 +6602,26 @@ function getForkPath() {
|
|
|
5994
6602
|
const envPath = getEnv("CELLA_FORK_PATH");
|
|
5995
6603
|
if (envPath) {
|
|
5996
6604
|
const resolved = resolve7(envPath);
|
|
5997
|
-
if (!
|
|
6605
|
+
if (!existsSync14(resolved) || !statSync3(resolved).isDirectory()) {
|
|
5998
6606
|
throw new Error(`CELLA_FORK_PATH is not a valid directory: ${resolved}`);
|
|
5999
6607
|
}
|
|
6000
6608
|
return resolved;
|
|
6001
6609
|
}
|
|
6002
|
-
let cwd =
|
|
6610
|
+
let cwd = process11.cwd();
|
|
6003
6611
|
if (cwd.endsWith("/cli/cella") || cwd.endsWith("\\cli\\cella")) {
|
|
6004
6612
|
cwd = resolve7(cwd, "../..");
|
|
6005
6613
|
}
|
|
6006
6614
|
return cwd;
|
|
6007
6615
|
}
|
|
6008
6616
|
function preflight(forkPath) {
|
|
6009
|
-
if (!
|
|
6617
|
+
if (!existsSync14(join19(forkPath, ".git"))) {
|
|
6010
6618
|
throw new Error(`not a git repository: ${forkPath}`);
|
|
6011
6619
|
}
|
|
6012
6620
|
}
|
|
6013
6621
|
var services = {
|
|
6014
6622
|
analyze: runAnalyze,
|
|
6015
6623
|
sync: runSyncCommand,
|
|
6016
|
-
|
|
6624
|
+
align: runAlign,
|
|
6017
6625
|
audit: runAudit,
|
|
6018
6626
|
forks: runForks,
|
|
6019
6627
|
contributions: runContributions,
|
|
@@ -6024,7 +6632,7 @@ var services = {
|
|
|
6024
6632
|
}
|
|
6025
6633
|
};
|
|
6026
6634
|
function isHelpOrVersionRequest() {
|
|
6027
|
-
return
|
|
6635
|
+
return process11.argv.slice(2).some((arg) => arg === "--help" || arg === "-h" || arg === "--version" || arg === "-v") || process11.argv[2] === "help";
|
|
6028
6636
|
}
|
|
6029
6637
|
async function main() {
|
|
6030
6638
|
try {
|
|
@@ -6043,7 +6651,7 @@ async function main() {
|
|
|
6043
6651
|
} catch (error) {
|
|
6044
6652
|
console.error();
|
|
6045
6653
|
console.error(`${colors_default.red("\u2717")} ${errorMessage(error)}`);
|
|
6046
|
-
|
|
6654
|
+
process11.exit(1);
|
|
6047
6655
|
}
|
|
6048
6656
|
}
|
|
6049
6657
|
main();
|