@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.
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.101",
3
+ "version": "0.0.104",
4
4
  "type": "commonjs"
5
5
  }
@@ -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
- PROJECT_MIRROR_FETCH_BACKSTOP_MS: () => PROJECT_MIRROR_FETCH_BACKSTOP_MS,
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
- PROJECT_MIRROR_FETCH_BACKSTOP_MS,
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
- computeLocalProjectMirrorRefsToken: () => computeLocalProjectMirrorRefsToken,
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
- fsyncTree(snapshot.manifest.projectRoot);
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 mutatedBranches = /* @__PURE__ */ new Set();
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
- if (branch.branchName === input.primaryBranchName || await commonGitDirectoryAsync(checkoutPath) === await commonGitDirectoryAsync(primaryPath)) {
1004
- const currentHead = await gitAsync(checkoutPath, ["rev-parse", "HEAD"], `resolve linked worktree head ${branch.branchName}`);
1005
- if (currentHead === resolved.seed) continue;
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
- input.beforeDestructiveMutation();
1013
- mutatedBranches.add(branch.branchName);
1014
- import_node_fs.default.rmSync(checkoutPath, { recursive: true, force: true });
1015
- await gitAsync(primaryPath, ["branch", "-f", branch.branchName, resolved.seed], `anchor project branch ${branch.branchName}`);
1016
- import_node_fs.default.mkdirSync(import_node_path.default.dirname(checkoutPath), { recursive: true });
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, branch.branchName],
1020
- `create linked worktree ${branch.branchName}`
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(projectRoot, input.branches, snapshotStorageRoot, input.onSnapshotProgress);
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) persistReconciledProjectAndConsumeSnapshot(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
- git(targetPath, ["read-tree", sourceHead], `initialize linked worktree index ${input.branchName}`);
1178
- (0, import_working_tree_mirror.mirrorWorkingTree)({ sourceRoot: sourcePath, targetRoot: targetPath, sourceMode: "git", deletionMode: "git" });
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
- throw new Error(`Project branch path ${input.branchName} is not a linked worktree of the configured project`);
1254
- }
1255
- if (projectWorktreeOperationInProgress(checkoutPath)) {
1256
- throw new Error(`Project branch ${input.branchName} has an in-progress Git operation`);
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
- const fetch = await gitResultAsync(
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 computeLocalProjectMirrorRefsToken(input) {
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", "--format=%(objectname) %(refname)", import_project_mirror_refs_token.PROJECT_MIRROR_REF_PREFIX, import_project_mirror_refs_token.PROJECT_SERVER_SYNCED_REF_PREFIX],
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
- const refs = rows.split(/\r?\n/).filter(Boolean).map((row) => {
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
- computeLocalProjectMirrorRefsToken,
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,