@cellajs/cli 0.0.3 → 0.0.5
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/dist/config.js.map +1 -1
- package/dist/index.js +223 -93
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env tsx
|
|
2
2
|
|
|
3
3
|
// src/cella-cli.ts
|
|
4
|
-
import { existsSync as
|
|
5
|
-
import { join as
|
|
4
|
+
import { existsSync as existsSync10, statSync as statSync2 } from "fs";
|
|
5
|
+
import { join as join17, resolve as resolve5 } from "path";
|
|
6
6
|
import process9 from "process";
|
|
7
7
|
|
|
8
8
|
// src/cli.ts
|
|
9
9
|
import { readFileSync } from "fs";
|
|
10
|
-
import { join as
|
|
10
|
+
import { join as join5 } from "path";
|
|
11
11
|
import process5 from "process";
|
|
12
12
|
import { select } from "@inquirer/prompts";
|
|
13
13
|
import { Command } from "commander";
|
|
@@ -53,8 +53,8 @@ var pc = {
|
|
|
53
53
|
var colors_default = pc;
|
|
54
54
|
|
|
55
55
|
// src/utils/config.ts
|
|
56
|
-
import { existsSync, realpathSync } from "fs";
|
|
57
|
-
import { join, resolve } from "path";
|
|
56
|
+
import { existsSync as existsSync2, realpathSync } from "fs";
|
|
57
|
+
import { join as join2, resolve } from "path";
|
|
58
58
|
import { z as z2 } from "zod";
|
|
59
59
|
|
|
60
60
|
// src/config/types.ts
|
|
@@ -96,9 +96,11 @@ var cellaConfigSchema = z.object({
|
|
|
96
96
|
}).strict();
|
|
97
97
|
|
|
98
98
|
// src/utils/module-territory.ts
|
|
99
|
-
import {
|
|
99
|
+
import { existsSync } from "fs";
|
|
100
|
+
import { basename, join, relative, sep } from "path";
|
|
100
101
|
import { Project, SyntaxKind } from "ts-morph";
|
|
101
102
|
var MODULE_FILE_GLOBS = ["backend/src/modules/*/*-module.ts", "frontend/src/modules/*/*-module.ts"];
|
|
103
|
+
var FRONTEND_STATIC_ROOT = "frontend/public/static";
|
|
102
104
|
function readOwner(call) {
|
|
103
105
|
const arg = call.getArguments()[0]?.asKind(SyntaxKind.ObjectLiteralExpression);
|
|
104
106
|
const initializer = arg?.getProperty("owner")?.asKind(SyntaxKind.PropertyAssignment)?.getInitializer();
|
|
@@ -112,11 +114,19 @@ function resolveAppModuleFolders(repoPath) {
|
|
|
112
114
|
});
|
|
113
115
|
const sourceFiles = project.addSourceFilesAtPaths(MODULE_FILE_GLOBS.map((glob) => `${repoPath}/${glob}`));
|
|
114
116
|
const folders = /* @__PURE__ */ new Set();
|
|
117
|
+
const moduleNames = /* @__PURE__ */ new Set();
|
|
115
118
|
for (const sourceFile of sourceFiles) {
|
|
116
119
|
const ownsApp = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression).some((call) => call.getExpression().getText() === "registerModule" && readOwner(call) === "app");
|
|
117
120
|
if (!ownsApp) continue;
|
|
118
121
|
const folder = relative(repoPath, sourceFile.getDirectoryPath()).split(sep).join("/");
|
|
119
122
|
folders.add(folder);
|
|
123
|
+
moduleNames.add(basename(folder));
|
|
124
|
+
}
|
|
125
|
+
for (const moduleName of moduleNames) {
|
|
126
|
+
const staticFolder = `${FRONTEND_STATIC_ROOT}/${moduleName}`;
|
|
127
|
+
if (existsSync(join(repoPath, staticFolder))) {
|
|
128
|
+
folders.add(staticFolder);
|
|
129
|
+
}
|
|
120
130
|
}
|
|
121
131
|
return [...folders];
|
|
122
132
|
}
|
|
@@ -145,8 +155,8 @@ function resolveUpstream(settings) {
|
|
|
145
155
|
return { track, branchRef };
|
|
146
156
|
}
|
|
147
157
|
async function loadConfig(forkPath) {
|
|
148
|
-
const configPath =
|
|
149
|
-
if (!
|
|
158
|
+
const configPath = join2(forkPath, "cella.config.ts");
|
|
159
|
+
if (!existsSync2(configPath)) {
|
|
150
160
|
throw new Error(`config file not found: ${configPath}`);
|
|
151
161
|
}
|
|
152
162
|
const realConfigPath = realpathSync(configPath);
|
|
@@ -180,13 +190,13 @@ ${z2.prettifyError(result.error)}`);
|
|
|
180
190
|
|
|
181
191
|
// src/utils/display.ts
|
|
182
192
|
import { writeFileSync } from "fs";
|
|
183
|
-
import { join as
|
|
193
|
+
import { join as join3, resolve as resolve2 } from "path";
|
|
184
194
|
import process4 from "process";
|
|
185
195
|
|
|
186
196
|
// package.json
|
|
187
197
|
var package_default = {
|
|
188
198
|
name: "@cellajs/cli",
|
|
189
|
-
version: "0.0.
|
|
199
|
+
version: "0.0.5",
|
|
190
200
|
private: false,
|
|
191
201
|
license: "MIT",
|
|
192
202
|
description: "CLI for managing your monorepo and syncing with upstream Cella.",
|
|
@@ -505,7 +515,7 @@ var summaryStatusConfig = {
|
|
|
505
515
|
icon: colors_default.cyan("\u25C7"),
|
|
506
516
|
label: "managed",
|
|
507
517
|
color: colors_default.cyan,
|
|
508
|
-
description: "package
|
|
518
|
+
description: "package.jsons & cella.config.ts handled separately"
|
|
509
519
|
},
|
|
510
520
|
ignored: { ...statusConfig.ignored, description: "protected by ignored config" }
|
|
511
521
|
};
|
|
@@ -593,7 +603,7 @@ function printAnalysisFileGroups(files, linkOptions) {
|
|
|
593
603
|
});
|
|
594
604
|
}
|
|
595
605
|
function writeLogFile(forkPath, files) {
|
|
596
|
-
const logPath =
|
|
606
|
+
const logPath = join3(forkPath, "cella-sync.log");
|
|
597
607
|
const lines = [
|
|
598
608
|
`cella sync analysis - ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
599
609
|
DIVIDER,
|
|
@@ -651,8 +661,8 @@ function printFlagWarnings(options) {
|
|
|
651
661
|
}
|
|
652
662
|
|
|
653
663
|
// src/utils/overrides.ts
|
|
654
|
-
import { existsSync as
|
|
655
|
-
import { join as
|
|
664
|
+
import { existsSync as existsSync3 } from "fs";
|
|
665
|
+
import { join as join4 } from "path";
|
|
656
666
|
function isUnderAnyFolder(filePath, folders) {
|
|
657
667
|
return folders.some((folder) => {
|
|
658
668
|
const entry = folder.replace(/\/+$/, "");
|
|
@@ -684,7 +694,7 @@ function validateOverrides(config, forkPath) {
|
|
|
684
694
|
pattern: entry,
|
|
685
695
|
message: `pin uses glob (not supported, use a path or folder): ${entry}`
|
|
686
696
|
});
|
|
687
|
-
} else if (!
|
|
697
|
+
} else if (!existsSync3(join4(forkPath, entry))) {
|
|
688
698
|
warnings.push({
|
|
689
699
|
type: "pinned-not-found",
|
|
690
700
|
pattern: entry,
|
|
@@ -700,7 +710,7 @@ function validateOverrides(config, forkPath) {
|
|
|
700
710
|
pattern: entry,
|
|
701
711
|
message: `ignored entry uses glob (not supported, use a path or folder): ${entry}`
|
|
702
712
|
});
|
|
703
|
-
} else if (!
|
|
713
|
+
} else if (!existsSync3(join4(forkPath, entry.replace(/\/+$/, "")))) {
|
|
704
714
|
warnings.push({
|
|
705
715
|
type: "ignored-not-found",
|
|
706
716
|
pattern: entry,
|
|
@@ -810,7 +820,7 @@ var serviceDefinitions = [
|
|
|
810
820
|
async function getMenuContext(userConfig, forkPath) {
|
|
811
821
|
return {
|
|
812
822
|
hasForks: (userConfig.forks?.length ?? 0) > 0,
|
|
813
|
-
isUpstreamRepo: JSON.parse(readFileSync(
|
|
823
|
+
isUpstreamRepo: JSON.parse(readFileSync(join5(forkPath, "package.json"), "utf8")).name === "cella"
|
|
814
824
|
};
|
|
815
825
|
}
|
|
816
826
|
function buildServiceChoices(context) {
|
|
@@ -909,13 +919,13 @@ async function parseCli(userConfig, forkPath) {
|
|
|
909
919
|
}
|
|
910
920
|
|
|
911
921
|
// src/services/analyze.ts
|
|
912
|
-
import { basename as
|
|
922
|
+
import { basename as basename4 } from "path";
|
|
913
923
|
|
|
914
924
|
// src/utils/diff.ts
|
|
915
925
|
import { execFileSync } from "child_process";
|
|
916
926
|
import { mkdirSync, writeFileSync as writeFileSync2 } from "fs";
|
|
917
927
|
import { tmpdir } from "os";
|
|
918
|
-
import { basename, join as
|
|
928
|
+
import { basename as basename2, join as join6 } from "path";
|
|
919
929
|
import process6 from "process";
|
|
920
930
|
function gitDiffFile(cwd, range, filePath, options = {}) {
|
|
921
931
|
const args = ["diff", "--no-color"];
|
|
@@ -1009,34 +1019,34 @@ function openWithDefaultApp(target) {
|
|
|
1009
1019
|
}
|
|
1010
1020
|
async function openDiffInBrowser(patch, meta, repoPath) {
|
|
1011
1021
|
const html = await renderDiffPage(patch, meta);
|
|
1012
|
-
const dir =
|
|
1022
|
+
const dir = join6(tmpdir(), `cella-diff-${basename2(repoPath)}`);
|
|
1013
1023
|
mkdirSync(dir, { recursive: true });
|
|
1014
|
-
const outPath =
|
|
1024
|
+
const outPath = join6(dir, `${meta.filePath.replace(/[^\w.-]+/g, "-")}.html`);
|
|
1015
1025
|
writeFileSync2(outPath, html);
|
|
1016
1026
|
openWithDefaultApp(outPath);
|
|
1017
1027
|
return outPath;
|
|
1018
1028
|
}
|
|
1019
1029
|
|
|
1020
1030
|
// src/utils/cleanup.ts
|
|
1021
|
-
import { existsSync as
|
|
1031
|
+
import { existsSync as existsSync5, rmSync } from "fs";
|
|
1022
1032
|
import { tmpdir as tmpdir2 } from "os";
|
|
1023
|
-
import { basename as
|
|
1033
|
+
import { basename as basename3, join as join9 } from "path";
|
|
1024
1034
|
import process7 from "process";
|
|
1025
1035
|
|
|
1026
1036
|
// src/utils/git.ts
|
|
1027
1037
|
import { execFile } from "child_process";
|
|
1028
|
-
import { existsSync as
|
|
1038
|
+
import { existsSync as existsSync4 } from "fs";
|
|
1029
1039
|
import { mkdir, readdir, rmdir, unlink } from "fs/promises";
|
|
1030
|
-
import { dirname, join as
|
|
1040
|
+
import { dirname, join as join8 } from "path";
|
|
1031
1041
|
import { promisify } from "util";
|
|
1032
1042
|
|
|
1033
1043
|
// src/utils/manifest.ts
|
|
1034
1044
|
import { readFile, writeFile } from "fs/promises";
|
|
1035
|
-
import { join as
|
|
1045
|
+
import { join as join7 } from "path";
|
|
1036
1046
|
var MANIFEST_FILE = "cella.manifest.json";
|
|
1037
1047
|
async function readSyncManifest(cwd) {
|
|
1038
1048
|
try {
|
|
1039
|
-
const parsed = JSON.parse(await readFile(
|
|
1049
|
+
const parsed = JSON.parse(await readFile(join7(cwd, MANIFEST_FILE), "utf8"));
|
|
1040
1050
|
const commit = parsed?.upstream?.commit;
|
|
1041
1051
|
if (typeof commit !== "string" || !/^[0-9a-f]{40}$/i.test(commit)) return null;
|
|
1042
1052
|
return parsed;
|
|
@@ -1049,7 +1059,7 @@ async function readManifestBase(cwd) {
|
|
|
1049
1059
|
return manifest ? manifest.upstream.commit.toLowerCase() : null;
|
|
1050
1060
|
}
|
|
1051
1061
|
async function writeSyncManifest(cwd, manifest) {
|
|
1052
|
-
await writeFile(
|
|
1062
|
+
await writeFile(join7(cwd, MANIFEST_FILE), `${JSON.stringify(manifest, null, 2)}
|
|
1053
1063
|
`, "utf8");
|
|
1054
1064
|
}
|
|
1055
1065
|
|
|
@@ -1120,8 +1130,8 @@ async function flattenBranch(cwd, baseRef, message) {
|
|
|
1120
1130
|
}
|
|
1121
1131
|
async function commitSquash(cwd, message) {
|
|
1122
1132
|
for (const file of ["MERGE_HEAD", "MERGE_MSG", "MERGE_MODE"]) {
|
|
1123
|
-
const path4 =
|
|
1124
|
-
if (
|
|
1133
|
+
const path4 = join8(cwd, ".git", file);
|
|
1134
|
+
if (existsSync4(path4)) await unlink(path4);
|
|
1125
1135
|
}
|
|
1126
1136
|
await git(["commit", "-m", message], cwd, { skipEditor: true });
|
|
1127
1137
|
}
|
|
@@ -1129,7 +1139,7 @@ async function stageAll(cwd) {
|
|
|
1129
1139
|
await git(["add", "-A"], cwd);
|
|
1130
1140
|
}
|
|
1131
1141
|
function mergeInProgress(cwd) {
|
|
1132
|
-
return
|
|
1142
|
+
return existsSync4(join8(cwd, ".git", "MERGE_HEAD"));
|
|
1133
1143
|
}
|
|
1134
1144
|
async function getShortSha(cwd, ref) {
|
|
1135
1145
|
return git(["rev-parse", "--short", ref], cwd);
|
|
@@ -1206,21 +1216,23 @@ async function listCommitsBetween(cwd, fromRef, toRef, options = {}) {
|
|
|
1206
1216
|
async function getFileChangeInfo(cwd, fromRef, toRef) {
|
|
1207
1217
|
const info = /* @__PURE__ */ new Map();
|
|
1208
1218
|
try {
|
|
1209
|
-
const output = await git(["log", "--name-only", "--format=%h %ar", `${fromRef}..${toRef}`], cwd, {
|
|
1219
|
+
const output = await git(["log", "--name-only", "--format=%h %ct %ar", `${fromRef}..${toRef}`], cwd, {
|
|
1210
1220
|
ignoreErrors: true
|
|
1211
1221
|
});
|
|
1212
1222
|
if (!output.trim()) return info;
|
|
1213
1223
|
const lines = output.split("\n");
|
|
1214
1224
|
let currentHash = "";
|
|
1215
1225
|
let currentDate = "";
|
|
1226
|
+
let currentTimestamp = 0;
|
|
1216
1227
|
for (const line of lines) {
|
|
1217
1228
|
if (!line) continue;
|
|
1218
|
-
const match = line.match(/^([a-f0-9]{7,})\s+(.+)$/);
|
|
1229
|
+
const match = line.match(/^([a-f0-9]{7,})\s+(\d+)\s+(.+)$/);
|
|
1219
1230
|
if (match) {
|
|
1220
1231
|
currentHash = match[1];
|
|
1221
|
-
|
|
1232
|
+
currentTimestamp = Number(match[2]);
|
|
1233
|
+
currentDate = match[3];
|
|
1222
1234
|
} else if (currentHash && currentDate && !info.has(line)) {
|
|
1223
|
-
info.set(line, { date: currentDate, hash: currentHash });
|
|
1235
|
+
info.set(line, { date: currentDate, hash: currentHash, timestamp: currentTimestamp });
|
|
1224
1236
|
}
|
|
1225
1237
|
}
|
|
1226
1238
|
} catch {
|
|
@@ -1359,15 +1371,15 @@ async function batchUnstageFiles(cwd, filePaths) {
|
|
|
1359
1371
|
await git(["rm", "--cached", "-f", "--", ...filePaths], cwd, { ignoreErrors: true });
|
|
1360
1372
|
}
|
|
1361
1373
|
async function gitMv(cwd, oldPath, newPath) {
|
|
1362
|
-
const newDir = dirname(
|
|
1374
|
+
const newDir = dirname(join8(cwd, newPath));
|
|
1363
1375
|
await mkdir(newDir, { recursive: true });
|
|
1364
1376
|
await git(["mv", "-f", oldPath, newPath], cwd);
|
|
1365
1377
|
}
|
|
1366
1378
|
async function fileExistsInWorktree(cwd, filePath) {
|
|
1367
|
-
return
|
|
1379
|
+
return existsSync4(join8(cwd, filePath));
|
|
1368
1380
|
}
|
|
1369
1381
|
async function removeFileFromWorktree(cwd, filePath) {
|
|
1370
|
-
const fullPath =
|
|
1382
|
+
const fullPath = join8(cwd, filePath);
|
|
1371
1383
|
try {
|
|
1372
1384
|
await unlink(fullPath);
|
|
1373
1385
|
await cleanupEmptyParentDirs(cwd, dirname(filePath));
|
|
@@ -1380,7 +1392,7 @@ async function removeFileFully(cwd, filePath) {
|
|
|
1380
1392
|
}
|
|
1381
1393
|
async function cleanupEmptyParentDirs(cwd, relativePath) {
|
|
1382
1394
|
if (!relativePath || relativePath === ".") return;
|
|
1383
|
-
const fullPath =
|
|
1395
|
+
const fullPath = join8(cwd, relativePath);
|
|
1384
1396
|
try {
|
|
1385
1397
|
const entries = await readdir(fullPath);
|
|
1386
1398
|
if (entries.length === 0) {
|
|
@@ -1408,14 +1420,28 @@ async function getStoredSyncHead(cwd) {
|
|
|
1408
1420
|
const ref = await git(["rev-parse", "refs/cella/last-sync-head"], cwd, { ignoreErrors: true });
|
|
1409
1421
|
return ref || null;
|
|
1410
1422
|
}
|
|
1411
|
-
async function
|
|
1423
|
+
async function readManifestAtRef(cwd, ref) {
|
|
1412
1424
|
const raw = await git(["show", `${ref}:${MANIFEST_FILE}`], cwd, { ignoreErrors: true });
|
|
1413
1425
|
if (!raw) return null;
|
|
1414
1426
|
try {
|
|
1415
1427
|
const parsed = JSON.parse(raw);
|
|
1416
1428
|
const commit = parsed?.upstream?.commit;
|
|
1417
1429
|
if (typeof commit !== "string" || !/^[0-9a-f]{40}$/i.test(commit)) return null;
|
|
1418
|
-
return
|
|
1430
|
+
return parsed;
|
|
1431
|
+
} catch {
|
|
1432
|
+
return null;
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
async function readManifestBaseAtRef(cwd, ref) {
|
|
1436
|
+
const manifest = await readManifestAtRef(cwd, ref);
|
|
1437
|
+
return manifest ? manifest.upstream.commit.toLowerCase() : null;
|
|
1438
|
+
}
|
|
1439
|
+
async function readPackageVersionAtRef(cwd, ref) {
|
|
1440
|
+
const raw = await git(["show", `${ref}:package.json`], cwd, { ignoreErrors: true });
|
|
1441
|
+
if (!raw) return null;
|
|
1442
|
+
try {
|
|
1443
|
+
const version = JSON.parse(raw).version;
|
|
1444
|
+
return typeof version === "string" && version ? version : null;
|
|
1419
1445
|
} catch {
|
|
1420
1446
|
return null;
|
|
1421
1447
|
}
|
|
@@ -1433,28 +1459,82 @@ async function getRootCommit(cwd, ref) {
|
|
|
1433
1459
|
const roots = out.split("\n").filter(Boolean);
|
|
1434
1460
|
return roots.length > 0 ? roots[roots.length - 1] : null;
|
|
1435
1461
|
}
|
|
1462
|
+
var SCAFFOLD_CANDIDATE_LIMIT = 400;
|
|
1463
|
+
var SCAFFOLD_SCORE_CONCURRENCY = 10;
|
|
1464
|
+
var SCAFFOLD_MATCH_MAX_DIFF_RATIO = 0.3;
|
|
1465
|
+
async function inferScaffoldBase(cwd, headRef, upstreamRef) {
|
|
1466
|
+
const root = await getRootCommit(cwd, headRef);
|
|
1467
|
+
if (!root) return null;
|
|
1468
|
+
const rootDate = await git(["show", "-s", "--format=%cI", root], cwd, { ignoreErrors: true });
|
|
1469
|
+
if (!rootDate) return null;
|
|
1470
|
+
const before = new Date(new Date(rootDate).getTime() + 24 * 60 * 60 * 1e3).toISOString();
|
|
1471
|
+
const revList = await git(
|
|
1472
|
+
["rev-list", `--max-count=${SCAFFOLD_CANDIDATE_LIMIT}`, `--before=${before}`, upstreamRef],
|
|
1473
|
+
cwd,
|
|
1474
|
+
{ ignoreErrors: true }
|
|
1475
|
+
);
|
|
1476
|
+
const candidates = revList.split("\n").filter(Boolean);
|
|
1477
|
+
if (candidates.length === 0) return null;
|
|
1478
|
+
const totalPaths = (await git(["ls-tree", "-r", "--name-only", root], cwd)).split("\n").filter(Boolean).length;
|
|
1479
|
+
if (totalPaths === 0) return null;
|
|
1480
|
+
let best = null;
|
|
1481
|
+
outer: for (let i = 0; i < candidates.length; i += SCAFFOLD_SCORE_CONCURRENCY) {
|
|
1482
|
+
const chunk = candidates.slice(i, i + SCAFFOLD_SCORE_CONCURRENCY);
|
|
1483
|
+
const scores = await Promise.all(
|
|
1484
|
+
chunk.map(async (sha) => {
|
|
1485
|
+
try {
|
|
1486
|
+
const out = await git(["diff-tree", "-r", "--name-only", sha, root], cwd);
|
|
1487
|
+
return { sha, diff: out ? out.split("\n").length : 0 };
|
|
1488
|
+
} catch {
|
|
1489
|
+
return { sha, diff: Number.POSITIVE_INFINITY };
|
|
1490
|
+
}
|
|
1491
|
+
})
|
|
1492
|
+
);
|
|
1493
|
+
for (const score of scores) {
|
|
1494
|
+
if (!best || score.diff < best.diff) best = score;
|
|
1495
|
+
if (best.diff === 0) break outer;
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
if (!best || best.diff / totalPaths > SCAFFOLD_MATCH_MAX_DIFF_RATIO) return null;
|
|
1499
|
+
return best.sha;
|
|
1500
|
+
}
|
|
1501
|
+
async function readRootTrailerBase(cwd, headRef) {
|
|
1502
|
+
const root = await getRootCommit(cwd, headRef);
|
|
1503
|
+
if (!root) return null;
|
|
1504
|
+
const message = await git(["show", "-s", "--format=%B", root], cwd, { ignoreErrors: true });
|
|
1505
|
+
const match = message.match(/^cella-base:\s*([0-9a-f]{40})$/im);
|
|
1506
|
+
return match ? match[1].toLowerCase() : null;
|
|
1507
|
+
}
|
|
1436
1508
|
async function ensureSyncBase(cwd, headRef, upstreamRef) {
|
|
1437
1509
|
const nativeBase = await git(["merge-base", headRef, upstreamRef], cwd, { ignoreErrors: true });
|
|
1438
1510
|
if (nativeBase) return;
|
|
1439
|
-
const
|
|
1511
|
+
const sources = [
|
|
1512
|
+
() => getStoredSyncRef(cwd),
|
|
1513
|
+
() => readManifestBaseAtRef(cwd, headRef),
|
|
1514
|
+
() => readManifestBase(cwd),
|
|
1515
|
+
() => readRootTrailerBase(cwd, headRef),
|
|
1516
|
+
() => inferScaffoldBase(cwd, headRef, upstreamRef)
|
|
1517
|
+
];
|
|
1518
|
+
let baseSha = null;
|
|
1519
|
+
for (const source of sources) {
|
|
1520
|
+
const sha = await source();
|
|
1521
|
+
if (sha && await commitObjectExists(cwd, sha)) {
|
|
1522
|
+
baseSha = sha;
|
|
1523
|
+
break;
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1440
1526
|
if (!baseSha) {
|
|
1441
1527
|
throw new Error(
|
|
1442
|
-
`no common ancestor between the fork and '${upstreamRef}', and no sync base
|
|
1528
|
+
`no common ancestor between the fork and '${upstreamRef}', and no sync base could be determined.
|
|
1443
1529
|
|
|
1444
1530
|
This fork has unrelated history with upstream \u2014 typical for a create-cella scaffold, or after
|
|
1445
|
-
upstream squashed its history
|
|
1446
|
-
then re-run sync:
|
|
1531
|
+
upstream squashed its history \u2014 and no recorded or inferable base commit exists after fetching
|
|
1532
|
+
upstream. Seed it manually with the upstream commit the fork was created from, then re-run sync:
|
|
1447
1533
|
|
|
1448
1534
|
git update-ref refs/cella/last-sync <upstream-sha>
|
|
1449
1535
|
`
|
|
1450
1536
|
);
|
|
1451
1537
|
}
|
|
1452
|
-
if (!await commitObjectExists(cwd, baseSha)) {
|
|
1453
|
-
throw new Error(
|
|
1454
|
-
`recorded sync base ${baseSha.slice(0, 12)} is not present after fetching '${upstreamRef}'.
|
|
1455
|
-
It may belong to a different upstream, or have been dropped by an upstream history rewrite.`
|
|
1456
|
-
);
|
|
1457
|
-
}
|
|
1458
1538
|
const root = await getRootCommit(cwd, headRef);
|
|
1459
1539
|
if (!root) throw new Error("could not determine the fork's root commit to bootstrap sync ancestry");
|
|
1460
1540
|
await git(["replace", "-f", "--graft", root, baseSha], cwd);
|
|
@@ -1497,7 +1577,7 @@ var WORKTREE_PREFIXES = {
|
|
|
1497
1577
|
view: "cella-view-"
|
|
1498
1578
|
};
|
|
1499
1579
|
function buildWorktreePath(kind, repoPath) {
|
|
1500
|
-
return
|
|
1580
|
+
return join9(tmpdir2(), `${WORKTREE_PREFIXES[kind]}${basename3(repoPath)}`);
|
|
1501
1581
|
}
|
|
1502
1582
|
function getWorktreePath(repoPath) {
|
|
1503
1583
|
return buildWorktreePath("sync", repoPath);
|
|
@@ -1515,7 +1595,7 @@ function unregisterWorktree() {
|
|
|
1515
1595
|
}
|
|
1516
1596
|
async function cleanupWorktree(repoPath, worktreePath) {
|
|
1517
1597
|
await removeWorktree(repoPath, worktreePath);
|
|
1518
|
-
if (
|
|
1598
|
+
if (existsSync5(worktreePath)) {
|
|
1519
1599
|
rmSync(worktreePath, { recursive: true, force: true });
|
|
1520
1600
|
}
|
|
1521
1601
|
unregisterWorktree();
|
|
@@ -1523,14 +1603,14 @@ async function cleanupWorktree(repoPath, worktreePath) {
|
|
|
1523
1603
|
async function cleanupLeftoverWorktrees(repoPath) {
|
|
1524
1604
|
for (const kind of Object.keys(WORKTREE_PREFIXES)) {
|
|
1525
1605
|
const worktreePath = buildWorktreePath(kind, repoPath);
|
|
1526
|
-
if (
|
|
1606
|
+
if (existsSync5(worktreePath)) {
|
|
1527
1607
|
await cleanupWorktree(repoPath, worktreePath);
|
|
1528
1608
|
}
|
|
1529
1609
|
}
|
|
1530
1610
|
const prefixes = Object.values(WORKTREE_PREFIXES);
|
|
1531
1611
|
const worktrees = await listWorktrees(repoPath);
|
|
1532
1612
|
for (const wt of worktrees) {
|
|
1533
|
-
if (prefixes.some((prefix) => wt.includes(prefix)) && !
|
|
1613
|
+
if (prefixes.some((prefix) => wt.includes(prefix)) && !existsSync5(wt)) {
|
|
1534
1614
|
await removeWorktree(repoPath, wt);
|
|
1535
1615
|
}
|
|
1536
1616
|
}
|
|
@@ -1695,12 +1775,14 @@ async function enrichChangeInfo(repoPath, files, mergeBaseRef, localRef, incomin
|
|
|
1695
1775
|
const info = forkInfo.get(file.path);
|
|
1696
1776
|
if (info) {
|
|
1697
1777
|
file.changedAt = info.date;
|
|
1778
|
+
file.changedTs = info.timestamp;
|
|
1698
1779
|
file.changedCommit = info.hash;
|
|
1699
1780
|
}
|
|
1700
1781
|
} else if (file.status === "behind") {
|
|
1701
1782
|
const info = upstreamInfo.get(file.path);
|
|
1702
1783
|
if (info) {
|
|
1703
1784
|
file.changedAt = info.date;
|
|
1785
|
+
file.changedTs = info.timestamp;
|
|
1704
1786
|
file.changedCommit = info.hash;
|
|
1705
1787
|
}
|
|
1706
1788
|
} else if (file.status === "diverged" || file.status === "pinned") {
|
|
@@ -1708,10 +1790,12 @@ async function enrichChangeInfo(repoPath, files, mergeBaseRef, localRef, incomin
|
|
|
1708
1790
|
const upstreamFileInfo = upstreamInfo.get(file.path);
|
|
1709
1791
|
if (forkFileInfo) {
|
|
1710
1792
|
file.changedAt = forkFileInfo.date;
|
|
1793
|
+
file.changedTs = forkFileInfo.timestamp;
|
|
1711
1794
|
file.changedCommit = forkFileInfo.hash;
|
|
1712
1795
|
}
|
|
1713
1796
|
if (upstreamFileInfo) {
|
|
1714
1797
|
file.upstreamChangedAt = upstreamFileInfo.date;
|
|
1798
|
+
file.upstreamChangedTs = upstreamFileInfo.timestamp;
|
|
1715
1799
|
file.upstreamCommit = upstreamFileInfo.hash;
|
|
1716
1800
|
}
|
|
1717
1801
|
}
|
|
@@ -2073,7 +2157,7 @@ function findTargetFile(files, targetPath) {
|
|
|
2073
2157
|
}
|
|
2074
2158
|
function printUnifiedDiff(config, filePath) {
|
|
2075
2159
|
const diff = gitDiffFile(config.forkPath, `${config.upstreamRef}..HEAD`, filePath, {
|
|
2076
|
-
dstPrefix:
|
|
2160
|
+
dstPrefix: basename4(config.forkPath)
|
|
2077
2161
|
});
|
|
2078
2162
|
writeStdout(diff.toString());
|
|
2079
2163
|
}
|
|
@@ -2128,7 +2212,7 @@ async function runAnalyze(config) {
|
|
|
2128
2212
|
{
|
|
2129
2213
|
filePath: file.path,
|
|
2130
2214
|
srcLabel: "cella",
|
|
2131
|
-
dstLabel:
|
|
2215
|
+
dstLabel: basename4(config.forkPath),
|
|
2132
2216
|
note: "local working tree"
|
|
2133
2217
|
},
|
|
2134
2218
|
config.forkPath
|
|
@@ -2923,7 +3007,7 @@ async function checkOverrides(forkPath) {
|
|
|
2923
3007
|
|
|
2924
3008
|
// src/services/contributions.ts
|
|
2925
3009
|
import { readFileSync as readFileSync4 } from "fs";
|
|
2926
|
-
import { join as
|
|
3010
|
+
import { join as join13 } from "path";
|
|
2927
3011
|
import {
|
|
2928
3012
|
createPrompt,
|
|
2929
3013
|
isDownKey,
|
|
@@ -2938,7 +3022,7 @@ import {
|
|
|
2938
3022
|
import { select as select2 } from "@inquirer/prompts";
|
|
2939
3023
|
|
|
2940
3024
|
// src/services/contrib-core.ts
|
|
2941
|
-
import { dirname as dirname2, join as
|
|
3025
|
+
import { dirname as dirname2, join as join12 } from "path";
|
|
2942
3026
|
async function listFilesAtRef(repoPath, ref) {
|
|
2943
3027
|
const raw = await git(["ls-tree", "-r", "--name-only", ref], repoPath);
|
|
2944
3028
|
return new Set(raw.split("\n").filter(Boolean));
|
|
@@ -2976,7 +3060,8 @@ async function detectContributableFiles(repoPath, baseRef, forkRef, config, fork
|
|
|
2976
3060
|
kind = "modified";
|
|
2977
3061
|
}
|
|
2978
3062
|
const changedAt = file.status === "diverged" ? file.upstreamChangedAt : file.changedAt;
|
|
2979
|
-
|
|
3063
|
+
const changedTs = file.status === "diverged" ? file.upstreamChangedTs : file.changedTs;
|
|
3064
|
+
files.push({ path: file.path, kind, status: file.status, changedAt, changedTs });
|
|
2980
3065
|
}
|
|
2981
3066
|
return {
|
|
2982
3067
|
modified: files.filter((f) => f.kind === "modified").map((f) => f.path),
|
|
@@ -3000,7 +3085,7 @@ async function readBlobInfo(repoPath, ref, filePath) {
|
|
|
3000
3085
|
}
|
|
3001
3086
|
async function buildContribBranch(repoPath, baseRef, forkRef, detection, forkName) {
|
|
3002
3087
|
const branch = `contrib/${forkName}`;
|
|
3003
|
-
const tmpIndex =
|
|
3088
|
+
const tmpIndex = join12(repoPath, ".git", `tmp-contrib-index-${forkName.replace(/[^\w.-]/g, "_")}`);
|
|
3004
3089
|
const indexEnv = { GIT_INDEX_FILE: tmpIndex };
|
|
3005
3090
|
try {
|
|
3006
3091
|
await git(["read-tree", baseRef], repoPath, { env: indexEnv });
|
|
@@ -3043,16 +3128,16 @@ ${commitBody}`;
|
|
|
3043
3128
|
}
|
|
3044
3129
|
|
|
3045
3130
|
// src/services/fork-utils.ts
|
|
3046
|
-
import { existsSync as
|
|
3131
|
+
import { existsSync as existsSync7 } from "fs";
|
|
3047
3132
|
import { resolve as resolve3 } from "path";
|
|
3048
3133
|
function validateForkPath(fork, basePath, requireConfig = false) {
|
|
3049
3134
|
const resolvedPath = resolve3(basePath, fork.localPath);
|
|
3050
3135
|
if (!resolvedPath.startsWith(resolve3(basePath, ".."))) {
|
|
3051
3136
|
return { fork, resolvedPath, valid: false, error: "path resolves outside parent directory" };
|
|
3052
3137
|
}
|
|
3053
|
-
if (!
|
|
3054
|
-
if (!
|
|
3055
|
-
if (requireConfig && !
|
|
3138
|
+
if (!existsSync7(resolvedPath)) return { fork, resolvedPath, valid: false, error: "path does not exist" };
|
|
3139
|
+
if (!existsSync7(`${resolvedPath}/.git`)) return { fork, resolvedPath, valid: false, error: "not a git repository" };
|
|
3140
|
+
if (requireConfig && !existsSync7(`${resolvedPath}/cella.config.ts`)) {
|
|
3056
3141
|
return { fork, resolvedPath, valid: false, error: "missing cella.config.ts" };
|
|
3057
3142
|
}
|
|
3058
3143
|
return { fork, resolvedPath, valid: true };
|
|
@@ -3070,7 +3155,7 @@ async function showContribDiff(item, baseRef, cwd, forkName) {
|
|
|
3070
3155
|
return openDiffInBrowser(patch.toString(), { filePath: item.path, srcLabel: "cella", dstLabel: forkName }, cwd);
|
|
3071
3156
|
}
|
|
3072
3157
|
function shouldApplyContributionsUnstaged(repoPath) {
|
|
3073
|
-
return JSON.parse(readFileSync4(
|
|
3158
|
+
return JSON.parse(readFileSync4(join13(repoPath, "package.json"), "utf8")).name === "cella";
|
|
3074
3159
|
}
|
|
3075
3160
|
var contribPrompt = createPrompt((config, done) => {
|
|
3076
3161
|
const { pageSize = 20, baseRef, cwd, forkName } = config;
|
|
@@ -3295,6 +3380,7 @@ async function runContributions(config) {
|
|
|
3295
3380
|
created: fileMeta?.kind === "created",
|
|
3296
3381
|
status: fileMeta?.status,
|
|
3297
3382
|
changedAt: fileMeta?.changedAt,
|
|
3383
|
+
changedTs: fileMeta?.changedTs,
|
|
3298
3384
|
additions: stat.get(path4)?.additions ?? null,
|
|
3299
3385
|
deletions: stat.get(path4)?.deletions ?? null,
|
|
3300
3386
|
checked: false
|
|
@@ -3308,6 +3394,7 @@ async function runContributions(config) {
|
|
|
3308
3394
|
deleted: true,
|
|
3309
3395
|
status: fileMeta?.status,
|
|
3310
3396
|
changedAt: fileMeta?.changedAt,
|
|
3397
|
+
changedTs: fileMeta?.changedTs,
|
|
3311
3398
|
additions: stat.get(path4)?.additions ?? null,
|
|
3312
3399
|
deletions: stat.get(path4)?.deletions ?? null,
|
|
3313
3400
|
checked: false
|
|
@@ -3324,7 +3411,7 @@ async function runContributions(config) {
|
|
|
3324
3411
|
spinnerSuccess("no contributions found");
|
|
3325
3412
|
return;
|
|
3326
3413
|
}
|
|
3327
|
-
allItems.sort((a, b) => a.path.localeCompare(b.path));
|
|
3414
|
+
allItems.sort((a, b) => (b.changedTs ?? 0) - (a.changedTs ?? 0) || a.path.localeCompare(b.path));
|
|
3328
3415
|
spinnerSuccess(`${allItems.length} files from ${forkName}`);
|
|
3329
3416
|
if (!config.list && !config.diff && forkBanner) {
|
|
3330
3417
|
console.info(
|
|
@@ -3351,6 +3438,7 @@ async function runContributions(config) {
|
|
|
3351
3438
|
status: item.status ?? null,
|
|
3352
3439
|
kind: item.deleted ? "deleted" : "modified",
|
|
3353
3440
|
changedAt: item.changedAt ?? null,
|
|
3441
|
+
changedTs: item.changedTs ?? null,
|
|
3354
3442
|
additions: item.additions ?? null,
|
|
3355
3443
|
deletions: item.deletions ?? null
|
|
3356
3444
|
}));
|
|
@@ -3417,11 +3505,6 @@ async function runContributions(config) {
|
|
|
3417
3505
|
spinnerSuccess(`applied ${applied} files ${colors_default.dim(applyUnstaged ? "(unstaged)" : "(staged)")}`);
|
|
3418
3506
|
}
|
|
3419
3507
|
console.info();
|
|
3420
|
-
console.info(` ${colors_default.cyan(forkName)} ${colors_default.dim(`(${selected.length} files)`)}`);
|
|
3421
|
-
for (const item of selected) {
|
|
3422
|
-
console.info(` ${colors_default.dim("\u2192")} ${item.deleted ? `(deleted) ${item.path}` : item.path}`);
|
|
3423
|
-
}
|
|
3424
|
-
console.info();
|
|
3425
3508
|
console.info(
|
|
3426
3509
|
colors_default.dim(
|
|
3427
3510
|
applyUnstaged ? " files are unstaged \u2014 review, stage what you want, and commit when ready" : " files are staged \u2014 review and commit when ready"
|
|
@@ -3439,8 +3522,8 @@ import { Separator as Separator2, select as select3 } from "@inquirer/prompts";
|
|
|
3439
3522
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
3440
3523
|
|
|
3441
3524
|
// src/services/packages.ts
|
|
3442
|
-
import { existsSync as
|
|
3443
|
-
import { join as
|
|
3525
|
+
import { existsSync as existsSync8, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
|
|
3526
|
+
import { join as join14 } from "path";
|
|
3444
3527
|
function parseComparableVersion(version) {
|
|
3445
3528
|
const trimmed = version.trim();
|
|
3446
3529
|
if (trimmed === "" || trimmed === "*" || trimmed.includes("workspace:") || trimmed.includes("catalog:") || trimmed.includes("file:") || trimmed.includes("link:") || trimmed.includes("git+") || trimmed.includes("github:") || trimmed.includes("http://") || trimmed.includes("https://") || trimmed.includes("||")) {
|
|
@@ -3544,7 +3627,7 @@ function safeMergePnpm(forkPnpm, upstreamPnpm) {
|
|
|
3544
3627
|
return { merged, changed };
|
|
3545
3628
|
}
|
|
3546
3629
|
function readPackageJson(filePath) {
|
|
3547
|
-
if (!
|
|
3630
|
+
if (!existsSync8(filePath)) return null;
|
|
3548
3631
|
try {
|
|
3549
3632
|
return JSON.parse(readFileSync5(filePath, "utf-8"));
|
|
3550
3633
|
} catch {
|
|
@@ -3566,11 +3649,11 @@ async function getUpstreamPackageJson(forkPath, upstreamRef, relativePath) {
|
|
|
3566
3649
|
async function discoverPackageLocations(forkPath, upstreamRef) {
|
|
3567
3650
|
const stdout = await git(["ls-tree", "-r", "--name-only", upstreamRef], forkPath, { ignoreErrors: true });
|
|
3568
3651
|
const upstreamPaths = stdout.split("\n").filter((p) => p.endsWith("/package.json") || p === "package.json").map((p) => p === "package.json" ? "" : p.replace("/package.json", ""));
|
|
3569
|
-
return upstreamPaths.filter((loc) =>
|
|
3652
|
+
return upstreamPaths.filter((loc) => existsSync8(join14(forkPath, loc, "package.json")));
|
|
3570
3653
|
}
|
|
3571
3654
|
async function syncPackageJson(forkPath, upstreamRef, relativePath, keysToSync) {
|
|
3572
3655
|
const changes = [];
|
|
3573
|
-
const pkgPath =
|
|
3656
|
+
const pkgPath = join14(forkPath, relativePath, "package.json");
|
|
3574
3657
|
const forkPkg = readPackageJson(pkgPath);
|
|
3575
3658
|
const upstreamPkg = await getUpstreamPackageJson(forkPath, upstreamRef, relativePath);
|
|
3576
3659
|
if (!forkPkg || !upstreamPkg) {
|
|
@@ -3718,12 +3801,57 @@ async function runSync(config, options) {
|
|
|
3718
3801
|
return result;
|
|
3719
3802
|
}
|
|
3720
3803
|
var SYNC_PR_TITLE = "chore: sync upstream cella";
|
|
3721
|
-
|
|
3722
|
-
|
|
3804
|
+
var PR_BODY_COMMIT_MAX = 50;
|
|
3805
|
+
async function buildSyncCommitMessage(forkPath, upstreamRef) {
|
|
3806
|
+
const shortSha = await getShortSha(forkPath, upstreamRef).catch(() => "");
|
|
3807
|
+
if (!shortSha) return SYNC_PR_TITLE;
|
|
3808
|
+
const manifest = await readSyncManifest(forkPath);
|
|
3809
|
+
const version = manifest?.upstream.release ?? await readPackageVersionAtRef(forkPath, upstreamRef).catch(() => null);
|
|
3810
|
+
return version ? `${SYNC_PR_TITLE} v${version.replace(/^v/, "")} (${shortSha})` : `${SYNC_PR_TITLE} ${shortSha}`;
|
|
3811
|
+
}
|
|
3812
|
+
function qualifyPrRefs(subject, repoSlug) {
|
|
3813
|
+
if (!repoSlug) return subject;
|
|
3814
|
+
return subject.replace(/(^|[^\w/])#(\d+)\b/g, `$1${repoSlug}#$2`);
|
|
3815
|
+
}
|
|
3816
|
+
function buildSyncPrBody(input) {
|
|
3817
|
+
const { repoSlug, version, fromSha, toSha, commits, totalCount } = input;
|
|
3818
|
+
const githubUrl = repoSlug ? `https://github.com/${repoSlug}` : void 0;
|
|
3819
|
+
const short = (sha) => sha.slice(0, 7);
|
|
3820
|
+
const commitRef = (sha) => githubUrl ? `[\`${short(sha)}\`](${githubUrl}/commit/${sha})` : `\`${short(sha)}\``;
|
|
3821
|
+
const upstreamName = githubUrl ? `[${repoSlug}](${githubUrl})` : "cella";
|
|
3822
|
+
const versionSuffix = version ? ` (v${version.replace(/^v/, "")})` : "";
|
|
3823
|
+
const lines = [`Syncs upstream ${upstreamName} to ${commitRef(toSha)}${versionSuffix}.`];
|
|
3824
|
+
if (totalCount > 0 && fromSha) {
|
|
3825
|
+
const compare = githubUrl ? ` ([compare](${githubUrl}/compare/${short(fromSha)}...${short(toSha)}))` : "";
|
|
3826
|
+
lines.push("", `**${totalCount} upstream commit${totalCount === 1 ? "" : "s"} since last sync**${compare}:`, "");
|
|
3827
|
+
if (totalCount > commits.length) lines.push(`- \u2026${totalCount - commits.length} earlier commit(s) not shown`);
|
|
3828
|
+
for (const commit of commits) {
|
|
3829
|
+
lines.push(`- ${commitRef(commit.hash)} ${qualifyPrRefs(commit.message, repoSlug)}`);
|
|
3830
|
+
}
|
|
3831
|
+
}
|
|
3832
|
+
return `${lines.join("\n")}
|
|
3833
|
+
`;
|
|
3834
|
+
}
|
|
3835
|
+
async function buildSyncPrBodyForBranch(forkPath, base) {
|
|
3836
|
+
const manifest = await readManifestAtRef(forkPath, "HEAD");
|
|
3837
|
+
if (!manifest) return void 0;
|
|
3838
|
+
const toSha = manifest.upstream.commit.toLowerCase();
|
|
3839
|
+
const fromSha = await readManifestBaseAtRef(forkPath, base) ?? await getMergeBase(forkPath, base, toSha).catch(() => null);
|
|
3840
|
+
const version = manifest.upstream.release ?? await readPackageVersionAtRef(forkPath, toSha).catch(() => null);
|
|
3841
|
+
const totalCount = fromSha ? await countCommitsBetween(forkPath, fromSha, toSha).catch(() => 0) : 0;
|
|
3842
|
+
const commits = fromSha && totalCount > 0 ? await listCommitsBetween(forkPath, fromSha, toSha, {
|
|
3843
|
+
oldestFirst: true,
|
|
3844
|
+
skip: totalCount > PR_BODY_COMMIT_MAX ? totalCount - PR_BODY_COMMIT_MAX : 0,
|
|
3845
|
+
limit: PR_BODY_COMMIT_MAX
|
|
3846
|
+
}) : [];
|
|
3847
|
+
return buildSyncPrBody({ repoSlug: manifest.upstream.repo, version, fromSha, toSha, commits, totalCount });
|
|
3848
|
+
}
|
|
3849
|
+
function printPrCreateStep(branch, base, title = SYNC_PR_TITLE) {
|
|
3850
|
+
console.info(colors_default.dim(` gh pr create --base ${base} --head ${branch} --title "${title}" --fill`));
|
|
3723
3851
|
}
|
|
3724
|
-
function printShipSteps(temporaryBranch, base) {
|
|
3852
|
+
function printShipSteps(temporaryBranch, base, title) {
|
|
3725
3853
|
console.info(colors_default.dim(` git push -u origin ${temporaryBranch}`));
|
|
3726
|
-
printPrCreateStep(temporaryBranch, base);
|
|
3854
|
+
printPrCreateStep(temporaryBranch, base, title);
|
|
3727
3855
|
}
|
|
3728
3856
|
function printFinishSteps() {
|
|
3729
3857
|
console.info(colors_default.dim(" pnpm cella sync"));
|
|
@@ -3740,8 +3868,7 @@ function extractFirstUrl(output) {
|
|
|
3740
3868
|
async function flattenSyncBranch(forkPath, branch, base) {
|
|
3741
3869
|
const mergeCommits = await listBranchMergeCommits(forkPath, base);
|
|
3742
3870
|
if (mergeCommits.length === 0) return false;
|
|
3743
|
-
const
|
|
3744
|
-
const message = upstreamSha ? `${SYNC_PR_TITLE} ${upstreamSha}` : SYNC_PR_TITLE;
|
|
3871
|
+
const message = await buildSyncCommitMessage(forkPath, `${mergeCommits[0]}^2`);
|
|
3745
3872
|
console.info(
|
|
3746
3873
|
colors_default.yellow(
|
|
3747
3874
|
`'${branch}' contains ${mergeCommits.length} merge commit(s) \u2014 the PR would list the entire upstream history.`
|
|
@@ -3757,18 +3884,22 @@ async function shipSyncBranch(config, branch) {
|
|
|
3757
3884
|
const flattened = await flattenSyncBranch(forkPath, branch, base);
|
|
3758
3885
|
let prUrl;
|
|
3759
3886
|
let prOpened = false;
|
|
3887
|
+
const headSubject = (await getCommitInfo(forkPath, "HEAD").catch(() => null))?.message;
|
|
3888
|
+
const prTitle = headSubject?.startsWith(SYNC_PR_TITLE) ? headSubject : SYNC_PR_TITLE;
|
|
3760
3889
|
console.info(colors_default.dim(`pushing '${branch}' to origin...`));
|
|
3761
3890
|
try {
|
|
3762
3891
|
await pushBranch(forkPath, "origin", branch, { forceWithLease: flattened });
|
|
3763
3892
|
} catch (error) {
|
|
3764
3893
|
const detail = error instanceof Error ? error.message : String(error);
|
|
3765
3894
|
console.info(colors_default.yellow(`push failed (${detail.split("\n")[0]}). finish manually:`));
|
|
3766
|
-
printShipSteps(branch, base);
|
|
3895
|
+
printShipSteps(branch, base, prTitle);
|
|
3767
3896
|
return;
|
|
3768
3897
|
}
|
|
3769
3898
|
if (ghAvailable()) {
|
|
3770
3899
|
console.info(colors_default.dim("opening a pull request..."));
|
|
3771
|
-
const
|
|
3900
|
+
const prBody = await buildSyncPrBodyForBranch(forkPath, base);
|
|
3901
|
+
const bodyArgs = prBody ? ["--body", prBody] : ["--fill"];
|
|
3902
|
+
const pr = spawnSync2("gh", ["pr", "create", "--base", base, "--head", branch, "--title", prTitle, ...bodyArgs], {
|
|
3772
3903
|
cwd: forkPath,
|
|
3773
3904
|
encoding: "utf-8",
|
|
3774
3905
|
stdio: ["ignore", "pipe", "pipe"]
|
|
@@ -3779,11 +3910,11 @@ ${pr.stderr ?? ""}`);
|
|
|
3779
3910
|
if (!prOpened) {
|
|
3780
3911
|
console.info(colors_default.yellow("could not open the PR automatically (it may already exist). open it with:"));
|
|
3781
3912
|
if (prUrl) console.info(colors_default.dim(` ${prUrl}`));
|
|
3782
|
-
printPrCreateStep(branch, base);
|
|
3913
|
+
printPrCreateStep(branch, base, prTitle);
|
|
3783
3914
|
}
|
|
3784
3915
|
} else {
|
|
3785
3916
|
console.info(colors_default.yellow("`gh` not found \u2014 open the PR manually:"));
|
|
3786
|
-
printPrCreateStep(branch, base);
|
|
3917
|
+
printPrCreateStep(branch, base, prTitle);
|
|
3787
3918
|
}
|
|
3788
3919
|
console.info(colors_default.dim(`switching back to '${base}'...`));
|
|
3789
3920
|
await switchBranch(forkPath, base);
|
|
@@ -3839,9 +3970,8 @@ async function resumeSyncMerge(config, branch) {
|
|
|
3839
3970
|
);
|
|
3840
3971
|
return;
|
|
3841
3972
|
}
|
|
3842
|
-
const
|
|
3973
|
+
const message = await buildSyncCommitMessage(forkPath, "MERGE_HEAD");
|
|
3843
3974
|
await stageAll(forkPath);
|
|
3844
|
-
const message = upstreamSha ? `${SYNC_PR_TITLE} ${upstreamSha}` : SYNC_PR_TITLE;
|
|
3845
3975
|
await commitSquash(forkPath, message);
|
|
3846
3976
|
console.info();
|
|
3847
3977
|
console.info(colors_default.green(`committed the sync on '${branch}' as '${message}'.`));
|
|
@@ -3999,7 +4129,7 @@ async function runForks(config) {
|
|
|
3999
4129
|
|
|
4000
4130
|
// src/services/stats.ts
|
|
4001
4131
|
import { readFile as readFile2 } from "fs/promises";
|
|
4002
|
-
import { join as
|
|
4132
|
+
import { join as join16 } from "path";
|
|
4003
4133
|
|
|
4004
4134
|
// src/utils/coverage-utils.ts
|
|
4005
4135
|
import { spawnSync as spawnSync3 } from "child_process";
|
|
@@ -4087,7 +4217,7 @@ function printCoverageSummary(forkPath, options = {}) {
|
|
|
4087
4217
|
// src/services/stats.ts
|
|
4088
4218
|
var sourceExtensions = /* @__PURE__ */ new Set(["ts", "tsx", "js", "jsx", "mjs", "cjs", "json", "jsonc", "css", "html"]);
|
|
4089
4219
|
async function getWorkspacePackages(forkPath) {
|
|
4090
|
-
const content = await readFile2(
|
|
4220
|
+
const content = await readFile2(join16(forkPath, "pnpm-workspace.yaml"), "utf-8");
|
|
4091
4221
|
return parseYamlBlockList(content, "packages").map((pattern) => {
|
|
4092
4222
|
if (pattern.endsWith("/*")) {
|
|
4093
4223
|
const base = pattern.slice(0, -2);
|
|
@@ -4245,7 +4375,7 @@ function getForkPath() {
|
|
|
4245
4375
|
const envPath = getEnv("CELLA_FORK_PATH");
|
|
4246
4376
|
if (envPath) {
|
|
4247
4377
|
const resolved = resolve5(envPath);
|
|
4248
|
-
if (!
|
|
4378
|
+
if (!existsSync10(resolved) || !statSync2(resolved).isDirectory()) {
|
|
4249
4379
|
throw new Error(`CELLA_FORK_PATH is not a valid directory: ${resolved}`);
|
|
4250
4380
|
}
|
|
4251
4381
|
return resolved;
|
|
@@ -4257,7 +4387,7 @@ function getForkPath() {
|
|
|
4257
4387
|
return cwd;
|
|
4258
4388
|
}
|
|
4259
4389
|
async function preflight(forkPath) {
|
|
4260
|
-
if (!
|
|
4390
|
+
if (!existsSync10(join17(forkPath, ".git"))) {
|
|
4261
4391
|
throw new Error(`not a git repository: ${forkPath}`);
|
|
4262
4392
|
}
|
|
4263
4393
|
}
|