@ricsam/r5d-worker 0.0.101 → 0.0.103
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 +16 -3
- package/dist/cjs/main.cjs +390 -322
- package/dist/cjs/package.json +1 -1
- package/dist/cjs/project-mirror-fetch-policy.cjs +48 -7
- package/dist/cjs/project-mirror-refs-token.cjs +36 -2
- package/dist/cjs/project-worktrees.cjs +49 -32
- package/dist/cjs/workspace-git-sync.cjs +45 -6
- package/dist/cjs/workspace-mutation-gate.cjs +35 -0
- package/dist/cjs/workspace-projection-ledger.cjs +81 -0
- package/dist/cjs/workspace-sync-coalescer.cjs +58 -0
- package/dist/mjs/main.mjs +395 -324
- package/dist/mjs/package.json +1 -1
- package/dist/mjs/project-mirror-fetch-policy.mjs +46 -6
- package/dist/mjs/project-mirror-refs-token.mjs +30 -1
- package/dist/mjs/project-worktrees.mjs +52 -33
- package/dist/mjs/workspace-git-sync.mjs +45 -6
- package/dist/mjs/workspace-mutation-gate.mjs +35 -0
- package/dist/mjs/workspace-projection-ledger.mjs +57 -0
- package/dist/mjs/workspace-sync-coalescer.mjs +34 -0
- package/dist/types/main.d.ts +29 -17
- package/dist/types/project-mirror-fetch-policy.d.ts +70 -15
- package/dist/types/project-mirror-refs-token.d.ts +34 -0
- package/dist/types/project-worktrees.d.ts +26 -8
- package/dist/types/workspace-git-sync.d.ts +24 -2
- package/dist/types/workspace-mutation-gate.d.ts +24 -3
- package/dist/types/workspace-projection-ledger.d.ts +59 -0
- package/dist/types/workspace-sync-coalescer.d.ts +28 -0
- package/package.json +1 -1
package/dist/cjs/package.json
CHANGED
|
@@ -18,24 +18,65 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
18
18
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
19
|
var project_mirror_fetch_policy_exports = {};
|
|
20
20
|
__export(project_mirror_fetch_policy_exports, {
|
|
21
|
-
|
|
21
|
+
parseProjectMirrorRefsTokensMessage: () => parseProjectMirrorRefsTokensMessage,
|
|
22
|
+
planProjectMirrorObservationFetch: () => planProjectMirrorObservationFetch,
|
|
22
23
|
projectMirrorFetchDecision: () => projectMirrorFetchDecision
|
|
23
24
|
});
|
|
24
25
|
module.exports = __toCommonJS(project_mirror_fetch_policy_exports);
|
|
25
|
-
const PROJECT_MIRROR_FETCH_BACKSTOP_MS = 10 * 6e4;
|
|
26
26
|
function projectMirrorFetchDecision(input) {
|
|
27
27
|
if (!input.skippingEnabled) return { fetch: true, reason: "skipping_disabled" };
|
|
28
28
|
if (input.advertisedToken === void 0) return { fetch: true, reason: "no_advertised_token" };
|
|
29
29
|
if (input.fetchedToken === void 0) return { fetch: true, reason: "never_fetched" };
|
|
30
30
|
if (input.advertisedToken !== input.fetchedToken) return { fetch: true, reason: "token_changed" };
|
|
31
|
-
const backstopMs = input.backstopMs ?? PROJECT_MIRROR_FETCH_BACKSTOP_MS;
|
|
32
|
-
if (input.lastFetchAtMs === void 0 || input.nowMs - input.lastFetchAtMs >= backstopMs) {
|
|
33
|
-
return { fetch: true, reason: "backstop_elapsed" };
|
|
34
|
-
}
|
|
35
31
|
return { fetch: false, reason: "token_match" };
|
|
36
32
|
}
|
|
33
|
+
function planProjectMirrorObservationFetch(input) {
|
|
34
|
+
if (!input.skippingEnabled) return { scope: "project", reason: "skipping_disabled" };
|
|
35
|
+
if (input.advertisedMountTokens === void 0) {
|
|
36
|
+
const decision = projectMirrorFetchDecision({
|
|
37
|
+
advertisedToken: input.advertisedProjectToken,
|
|
38
|
+
fetchedToken: input.fetchedProjectToken,
|
|
39
|
+
skippingEnabled: true
|
|
40
|
+
});
|
|
41
|
+
return decision.fetch ? { scope: "project", reason: decision.reason } : { scope: "none" };
|
|
42
|
+
}
|
|
43
|
+
const mounts = [];
|
|
44
|
+
for (const branchName of [...input.branchNames].sort()) {
|
|
45
|
+
const decision = projectMirrorFetchDecision({
|
|
46
|
+
advertisedToken: input.advertisedMountTokens.get(branchName),
|
|
47
|
+
fetchedToken: input.fetchedMountTokens.get(branchName),
|
|
48
|
+
skippingEnabled: true
|
|
49
|
+
});
|
|
50
|
+
if (decision.fetch) mounts.push({ branchName, reason: decision.reason });
|
|
51
|
+
}
|
|
52
|
+
return mounts.length === 0 ? { scope: "none" } : { scope: "mounts", mounts };
|
|
53
|
+
}
|
|
54
|
+
const TOKEN_RE = /^[0-9a-f]{64}$/;
|
|
55
|
+
const GIT_OBJECT_NAME_RE = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;
|
|
56
|
+
function parseProjectMirrorRefsTokensMessage(message) {
|
|
57
|
+
const projectTokens = /* @__PURE__ */ new Map();
|
|
58
|
+
if (message.tokens && typeof message.tokens === "object") {
|
|
59
|
+
for (const [projectId, token] of Object.entries(message.tokens)) {
|
|
60
|
+
if (typeof token === "string" && TOKEN_RE.test(token)) projectTokens.set(projectId, token);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const mountTokens = /* @__PURE__ */ new Map();
|
|
64
|
+
if (message.mountTokens && typeof message.mountTokens === "object") {
|
|
65
|
+
for (const [projectId, tokensByBranch] of Object.entries(message.mountTokens)) {
|
|
66
|
+
if (!tokensByBranch || typeof tokensByBranch !== "object") continue;
|
|
67
|
+
const perMount = /* @__PURE__ */ new Map();
|
|
68
|
+
for (const [branchName, token] of Object.entries(tokensByBranch)) {
|
|
69
|
+
if (typeof token === "string" && TOKEN_RE.test(token)) perMount.set(branchName, token);
|
|
70
|
+
}
|
|
71
|
+
mountTokens.set(projectId, perMount);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const workspaceHead = typeof message.workspaceHead === "string" && GIT_OBJECT_NAME_RE.test(message.workspaceHead) ? message.workspaceHead : void 0;
|
|
75
|
+
return { projectTokens, mountTokens, workspaceHead };
|
|
76
|
+
}
|
|
37
77
|
// Annotate the CommonJS export names for ESM import in node:
|
|
38
78
|
0 && (module.exports = {
|
|
39
|
-
|
|
79
|
+
parseProjectMirrorRefsTokensMessage,
|
|
80
|
+
planProjectMirrorObservationFetch,
|
|
40
81
|
projectMirrorFetchDecision
|
|
41
82
|
});
|
|
@@ -18,19 +18,38 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
18
18
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
19
|
var project_mirror_refs_token_exports = {};
|
|
20
20
|
__export(project_mirror_refs_token_exports, {
|
|
21
|
+
PROJECT_MIRROR_REFS_TOKEN_NAMESPACES: () => PROJECT_MIRROR_REFS_TOKEN_NAMESPACES,
|
|
22
|
+
PROJECT_MIRROR_REF_LISTING_FORMAT: () => PROJECT_MIRROR_REF_LISTING_FORMAT,
|
|
21
23
|
PROJECT_MIRROR_REF_PREFIX: () => PROJECT_MIRROR_REF_PREFIX,
|
|
22
24
|
PROJECT_SERVER_SYNCED_REF_PREFIX: () => PROJECT_SERVER_SYNCED_REF_PREFIX,
|
|
23
25
|
canonicalizeLocalProjectMirrorRefs: () => canonicalizeLocalProjectMirrorRefs,
|
|
24
|
-
hashProjectMirrorRefs: () => hashProjectMirrorRefs
|
|
26
|
+
hashProjectMirrorRefs: () => hashProjectMirrorRefs,
|
|
27
|
+
parseProjectMirrorRefListing: () => parseProjectMirrorRefListing,
|
|
28
|
+
projectMirrorMountRefs: () => projectMirrorMountRefs,
|
|
29
|
+
projectMirrorRefsTokens: () => projectMirrorRefsTokens
|
|
25
30
|
});
|
|
26
31
|
module.exports = __toCommonJS(project_mirror_refs_token_exports);
|
|
27
32
|
var import_node_crypto = require("node:crypto");
|
|
33
|
+
const PROJECT_MIRROR_REFS_TOKEN_NAMESPACES = ["refs/heads/", "refs/r5d/server-synced/"];
|
|
28
34
|
const PROJECT_MIRROR_REF_PREFIX = "refs/r5d/mirror/";
|
|
29
35
|
const PROJECT_SERVER_SYNCED_REF_PREFIX = "refs/r5d/server-synced/";
|
|
36
|
+
const PROJECT_MIRROR_REF_LISTING_FORMAT = "--format=%(objectname) %(refname)";
|
|
37
|
+
const GIT_OBJECT_NAME_RE = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;
|
|
30
38
|
function hashProjectMirrorRefs(refs) {
|
|
31
39
|
const lines = refs.map(({ objectName, refname }) => `${objectName} ${refname}`).sort((left, right) => left < right ? -1 : left > right ? 1 : 0);
|
|
32
40
|
return (0, import_node_crypto.createHash)("sha256").update(lines.join("\n")).digest("hex");
|
|
33
41
|
}
|
|
42
|
+
function parseProjectMirrorRefListing(output, options = {}) {
|
|
43
|
+
return output.split(/\r?\n/).filter(Boolean).map((row) => {
|
|
44
|
+
const separator = row.indexOf(" ");
|
|
45
|
+
const objectName = separator < 0 ? "" : row.slice(0, separator);
|
|
46
|
+
const refname = separator < 0 ? "" : row.slice(separator + 1);
|
|
47
|
+
if (!GIT_OBJECT_NAME_RE.test(objectName) || refname.length === 0 || options.namespaces !== void 0 && !options.namespaces.some((prefix) => refname.startsWith(prefix))) {
|
|
48
|
+
throw new Error(`Invalid ref listing row: ${row}`);
|
|
49
|
+
}
|
|
50
|
+
return { refname, objectName };
|
|
51
|
+
});
|
|
52
|
+
}
|
|
34
53
|
function canonicalizeLocalProjectMirrorRefs(refs) {
|
|
35
54
|
const canonical = [];
|
|
36
55
|
for (const { refname, objectName } of refs) {
|
|
@@ -42,10 +61,25 @@ function canonicalizeLocalProjectMirrorRefs(refs) {
|
|
|
42
61
|
}
|
|
43
62
|
return canonical;
|
|
44
63
|
}
|
|
64
|
+
function projectMirrorMountRefs(refs, mount) {
|
|
65
|
+
const headRef = `refs/heads/${mount.branchName}`;
|
|
66
|
+
const handoffPrefix = `${PROJECT_SERVER_SYNCED_REF_PREFIX}${mount.branchId}/`;
|
|
67
|
+
return refs.filter(({ refname }) => refname === headRef || refname.startsWith(handoffPrefix));
|
|
68
|
+
}
|
|
69
|
+
function projectMirrorRefsTokens(refs, mounts) {
|
|
70
|
+
const mountTokens = /* @__PURE__ */ new Map();
|
|
71
|
+
for (const mount of mounts) mountTokens.set(mount.branchName, hashProjectMirrorRefs(projectMirrorMountRefs(refs, mount)));
|
|
72
|
+
return { project: hashProjectMirrorRefs(refs), mounts: mountTokens };
|
|
73
|
+
}
|
|
45
74
|
// Annotate the CommonJS export names for ESM import in node:
|
|
46
75
|
0 && (module.exports = {
|
|
76
|
+
PROJECT_MIRROR_REFS_TOKEN_NAMESPACES,
|
|
77
|
+
PROJECT_MIRROR_REF_LISTING_FORMAT,
|
|
47
78
|
PROJECT_MIRROR_REF_PREFIX,
|
|
48
79
|
PROJECT_SERVER_SYNCED_REF_PREFIX,
|
|
49
80
|
canonicalizeLocalProjectMirrorRefs,
|
|
50
|
-
hashProjectMirrorRefs
|
|
81
|
+
hashProjectMirrorRefs,
|
|
82
|
+
parseProjectMirrorRefListing,
|
|
83
|
+
projectMirrorMountRefs,
|
|
84
|
+
projectMirrorRefsTokens
|
|
51
85
|
});
|
|
@@ -32,7 +32,7 @@ __export(project_worktrees_exports, {
|
|
|
32
32
|
ProjectBranchCreationRollbackIncompleteError: () => ProjectBranchCreationRollbackIncompleteError,
|
|
33
33
|
applyObservedProjectMirrorHeads: () => applyObservedProjectMirrorHeads,
|
|
34
34
|
assertProjectMirrorHeadsPushed: () => assertProjectMirrorHeadsPushed,
|
|
35
|
-
|
|
35
|
+
computeLocalProjectMirrorRefsTokens: () => computeLocalProjectMirrorRefsTokens,
|
|
36
36
|
createLinkedProjectBranch: () => createLinkedProjectBranch,
|
|
37
37
|
createOrRetryLinkedProjectBranch: () => createOrRetryLinkedProjectBranch,
|
|
38
38
|
deleteLinkedProjectBranch: () => deleteLinkedProjectBranch,
|
|
@@ -1174,8 +1174,12 @@ function createLinkedProjectBranch(input) {
|
|
|
1174
1174
|
);
|
|
1175
1175
|
configureProjectBranchTracking(targetPath, input.branchName, input.branchName);
|
|
1176
1176
|
assertNoSymlinkBranchPathComponents(input.projectRoot, targetPath);
|
|
1177
|
-
|
|
1178
|
-
|
|
1177
|
+
if (input.workingTree === "carry") {
|
|
1178
|
+
git(targetPath, ["read-tree", sourceHead], `initialize linked worktree index ${input.branchName}`);
|
|
1179
|
+
(0, import_working_tree_mirror.mirrorWorkingTree)({ sourceRoot: sourcePath, targetRoot: targetPath, sourceMode: "git", deletionMode: "git" });
|
|
1180
|
+
} else {
|
|
1181
|
+
git(targetPath, ["reset", "--hard", sourceHead], `check out linked worktree ${input.branchName}`);
|
|
1182
|
+
}
|
|
1179
1183
|
return { branchPath: targetPath, baseCommitHash: sourceHead };
|
|
1180
1184
|
} catch (error) {
|
|
1181
1185
|
if (branchCreated) {
|
|
@@ -1365,24 +1369,46 @@ function assertProjectMirrorHeadsPushed(results) {
|
|
|
1365
1369
|
throw new Error(`Hidden project mirror publication skipped ${skipped.branchName}${skipped.reason ? ` (${skipped.reason})` : ""}`);
|
|
1366
1370
|
}
|
|
1367
1371
|
}
|
|
1372
|
+
const PROJECT_MIRROR_FETCH_REFSPECS = ["+refs/heads/*:refs/r5d/mirror/*", "+refs/r5d/server-synced/*:refs/r5d/server-synced/*"];
|
|
1373
|
+
function projectMirrorMountFetchRefspecs(branchName, branchId) {
|
|
1374
|
+
(0, import_managed_paths.validateManagedBranchName)(branchName);
|
|
1375
|
+
if (branchId !== void 0 && !/^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/.test(branchId)) throw new Error(`Invalid branch id for ${branchName}`);
|
|
1376
|
+
return [
|
|
1377
|
+
`+refs/heads/${branchName}:refs/r5d/mirror/${branchName}`,
|
|
1378
|
+
branchId === void 0 ? "+refs/r5d/server-synced/*:refs/r5d/server-synced/*" : `+refs/r5d/server-synced/${branchId}/*:refs/r5d/server-synced/${branchId}/*`
|
|
1379
|
+
];
|
|
1380
|
+
}
|
|
1381
|
+
async function fetchProjectMirrorRefs(input) {
|
|
1382
|
+
const fetch = (refspecs) => gitResultAsync(
|
|
1383
|
+
input.primaryPath,
|
|
1384
|
+
[
|
|
1385
|
+
...(0, import_git_process_environment.gitTransportSecurityArgs)(input.mirrorUrl, input.credentialHelper, input.credentialUsername),
|
|
1386
|
+
"fetch",
|
|
1387
|
+
"--no-recurse-submodules",
|
|
1388
|
+
"--prune",
|
|
1389
|
+
input.mirrorUrl,
|
|
1390
|
+
...refspecs
|
|
1391
|
+
],
|
|
1392
|
+
PROJECT_GIT_TRANSPORT_TIMEOUT_MS
|
|
1393
|
+
);
|
|
1394
|
+
if (input.fetchOnlyBranches && input.fetchOnlyBranches.size > 0) {
|
|
1395
|
+
const refspecs = [...input.fetchOnlyBranches].sort().flatMap((branchName) => projectMirrorMountFetchRefspecs(branchName, input.branchIds?.get(branchName)));
|
|
1396
|
+
const scoped = await fetch(refspecs);
|
|
1397
|
+
if (scoped.exitCode === 0) return;
|
|
1398
|
+
if (!/(couldn't find remote ref|no such ref)/i.test(`${scoped.stderr}
|
|
1399
|
+
${scoped.stdout}`)) {
|
|
1400
|
+
throw new Error(`Refresh project head mirror: ${scoped.stderr || scoped.stdout || `git exited ${scoped.exitCode}`}`);
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
const whole = await fetch(PROJECT_MIRROR_FETCH_REFSPECS);
|
|
1404
|
+
if (whole.exitCode !== 0) {
|
|
1405
|
+
throw new Error(`Refresh project head mirror: ${whole.stderr || whole.stdout || `git exited ${whole.exitCode}`}`);
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1368
1408
|
async function observeProjectMirrorHeads(input) {
|
|
1369
1409
|
const primaryPath = branchPath(input.projectRoot, input.primaryBranchName);
|
|
1370
|
-
if (input.refreshRemote !== false) {
|
|
1371
|
-
|
|
1372
|
-
primaryPath,
|
|
1373
|
-
[
|
|
1374
|
-
...(0, import_git_process_environment.gitTransportSecurityArgs)(input.mirrorUrl, input.credentialHelper, input.credentialUsername),
|
|
1375
|
-
"fetch",
|
|
1376
|
-
"--no-recurse-submodules",
|
|
1377
|
-
"--prune",
|
|
1378
|
-
input.mirrorUrl,
|
|
1379
|
-
"+refs/heads/*:refs/r5d/mirror/*",
|
|
1380
|
-
"+refs/r5d/server-synced/*:refs/r5d/server-synced/*"
|
|
1381
|
-
],
|
|
1382
|
-
PROJECT_GIT_TRANSPORT_TIMEOUT_MS
|
|
1383
|
-
);
|
|
1384
|
-
if (fetch.exitCode !== 0)
|
|
1385
|
-
throw new Error(`Refresh project head mirror: ${fetch.stderr || fetch.stdout || `git exited ${fetch.exitCode}`}`);
|
|
1410
|
+
if (input.refreshRemote !== false && !(input.fetchOnlyBranches && input.fetchOnlyBranches.size === 0)) {
|
|
1411
|
+
await fetchProjectMirrorRefs({ ...input, primaryPath });
|
|
1386
1412
|
}
|
|
1387
1413
|
const observations = [];
|
|
1388
1414
|
for (const branchName of [...input.branchNames].sort()) {
|
|
@@ -1429,23 +1455,14 @@ async function observeProjectMirrorHeads(input) {
|
|
|
1429
1455
|
}
|
|
1430
1456
|
return observations;
|
|
1431
1457
|
}
|
|
1432
|
-
async function
|
|
1458
|
+
async function computeLocalProjectMirrorRefsTokens(input) {
|
|
1433
1459
|
const primaryPath = branchPath(input.projectRoot, input.primaryBranchName);
|
|
1434
1460
|
const rows = await gitAsync(
|
|
1435
1461
|
primaryPath,
|
|
1436
|
-
["for-each-ref",
|
|
1462
|
+
["for-each-ref", import_project_mirror_refs_token.PROJECT_MIRROR_REF_LISTING_FORMAT, import_project_mirror_refs_token.PROJECT_MIRROR_REF_PREFIX, import_project_mirror_refs_token.PROJECT_SERVER_SYNCED_REF_PREFIX],
|
|
1437
1463
|
"list cached project mirror refs"
|
|
1438
1464
|
);
|
|
1439
|
-
|
|
1440
|
-
const separator = row.indexOf(" ");
|
|
1441
|
-
const objectName = separator < 0 ? "" : row.slice(0, separator);
|
|
1442
|
-
const refname = separator < 0 ? "" : row.slice(separator + 1);
|
|
1443
|
-
if (!/^[0-9a-f]{40,64}$/.test(objectName) || refname.length === 0) {
|
|
1444
|
-
throw new Error(`Invalid cached mirror ref listing row: ${row}`);
|
|
1445
|
-
}
|
|
1446
|
-
return { refname, objectName };
|
|
1447
|
-
});
|
|
1448
|
-
return (0, import_project_mirror_refs_token.hashProjectMirrorRefs)((0, import_project_mirror_refs_token.canonicalizeLocalProjectMirrorRefs)(refs));
|
|
1465
|
+
return (0, import_project_mirror_refs_token.projectMirrorRefsTokens)((0, import_project_mirror_refs_token.canonicalizeLocalProjectMirrorRefs)((0, import_project_mirror_refs_token.parseProjectMirrorRefListing)(rows)), input.mounts);
|
|
1449
1466
|
}
|
|
1450
1467
|
async function applyObservedProjectMirrorHeads(input) {
|
|
1451
1468
|
const results = [];
|
|
@@ -1529,7 +1546,7 @@ const projectWorktreesTestHarness = {
|
|
|
1529
1546
|
ProjectBranchCreationRollbackIncompleteError,
|
|
1530
1547
|
applyObservedProjectMirrorHeads,
|
|
1531
1548
|
assertProjectMirrorHeadsPushed,
|
|
1532
|
-
|
|
1549
|
+
computeLocalProjectMirrorRefsTokens,
|
|
1533
1550
|
createLinkedProjectBranch,
|
|
1534
1551
|
createOrRetryLinkedProjectBranch,
|
|
1535
1552
|
deleteLinkedProjectBranch,
|
|
@@ -1389,6 +1389,19 @@ ${result.stdout}`;
|
|
|
1389
1389
|
}
|
|
1390
1390
|
return revParse(workspacePath, `refs/remotes/origin/${WORKSPACE_GIT_BRANCH}`);
|
|
1391
1391
|
}
|
|
1392
|
+
async function resolveWorkspaceRemoteHead(workspacePath, input) {
|
|
1393
|
+
const advertised = input.advertisedRemoteHead;
|
|
1394
|
+
if (advertised && /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(advertised)) {
|
|
1395
|
+
const tracked = await revParseAsync(workspacePath, `refs/remotes/origin/${WORKSPACE_GIT_BRANCH}`);
|
|
1396
|
+
if (tracked === advertised && await tryGitAsync(workspacePath, ["cat-file", "-e", `${tracked}^{commit}`])) {
|
|
1397
|
+
return { head: tracked, fetched: false };
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
return {
|
|
1401
|
+
head: await fetchWorkspaceHead(workspacePath, input.remoteUrl, input.credentialHelper, input.credentialUsername),
|
|
1402
|
+
fetched: true
|
|
1403
|
+
};
|
|
1404
|
+
}
|
|
1392
1405
|
async function ensureWorkspaceGitClone(input) {
|
|
1393
1406
|
const workspacePath = import_node_path.default.resolve(input.workspacePath);
|
|
1394
1407
|
const workspaceParent = import_node_path.default.dirname(workspacePath);
|
|
@@ -1436,7 +1449,8 @@ async function ensureWorkspaceGitClone(input) {
|
|
|
1436
1449
|
if (!await revParseAsync(workspacePath, WORKSPACE_GIT_INTEGRATED_REF) && previousRemoteHead && localHeadBeforeFetch && await tryGitAsync(workspacePath, ["merge-base", "--is-ancestor", previousRemoteHead, localHeadBeforeFetch])) {
|
|
1437
1450
|
await updateIntegratedWorkspaceHead(workspacePath, previousRemoteHead);
|
|
1438
1451
|
}
|
|
1439
|
-
const
|
|
1452
|
+
const resolvedRemoteHead = await resolveWorkspaceRemoteHead(workspacePath, input);
|
|
1453
|
+
const remoteHead = resolvedRemoteHead.head;
|
|
1440
1454
|
let localHead = await revParseAsync(workspacePath, "HEAD");
|
|
1441
1455
|
if (!localHead && remoteHead) {
|
|
1442
1456
|
await runWorkspaceCheckoutTransition(workspacePath, async () => {
|
|
@@ -1457,7 +1471,7 @@ async function ensureWorkspaceGitClone(input) {
|
|
|
1457
1471
|
await updateIntegratedWorkspaceHead(workspacePath, remoteHead);
|
|
1458
1472
|
localHead = remoteHead;
|
|
1459
1473
|
}
|
|
1460
|
-
return { localHead, remoteHead };
|
|
1474
|
+
return { localHead, remoteHead, remoteHeadFetched: resolvedRemoteHead.fetched };
|
|
1461
1475
|
}
|
|
1462
1476
|
function activeMounts(mounts) {
|
|
1463
1477
|
const active = [];
|
|
@@ -2106,6 +2120,11 @@ async function synchronizeWithFetchedHead(input) {
|
|
|
2106
2120
|
return rebased.ok ? { kind: "ready", updated: false, rebased: true } : { kind: "conflict", conflictPaths: rebased.conflictPaths, refs: rebased.refs, error: rebased.error };
|
|
2107
2121
|
}
|
|
2108
2122
|
async function synchronizeWorkspaceGit(input) {
|
|
2123
|
+
const remoteHeadFetches = { count: 0 };
|
|
2124
|
+
const result = await runWorkspaceGitSynchronization(input, remoteHeadFetches);
|
|
2125
|
+
return { ...result, remoteHeadFetches: remoteHeadFetches.count };
|
|
2126
|
+
}
|
|
2127
|
+
async function runWorkspaceGitSynchronization(input, remoteHeadFetches) {
|
|
2109
2128
|
const workspacePath = import_node_path.default.resolve(input.workspacePath);
|
|
2110
2129
|
const attemptId = input.attemptId ?? crypto.randomUUID();
|
|
2111
2130
|
const maxDiffBytes = input.maxDiffBytes ?? MAX_WORKSPACE_GIT_DIFF_BYTES;
|
|
@@ -2114,6 +2133,7 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
2114
2133
|
const requiredAncestorHeads = requiredWorkspaceAncestorHeads(input.requiredAncestorHeads);
|
|
2115
2134
|
const preserveResolutionInProgress = input.skipMountMirror === true;
|
|
2116
2135
|
const initial = await ensureWorkspaceGitClone({ ...input, workspacePath, preserveResolutionInProgress });
|
|
2136
|
+
if (initial.remoteHeadFetched) remoteHeadFetches.count += 1;
|
|
2117
2137
|
assertWorkspaceRemediationAncestry({
|
|
2118
2138
|
workspacePath,
|
|
2119
2139
|
currentHead: initial.localHead,
|
|
@@ -2161,11 +2181,22 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
2161
2181
|
const classifyMounts = (activeCandidates, additionalSkippedIds = []) => {
|
|
2162
2182
|
refreshCycleSkippedMounts();
|
|
2163
2183
|
const skippedIds = /* @__PURE__ */ new Set([...cycleSkippedMountIds, ...projectionSkippedIds, ...additionalSkippedIds]);
|
|
2184
|
+
const activeMountIds = [...new Set(activeCandidates.filter(({ id }) => !skippedIds.has(id)).map(({ id }) => id))].sort();
|
|
2164
2185
|
return {
|
|
2165
|
-
activeMountIds
|
|
2166
|
-
skippedMountIds: [...skippedIds].sort()
|
|
2186
|
+
activeMountIds,
|
|
2187
|
+
skippedMountIds: [...skippedIds].sort(),
|
|
2188
|
+
// An active mount's token is unchanged since capture (a moved token
|
|
2189
|
+
// lands it in cycleSkippedMountIds above), so this is the token its
|
|
2190
|
+
// projected bytes belong to.
|
|
2191
|
+
projectedMutationTokens: Object.fromEntries(
|
|
2192
|
+
activeMountIds.flatMap((id) => {
|
|
2193
|
+
const token = projectionMutationTokens.get(id);
|
|
2194
|
+
return token === void 0 ? [] : [[id, token]];
|
|
2195
|
+
})
|
|
2196
|
+
)
|
|
2167
2197
|
};
|
|
2168
2198
|
};
|
|
2199
|
+
const mountProjectionIsCurrent = (mount) => mount.lastProjectedMutationToken !== void 0 && !mount.deleteWhenSourceMissing && mount.preserveLocalOnHydrationBasisChange !== true && projectionBasisHead !== null && workspaceHydrationReceiptMountBasisHead(projectionBasisReceipt, mount) === projectionBasisHead && projectionMutationTokens.get(mount.id) === mount.lastProjectedMutationToken && mount.mutationToken?.() === mount.lastProjectedMutationToken;
|
|
2169
2200
|
if (!input.skipMountMirror) {
|
|
2170
2201
|
const mergeProjectionEnabled = process.env.R5D_WORKSPACE_MERGE_PROJECTION !== "0";
|
|
2171
2202
|
const mergeSupport = mergeProjectionEnabled ? (0, import_workspace_merge_projection.workspaceMergeProjectionSupport)() : { supported: false, error: "Workspace merge projection is disabled by R5D_WORKSPACE_MERGE_PROJECTION=0" };
|
|
@@ -2304,6 +2335,7 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
2304
2335
|
}
|
|
2305
2336
|
};
|
|
2306
2337
|
const projectedFastMounts = [];
|
|
2338
|
+
const currentProjectionMounts = [];
|
|
2307
2339
|
if (!input.skipMountMirror) {
|
|
2308
2340
|
beginWorkspaceCheckoutTransition(workspacePath);
|
|
2309
2341
|
try {
|
|
@@ -2315,6 +2347,10 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
2315
2347
|
cycleSkippedMountIds.add(mount.id);
|
|
2316
2348
|
continue;
|
|
2317
2349
|
}
|
|
2350
|
+
if (mountProjectionIsCurrent(mount)) {
|
|
2351
|
+
currentProjectionMounts.push(mount);
|
|
2352
|
+
continue;
|
|
2353
|
+
}
|
|
2318
2354
|
projectedGitlinks.push(...mirrorMountsToWorkspace(workspacePath, [mount]));
|
|
2319
2355
|
projectedFastMounts.push(mount);
|
|
2320
2356
|
await yieldToWorkerEventLoop();
|
|
@@ -2348,7 +2384,7 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
2348
2384
|
const targetProjectionReceipt = receiptWithMountsAtHead(
|
|
2349
2385
|
projectionBasisReceipt,
|
|
2350
2386
|
projectedHead,
|
|
2351
|
-
projectedFastMounts,
|
|
2387
|
+
[...projectedFastMounts, ...currentProjectionMounts],
|
|
2352
2388
|
allProjectionReceiptMounts
|
|
2353
2389
|
);
|
|
2354
2390
|
const currentProjectionReceipt = await readHydratedWorkspaceReceiptAsync(workspacePath);
|
|
@@ -2361,7 +2397,9 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
2361
2397
|
}
|
|
2362
2398
|
await completeWorkspaceCheckoutTransition(workspacePath);
|
|
2363
2399
|
}
|
|
2364
|
-
|
|
2400
|
+
const prePublishRemoteHead = await resolveWorkspaceRemoteHead(workspacePath, input);
|
|
2401
|
+
if (prePublishRemoteHead.fetched) remoteHeadFetches.count += 1;
|
|
2402
|
+
let remoteHead = prePublishRemoteHead.head;
|
|
2365
2403
|
let rebaseCount = 0;
|
|
2366
2404
|
let updated = false;
|
|
2367
2405
|
for (let pushAttempt = 0; pushAttempt < maxPushAttempts; pushAttempt += 1) {
|
|
@@ -2518,6 +2556,7 @@ ${push.stdout}`)) {
|
|
|
2518
2556
|
throw new Error(`Push workspace main: ${push.stderr || push.stdout || `git exited ${push.exitCode}`}`);
|
|
2519
2557
|
}
|
|
2520
2558
|
remoteHead = await fetchWorkspaceHead(workspacePath, input.remoteUrl, input.credentialHelper, input.credentialUsername);
|
|
2559
|
+
remoteHeadFetches.count += 1;
|
|
2521
2560
|
}
|
|
2522
2561
|
throw new Error(`Workspace push did not converge after ${maxPushAttempts} attempts`);
|
|
2523
2562
|
}
|
|
@@ -50,8 +50,43 @@ class WorkspaceMutationGate {
|
|
|
50
50
|
release();
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
|
+
/** A worktree create/delete lease; see the branch lane in the class comment. */
|
|
54
|
+
acquireBranchMutation() {
|
|
55
|
+
return new Promise((resolve) => {
|
|
56
|
+
this.waiters.push({ kind: "branch", resolve });
|
|
57
|
+
this.drain();
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
async runBranchMutation(operation) {
|
|
61
|
+
const release = await this.acquireBranchMutation();
|
|
62
|
+
try {
|
|
63
|
+
return await operation();
|
|
64
|
+
} finally {
|
|
65
|
+
release();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Operations a branch mutation requested now would wait for: the running
|
|
70
|
+
* sync, if any, plus branch mutations queued behind it. Zero means it runs
|
|
71
|
+
* at once. Ordinary waiters are not counted because the branch lane admits
|
|
72
|
+
* ahead of them.
|
|
73
|
+
*/
|
|
74
|
+
branchMutationQueuePosition() {
|
|
75
|
+
if (!this.syncActive) return 0;
|
|
76
|
+
return 1 + this.waiters.filter((waiter) => waiter.kind === "branch").length;
|
|
77
|
+
}
|
|
53
78
|
drain() {
|
|
54
79
|
if (this.syncActive || this.waiters.length === 0) return;
|
|
80
|
+
for (let index = 0; index < this.waiters.length; ) {
|
|
81
|
+
const waiter = this.waiters[index];
|
|
82
|
+
if (waiter?.kind !== "branch") {
|
|
83
|
+
index += 1;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
this.waiters.splice(index, 1);
|
|
87
|
+
this.activeMutations += 1;
|
|
88
|
+
waiter.resolve(this.releaseMutation());
|
|
89
|
+
}
|
|
55
90
|
const first = this.waiters[0];
|
|
56
91
|
if (first?.kind === "sync") {
|
|
57
92
|
if (this.activeMutations > 0) return;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var workspace_projection_ledger_exports = {};
|
|
20
|
+
__export(workspace_projection_ledger_exports, {
|
|
21
|
+
WorkspaceProjectionLedger: () => WorkspaceProjectionLedger
|
|
22
|
+
});
|
|
23
|
+
module.exports = __toCommonJS(workspace_projection_ledger_exports);
|
|
24
|
+
const RETAINED_OUTCOMES = /* @__PURE__ */ new Set(["no_change", "updated", "published", "large_diff_blocked"]);
|
|
25
|
+
class WorkspaceProjectionLedger {
|
|
26
|
+
projectedTokens = /* @__PURE__ */ new Map();
|
|
27
|
+
lastCycle = null;
|
|
28
|
+
static recordKey(mount) {
|
|
29
|
+
return `${mount.hydrationIncarnationKey}\0${mount.id}`;
|
|
30
|
+
}
|
|
31
|
+
get lastCycleResult() {
|
|
32
|
+
return this.lastCycle;
|
|
33
|
+
}
|
|
34
|
+
lastProjectedToken(mount) {
|
|
35
|
+
return this.projectedTokens.get(WorkspaceProjectionLedger.recordKey(mount));
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Record a completed cycle. `projectedMutationTokens` names the token each
|
|
39
|
+
* active mount carried when the cycle projected it; a mount absent from it
|
|
40
|
+
* keeps its earlier record. Any outcome that leaves the outer clone without
|
|
41
|
+
* a trustworthy projection clears the ledger.
|
|
42
|
+
*/
|
|
43
|
+
recordCycle(input) {
|
|
44
|
+
if (!RETAINED_OUTCOMES.has(input.result.outcome)) {
|
|
45
|
+
this.reset();
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const mountsById = new Map(input.mounts.map((mount) => [mount.id, mount]));
|
|
49
|
+
for (const [mountId, token] of Object.entries(input.projectedMutationTokens ?? {})) {
|
|
50
|
+
const mount = mountsById.get(mountId);
|
|
51
|
+
if (mount) this.projectedTokens.set(WorkspaceProjectionLedger.recordKey(mount), token);
|
|
52
|
+
}
|
|
53
|
+
this.lastCycle = input.result;
|
|
54
|
+
}
|
|
55
|
+
reset() {
|
|
56
|
+
this.projectedTokens.clear();
|
|
57
|
+
this.lastCycle = null;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The previous cycle's result when every request targets a configured
|
|
61
|
+
* mount and no mount's token moved since its recorded projection. A request
|
|
62
|
+
* without a target (a workspace-rooted command may touch any checkout), a
|
|
63
|
+
* mount without a token (a pending deletion), or pending deletion work all
|
|
64
|
+
* require a real cycle.
|
|
65
|
+
*/
|
|
66
|
+
answerFromLastCycle(input) {
|
|
67
|
+
if (!this.lastCycle || input.pendingDeletions || input.targetMountIds.length === 0) return null;
|
|
68
|
+
const mountIds = new Set(input.mounts.map(({ id }) => id));
|
|
69
|
+
if (!input.targetMountIds.every((mountId) => mountId !== null && mountIds.has(mountId))) return null;
|
|
70
|
+
for (const mount of input.mounts) {
|
|
71
|
+
if (!mount.mutationToken) return null;
|
|
72
|
+
const recorded = this.projectedTokens.get(WorkspaceProjectionLedger.recordKey(mount));
|
|
73
|
+
if (recorded === void 0 || recorded !== mount.mutationToken()) return null;
|
|
74
|
+
}
|
|
75
|
+
return this.lastCycle;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
79
|
+
0 && (module.exports = {
|
|
80
|
+
WorkspaceProjectionLedger
|
|
81
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var workspace_sync_coalescer_exports = {};
|
|
20
|
+
__export(workspace_sync_coalescer_exports, {
|
|
21
|
+
WorkspaceSyncCoalescer: () => WorkspaceSyncCoalescer
|
|
22
|
+
});
|
|
23
|
+
module.exports = __toCommonJS(workspace_sync_coalescer_exports);
|
|
24
|
+
class WorkspaceSyncCoalescer {
|
|
25
|
+
constructor(options) {
|
|
26
|
+
this.options = options;
|
|
27
|
+
}
|
|
28
|
+
options;
|
|
29
|
+
pendingBatch = null;
|
|
30
|
+
batchesStarted = 0;
|
|
31
|
+
/** Requests attached to the batch that has not started yet. */
|
|
32
|
+
get queuedRequestCount() {
|
|
33
|
+
return this.pendingBatch?.requests.length ?? 0;
|
|
34
|
+
}
|
|
35
|
+
/** Batches whose body has been entered, whether or not they ran a cycle. */
|
|
36
|
+
get startedBatchCount() {
|
|
37
|
+
return this.batchesStarted;
|
|
38
|
+
}
|
|
39
|
+
request(request) {
|
|
40
|
+
const queued = this.pendingBatch;
|
|
41
|
+
if (queued) {
|
|
42
|
+
queued.requests.push(request);
|
|
43
|
+
return queued.settlement;
|
|
44
|
+
}
|
|
45
|
+
const batch = { requests: [request], settlement: Promise.resolve() };
|
|
46
|
+
this.pendingBatch = batch;
|
|
47
|
+
batch.settlement = this.options.runExclusive(async () => {
|
|
48
|
+
if (this.pendingBatch === batch) this.pendingBatch = null;
|
|
49
|
+
this.batchesStarted += 1;
|
|
50
|
+
return await this.options.runBatch(batch.requests);
|
|
51
|
+
});
|
|
52
|
+
return batch.settlement;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
56
|
+
0 && (module.exports = {
|
|
57
|
+
WorkspaceSyncCoalescer
|
|
58
|
+
});
|