@ricsam/r5d-worker 0.0.93 → 0.0.95

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.
@@ -32,6 +32,7 @@ __export(project_worktrees_exports, {
32
32
  ProjectBranchCreationRollbackIncompleteError: () => ProjectBranchCreationRollbackIncompleteError,
33
33
  applyObservedProjectMirrorHeads: () => applyObservedProjectMirrorHeads,
34
34
  assertProjectMirrorHeadsPushed: () => assertProjectMirrorHeadsPushed,
35
+ computeLocalProjectMirrorRefsToken: () => computeLocalProjectMirrorRefsToken,
35
36
  createLinkedProjectBranch: () => createLinkedProjectBranch,
36
37
  createOrRetryLinkedProjectBranch: () => createOrRetryLinkedProjectBranch,
37
38
  deleteLinkedProjectBranch: () => deleteLinkedProjectBranch,
@@ -57,6 +58,7 @@ var import_node_os = __toESM(require("node:os"), 1);
57
58
  var import_node_path = __toESM(require("node:path"), 1);
58
59
  var import_git_process_environment = require("./git-process-environment.cjs");
59
60
  var import_managed_paths = require("./managed-paths.cjs");
61
+ var import_project_mirror_refs_token = require("./project-mirror-refs-token.cjs");
60
62
  var import_working_tree_mirror = require("./working-tree-mirror.cjs");
61
63
  const PROJECT_WORKTREE_SNAPSHOT_PREFIX = "r5d-project-worktrees-";
62
64
  const PROJECT_WORKTREE_SNAPSHOT_MANIFEST = "transaction.json";
@@ -64,6 +66,7 @@ const PROJECT_WORKTREE_SNAPSHOT_MANIFEST_VERSION = 1;
64
66
  const MAX_PROJECT_WORKTREE_SNAPSHOT_MANIFEST_BYTES = 64 * 1024;
65
67
  const PROJECT_WORKTREE_SNAPSHOT_OWNER_SESSION_ID = (0, import_node_crypto.randomUUID)();
66
68
  const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
69
+ const PROJECT_GIT_TRANSPORT_TIMEOUT_MS = 3e4;
67
70
  const PROJECT_WORKTREE_SNAPSHOT_NAME = new RegExp(
68
71
  `^${PROJECT_WORKTREE_SNAPSHOT_PREFIX.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}([1-9]\\d*)-([A-Za-z0-9]{6})$`
69
72
  );
@@ -133,7 +136,7 @@ function git(cwd, args, action) {
133
136
  function tryGit(cwd, args) {
134
137
  return gitResult(cwd, args).exitCode === 0;
135
138
  }
136
- async function gitResultAsync(cwd, args) {
139
+ async function gitResultAsync(cwd, args, timeoutMs) {
137
140
  const subprocess = Bun.spawn(gitCommandArgs(args), {
138
141
  cwd,
139
142
  stdin: "ignore",
@@ -141,12 +144,22 @@ async function gitResultAsync(cwd, args) {
141
144
  stderr: "pipe",
142
145
  env: (0, import_git_process_environment.workerGitProcessEnvironment)()
143
146
  });
144
- const [stdout, stderr, exitCode] = await Promise.all([
145
- new Response(subprocess.stdout).text(),
146
- new Response(subprocess.stderr).text(),
147
- subprocess.exited
148
- ]);
149
- return { exitCode, stdout: stdout.trim(), stderr: stderr.trim() };
147
+ let timedOut = false;
148
+ const timeout = timeoutMs ? setTimeout(() => {
149
+ timedOut = true;
150
+ subprocess.kill("SIGKILL");
151
+ }, timeoutMs) : void 0;
152
+ try {
153
+ const [stdout, stderr, exitCode] = await Promise.all([
154
+ new Response(subprocess.stdout).text(),
155
+ new Response(subprocess.stderr).text(),
156
+ subprocess.exited
157
+ ]);
158
+ if (timedOut) throw new Error(`Git transport timed out after ${timeoutMs}ms`);
159
+ return { exitCode, stdout: stdout.trim(), stderr: stderr.trim() };
160
+ } finally {
161
+ if (timeout) clearTimeout(timeout);
162
+ }
150
163
  }
151
164
  async function gitAsync(cwd, args, action) {
152
165
  const result = await gitResultAsync(cwd, args);
@@ -861,25 +874,33 @@ async function configureRepository(input) {
861
874
  }
862
875
  }
863
876
  async function fetchProjectHeads(input) {
864
- const originFetch = await gitResultAsync(input.primaryPath, [
865
- ...(0, import_git_process_environment.gitTransportSecurityArgs)(input.originUrl, input.credentialHelper, input.originCredentialUsername),
866
- "fetch",
867
- "--no-recurse-submodules",
868
- "--prune",
869
- "origin",
870
- "+refs/heads/*:refs/remotes/origin/*"
871
- ]);
877
+ const originFetch = await gitResultAsync(
878
+ input.primaryPath,
879
+ [
880
+ ...(0, import_git_process_environment.gitTransportSecurityArgs)(input.originUrl, input.credentialHelper, input.originCredentialUsername),
881
+ "fetch",
882
+ "--no-recurse-submodules",
883
+ "--prune",
884
+ "origin",
885
+ "+refs/heads/*:refs/remotes/origin/*"
886
+ ],
887
+ PROJECT_GIT_TRANSPORT_TIMEOUT_MS
888
+ );
872
889
  if (originFetch.exitCode !== 0) {
873
890
  throw new Error(`Fetch project origin: ${originFetch.stderr || originFetch.stdout || `git exited ${originFetch.exitCode}`}`);
874
891
  }
875
- const mirrorFetch = await gitResultAsync(input.primaryPath, [
876
- ...(0, import_git_process_environment.gitTransportSecurityArgs)(input.mirrorUrl, input.credentialHelper, input.mirrorCredentialUsername),
877
- "fetch",
878
- "--no-recurse-submodules",
879
- "--prune",
880
- input.mirrorUrl,
881
- "+refs/heads/*:refs/r5d/mirror/*"
882
- ]);
892
+ const mirrorFetch = await gitResultAsync(
893
+ input.primaryPath,
894
+ [
895
+ ...(0, import_git_process_environment.gitTransportSecurityArgs)(input.mirrorUrl, input.credentialHelper, input.mirrorCredentialUsername),
896
+ "fetch",
897
+ "--no-recurse-submodules",
898
+ "--prune",
899
+ input.mirrorUrl,
900
+ "+refs/heads/*:refs/r5d/mirror/*"
901
+ ],
902
+ PROJECT_GIT_TRANSPORT_TIMEOUT_MS
903
+ );
883
904
  if (mirrorFetch.exitCode !== 0 && !/(couldn't find remote ref|does not have any commits|remote repository is empty|no such ref)/i.test(
884
905
  `${mirrorFetch.stderr}
885
906
  ${mirrorFetch.stdout}`
@@ -1258,19 +1279,24 @@ function removeProjectWorktrees(input) {
1258
1279
  }
1259
1280
  async function deleteProjectMirrorBranch(input) {
1260
1281
  (0, import_managed_paths.validateManagedBranchName)(input.branchName);
1261
- const deleted = await gitResultAsync(input.gitDirectory, [
1262
- ...(0, import_git_process_environment.gitTransportSecurityArgs)(input.mirrorUrl, input.credentialHelper, input.credentialUsername),
1263
- "push",
1264
- "--no-recurse-submodules",
1265
- input.mirrorUrl,
1266
- `:refs/heads/${input.branchName}`
1267
- ]);
1282
+ const deleted = await gitResultAsync(
1283
+ input.gitDirectory,
1284
+ [
1285
+ ...(0, import_git_process_environment.gitTransportSecurityArgs)(input.mirrorUrl, input.credentialHelper, input.credentialUsername),
1286
+ "push",
1287
+ "--no-recurse-submodules",
1288
+ input.mirrorUrl,
1289
+ `:refs/heads/${input.branchName}`
1290
+ ],
1291
+ PROJECT_GIT_TRANSPORT_TIMEOUT_MS
1292
+ );
1268
1293
  if (deleted.exitCode !== 0 && !/(remote ref does not exist|unable to delete|no such ref)/i.test(`${deleted.stderr}
1269
1294
  ${deleted.stdout}`)) {
1270
1295
  throw new Error(
1271
1296
  `Delete hidden project mirror for ${input.branchName}: ${deleted.stderr || deleted.stdout || `git exited ${deleted.exitCode}`}`
1272
1297
  );
1273
1298
  }
1299
+ await gitResultAsync(input.gitDirectory, ["update-ref", "-d", `refs/r5d/mirror/${input.branchName}`]);
1274
1300
  }
1275
1301
  async function pushProjectMirrorHeads(input) {
1276
1302
  const results = [];
@@ -1284,21 +1310,51 @@ async function pushProjectMirrorHeads(input) {
1284
1310
  if (!await tryGitAsync(checkoutPath, ["cat-file", "-e", `${head}^{commit}`])) {
1285
1311
  throw new Error(`Exact publication head ${head} is unavailable for project branch ${branchName}`);
1286
1312
  }
1313
+ if (!input.expectedMirrorHeads.has(branchName)) {
1314
+ throw new Error(`Missing hidden-mirror lease for project branch ${branchName}`);
1315
+ }
1316
+ const expectedMirrorHead = input.expectedMirrorHeads.get(branchName) ?? null;
1317
+ if (expectedMirrorHead !== null && !/^[0-9a-f]{40}(?:[0-9a-f]{24})?$/.test(expectedMirrorHead)) {
1318
+ throw new Error(`Invalid hidden-mirror lease for project branch ${branchName}`);
1319
+ }
1320
+ const branchId = input.branchIds.get(branchName);
1321
+ if (!branchId || !/^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/.test(branchId)) {
1322
+ throw new Error(`Missing valid branch incarnation for project branch ${branchName}`);
1323
+ }
1324
+ if (expectedMirrorHead) {
1325
+ const handoffPrefix = `refs/r5d/server-synced/${branchId}/`;
1326
+ const markedCommits = (await gitAsync(
1327
+ checkoutPath,
1328
+ ["for-each-ref", "--format=%(objectname)", handoffPrefix],
1329
+ `inspect synchronized server commits before publishing ${branchName}`
1330
+ )).split(/\r?\n/).filter((value) => /^[0-9a-f]{40}(?:[0-9a-f]{24})?$/.test(value));
1331
+ for (const markedCommit of markedCommits) {
1332
+ const markerIsInLease = await tryGitAsync(checkoutPath, ["merge-base", "--is-ancestor", markedCommit, expectedMirrorHead]);
1333
+ if (markerIsInLease && !await tryGitAsync(checkoutPath, ["merge-base", "--is-ancestor", markedCommit, head])) {
1334
+ throw new Error(`Refusing to overwrite synchronized server commit history for project branch ${branchName}`);
1335
+ }
1336
+ }
1337
+ }
1287
1338
  if (projectWorktreeOperationInProgress(checkoutPath)) {
1288
1339
  results.push({ branchName, head, pushed: false, reason: "git_operation_in_progress" });
1289
1340
  continue;
1290
1341
  }
1291
- await gitAsync(
1342
+ const pushed = await gitResultAsync(
1292
1343
  checkoutPath,
1293
1344
  [
1294
1345
  ...(0, import_git_process_environment.gitTransportSecurityArgs)(input.mirrorUrl, input.credentialHelper, input.credentialUsername),
1295
1346
  "push",
1296
1347
  "--no-recurse-submodules",
1348
+ `--force-with-lease=refs/heads/${branchName}:${expectedMirrorHead ?? ""}`,
1297
1349
  input.mirrorUrl,
1298
- `+${head}:refs/heads/${branchName}`
1350
+ `${head}:refs/heads/${branchName}`
1299
1351
  ],
1300
- `push hidden project mirror for ${branchName}`
1352
+ PROJECT_GIT_TRANSPORT_TIMEOUT_MS
1301
1353
  );
1354
+ if (pushed.exitCode !== 0) {
1355
+ throw new Error(`Push hidden project mirror for ${branchName}: ${pushed.stderr || pushed.stdout || `git exited ${pushed.exitCode}`}`);
1356
+ }
1357
+ await gitAsync(checkoutPath, ["update-ref", `refs/r5d/mirror/${branchName}`, head], `record pushed mirror tip ${branchName}`);
1302
1358
  results.push({ branchName, head, pushed: true });
1303
1359
  }
1304
1360
  return results;
@@ -1311,42 +1367,94 @@ function assertProjectMirrorHeadsPushed(results) {
1311
1367
  }
1312
1368
  async function observeProjectMirrorHeads(input) {
1313
1369
  const primaryPath = branchPath(input.projectRoot, input.primaryBranchName);
1314
- const fetch = await gitResultAsync(primaryPath, [
1315
- ...(0, import_git_process_environment.gitTransportSecurityArgs)(input.mirrorUrl, input.credentialHelper, input.credentialUsername),
1316
- "fetch",
1317
- "--no-recurse-submodules",
1318
- "--prune",
1319
- input.mirrorUrl,
1320
- "+refs/heads/*:refs/r5d/mirror/*"
1321
- ]);
1322
- if (fetch.exitCode !== 0)
1323
- throw new Error(`Refresh project head mirror: ${fetch.stderr || fetch.stdout || `git exited ${fetch.exitCode}`}`);
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}`}`);
1386
+ }
1324
1387
  const observations = [];
1325
1388
  for (const branchName of [...input.branchNames].sort()) {
1326
1389
  const checkoutPath = branchPath(input.projectRoot, branchName);
1327
1390
  const previousHead = await gitAsync(checkoutPath, ["rev-parse", "HEAD"], `resolve local project head ${branchName}`);
1328
1391
  const mirrorRef = `refs/r5d/mirror/${branchName}`;
1329
1392
  const mirrorHead = await tryGitAsync(primaryPath, ["show-ref", "--verify", "--quiet", mirrorRef]) ? await gitAsync(primaryPath, ["rev-parse", mirrorRef], `resolve refreshed mirror head ${branchName}`) : null;
1330
- const shouldMove = Boolean(
1331
- mirrorHead && mirrorHead !== previousHead && (input.allowNonFastForward || await tryGitAsync(checkoutPath, ["merge-base", "--is-ancestor", previousHead, mirrorHead]))
1393
+ const branchId = input.branchIds?.get(branchName);
1394
+ if (branchId && !/^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/.test(branchId)) throw new Error(`Invalid branch id for ${branchName}`);
1395
+ const handoffPrefix = branchId ? `refs/r5d/server-synced/${branchId}/` : null;
1396
+ const handoffRows = handoffPrefix ? await gitAsync(
1397
+ primaryPath,
1398
+ ["for-each-ref", "--format=%(objectname) %(refname)", handoffPrefix],
1399
+ `inspect synchronized server commits for ${branchName}`
1400
+ ) : "";
1401
+ const serverSyncedWorkspaceHeads = (await Promise.all(
1402
+ handoffRows.split(/\r?\n/).map(async (row) => {
1403
+ const separator = row.indexOf(" ");
1404
+ if (separator < 0 || !mirrorHead) return null;
1405
+ const markedCommit = row.slice(0, separator);
1406
+ const ref = row.slice(separator + 1);
1407
+ if (!/^[0-9a-f]{40}(?:[0-9a-f]{24})?$/.test(markedCommit) || !handoffPrefix || !ref.startsWith(handoffPrefix)) {
1408
+ return null;
1409
+ }
1410
+ const markerIsIntegrated = markedCommit === mirrorHead || await tryGitAsync(primaryPath, ["merge-base", "--is-ancestor", markedCommit, mirrorHead]);
1411
+ if (!markerIsIntegrated) return null;
1412
+ const workspaceHead = ref.slice(handoffPrefix.length);
1413
+ return /^[0-9a-f]{40}(?:[0-9a-f]{24})?$/.test(workspaceHead) ? workspaceHead : null;
1414
+ })
1415
+ )).filter((workspaceHead) => workspaceHead !== null).sort();
1416
+ const fastForward = Boolean(
1417
+ mirrorHead && mirrorHead !== previousHead && await tryGitAsync(checkoutPath, ["merge-base", "--is-ancestor", previousHead, mirrorHead])
1332
1418
  );
1419
+ const serverSynced = Boolean(mirrorHead && mirrorHead !== previousHead && serverSyncedWorkspaceHeads.length > 0);
1420
+ const shouldMove = Boolean(mirrorHead && mirrorHead !== previousHead && (input.allowNonFastForward || fastForward || serverSynced));
1333
1421
  observations.push({
1334
1422
  branchName,
1335
1423
  previousHead,
1336
1424
  mirrorHead,
1337
- mode: input.allowNonFastForward ? "reseed" : "fast_forward",
1425
+ mode: input.allowNonFastForward ? "reseed" : serverSynced && !fastForward ? "server_synced" : "fast_forward",
1426
+ serverSyncedWorkspaceHeads,
1338
1427
  shouldMove
1339
1428
  });
1340
1429
  }
1341
1430
  return observations;
1342
1431
  }
1432
+ async function computeLocalProjectMirrorRefsToken(input) {
1433
+ const primaryPath = branchPath(input.projectRoot, input.primaryBranchName);
1434
+ const rows = await gitAsync(
1435
+ 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],
1437
+ "list cached project mirror refs"
1438
+ );
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));
1449
+ }
1343
1450
  async function applyObservedProjectMirrorHeads(input) {
1344
1451
  const results = [];
1345
1452
  for (const observation of [...input.observations].sort((left, right) => left.branchName.localeCompare(right.branchName))) {
1346
1453
  const { branchName, mirrorHead } = observation;
1347
1454
  const checkoutPath = branchPath(input.projectRoot, branchName);
1348
1455
  const currentHead = await gitAsync(checkoutPath, ["rev-parse", "HEAD"], `resolve local project head ${branchName}`);
1349
- if (!observation.shouldMove || !mirrorHead || currentHead === mirrorHead || currentHead !== observation.previousHead) {
1456
+ const serverSyncedCovered = observation.mode !== "server_synced" || (input.serverSyncedWorkspaceCovered ? await input.serverSyncedWorkspaceCovered(observation) : false);
1457
+ if (!observation.shouldMove || !serverSyncedCovered || !mirrorHead || currentHead === mirrorHead || currentHead !== observation.previousHead) {
1350
1458
  results.push({ branchName, previousHead: currentHead, mirrorHead, moved: false });
1351
1459
  continue;
1352
1460
  }
@@ -1358,14 +1466,14 @@ async function applyObservedProjectMirrorHeads(input) {
1358
1466
  results.push({ branchName, previousHead: currentHead, mirrorHead, moved: false });
1359
1467
  continue;
1360
1468
  }
1361
- if (observation.mode === "reseed") {
1469
+ if (observation.mode === "reseed" || observation.mode === "server_synced") {
1362
1470
  await preserveHead(checkoutPath);
1363
1471
  if (!mutationAllowed()) {
1364
1472
  results.push({ branchName, previousHead: currentHead, mirrorHead, moved: false });
1365
1473
  continue;
1366
1474
  }
1367
1475
  }
1368
- const action = observation.mode === "reseed" ? "reseed" : "fast-forward";
1476
+ const action = observation.mode === "reseed" ? "reseed" : observation.mode === "server_synced" ? "adopt server commit" : "fast-forward";
1369
1477
  await gitAsync(checkoutPath, ["read-tree", "--reset", mirrorHead], `${action} project index ${branchName}`);
1370
1478
  await gitAsync(
1371
1479
  checkoutPath,
@@ -1421,6 +1529,7 @@ const projectWorktreesTestHarness = {
1421
1529
  ProjectBranchCreationRollbackIncompleteError,
1422
1530
  applyObservedProjectMirrorHeads,
1423
1531
  assertProjectMirrorHeadsPushed,
1532
+ computeLocalProjectMirrorRefsToken,
1424
1533
  createLinkedProjectBranch,
1425
1534
  createOrRetryLinkedProjectBranch,
1426
1535
  deleteLinkedProjectBranch,