@ricsam/r5d-worker 0.0.101 → 0.0.104
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 +467 -336
- 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 +172 -63
- 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 +473 -338
- 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 +172 -64
- 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 +35 -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 +73 -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
|
});
|
|
@@ -28,11 +28,13 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
28
28
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
29
|
var project_worktrees_exports = {};
|
|
30
30
|
__export(project_worktrees_exports, {
|
|
31
|
+
PROJECT_REMOVED_CHECKOUTS_DIRECTORY: () => PROJECT_REMOVED_CHECKOUTS_DIRECTORY,
|
|
31
32
|
PROJECT_WORKTREE_SNAPSHOT_PREFIX: () => PROJECT_WORKTREE_SNAPSHOT_PREFIX,
|
|
32
33
|
ProjectBranchCreationRollbackIncompleteError: () => ProjectBranchCreationRollbackIncompleteError,
|
|
33
34
|
applyObservedProjectMirrorHeads: () => applyObservedProjectMirrorHeads,
|
|
34
35
|
assertProjectMirrorHeadsPushed: () => assertProjectMirrorHeadsPushed,
|
|
35
|
-
|
|
36
|
+
classifyProjectCheckout: () => classifyProjectCheckout,
|
|
37
|
+
computeLocalProjectMirrorRefsTokens: () => computeLocalProjectMirrorRefsTokens,
|
|
36
38
|
createLinkedProjectBranch: () => createLinkedProjectBranch,
|
|
37
39
|
createOrRetryLinkedProjectBranch: () => createOrRetryLinkedProjectBranch,
|
|
38
40
|
deleteLinkedProjectBranch: () => deleteLinkedProjectBranch,
|
|
@@ -40,6 +42,7 @@ __export(project_worktrees_exports, {
|
|
|
40
42
|
ensureProjectWorktrees: () => ensureProjectWorktrees,
|
|
41
43
|
fastForwardProjectHeadsFromMirror: () => fastForwardProjectHeadsFromMirror,
|
|
42
44
|
hasLinkedProjectWorktreeLayout: () => hasLinkedProjectWorktreeLayout,
|
|
45
|
+
inventoryProjectCheckouts: () => inventoryProjectCheckouts,
|
|
43
46
|
observeProjectMirrorHeads: () => observeProjectMirrorHeads,
|
|
44
47
|
projectBranchMayExistAfterCreateFailure: () => projectBranchMayExistAfterCreateFailure,
|
|
45
48
|
projectOriginBranchName: () => projectOriginBranchName,
|
|
@@ -334,6 +337,8 @@ function normalizeSnapshotManifest(value) {
|
|
|
334
337
|
if (record.state !== "building" && record.state !== "prepared" && record.state !== "restoring" && record.state !== "consumed") {
|
|
335
338
|
throw new Error("Project worktree snapshot manifest has an invalid state");
|
|
336
339
|
}
|
|
340
|
+
const scope = record.scope === void 0 ? "project" : record.scope;
|
|
341
|
+
if (scope !== "project" && scope !== "branches") throw new Error("Project worktree snapshot manifest has an invalid scope");
|
|
337
342
|
if (!Number.isSafeInteger(record.ownerProcessId) || record.ownerProcessId <= 0) {
|
|
338
343
|
throw new Error("Project worktree snapshot manifest has an invalid owner process");
|
|
339
344
|
}
|
|
@@ -369,6 +374,7 @@ function normalizeSnapshotManifest(value) {
|
|
|
369
374
|
return {
|
|
370
375
|
version: PROJECT_WORKTREE_SNAPSHOT_MANIFEST_VERSION,
|
|
371
376
|
state: record.state,
|
|
377
|
+
scope,
|
|
372
378
|
ownerProcessId: record.ownerProcessId,
|
|
373
379
|
ownerSessionId: record.ownerSessionId,
|
|
374
380
|
projectRoot: record.projectRoot,
|
|
@@ -405,7 +411,7 @@ function writeSnapshotManifest(snapshotRoot, manifest) {
|
|
|
405
411
|
return readSnapshotManifest(snapshotRoot);
|
|
406
412
|
}
|
|
407
413
|
function assertSameSnapshotIdentity(expected, actual) {
|
|
408
|
-
if (expected.ownerProcessId !== actual.ownerProcessId || expected.ownerSessionId !== actual.ownerSessionId || expected.projectRoot !== actual.projectRoot || expected.createdAt !== actual.createdAt || JSON.stringify(expected.branches) !== JSON.stringify(actual.branches)) {
|
|
414
|
+
if (expected.scope !== actual.scope || expected.ownerProcessId !== actual.ownerProcessId || expected.ownerSessionId !== actual.ownerSessionId || expected.projectRoot !== actual.projectRoot || expected.createdAt !== actual.createdAt || JSON.stringify(expected.branches) !== JSON.stringify(actual.branches)) {
|
|
409
415
|
throw new Error("Project worktree snapshot identity changed");
|
|
410
416
|
}
|
|
411
417
|
}
|
|
@@ -472,7 +478,7 @@ function assertNoOutstandingSnapshotForProject(temporaryRoot, projectRoot) {
|
|
|
472
478
|
}
|
|
473
479
|
}
|
|
474
480
|
}
|
|
475
|
-
function snapshotBranchTrees(projectRoot, branches, temporaryRoot, onProgress) {
|
|
481
|
+
function snapshotBranchTrees(projectRoot, branches, temporaryRoot, onProgress, scope = "project") {
|
|
476
482
|
const resolvedProjectRoot = import_node_path.default.resolve(projectRoot);
|
|
477
483
|
const resolvedTemporaryRoot = import_node_path.default.resolve(temporaryRoot ?? import_node_os.default.tmpdir());
|
|
478
484
|
assertNoOutstandingSnapshotForProject(resolvedTemporaryRoot, resolvedProjectRoot);
|
|
@@ -493,6 +499,7 @@ function snapshotBranchTrees(projectRoot, branches, temporaryRoot, onProgress) {
|
|
|
493
499
|
let manifest = writeSnapshotManifest(root, {
|
|
494
500
|
version: PROJECT_WORKTREE_SNAPSHOT_MANIFEST_VERSION,
|
|
495
501
|
state: "building",
|
|
502
|
+
scope,
|
|
496
503
|
ownerProcessId: process.pid,
|
|
497
504
|
ownerSessionId: PROJECT_WORKTREE_SNAPSHOT_OWNER_SESSION_ID,
|
|
498
505
|
projectRoot: resolvedProjectRoot,
|
|
@@ -599,7 +606,7 @@ function restoreProjectWorktreeSnapshot(snapshotRoot, manifest, onProgress) {
|
|
|
599
606
|
const presentBranches = current.branches.filter(({ snapshotPresent }) => snapshotPresent);
|
|
600
607
|
const destinationParent = import_node_path.default.dirname(current.projectRoot);
|
|
601
608
|
import_node_fs.default.mkdirSync(destinationParent, { recursive: true });
|
|
602
|
-
const canMoveWholeSnapshot = presentBranches.length > 0 && (current.state === "restoring" || treesRootStat !== null && treesRootStat.dev === import_node_fs.default.lstatSync(destinationParent).dev);
|
|
609
|
+
const canMoveWholeSnapshot = current.scope === "project" && presentBranches.length > 0 && (current.state === "restoring" || treesRootStat !== null && treesRootStat.dev === import_node_fs.default.lstatSync(destinationParent).dev);
|
|
603
610
|
if (canMoveWholeSnapshot) {
|
|
604
611
|
if (current.state === "prepared") current = advanceSnapshotState(snapshotRoot, current, "restoring");
|
|
605
612
|
onProgress?.({
|
|
@@ -690,8 +697,12 @@ function consumeAndRemoveProjectWorktreeSnapshot(snapshotRoot, manifest) {
|
|
|
690
697
|
} catch {
|
|
691
698
|
}
|
|
692
699
|
}
|
|
693
|
-
function persistReconciledProjectAndConsumeSnapshot(snapshot, afterProjectTreeFsync) {
|
|
694
|
-
|
|
700
|
+
function persistReconciledProjectAndConsumeSnapshot(snapshot, afterProjectTreeFsync, durabilityPaths) {
|
|
701
|
+
if (durabilityPaths) {
|
|
702
|
+
for (const durabilityPath of durabilityPaths) if (import_node_fs.default.existsSync(durabilityPath)) fsyncTree(durabilityPath);
|
|
703
|
+
} else {
|
|
704
|
+
fsyncTree(snapshot.manifest.projectRoot);
|
|
705
|
+
}
|
|
695
706
|
fsyncManagedProjectRootEntry(snapshot.manifest.projectRoot);
|
|
696
707
|
afterProjectTreeFsync?.();
|
|
697
708
|
consumeAndRemoveProjectWorktreeSnapshot(snapshot.root, snapshot.manifest);
|
|
@@ -989,8 +1000,10 @@ async function reconcileLinkedLayout(input) {
|
|
|
989
1000
|
await configureRepository({ ...input, primaryPath });
|
|
990
1001
|
await tryGitAsync(primaryPath, ["worktree", "prune"]);
|
|
991
1002
|
await fetchProjectHeads({ ...input, primaryPath });
|
|
1003
|
+
const primaryCommonDir = await commonGitDirectoryAsync(primaryPath);
|
|
1004
|
+
if (!primaryCommonDir) throw new Error("Primary project checkout is unavailable");
|
|
992
1005
|
const mirrorHeads = /* @__PURE__ */ new Map();
|
|
993
|
-
const
|
|
1006
|
+
const plan = [];
|
|
994
1007
|
for (const branch of input.branches) {
|
|
995
1008
|
const checkoutPath = branchPath(input.projectRoot, branch.branchName);
|
|
996
1009
|
const resolved = await seedForBranch({
|
|
@@ -1000,24 +1013,30 @@ async function reconcileLinkedLayout(input) {
|
|
|
1000
1013
|
requireMirrorHead: input.exactMirrorBranches?.has(branch.branchName)
|
|
1001
1014
|
});
|
|
1002
1015
|
mirrorHeads.set(branch.branchName, resolved.mirrorHead);
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
input.beforeDestructiveMutation();
|
|
1007
|
-
mutatedBranches.add(branch.branchName);
|
|
1008
|
-
await preserveHead(checkoutPath);
|
|
1009
|
-
await gitAsync(checkoutPath, ["reset", "--hard", resolved.seed], `anchor linked worktree ${branch.branchName}`);
|
|
1016
|
+
const linked = branch.branchName === input.primaryBranchName || gitDirectoryKind(checkoutPath) !== null && await commonGitDirectoryAsync(checkoutPath) === primaryCommonDir;
|
|
1017
|
+
if (!linked) {
|
|
1018
|
+
plan.push({ branchName: branch.branchName, checkoutPath, seed: resolved.seed, action: "recreate" });
|
|
1010
1019
|
continue;
|
|
1011
1020
|
}
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1021
|
+
const currentHead = await gitAsync(checkoutPath, ["rev-parse", "HEAD"], `resolve linked worktree head ${branch.branchName}`);
|
|
1022
|
+
plan.push({ branchName: branch.branchName, checkoutPath, seed: resolved.seed, action: currentHead === resolved.seed ? "keep" : "reset" });
|
|
1023
|
+
}
|
|
1024
|
+
const mutatedBranches = new Set(plan.filter(({ action }) => action !== "keep").map(({ branchName }) => branchName));
|
|
1025
|
+
if (mutatedBranches.size > 0) input.beforeDestructiveMutation(mutatedBranches);
|
|
1026
|
+
for (const entry of plan) {
|
|
1027
|
+
if (entry.action === "keep") continue;
|
|
1028
|
+
if (entry.action === "reset") {
|
|
1029
|
+
await preserveHead(entry.checkoutPath);
|
|
1030
|
+
await gitAsync(entry.checkoutPath, ["reset", "--hard", entry.seed], `anchor linked worktree ${entry.branchName}`);
|
|
1031
|
+
continue;
|
|
1032
|
+
}
|
|
1033
|
+
import_node_fs.default.rmSync(entry.checkoutPath, { recursive: true, force: true });
|
|
1034
|
+
await gitAsync(primaryPath, ["branch", "-f", entry.branchName, entry.seed], `anchor project branch ${entry.branchName}`);
|
|
1035
|
+
import_node_fs.default.mkdirSync(import_node_path.default.dirname(entry.checkoutPath), { recursive: true });
|
|
1017
1036
|
await gitAsync(
|
|
1018
1037
|
primaryPath,
|
|
1019
|
-
["worktree", "add", "--force", checkoutPath,
|
|
1020
|
-
`create linked worktree ${
|
|
1038
|
+
["worktree", "add", "--force", entry.checkoutPath, entry.branchName],
|
|
1039
|
+
`create linked worktree ${entry.branchName}`
|
|
1021
1040
|
);
|
|
1022
1041
|
}
|
|
1023
1042
|
return { mirrorHeads, mutatedBranches };
|
|
@@ -1044,8 +1063,14 @@ async function ensureProjectWorktrees(input) {
|
|
|
1044
1063
|
const snapshotStorageRoot = import_node_path.default.resolve(input.snapshotRoot ?? import_node_os.default.tmpdir());
|
|
1045
1064
|
assertNoOutstandingSnapshotForProject(snapshotStorageRoot, projectRoot);
|
|
1046
1065
|
let snapshot;
|
|
1047
|
-
const ensureSnapshot = () => {
|
|
1048
|
-
snapshot ??= snapshotBranchTrees(
|
|
1066
|
+
const ensureSnapshot = (branchNames) => {
|
|
1067
|
+
snapshot ??= snapshotBranchTrees(
|
|
1068
|
+
projectRoot,
|
|
1069
|
+
branchNames ? input.branches.filter(({ branchName }) => branchNames.has(branchName)) : input.branches,
|
|
1070
|
+
snapshotStorageRoot,
|
|
1071
|
+
input.onSnapshotProgress,
|
|
1072
|
+
branchNames ? "branches" : "project"
|
|
1073
|
+
);
|
|
1049
1074
|
return snapshot;
|
|
1050
1075
|
};
|
|
1051
1076
|
try {
|
|
@@ -1097,7 +1122,21 @@ async function ensureProjectWorktrees(input) {
|
|
|
1097
1122
|
});
|
|
1098
1123
|
}
|
|
1099
1124
|
states.sort((left, right) => left.branchName.localeCompare(right.branchName));
|
|
1100
|
-
if (snapshot)
|
|
1125
|
+
if (snapshot) {
|
|
1126
|
+
const primaryGitDirectory = import_node_path.default.join(branchPath(projectRoot, input.primaryBranchName), ".git");
|
|
1127
|
+
persistReconciledProjectAndConsumeSnapshot(
|
|
1128
|
+
snapshot,
|
|
1129
|
+
void 0,
|
|
1130
|
+
snapshot.manifest.scope === "branches" ? [
|
|
1131
|
+
...snapshot.manifest.branches.map(({ branchName }) => branchPath(projectRoot, branchName)),
|
|
1132
|
+
// Branch refs and worktree registrations moved in the shared
|
|
1133
|
+
// administrative directory; objects are fsynced by git itself.
|
|
1134
|
+
import_node_path.default.join(primaryGitDirectory, "refs"),
|
|
1135
|
+
import_node_path.default.join(primaryGitDirectory, "packed-refs"),
|
|
1136
|
+
import_node_path.default.join(primaryGitDirectory, "worktrees")
|
|
1137
|
+
] : void 0
|
|
1138
|
+
);
|
|
1139
|
+
}
|
|
1101
1140
|
return states;
|
|
1102
1141
|
} catch (error) {
|
|
1103
1142
|
if (!snapshot) throw error;
|
|
@@ -1174,8 +1213,12 @@ function createLinkedProjectBranch(input) {
|
|
|
1174
1213
|
);
|
|
1175
1214
|
configureProjectBranchTracking(targetPath, input.branchName, input.branchName);
|
|
1176
1215
|
assertNoSymlinkBranchPathComponents(input.projectRoot, targetPath);
|
|
1177
|
-
|
|
1178
|
-
|
|
1216
|
+
if (input.workingTree === "carry") {
|
|
1217
|
+
git(targetPath, ["read-tree", sourceHead], `initialize linked worktree index ${input.branchName}`);
|
|
1218
|
+
(0, import_working_tree_mirror.mirrorWorkingTree)({ sourceRoot: sourcePath, targetRoot: targetPath, sourceMode: "git", deletionMode: "git" });
|
|
1219
|
+
} else {
|
|
1220
|
+
git(targetPath, ["reset", "--hard", sourceHead], `check out linked worktree ${input.branchName}`);
|
|
1221
|
+
}
|
|
1179
1222
|
return { branchPath: targetPath, baseCommitHash: sourceHead };
|
|
1180
1223
|
} catch (error) {
|
|
1181
1224
|
if (branchCreated) {
|
|
@@ -1239,6 +1282,53 @@ function createOrRetryLinkedProjectBranch(input) {
|
|
|
1239
1282
|
}
|
|
1240
1283
|
return createLinkedProjectBranch(input);
|
|
1241
1284
|
}
|
|
1285
|
+
function classifyProjectCheckout(input) {
|
|
1286
|
+
const checkoutPath = branchPath(input.projectRoot, input.branchName);
|
|
1287
|
+
let stat;
|
|
1288
|
+
try {
|
|
1289
|
+
stat = import_node_fs.default.lstatSync(checkoutPath);
|
|
1290
|
+
} catch {
|
|
1291
|
+
return "missing";
|
|
1292
|
+
}
|
|
1293
|
+
if (!stat.isDirectory()) return "not_a_worktree";
|
|
1294
|
+
if (input.branchName === input.primaryBranchName) return gitDirectoryKind(checkoutPath) === "directory" ? "linked" : "not_a_worktree";
|
|
1295
|
+
if (gitDirectoryKind(checkoutPath) !== "file") return "not_a_worktree";
|
|
1296
|
+
const gitDirectory = perWorktreeGitDirectory(checkoutPath);
|
|
1297
|
+
if (!gitDirectory) return "not_a_worktree";
|
|
1298
|
+
let registry;
|
|
1299
|
+
let registered;
|
|
1300
|
+
try {
|
|
1301
|
+
registry = import_node_fs.default.realpathSync(import_node_path.default.join(branchPath(input.projectRoot, input.primaryBranchName), ".git", "worktrees"));
|
|
1302
|
+
registered = import_node_fs.default.realpathSync(gitDirectory);
|
|
1303
|
+
} catch {
|
|
1304
|
+
return "not_a_worktree";
|
|
1305
|
+
}
|
|
1306
|
+
const relative = import_node_path.default.relative(registry, registered);
|
|
1307
|
+
if (!relative || relative === ".." || relative.startsWith(`..${import_node_path.default.sep}`) || import_node_path.default.isAbsolute(relative)) return "not_a_worktree";
|
|
1308
|
+
return "linked";
|
|
1309
|
+
}
|
|
1310
|
+
function inventoryProjectCheckouts(input) {
|
|
1311
|
+
const states = input.branchNames.map((branchName) => ({
|
|
1312
|
+
branchName,
|
|
1313
|
+
checkoutPath: branchPath(input.projectRoot, branchName),
|
|
1314
|
+
state: classifyProjectCheckout({ ...input, branchName })
|
|
1315
|
+
}));
|
|
1316
|
+
const primaryLinked = states.some(({ branchName, state }) => branchName === input.primaryBranchName && state === "linked");
|
|
1317
|
+
const inventory = { linkedBranchNames: [], missingCheckouts: [] };
|
|
1318
|
+
for (const { branchName, checkoutPath, state } of states) {
|
|
1319
|
+
if (primaryLinked && state === "linked") inventory.linkedBranchNames.push(branchName);
|
|
1320
|
+
else inventory.missingCheckouts.push({ branchName, checkoutPath, reason: state === "linked" ? "not_a_worktree" : state });
|
|
1321
|
+
}
|
|
1322
|
+
return inventory;
|
|
1323
|
+
}
|
|
1324
|
+
const PROJECT_REMOVED_CHECKOUTS_DIRECTORY = ".r5d-removed";
|
|
1325
|
+
function moveProjectCheckoutAside(projectRoot, branchName, checkoutPath) {
|
|
1326
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
1327
|
+
const target = import_node_path.default.join(projectRoot, PROJECT_REMOVED_CHECKOUTS_DIRECTORY, ...`${branchName}-${timestamp}`.split("/"));
|
|
1328
|
+
import_node_fs.default.mkdirSync(import_node_path.default.dirname(target), { recursive: true });
|
|
1329
|
+
import_node_fs.default.renameSync(checkoutPath, target);
|
|
1330
|
+
return target;
|
|
1331
|
+
}
|
|
1242
1332
|
function deleteLinkedProjectBranch(input) {
|
|
1243
1333
|
(0, import_managed_paths.validateManagedBranchName)(input.primaryBranchName);
|
|
1244
1334
|
(0, import_managed_paths.validateManagedBranchName)(input.branchName);
|
|
@@ -1247,15 +1337,18 @@ function deleteLinkedProjectBranch(input) {
|
|
|
1247
1337
|
const checkoutPath = branchPath(input.projectRoot, input.branchName);
|
|
1248
1338
|
const primaryCommonDir = commonGitDirectory(primaryPath);
|
|
1249
1339
|
if (!primaryCommonDir) throw new Error("Primary project checkout is unavailable");
|
|
1340
|
+
let movedAsidePath;
|
|
1250
1341
|
if (import_node_fs.default.existsSync(checkoutPath)) {
|
|
1251
|
-
const checkoutCommonDir = commonGitDirectory(checkoutPath);
|
|
1342
|
+
const checkoutCommonDir = import_node_fs.default.lstatSync(checkoutPath).isDirectory() ? commonGitDirectory(checkoutPath) : null;
|
|
1252
1343
|
if (checkoutCommonDir !== primaryCommonDir) {
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1344
|
+
movedAsidePath = moveProjectCheckoutAside(input.projectRoot, input.branchName, checkoutPath);
|
|
1345
|
+
tryGit(primaryPath, ["worktree", "prune"]);
|
|
1346
|
+
} else {
|
|
1347
|
+
if (projectWorktreeOperationInProgress(checkoutPath)) {
|
|
1348
|
+
throw new Error(`Project branch ${input.branchName} has an in-progress Git operation`);
|
|
1349
|
+
}
|
|
1350
|
+
git(primaryPath, ["worktree", "remove", "--force", checkoutPath], `remove linked worktree ${input.branchName}`);
|
|
1257
1351
|
}
|
|
1258
|
-
git(primaryPath, ["worktree", "remove", "--force", checkoutPath], `remove linked worktree ${input.branchName}`);
|
|
1259
1352
|
} else {
|
|
1260
1353
|
tryGit(primaryPath, ["worktree", "prune"]);
|
|
1261
1354
|
}
|
|
@@ -1263,7 +1356,7 @@ function deleteLinkedProjectBranch(input) {
|
|
|
1263
1356
|
if (tryGit(primaryPath, ["show-ref", "--verify", "--quiet", branchRef])) {
|
|
1264
1357
|
git(primaryPath, ["branch", "-D", input.branchName], `delete project branch ${input.branchName}`);
|
|
1265
1358
|
}
|
|
1266
|
-
return { branchName: input.branchName };
|
|
1359
|
+
return { branchName: input.branchName, ...movedAsidePath ? { movedAsidePath } : {} };
|
|
1267
1360
|
}
|
|
1268
1361
|
function removeProjectWorktrees(input) {
|
|
1269
1362
|
const projectRoot = import_node_path.default.resolve(input.projectRoot);
|
|
@@ -1365,24 +1458,46 @@ function assertProjectMirrorHeadsPushed(results) {
|
|
|
1365
1458
|
throw new Error(`Hidden project mirror publication skipped ${skipped.branchName}${skipped.reason ? ` (${skipped.reason})` : ""}`);
|
|
1366
1459
|
}
|
|
1367
1460
|
}
|
|
1461
|
+
const PROJECT_MIRROR_FETCH_REFSPECS = ["+refs/heads/*:refs/r5d/mirror/*", "+refs/r5d/server-synced/*:refs/r5d/server-synced/*"];
|
|
1462
|
+
function projectMirrorMountFetchRefspecs(branchName, branchId) {
|
|
1463
|
+
(0, import_managed_paths.validateManagedBranchName)(branchName);
|
|
1464
|
+
if (branchId !== void 0 && !/^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/.test(branchId)) throw new Error(`Invalid branch id for ${branchName}`);
|
|
1465
|
+
return [
|
|
1466
|
+
`+refs/heads/${branchName}:refs/r5d/mirror/${branchName}`,
|
|
1467
|
+
branchId === void 0 ? "+refs/r5d/server-synced/*:refs/r5d/server-synced/*" : `+refs/r5d/server-synced/${branchId}/*:refs/r5d/server-synced/${branchId}/*`
|
|
1468
|
+
];
|
|
1469
|
+
}
|
|
1470
|
+
async function fetchProjectMirrorRefs(input) {
|
|
1471
|
+
const fetch = (refspecs) => gitResultAsync(
|
|
1472
|
+
input.primaryPath,
|
|
1473
|
+
[
|
|
1474
|
+
...(0, import_git_process_environment.gitTransportSecurityArgs)(input.mirrorUrl, input.credentialHelper, input.credentialUsername),
|
|
1475
|
+
"fetch",
|
|
1476
|
+
"--no-recurse-submodules",
|
|
1477
|
+
"--prune",
|
|
1478
|
+
input.mirrorUrl,
|
|
1479
|
+
...refspecs
|
|
1480
|
+
],
|
|
1481
|
+
PROJECT_GIT_TRANSPORT_TIMEOUT_MS
|
|
1482
|
+
);
|
|
1483
|
+
if (input.fetchOnlyBranches && input.fetchOnlyBranches.size > 0) {
|
|
1484
|
+
const refspecs = [...input.fetchOnlyBranches].sort().flatMap((branchName) => projectMirrorMountFetchRefspecs(branchName, input.branchIds?.get(branchName)));
|
|
1485
|
+
const scoped = await fetch(refspecs);
|
|
1486
|
+
if (scoped.exitCode === 0) return;
|
|
1487
|
+
if (!/(couldn't find remote ref|no such ref)/i.test(`${scoped.stderr}
|
|
1488
|
+
${scoped.stdout}`)) {
|
|
1489
|
+
throw new Error(`Refresh project head mirror: ${scoped.stderr || scoped.stdout || `git exited ${scoped.exitCode}`}`);
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
const whole = await fetch(PROJECT_MIRROR_FETCH_REFSPECS);
|
|
1493
|
+
if (whole.exitCode !== 0) {
|
|
1494
|
+
throw new Error(`Refresh project head mirror: ${whole.stderr || whole.stdout || `git exited ${whole.exitCode}`}`);
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1368
1497
|
async function observeProjectMirrorHeads(input) {
|
|
1369
1498
|
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}`}`);
|
|
1499
|
+
if (input.refreshRemote !== false && !(input.fetchOnlyBranches && input.fetchOnlyBranches.size === 0)) {
|
|
1500
|
+
await fetchProjectMirrorRefs({ ...input, primaryPath });
|
|
1386
1501
|
}
|
|
1387
1502
|
const observations = [];
|
|
1388
1503
|
for (const branchName of [...input.branchNames].sort()) {
|
|
@@ -1429,23 +1544,14 @@ async function observeProjectMirrorHeads(input) {
|
|
|
1429
1544
|
}
|
|
1430
1545
|
return observations;
|
|
1431
1546
|
}
|
|
1432
|
-
async function
|
|
1547
|
+
async function computeLocalProjectMirrorRefsTokens(input) {
|
|
1433
1548
|
const primaryPath = branchPath(input.projectRoot, input.primaryBranchName);
|
|
1434
1549
|
const rows = await gitAsync(
|
|
1435
1550
|
primaryPath,
|
|
1436
|
-
["for-each-ref",
|
|
1551
|
+
["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
1552
|
"list cached project mirror refs"
|
|
1438
1553
|
);
|
|
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));
|
|
1554
|
+
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
1555
|
}
|
|
1450
1556
|
async function applyObservedProjectMirrorHeads(input) {
|
|
1451
1557
|
const results = [];
|
|
@@ -1509,7 +1615,7 @@ const projectWorktreesTestHarness = {
|
|
|
1509
1615
|
commandArgs: gitCommandArgs,
|
|
1510
1616
|
configureRepository,
|
|
1511
1617
|
createPreparedSnapshot(input) {
|
|
1512
|
-
return { root: snapshotBranchTrees(input.projectRoot, input.branches, input.temporaryRoot).root };
|
|
1618
|
+
return { root: snapshotBranchTrees(input.projectRoot, input.branches, input.temporaryRoot, void 0, input.scope).root };
|
|
1513
1619
|
},
|
|
1514
1620
|
consumeSnapshot(snapshotRoot) {
|
|
1515
1621
|
const manifest = readSnapshotManifest(snapshotRoot);
|
|
@@ -1525,11 +1631,13 @@ const projectWorktreesTestHarness = {
|
|
|
1525
1631
|
};
|
|
1526
1632
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1527
1633
|
0 && (module.exports = {
|
|
1634
|
+
PROJECT_REMOVED_CHECKOUTS_DIRECTORY,
|
|
1528
1635
|
PROJECT_WORKTREE_SNAPSHOT_PREFIX,
|
|
1529
1636
|
ProjectBranchCreationRollbackIncompleteError,
|
|
1530
1637
|
applyObservedProjectMirrorHeads,
|
|
1531
1638
|
assertProjectMirrorHeadsPushed,
|
|
1532
|
-
|
|
1639
|
+
classifyProjectCheckout,
|
|
1640
|
+
computeLocalProjectMirrorRefsTokens,
|
|
1533
1641
|
createLinkedProjectBranch,
|
|
1534
1642
|
createOrRetryLinkedProjectBranch,
|
|
1535
1643
|
deleteLinkedProjectBranch,
|
|
@@ -1537,6 +1645,7 @@ const projectWorktreesTestHarness = {
|
|
|
1537
1645
|
ensureProjectWorktrees,
|
|
1538
1646
|
fastForwardProjectHeadsFromMirror,
|
|
1539
1647
|
hasLinkedProjectWorktreeLayout,
|
|
1648
|
+
inventoryProjectCheckouts,
|
|
1540
1649
|
observeProjectMirrorHeads,
|
|
1541
1650
|
projectBranchMayExistAfterCreateFailure,
|
|
1542
1651
|
projectOriginBranchName,
|