@ricsam/r5d-worker 0.0.99 → 0.0.101
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/main.cjs +37 -5
- package/dist/cjs/package.json +1 -1
- package/dist/cjs/working-tree-mirror.cjs +347 -99
- package/dist/cjs/workspace-git-sync.cjs +198 -60
- package/dist/cjs/workspace-merge-projection.cjs +5 -19
- package/dist/mjs/main.mjs +37 -5
- package/dist/mjs/package.json +1 -1
- package/dist/mjs/working-tree-mirror.mjs +347 -99
- package/dist/mjs/workspace-git-sync.mjs +198 -60
- package/dist/mjs/workspace-merge-projection.mjs +5 -19
- package/dist/types/main.d.ts +181 -0
- package/dist/types/working-tree-mirror.d.ts +56 -11
- package/dist/types/workspace-git-sync.d.ts +42 -2
- package/package.json +1 -1
|
@@ -128,6 +128,22 @@ async function gitAsync(cwd, args, action) {
|
|
|
128
128
|
if (result.exitCode !== 0) throw new Error(`${action}: ${result.stderr || result.stdout || `git exited ${result.exitCode}`}`);
|
|
129
129
|
return result.stdout;
|
|
130
130
|
}
|
|
131
|
+
async function gitOutputAsync(cwd, args, action, input) {
|
|
132
|
+
const subprocess = Bun.spawn(gitCommandArgs(args), {
|
|
133
|
+
cwd,
|
|
134
|
+
stdin: input ?? "ignore",
|
|
135
|
+
stdout: "pipe",
|
|
136
|
+
stderr: "pipe",
|
|
137
|
+
env: (0, import_git_process_environment.workerGitProcessEnvironment)()
|
|
138
|
+
});
|
|
139
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
140
|
+
new Response(subprocess.stdout).arrayBuffer(),
|
|
141
|
+
new Response(subprocess.stderr).text(),
|
|
142
|
+
subprocess.exited
|
|
143
|
+
]);
|
|
144
|
+
if (exitCode !== 0) throw new Error(`${action}: ${stderr.trim() || `git exited ${exitCode}`}`);
|
|
145
|
+
return Buffer.from(stdout);
|
|
146
|
+
}
|
|
131
147
|
async function tryGitAsync(cwd, args) {
|
|
132
148
|
return (await gitResultAsync(cwd, args)).exitCode === 0;
|
|
133
149
|
}
|
|
@@ -1139,7 +1155,13 @@ function beginHydrationTransaction(workspacePath, mounts, targetReceipt) {
|
|
|
1139
1155
|
candidate: outerPath,
|
|
1140
1156
|
label: `Workspace mount ${mount.id} outer source`
|
|
1141
1157
|
});
|
|
1142
|
-
const plan = (0, import_working_tree_mirror.planWorkingTreeMirror)({
|
|
1158
|
+
const plan = (0, import_working_tree_mirror.planWorkingTreeMirror)({
|
|
1159
|
+
sourceRoot: outerPath,
|
|
1160
|
+
targetRoot: sourcePath,
|
|
1161
|
+
sourceMode: "all",
|
|
1162
|
+
deletionMode: "git",
|
|
1163
|
+
sourceGitlinks: outerSubtreeGitlinks(workspacePath, mount)
|
|
1164
|
+
});
|
|
1143
1165
|
scope = (0, import_working_tree_mirror.captureWorkingTreeMirrorPlan)({
|
|
1144
1166
|
plan,
|
|
1145
1167
|
targetRoot: sourcePath,
|
|
@@ -1450,6 +1472,7 @@ function activeMounts(mounts) {
|
|
|
1450
1472
|
return { active, tombstones, skipped };
|
|
1451
1473
|
}
|
|
1452
1474
|
function mirrorMountsToWorkspace(workspacePath, mounts) {
|
|
1475
|
+
const projected = [];
|
|
1453
1476
|
for (const mount of mounts) {
|
|
1454
1477
|
(0, import_workspace_mount_boundary.assertManagedDirectoryPath)({
|
|
1455
1478
|
trustedRoot: mount.durabilityRootPath,
|
|
@@ -1461,13 +1484,61 @@ function mirrorMountsToWorkspace(workspacePath, mounts) {
|
|
|
1461
1484
|
candidate: import_node_path.default.join(workspacePath, ...mount.workspaceRelativePath.replace(/\\/g, "/").split("/")),
|
|
1462
1485
|
label: `Workspace mount ${mount.id} outer target`
|
|
1463
1486
|
});
|
|
1464
|
-
(0, import_working_tree_mirror.mirrorWorkingTree)({
|
|
1487
|
+
const mirrored = (0, import_working_tree_mirror.mirrorWorkingTree)({
|
|
1465
1488
|
sourceRoot: mount.sourcePath,
|
|
1466
1489
|
targetRoot: import_node_path.default.join(workspacePath, ...mount.workspaceRelativePath.replace(/\\/g, "/").split("/")),
|
|
1467
1490
|
sourceMode: mount.sourceMode,
|
|
1468
1491
|
deletionMode: "all"
|
|
1469
1492
|
});
|
|
1493
|
+
projected.push({ mount, gitlinks: mirrored.gitlinks });
|
|
1494
|
+
}
|
|
1495
|
+
return projected;
|
|
1496
|
+
}
|
|
1497
|
+
async function stageWorkspaceGitlinks(workspacePath, projected) {
|
|
1498
|
+
if (projected.length === 0) return;
|
|
1499
|
+
const mountPaths = projected.map(({ mount }) => normalizedWorkspaceMountPath(mount.workspaceRelativePath));
|
|
1500
|
+
const desired = /* @__PURE__ */ new Map();
|
|
1501
|
+
projected.forEach(({ gitlinks }, index) => {
|
|
1502
|
+
for (const gitlink of gitlinks) desired.set(`${mountPaths[index]}/${gitlink.relativePath}`, gitlink.objectId);
|
|
1503
|
+
});
|
|
1504
|
+
const desiredRoots = new Set(desired.keys());
|
|
1505
|
+
const listed = await gitOutputAsync(
|
|
1506
|
+
workspacePath,
|
|
1507
|
+
["ls-files", "-z", "--stage", "--", ...mountPaths.map((mountPath) => `:(literal)${mountPath}`)],
|
|
1508
|
+
"inspect staged workspace mount entries"
|
|
1509
|
+
);
|
|
1510
|
+
const feed = [];
|
|
1511
|
+
const current = /* @__PURE__ */ new Map();
|
|
1512
|
+
const drop = (objectId, relativePath) => {
|
|
1513
|
+
feed.push(Buffer.concat([Buffer.from(`0 ${"0".repeat(objectId.length)} `), Buffer.from(relativePath), Buffer.from([0])]));
|
|
1514
|
+
};
|
|
1515
|
+
for (const record of listed.toString().split("\0")) {
|
|
1516
|
+
const match = /^([0-7]{6}) ([0-9a-f]{40,64}) [0-3]\t(.+)$/su.exec(record);
|
|
1517
|
+
if (!match) continue;
|
|
1518
|
+
const mode = match[1];
|
|
1519
|
+
const objectId = match[2];
|
|
1520
|
+
const relativePath = match[3];
|
|
1521
|
+
if (mode === "160000") {
|
|
1522
|
+
current.set(relativePath, objectId);
|
|
1523
|
+
if (!desired.has(relativePath)) drop(objectId, relativePath);
|
|
1524
|
+
continue;
|
|
1525
|
+
}
|
|
1526
|
+
if (desired.has(relativePath) || hasAncestorIn(relativePath, desiredRoots)) drop(objectId, relativePath);
|
|
1470
1527
|
}
|
|
1528
|
+
for (const [relativePath, objectId] of desired) {
|
|
1529
|
+
if (current.get(relativePath) === objectId) continue;
|
|
1530
|
+
feed.push(Buffer.concat([Buffer.from(`160000 ${objectId} `), Buffer.from(relativePath), Buffer.from([0])]));
|
|
1531
|
+
}
|
|
1532
|
+
if (feed.length === 0) return;
|
|
1533
|
+
await gitOutputAsync(workspacePath, ["update-index", "-z", "--index-info"], "stage workspace gitlinks", Buffer.concat(feed));
|
|
1534
|
+
}
|
|
1535
|
+
function hasAncestorIn(relativePath, ancestors) {
|
|
1536
|
+
let parent = import_node_path.default.posix.dirname(relativePath);
|
|
1537
|
+
while (parent !== ".") {
|
|
1538
|
+
if (ancestors.has(parent)) return true;
|
|
1539
|
+
parent = import_node_path.default.posix.dirname(parent);
|
|
1540
|
+
}
|
|
1541
|
+
return false;
|
|
1471
1542
|
}
|
|
1472
1543
|
function removeWorkspaceMounts(workspacePath, mounts) {
|
|
1473
1544
|
for (const mount of mounts) {
|
|
@@ -1507,6 +1578,24 @@ function restoreDeferredWorkspaceMountsFromHead(workspacePath, mounts) {
|
|
|
1507
1578
|
function workspaceMountOuterPath(workspacePath, mount) {
|
|
1508
1579
|
return import_node_path.default.join(workspacePath, ...mount.workspaceRelativePath.replace(/\\/g, "/").split("/"));
|
|
1509
1580
|
}
|
|
1581
|
+
function outerSubtreeGitlinks(workspacePath, mount) {
|
|
1582
|
+
const gitlinks = /* @__PURE__ */ new Map();
|
|
1583
|
+
const head = revParse(workspacePath, "HEAD");
|
|
1584
|
+
if (!head) return gitlinks;
|
|
1585
|
+
const subtree = `${head}:${normalizedWorkspaceMountPath(mount.workspaceRelativePath)}`;
|
|
1586
|
+
const listed = Bun.spawnSync(gitCommandArgs(["ls-tree", "-r", "-z", subtree]), {
|
|
1587
|
+
cwd: workspacePath,
|
|
1588
|
+
stdout: "pipe",
|
|
1589
|
+
stderr: "pipe",
|
|
1590
|
+
env: (0, import_git_process_environment.workerGitProcessEnvironment)()
|
|
1591
|
+
});
|
|
1592
|
+
if (listed.exitCode !== 0) return gitlinks;
|
|
1593
|
+
for (const record of Buffer.from(listed.stdout).toString().split("\0")) {
|
|
1594
|
+
const match = /^160000 commit ([0-9a-f]{40,64})\t(.+)$/su.exec(record);
|
|
1595
|
+
if (match) gitlinks.set(match[2], match[1]);
|
|
1596
|
+
}
|
|
1597
|
+
return gitlinks;
|
|
1598
|
+
}
|
|
1510
1599
|
function hydrateWorkspaceGitMountsRaw(workspacePath, mounts, options = {}) {
|
|
1511
1600
|
const hydratedMountIds = [];
|
|
1512
1601
|
const skippedMountIds = [];
|
|
@@ -1550,6 +1639,9 @@ function hydrateWorkspaceGitMountsRaw(workspacePath, mounts, options = {}) {
|
|
|
1550
1639
|
sourceRoot,
|
|
1551
1640
|
targetRoot: mount.sourcePath,
|
|
1552
1641
|
sourceMode: "all",
|
|
1642
|
+
// The outer worktree shows a submodule as an empty directory; the tree
|
|
1643
|
+
// carries its pointer, which a Git checkout applies to its index (D4).
|
|
1644
|
+
sourceGitlinks: outerSubtreeGitlinks(workspacePath, mount),
|
|
1553
1645
|
// A missing checkout has no Git index to define git-only deletions. Its
|
|
1554
1646
|
// newly created directory is empty, so all-mode is equivalent and lets
|
|
1555
1647
|
// bootstrap hydration preserve the fetched tree before Git initializes.
|
|
@@ -1565,7 +1657,7 @@ async function selectMountsWithUnchangedOuterSubtree(workspacePath, receipt, hea
|
|
|
1565
1657
|
return basisHead && basisHead !== head ? [{ mount, basisHead }] : [];
|
|
1566
1658
|
});
|
|
1567
1659
|
if (candidates.length === 0) return /* @__PURE__ */ new Set();
|
|
1568
|
-
const objects = await
|
|
1660
|
+
const objects = await resolveGitObjectsAsync(
|
|
1569
1661
|
workspacePath,
|
|
1570
1662
|
candidates.flatMap(({ mount, basisHead }) => [
|
|
1571
1663
|
workspaceTreeObjectName(basisHead, mount.workspaceRelativePath),
|
|
@@ -1574,8 +1666,8 @@ async function selectMountsWithUnchangedOuterSubtree(workspacePath, receipt, hea
|
|
|
1574
1666
|
);
|
|
1575
1667
|
const unchanged = /* @__PURE__ */ new Set();
|
|
1576
1668
|
for (const { mount, basisHead } of candidates) {
|
|
1577
|
-
const before = objects.get(workspaceTreeObjectName(basisHead, mount.workspaceRelativePath)) ?? null;
|
|
1578
|
-
const after = objects.get(workspaceTreeObjectName(head, mount.workspaceRelativePath)) ?? null;
|
|
1669
|
+
const before = objects.get(workspaceTreeObjectName(basisHead, mount.workspaceRelativePath))?.objectId ?? null;
|
|
1670
|
+
const after = objects.get(workspaceTreeObjectName(head, mount.workspaceRelativePath))?.objectId ?? null;
|
|
1579
1671
|
if (before === after) unchanged.add(mount.id);
|
|
1580
1672
|
}
|
|
1581
1673
|
return unchanged;
|
|
@@ -1601,56 +1693,67 @@ async function hydrateWorkspaceGitMountsTransactionally(input) {
|
|
|
1601
1693
|
const busyMountIds = new Set(input.ignoreBusy ? [] : candidateMounts.filter((mount) => mount.busy?.()).map(({ id }) => id));
|
|
1602
1694
|
const requestedIds = new Set(input.mounts.map(({ id }) => id));
|
|
1603
1695
|
const requiredIds = new Set(requiredMounts.map(({ id }) => id));
|
|
1604
|
-
const
|
|
1605
|
-
const
|
|
1696
|
+
const hydrationMounts = input.mounts.filter(({ id }) => !busyMountIds.has(id));
|
|
1697
|
+
const advancedDurabilityMounts = durabilityMounts.filter(
|
|
1606
1698
|
({ id }) => !busyMountIds.has(id) && (requestedIds.has(id) || !requiredIds.has(id))
|
|
1607
1699
|
);
|
|
1608
1700
|
const unavailableRequiredIds = requiredMounts.filter(({ id }) => busyMountIds.has(id) || !requestedIds.has(id)).map(({ id }) => id);
|
|
1609
1701
|
const carriedMounts = carryMounts.filter(({ id }) => !busyMountIds.has(id));
|
|
1610
1702
|
return {
|
|
1611
1703
|
busyMountIds,
|
|
1612
|
-
hydrationMounts
|
|
1613
|
-
advancedDurabilityMounts
|
|
1704
|
+
hydrationMounts,
|
|
1705
|
+
advancedDurabilityMounts,
|
|
1614
1706
|
skippedMountIds: [.../* @__PURE__ */ new Set([...busyMountIds, ...unavailableRequiredIds])].sort(),
|
|
1615
|
-
targetReceipt: targetHead ? receiptWithMountsAtHead(receiptBeforeTransaction, targetHead, [...
|
|
1707
|
+
targetReceipt: targetHead ? receiptWithMountsAtHead(receiptBeforeTransaction, targetHead, [...advancedDurabilityMounts, ...carriedMounts], receiptMounts) : null
|
|
1616
1708
|
};
|
|
1617
1709
|
};
|
|
1618
1710
|
const noopCandidate = computeSelection();
|
|
1619
1711
|
if (noopCandidate.hydrationMounts.length === 0 && noopCandidate.advancedDurabilityMounts.length === 0 && noopCandidate.targetReceipt && lstatIfExists(hydrationTransactionPath(workspacePath)) === null && workspaceHydrationReceiptsEqual(receiptBeforeTransaction, noopCandidate.targetReceipt) && hydratedWorkspaceReceiptFileIsV3(workspacePath) && await workspaceBasisRefsAreCurrentAsync(workspacePath, noopCandidate.targetReceipt)) {
|
|
1620
1712
|
return { hydratedMountIds: [], skippedMountIds: noopCandidate.skippedMountIds };
|
|
1621
1713
|
}
|
|
1622
|
-
const
|
|
1623
|
-
const
|
|
1624
|
-
|
|
1714
|
+
const hooks = input.hydrationHooks;
|
|
1715
|
+
const context = {
|
|
1716
|
+
transactionId: crypto.randomUUID(),
|
|
1717
|
+
workspacePath,
|
|
1718
|
+
mountIds: [...new Set([...noopCandidate.hydrationMounts, ...noopCandidate.advancedDurabilityMounts].map(({ id }) => id))].sort()
|
|
1719
|
+
};
|
|
1720
|
+
if (hooks) await hooks.beforeTransaction(context);
|
|
1625
1721
|
try {
|
|
1626
|
-
const
|
|
1627
|
-
const
|
|
1628
|
-
|
|
1629
|
-
throw new Error("Workspace hydration did not include every required mount");
|
|
1630
|
-
}
|
|
1631
|
-
fsyncHydratedWorkspaceMounts(advancedDurabilityMounts, durabilityScopes);
|
|
1632
|
-
markHydrationTransactionDurable(workspacePath);
|
|
1633
|
-
if (targetReceipt) updateHydratedWorkspaceReceipt(workspacePath, targetReceipt);
|
|
1634
|
-
removeHydrationTransaction(workspacePath);
|
|
1635
|
-
return {
|
|
1636
|
-
hydratedMountIds: hydration.hydratedMountIds,
|
|
1637
|
-
skippedMountIds: selection.skippedMountIds
|
|
1638
|
-
};
|
|
1639
|
-
} catch (error) {
|
|
1640
|
-
const transactionPath = hydrationTransactionPath(workspacePath);
|
|
1641
|
-
if (lstatIfExists(transactionPath) && hydrationTransactionCommitted(transactionPath)) {
|
|
1642
|
-
throw error;
|
|
1643
|
-
}
|
|
1722
|
+
const selection = computeSelection();
|
|
1723
|
+
const { hydrationMounts, advancedDurabilityMounts, targetReceipt } = selection;
|
|
1724
|
+
const { manifest, durabilityScopes } = beginHydrationTransaction(workspacePath, hydrationMounts, targetReceipt);
|
|
1644
1725
|
try {
|
|
1645
|
-
|
|
1726
|
+
const hydration = hydrateWorkspaceGitMountsRaw(workspacePath, hydrationMounts, { ignoreBusy: true });
|
|
1727
|
+
const expectedHydratedIds = hydrationMounts.map(({ id }) => id).sort();
|
|
1728
|
+
if (hydration.skippedMountIds.length > 0 || hydration.hydratedMountIds.length !== expectedHydratedIds.length || hydration.hydratedMountIds.some((id, index) => id !== expectedHydratedIds[index])) {
|
|
1729
|
+
throw new Error("Workspace hydration did not include every required mount");
|
|
1730
|
+
}
|
|
1731
|
+
fsyncHydratedWorkspaceMounts(advancedDurabilityMounts, durabilityScopes);
|
|
1732
|
+
markHydrationTransactionDurable(workspacePath);
|
|
1733
|
+
if (targetReceipt) updateHydratedWorkspaceReceipt(workspacePath, targetReceipt);
|
|
1646
1734
|
removeHydrationTransaction(workspacePath);
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1735
|
+
return {
|
|
1736
|
+
hydratedMountIds: hydration.hydratedMountIds,
|
|
1737
|
+
skippedMountIds: selection.skippedMountIds
|
|
1738
|
+
};
|
|
1739
|
+
} catch (error) {
|
|
1740
|
+
const transactionPath = hydrationTransactionPath(workspacePath);
|
|
1741
|
+
if (lstatIfExists(transactionPath) && hydrationTransactionCommitted(transactionPath)) {
|
|
1742
|
+
throw error;
|
|
1743
|
+
}
|
|
1744
|
+
try {
|
|
1745
|
+
restoreHydrationTransaction(workspacePath, manifest);
|
|
1746
|
+
removeHydrationTransaction(workspacePath);
|
|
1747
|
+
} catch (restoreError) {
|
|
1748
|
+
throw new AggregateError(
|
|
1749
|
+
[error, restoreError],
|
|
1750
|
+
`Workspace hydration failed and its durable snapshot could not be restored: ${error instanceof Error ? error.message : String(error)}`
|
|
1751
|
+
);
|
|
1752
|
+
}
|
|
1753
|
+
throw error;
|
|
1652
1754
|
}
|
|
1653
|
-
|
|
1755
|
+
} finally {
|
|
1756
|
+
hooks?.afterTransaction(context);
|
|
1654
1757
|
}
|
|
1655
1758
|
}
|
|
1656
1759
|
async function hydrateWorkspaceGitMounts(workspacePath, mounts, options = {}) {
|
|
@@ -1662,7 +1765,8 @@ async function hydrateWorkspaceGitMounts(workspacePath, mounts, options = {}) {
|
|
|
1662
1765
|
return hydrateWorkspaceGitMountsTransactionally({
|
|
1663
1766
|
workspacePath,
|
|
1664
1767
|
mounts,
|
|
1665
|
-
ignoreBusy: options.ignoreBusy
|
|
1768
|
+
ignoreBusy: options.ignoreBusy,
|
|
1769
|
+
hydrationHooks: options.hydrationHooks
|
|
1666
1770
|
});
|
|
1667
1771
|
}
|
|
1668
1772
|
async function recoverWorkspaceGitHydration(workspacePath, mounts, options = {}) {
|
|
@@ -1702,7 +1806,8 @@ async function recoverWorkspaceGitHydration(workspacePath, mounts, options = {})
|
|
|
1702
1806
|
receiptMounts: configuredBasisMounts,
|
|
1703
1807
|
requiredMounts: hydrationTargets,
|
|
1704
1808
|
ignoreBusy: options.ignoreBusy,
|
|
1705
|
-
recordCurrentHead: true
|
|
1809
|
+
recordCurrentHead: true,
|
|
1810
|
+
hydrationHooks: options.hydrationHooks
|
|
1706
1811
|
});
|
|
1707
1812
|
void hydration;
|
|
1708
1813
|
}
|
|
@@ -1753,7 +1858,8 @@ async function resetWorkspaceGit(input) {
|
|
|
1753
1858
|
durabilityMounts: [...requiredLiveMounts, ...selected.tombstones, ...durablyAbsentMounts],
|
|
1754
1859
|
receiptMounts: input.mounts,
|
|
1755
1860
|
requiredMounts: requiredLiveMounts,
|
|
1756
|
-
recordCurrentHead: true
|
|
1861
|
+
recordCurrentHead: true,
|
|
1862
|
+
hydrationHooks: input.hydrationHooks
|
|
1757
1863
|
});
|
|
1758
1864
|
const requiredLiveIds = requiredLiveMounts.map(({ id }) => id).sort();
|
|
1759
1865
|
const resetHead = revParse(workspacePath, "HEAD");
|
|
@@ -1827,7 +1933,7 @@ async function diffSizeBytes(workspacePath, baseRevision, headRevision, limit, p
|
|
|
1827
1933
|
if (exitCode !== 0) throw new Error(`Measure workspace diff: ${stderr || `git exited ${exitCode}`}`);
|
|
1828
1934
|
return total;
|
|
1829
1935
|
}
|
|
1830
|
-
async function
|
|
1936
|
+
async function resolveGitObjectsAsync(workspacePath, names) {
|
|
1831
1937
|
const resolved = /* @__PURE__ */ new Map();
|
|
1832
1938
|
const unique = [...new Set(names)];
|
|
1833
1939
|
if (unique.length === 0) return resolved;
|
|
@@ -1850,21 +1956,43 @@ async function resolveGitObjectIdsAsync(workspacePath, names) {
|
|
|
1850
1956
|
throw new Error(`Resolve workspace objects: expected ${unique.length} results, received ${lines.length}`);
|
|
1851
1957
|
}
|
|
1852
1958
|
unique.forEach((name, index) => {
|
|
1853
|
-
const match = /^([0-9a-f]{40,64}) \S+ \d+$/.exec(lines[index]);
|
|
1854
|
-
resolved.set(name, match ? match[1] : null);
|
|
1959
|
+
const match = /^([0-9a-f]{40,64}) (\S+) \d+$/.exec(lines[index]);
|
|
1960
|
+
resolved.set(name, match ? { objectId: match[1], type: match[2] } : null);
|
|
1855
1961
|
});
|
|
1856
1962
|
return resolved;
|
|
1857
1963
|
}
|
|
1858
1964
|
function workspaceTreeObjectName(revision, workspaceRelativePath) {
|
|
1859
1965
|
return `${revision}:${normalizedWorkspaceMountPath(workspaceRelativePath)}`;
|
|
1860
1966
|
}
|
|
1967
|
+
async function pathsReplacedByGitlinks(workspacePath, baseRevision, headRevision) {
|
|
1968
|
+
const base = baseRevision ?? emptyTreeHash(workspacePath);
|
|
1969
|
+
const raw = await gitOutputAsync(
|
|
1970
|
+
workspacePath,
|
|
1971
|
+
["diff", "--raw", "--no-renames", "-z", base, headRevision],
|
|
1972
|
+
"inspect workspace gitlink changes"
|
|
1973
|
+
);
|
|
1974
|
+
const records = raw.toString().split("\0");
|
|
1975
|
+
const addedGitlinks = [];
|
|
1976
|
+
for (let index = 0; index + 1 < records.length; index += 2) {
|
|
1977
|
+
if (/^:[0-7]{6} 160000 \S+ \S+ A$/u.test(records[index])) addedGitlinks.push(records[index + 1]);
|
|
1978
|
+
}
|
|
1979
|
+
if (addedGitlinks.length === 0) return [];
|
|
1980
|
+
const objects = await resolveGitObjectsAsync(
|
|
1981
|
+
workspacePath,
|
|
1982
|
+
addedGitlinks.map((relativePath) => `${base}:${relativePath}`)
|
|
1983
|
+
);
|
|
1984
|
+
return addedGitlinks.filter((relativePath) => objects.get(`${base}:${relativePath}`)?.type === "tree").sort();
|
|
1985
|
+
}
|
|
1986
|
+
function gitlinkMigrationPathspecs(paths) {
|
|
1987
|
+
return paths.map((relativePath) => `:(exclude,literal)${relativePath}`);
|
|
1988
|
+
}
|
|
1861
1989
|
async function measureInitialPublicationDiffSize(input) {
|
|
1862
1990
|
const candidates = input.mounts.filter(
|
|
1863
1991
|
(mount) => typeof mount.initialPublicationSourceRelativePath === "string"
|
|
1864
1992
|
);
|
|
1865
1993
|
if (candidates.length === 0) return null;
|
|
1866
1994
|
const revisions = [input.localHead, ...input.remoteHead ? [input.remoteHead] : []];
|
|
1867
|
-
const objects = await
|
|
1995
|
+
const objects = await resolveGitObjectsAsync(
|
|
1868
1996
|
input.workspacePath,
|
|
1869
1997
|
candidates.flatMap(
|
|
1870
1998
|
(mount) => revisions.flatMap((revision) => [
|
|
@@ -1873,7 +2001,7 @@ async function measureInitialPublicationDiffSize(input) {
|
|
|
1873
2001
|
])
|
|
1874
2002
|
)
|
|
1875
2003
|
);
|
|
1876
|
-
const lookup = (revision, workspaceRelativePath) => revision ? objects.get(workspaceTreeObjectName(revision, workspaceRelativePath)) ?? null : null;
|
|
2004
|
+
const lookup = (revision, workspaceRelativePath) => revision ? objects.get(workspaceTreeObjectName(revision, workspaceRelativePath))?.objectId ?? null : null;
|
|
1877
2005
|
const initialPublications = candidates.flatMap((mount) => {
|
|
1878
2006
|
const published = lookup(input.localHead, mount.workspaceRelativePath);
|
|
1879
2007
|
if (!published || lookup(input.remoteHead, mount.workspaceRelativePath)) return [];
|
|
@@ -1881,16 +2009,14 @@ async function measureInitialPublicationDiffSize(input) {
|
|
|
1881
2009
|
return [{ mount, published, source }];
|
|
1882
2010
|
});
|
|
1883
2011
|
if (initialPublications.length === 0) return null;
|
|
1884
|
-
let size = await input.measure(
|
|
1885
|
-
|
|
1886
|
-
input.
|
|
1887
|
-
|
|
1888
|
-
input.limit,
|
|
1889
|
-
initialPublications.map(({ mount }) => `:(exclude,literal)${normalizedWorkspaceMountPath(mount.workspaceRelativePath)}`)
|
|
1890
|
-
);
|
|
2012
|
+
let size = await input.measure(input.workspacePath, input.remoteHead, input.localHead, input.limit, [
|
|
2013
|
+
...initialPublications.map(({ mount }) => `:(exclude,literal)${normalizedWorkspaceMountPath(mount.workspaceRelativePath)}`),
|
|
2014
|
+
...input.excludePathspecs ?? []
|
|
2015
|
+
]);
|
|
1891
2016
|
for (const { published, source } of initialPublications) {
|
|
1892
2017
|
if (size > input.limit) break;
|
|
1893
|
-
|
|
2018
|
+
const migrations = source ? await pathsReplacedByGitlinks(input.workspacePath, source, published) : [];
|
|
2019
|
+
size += await input.measure(input.workspacePath, source, published, input.limit - size, gitlinkMigrationPathspecs(migrations));
|
|
1894
2020
|
}
|
|
1895
2021
|
return { size, mountIds: initialPublications.map(({ mount }) => mount.id).sort() };
|
|
1896
2022
|
}
|
|
@@ -2008,7 +2134,8 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
2008
2134
|
await recoverWorkspaceGitHydration(workspacePath, input.mounts, {
|
|
2009
2135
|
...deferredMountIds.size > 0 ? { deferMountIds: deferredMountIds } : {},
|
|
2010
2136
|
preserveResolutionInProgress,
|
|
2011
|
-
preserveStaleBases: true
|
|
2137
|
+
preserveStaleBases: true,
|
|
2138
|
+
hydrationHooks: input.hydrationHooks
|
|
2012
2139
|
});
|
|
2013
2140
|
const selected = activeMounts(input.mounts);
|
|
2014
2141
|
const projectionMutationTokens = new Map(
|
|
@@ -2134,7 +2261,8 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
2134
2261
|
carryMounts: uncoveredCompletionMounts.filter(({ id }) => unchangedMountIds.has(id)),
|
|
2135
2262
|
receiptMounts: completionReceiptMounts,
|
|
2136
2263
|
requiredMounts: uncoveredRequiredLiveMounts.filter(({ id }) => !unchangedMountIds.has(id)),
|
|
2137
|
-
recordCurrentHead: true
|
|
2264
|
+
recordCurrentHead: true,
|
|
2265
|
+
hydrationHooks: input.hydrationHooks
|
|
2138
2266
|
});
|
|
2139
2267
|
for (const id of hydration.skippedMountIds) cycleSkippedMountIds.add(id);
|
|
2140
2268
|
refreshCycleSkippedMounts(completionReceiptMounts);
|
|
@@ -2152,10 +2280,12 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
2152
2280
|
return classifyMounts([...selected.active, ...selected.tombstones], hydration.skippedMountIds);
|
|
2153
2281
|
};
|
|
2154
2282
|
let newlyStagedPaths = [];
|
|
2283
|
+
const projectedGitlinks = [];
|
|
2155
2284
|
const stageAndCommitWorkspace = async () => {
|
|
2156
2285
|
if (workspaceResolutionInProgress(workspacePath) || await workspaceHasUnmergedEntries(workspacePath)) {
|
|
2157
2286
|
throw new Error("Outer workspace clone has an unfinished merge/rebase; finish or abort it before synchronizing");
|
|
2158
2287
|
}
|
|
2288
|
+
await stageWorkspaceGitlinks(workspacePath, projectedGitlinks);
|
|
2159
2289
|
await gitAsync(workspacePath, ["add", "-A", "--", "."], "stage workspace working trees");
|
|
2160
2290
|
newlyStagedPaths = await stagedPaths(workspacePath);
|
|
2161
2291
|
if (newlyStagedPaths.length > 0) {
|
|
@@ -2185,7 +2315,7 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
2185
2315
|
cycleSkippedMountIds.add(mount.id);
|
|
2186
2316
|
continue;
|
|
2187
2317
|
}
|
|
2188
|
-
mirrorMountsToWorkspace(workspacePath, [mount]);
|
|
2318
|
+
projectedGitlinks.push(...mirrorMountsToWorkspace(workspacePath, [mount]));
|
|
2189
2319
|
projectedFastMounts.push(mount);
|
|
2190
2320
|
await yieldToWorkerEventLoop();
|
|
2191
2321
|
}
|
|
@@ -2284,7 +2414,9 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
2284
2414
|
}
|
|
2285
2415
|
const paths = await changedPaths(workspacePath, remoteHead, localHead);
|
|
2286
2416
|
const measureDiffSize = input.measureDiffSize ?? diffSizeBytes;
|
|
2287
|
-
|
|
2417
|
+
const gitlinkMigrations = paths.length > 0 ? await pathsReplacedByGitlinks(workspacePath, remoteHead, localHead) : [];
|
|
2418
|
+
const gitlinkMigrationExcludes = gitlinkMigrationPathspecs(gitlinkMigrations);
|
|
2419
|
+
let size = paths.length > 0 ? await measureDiffSize(workspacePath, remoteHead, localHead, maxDiffBytes, gitlinkMigrationExcludes) : 0;
|
|
2288
2420
|
input.assertStillAdmitted?.();
|
|
2289
2421
|
let initialPublicationMountIds;
|
|
2290
2422
|
if (paths.length > 0 && size > maxDiffBytes && !input.allowLargeDiff) {
|
|
@@ -2294,7 +2426,8 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
2294
2426
|
localHead,
|
|
2295
2427
|
mounts: input.mounts,
|
|
2296
2428
|
limit: maxDiffBytes,
|
|
2297
|
-
measure: measureDiffSize
|
|
2429
|
+
measure: measureDiffSize,
|
|
2430
|
+
excludePathspecs: gitlinkMigrationExcludes
|
|
2298
2431
|
});
|
|
2299
2432
|
input.assertStillAdmitted?.();
|
|
2300
2433
|
if (initialPublication && initialPublication.size <= maxDiffBytes) {
|
|
@@ -2302,6 +2435,11 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
2302
2435
|
initialPublicationMountIds = initialPublication.mountIds;
|
|
2303
2436
|
}
|
|
2304
2437
|
}
|
|
2438
|
+
let gitlinkMigrationConfirmed = false;
|
|
2439
|
+
if (gitlinkMigrations.length > 0 && size <= maxDiffBytes && !input.allowLargeDiff) {
|
|
2440
|
+
gitlinkMigrationConfirmed = await measureDiffSize(workspacePath, remoteHead, localHead, maxDiffBytes) > maxDiffBytes;
|
|
2441
|
+
input.assertStillAdmitted?.();
|
|
2442
|
+
}
|
|
2305
2443
|
if (paths.length > 0 && size > maxDiffBytes && !input.allowLargeDiff) {
|
|
2306
2444
|
const classifiedMounts = classifyMounts([...selected.active, ...selected.tombstones]);
|
|
2307
2445
|
return {
|
|
@@ -2346,7 +2484,7 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
2346
2484
|
"push",
|
|
2347
2485
|
"--no-recurse-submodules"
|
|
2348
2486
|
];
|
|
2349
|
-
if (input.allowLargeDiff || initialPublicationMountIds) {
|
|
2487
|
+
if (input.allowLargeDiff || initialPublicationMountIds || gitlinkMigrationConfirmed) {
|
|
2350
2488
|
pushArgs.push(`--push-option=${WORKSPACE_GIT_CONFIRMED_LARGE_DIFF_PUSH_OPTION}`);
|
|
2351
2489
|
}
|
|
2352
2490
|
pushArgs.push("origin", `HEAD:refs/heads/${WORKSPACE_GIT_BRANCH}`);
|
|
@@ -206,14 +206,6 @@ function indexMode(entry) {
|
|
|
206
206
|
function indexInfoRecord(mode, objectId, relativePath) {
|
|
207
207
|
return Buffer.concat([Buffer.from(`${mode} ${objectId} `), Buffer.from(relativePath), Buffer.from([0])]);
|
|
208
208
|
}
|
|
209
|
-
function basisGitlinks(workspacePath, basis) {
|
|
210
|
-
const listed = gitResult(workspacePath, ["ls-tree", "-r", "-z", `${basis.basisHead}:${basis.workspaceRelativePath}`]);
|
|
211
|
-
if (listed.exitCode !== 0) return [];
|
|
212
|
-
return nulRecords(listed.stdout).flatMap((record) => {
|
|
213
|
-
const match = /^160000 commit ([0-9a-f]{40,64})\t(.+)$/su.exec(record.toString());
|
|
214
|
-
return match ? [{ objectId: match[1], relativePath: match[2] }] : [];
|
|
215
|
-
});
|
|
216
|
-
}
|
|
217
209
|
function synthesizeMountTree(input) {
|
|
218
210
|
const sourceRoot = import_node_path.default.resolve(input.sourcePath);
|
|
219
211
|
const entries = (0, import_working_tree_mirror.inspectWorkingTree)(sourceRoot, input.sourceMode);
|
|
@@ -224,19 +216,14 @@ function synthesizeMountTree(input) {
|
|
|
224
216
|
const records = [];
|
|
225
217
|
for (const [relativePath, entry] of [...entries.entries()].sort(([left], [right]) => left.localeCompare(right))) {
|
|
226
218
|
if (entry.kind === "directory") continue;
|
|
219
|
+
if (entry.kind === "gitlink") {
|
|
220
|
+
records.push(indexInfoRecord("160000", entry.objectId, relativePath));
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
227
223
|
const sourcePath = sourceEntryPath(sourceRoot, relativePath);
|
|
228
224
|
const objectId = entry.kind === "file" ? hashRegularFile(input.workspacePath, sourcePath, entry) : hashSymlink(input.workspacePath, sourcePath, entry);
|
|
229
225
|
records.push(indexInfoRecord(indexMode(entry), objectId, relativePath));
|
|
230
226
|
}
|
|
231
|
-
if (input.gitlinkBasis && input.sourceMode === "git") {
|
|
232
|
-
for (const gitlink of basisGitlinks(input.workspacePath, input.gitlinkBasis)) {
|
|
233
|
-
const populatedPrefix = `${gitlink.relativePath}/`;
|
|
234
|
-
const populated = [...entries.keys()].some((relativePath) => relativePath.startsWith(populatedPrefix));
|
|
235
|
-
if (entries.get(gitlink.relativePath)?.kind === "directory" && !populated) {
|
|
236
|
-
records.push(indexInfoRecord("160000", gitlink.objectId, gitlink.relativePath));
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
227
|
if (records.length > 0) {
|
|
241
228
|
git(input.workspacePath, ["update-index", "-z", "--index-info"], "populate workspace projection index", {
|
|
242
229
|
environment,
|
|
@@ -301,8 +288,7 @@ function synthesizeOursCommit(input) {
|
|
|
301
288
|
const mountTree = synthesizeMountTree({
|
|
302
289
|
workspacePath: input.workspacePath,
|
|
303
290
|
sourcePath: input.mount.sourcePath,
|
|
304
|
-
sourceMode: input.mount.sourceMode
|
|
305
|
-
gitlinkBasis: { basisHead: input.basisHead, workspaceRelativePath: input.workspaceRelativePath }
|
|
291
|
+
sourceMode: input.mount.sourceMode
|
|
306
292
|
});
|
|
307
293
|
const rootTree = graftMountTree({
|
|
308
294
|
workspacePath: input.workspacePath,
|
package/dist/mjs/main.mjs
CHANGED
|
@@ -104,9 +104,35 @@ function workerServerHeartbeatAction(input) {
|
|
|
104
104
|
if (input.watchdogSweepDelayed || input.probeElapsedMs === void 0) return "start_probe";
|
|
105
105
|
return input.probeElapsedMs >= input.timeoutMs ? "terminate" : "wait";
|
|
106
106
|
}
|
|
107
|
+
const WORKSPACE_HYDRATION_LEASE_OPERATION_PREFIX = "hydration:";
|
|
108
|
+
function createWorkspaceHydrationLeaseHooks(send, options = {}) {
|
|
109
|
+
const yieldToEventLoop = options.yieldToEventLoop ?? (() => new Promise((resolve) => setImmediate(resolve)));
|
|
110
|
+
const log = options.log ?? ((line) => process.stdout.write(`${line}
|
|
111
|
+
`));
|
|
112
|
+
const now = options.now ?? (() => performance.now());
|
|
113
|
+
const operationId = (context) => `${WORKSPACE_HYDRATION_LEASE_OPERATION_PREFIX}${context.transactionId}`;
|
|
114
|
+
const startedAtMs = /* @__PURE__ */ new Map();
|
|
115
|
+
return {
|
|
116
|
+
async beforeTransaction(context) {
|
|
117
|
+
send({ type: "heartbeat_lease", operationId: operationId(context), kind: "hydration", active: true });
|
|
118
|
+
startedAtMs.set(context.transactionId, now());
|
|
119
|
+
log(`[r5d-worker] hydrating ${context.mountIds.length} workspace mount(s) under hydration lease ${context.transactionId}`);
|
|
120
|
+
await yieldToEventLoop();
|
|
121
|
+
},
|
|
122
|
+
afterTransaction(context) {
|
|
123
|
+
const startedAt = startedAtMs.get(context.transactionId);
|
|
124
|
+
startedAtMs.delete(context.transactionId);
|
|
125
|
+
send({ type: "heartbeat_lease", operationId: operationId(context), kind: "hydration", active: false });
|
|
126
|
+
log(
|
|
127
|
+
`[r5d-worker] released hydration lease ${context.transactionId} after ${startedAt === void 0 ? "?" : Math.round(now() - startedAt)}ms`
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
}
|
|
107
132
|
const workerLifecycleTestHarness = {
|
|
108
133
|
boundedWorkerLifecycleProgress,
|
|
109
|
-
workerServerHeartbeatAction
|
|
134
|
+
workerServerHeartbeatAction,
|
|
135
|
+
createWorkspaceHydrationLeaseHooks
|
|
110
136
|
};
|
|
111
137
|
class ProjectWorkspaceConfigurationDeferredError extends Error {
|
|
112
138
|
}
|
|
@@ -4377,6 +4403,7 @@ async function startWorker(options) {
|
|
|
4377
4403
|
error: error instanceof Error ? error.message : String(error)
|
|
4378
4404
|
};
|
|
4379
4405
|
};
|
|
4406
|
+
const workspaceHydrationTransactionHooks = createWorkspaceHydrationLeaseHooks((message) => sendWorkerMessageFromCurrentSource(ws, message));
|
|
4380
4407
|
const performWorkspaceSync = async (input) => {
|
|
4381
4408
|
if (!workspaceRemoteUrl || !workspaceCredentialHelper || !workspaceCredentialUsername || !workspaceGitIdentity) {
|
|
4382
4409
|
throw new Error("Worker workspace configuration has not been received");
|
|
@@ -4399,7 +4426,8 @@ async function startWorker(options) {
|
|
|
4399
4426
|
credentialHelper: workspaceCredentialHelper,
|
|
4400
4427
|
credentialUsername: workspaceCredentialUsername,
|
|
4401
4428
|
gitIdentity: workspaceGitIdentity,
|
|
4402
|
-
mounts: configuredMounts
|
|
4429
|
+
mounts: configuredMounts,
|
|
4430
|
+
hydrationHooks: workspaceHydrationTransactionHooks
|
|
4403
4431
|
});
|
|
4404
4432
|
await applyObservedProjectHeadsAfterInboundWorkspace(observedProjectHeads2, reset.activeMountIds, true);
|
|
4405
4433
|
return {
|
|
@@ -4453,7 +4481,8 @@ async function startWorker(options) {
|
|
|
4453
4481
|
assertStillAdmitted: input.assertStillAdmitted,
|
|
4454
4482
|
afterWorkspacePublished: outerRemediation ? void 0 : async ({ activeMountIds, publishedHead }) => {
|
|
4455
4483
|
await pushChangedProjectHeads(activeMountIds, publishedHead, publicationHeads, expectedMirrorHeads, inboundMoveMountIds);
|
|
4456
|
-
}
|
|
4484
|
+
},
|
|
4485
|
+
hydrationHooks: workspaceHydrationTransactionHooks
|
|
4457
4486
|
});
|
|
4458
4487
|
input.assertStillAdmitted?.();
|
|
4459
4488
|
if (result.initialPublicationMountIds && result.initialPublicationMountIds.length > 0) {
|
|
@@ -5047,7 +5076,10 @@ async function startWorker(options) {
|
|
|
5047
5076
|
sourcePath: recoverySourceOverrides.get(mount.id) ?? mount.sourcePath,
|
|
5048
5077
|
busy: mount.busyForRecovery ?? mount.busy
|
|
5049
5078
|
}));
|
|
5050
|
-
await recoverWorkspaceGitHydration(workspaceShadowRoot, recoveryMounts, {
|
|
5079
|
+
await recoverWorkspaceGitHydration(workspaceShadowRoot, recoveryMounts, {
|
|
5080
|
+
preserveStaleBases: true,
|
|
5081
|
+
hydrationHooks: workspaceHydrationTransactionHooks
|
|
5082
|
+
});
|
|
5051
5083
|
for (const project of message.projects) {
|
|
5052
5084
|
for (const { branchName } of project.preserveOnlyBranches) {
|
|
5053
5085
|
if (stillPendingCreatedBranches.has(pendingCreatedBranchKey(project.projectId, branchName))) {
|
|
@@ -5091,7 +5123,7 @@ async function startWorker(options) {
|
|
|
5091
5123
|
// layouts can be marked ready. No process can resolve an unready
|
|
5092
5124
|
// target, and preserving the fetched outer tree here prevents clean
|
|
5093
5125
|
// checkout initialization from erasing synchronized dirt.
|
|
5094
|
-
{ ignoreBusy: true }
|
|
5126
|
+
{ ignoreBusy: true, hydrationHooks: workspaceHydrationTransactionHooks }
|
|
5095
5127
|
);
|
|
5096
5128
|
await ensureConfiguredProjects((progress) => {
|
|
5097
5129
|
sendLifecycleProgress({ phase: "configuring", operationId: configurationOperationId, ...progress });
|
package/dist/mjs/package.json
CHANGED