@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
|
@@ -91,6 +91,22 @@ async function gitAsync(cwd, args, action) {
|
|
|
91
91
|
if (result.exitCode !== 0) throw new Error(`${action}: ${result.stderr || result.stdout || `git exited ${result.exitCode}`}`);
|
|
92
92
|
return result.stdout;
|
|
93
93
|
}
|
|
94
|
+
async function gitOutputAsync(cwd, args, action, input) {
|
|
95
|
+
const subprocess = Bun.spawn(gitCommandArgs(args), {
|
|
96
|
+
cwd,
|
|
97
|
+
stdin: input ?? "ignore",
|
|
98
|
+
stdout: "pipe",
|
|
99
|
+
stderr: "pipe",
|
|
100
|
+
env: workerGitProcessEnvironment()
|
|
101
|
+
});
|
|
102
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
103
|
+
new Response(subprocess.stdout).arrayBuffer(),
|
|
104
|
+
new Response(subprocess.stderr).text(),
|
|
105
|
+
subprocess.exited
|
|
106
|
+
]);
|
|
107
|
+
if (exitCode !== 0) throw new Error(`${action}: ${stderr.trim() || `git exited ${exitCode}`}`);
|
|
108
|
+
return Buffer.from(stdout);
|
|
109
|
+
}
|
|
94
110
|
async function tryGitAsync(cwd, args) {
|
|
95
111
|
return (await gitResultAsync(cwd, args)).exitCode === 0;
|
|
96
112
|
}
|
|
@@ -1102,7 +1118,13 @@ function beginHydrationTransaction(workspacePath, mounts, targetReceipt) {
|
|
|
1102
1118
|
candidate: outerPath,
|
|
1103
1119
|
label: `Workspace mount ${mount.id} outer source`
|
|
1104
1120
|
});
|
|
1105
|
-
const plan = planWorkingTreeMirror({
|
|
1121
|
+
const plan = planWorkingTreeMirror({
|
|
1122
|
+
sourceRoot: outerPath,
|
|
1123
|
+
targetRoot: sourcePath,
|
|
1124
|
+
sourceMode: "all",
|
|
1125
|
+
deletionMode: "git",
|
|
1126
|
+
sourceGitlinks: outerSubtreeGitlinks(workspacePath, mount)
|
|
1127
|
+
});
|
|
1106
1128
|
scope = captureWorkingTreeMirrorPlan({
|
|
1107
1129
|
plan,
|
|
1108
1130
|
targetRoot: sourcePath,
|
|
@@ -1413,6 +1435,7 @@ function activeMounts(mounts) {
|
|
|
1413
1435
|
return { active, tombstones, skipped };
|
|
1414
1436
|
}
|
|
1415
1437
|
function mirrorMountsToWorkspace(workspacePath, mounts) {
|
|
1438
|
+
const projected = [];
|
|
1416
1439
|
for (const mount of mounts) {
|
|
1417
1440
|
assertManagedDirectoryPath({
|
|
1418
1441
|
trustedRoot: mount.durabilityRootPath,
|
|
@@ -1424,13 +1447,61 @@ function mirrorMountsToWorkspace(workspacePath, mounts) {
|
|
|
1424
1447
|
candidate: path.join(workspacePath, ...mount.workspaceRelativePath.replace(/\\/g, "/").split("/")),
|
|
1425
1448
|
label: `Workspace mount ${mount.id} outer target`
|
|
1426
1449
|
});
|
|
1427
|
-
mirrorWorkingTree({
|
|
1450
|
+
const mirrored = mirrorWorkingTree({
|
|
1428
1451
|
sourceRoot: mount.sourcePath,
|
|
1429
1452
|
targetRoot: path.join(workspacePath, ...mount.workspaceRelativePath.replace(/\\/g, "/").split("/")),
|
|
1430
1453
|
sourceMode: mount.sourceMode,
|
|
1431
1454
|
deletionMode: "all"
|
|
1432
1455
|
});
|
|
1456
|
+
projected.push({ mount, gitlinks: mirrored.gitlinks });
|
|
1457
|
+
}
|
|
1458
|
+
return projected;
|
|
1459
|
+
}
|
|
1460
|
+
async function stageWorkspaceGitlinks(workspacePath, projected) {
|
|
1461
|
+
if (projected.length === 0) return;
|
|
1462
|
+
const mountPaths = projected.map(({ mount }) => normalizedWorkspaceMountPath(mount.workspaceRelativePath));
|
|
1463
|
+
const desired = /* @__PURE__ */ new Map();
|
|
1464
|
+
projected.forEach(({ gitlinks }, index) => {
|
|
1465
|
+
for (const gitlink of gitlinks) desired.set(`${mountPaths[index]}/${gitlink.relativePath}`, gitlink.objectId);
|
|
1466
|
+
});
|
|
1467
|
+
const desiredRoots = new Set(desired.keys());
|
|
1468
|
+
const listed = await gitOutputAsync(
|
|
1469
|
+
workspacePath,
|
|
1470
|
+
["ls-files", "-z", "--stage", "--", ...mountPaths.map((mountPath) => `:(literal)${mountPath}`)],
|
|
1471
|
+
"inspect staged workspace mount entries"
|
|
1472
|
+
);
|
|
1473
|
+
const feed = [];
|
|
1474
|
+
const current = /* @__PURE__ */ new Map();
|
|
1475
|
+
const drop = (objectId, relativePath) => {
|
|
1476
|
+
feed.push(Buffer.concat([Buffer.from(`0 ${"0".repeat(objectId.length)} `), Buffer.from(relativePath), Buffer.from([0])]));
|
|
1477
|
+
};
|
|
1478
|
+
for (const record of listed.toString().split("\0")) {
|
|
1479
|
+
const match = /^([0-7]{6}) ([0-9a-f]{40,64}) [0-3]\t(.+)$/su.exec(record);
|
|
1480
|
+
if (!match) continue;
|
|
1481
|
+
const mode = match[1];
|
|
1482
|
+
const objectId = match[2];
|
|
1483
|
+
const relativePath = match[3];
|
|
1484
|
+
if (mode === "160000") {
|
|
1485
|
+
current.set(relativePath, objectId);
|
|
1486
|
+
if (!desired.has(relativePath)) drop(objectId, relativePath);
|
|
1487
|
+
continue;
|
|
1488
|
+
}
|
|
1489
|
+
if (desired.has(relativePath) || hasAncestorIn(relativePath, desiredRoots)) drop(objectId, relativePath);
|
|
1433
1490
|
}
|
|
1491
|
+
for (const [relativePath, objectId] of desired) {
|
|
1492
|
+
if (current.get(relativePath) === objectId) continue;
|
|
1493
|
+
feed.push(Buffer.concat([Buffer.from(`160000 ${objectId} `), Buffer.from(relativePath), Buffer.from([0])]));
|
|
1494
|
+
}
|
|
1495
|
+
if (feed.length === 0) return;
|
|
1496
|
+
await gitOutputAsync(workspacePath, ["update-index", "-z", "--index-info"], "stage workspace gitlinks", Buffer.concat(feed));
|
|
1497
|
+
}
|
|
1498
|
+
function hasAncestorIn(relativePath, ancestors) {
|
|
1499
|
+
let parent = path.posix.dirname(relativePath);
|
|
1500
|
+
while (parent !== ".") {
|
|
1501
|
+
if (ancestors.has(parent)) return true;
|
|
1502
|
+
parent = path.posix.dirname(parent);
|
|
1503
|
+
}
|
|
1504
|
+
return false;
|
|
1434
1505
|
}
|
|
1435
1506
|
function removeWorkspaceMounts(workspacePath, mounts) {
|
|
1436
1507
|
for (const mount of mounts) {
|
|
@@ -1470,6 +1541,24 @@ function restoreDeferredWorkspaceMountsFromHead(workspacePath, mounts) {
|
|
|
1470
1541
|
function workspaceMountOuterPath(workspacePath, mount) {
|
|
1471
1542
|
return path.join(workspacePath, ...mount.workspaceRelativePath.replace(/\\/g, "/").split("/"));
|
|
1472
1543
|
}
|
|
1544
|
+
function outerSubtreeGitlinks(workspacePath, mount) {
|
|
1545
|
+
const gitlinks = /* @__PURE__ */ new Map();
|
|
1546
|
+
const head = revParse(workspacePath, "HEAD");
|
|
1547
|
+
if (!head) return gitlinks;
|
|
1548
|
+
const subtree = `${head}:${normalizedWorkspaceMountPath(mount.workspaceRelativePath)}`;
|
|
1549
|
+
const listed = Bun.spawnSync(gitCommandArgs(["ls-tree", "-r", "-z", subtree]), {
|
|
1550
|
+
cwd: workspacePath,
|
|
1551
|
+
stdout: "pipe",
|
|
1552
|
+
stderr: "pipe",
|
|
1553
|
+
env: workerGitProcessEnvironment()
|
|
1554
|
+
});
|
|
1555
|
+
if (listed.exitCode !== 0) return gitlinks;
|
|
1556
|
+
for (const record of Buffer.from(listed.stdout).toString().split("\0")) {
|
|
1557
|
+
const match = /^160000 commit ([0-9a-f]{40,64})\t(.+)$/su.exec(record);
|
|
1558
|
+
if (match) gitlinks.set(match[2], match[1]);
|
|
1559
|
+
}
|
|
1560
|
+
return gitlinks;
|
|
1561
|
+
}
|
|
1473
1562
|
function hydrateWorkspaceGitMountsRaw(workspacePath, mounts, options = {}) {
|
|
1474
1563
|
const hydratedMountIds = [];
|
|
1475
1564
|
const skippedMountIds = [];
|
|
@@ -1513,6 +1602,9 @@ function hydrateWorkspaceGitMountsRaw(workspacePath, mounts, options = {}) {
|
|
|
1513
1602
|
sourceRoot,
|
|
1514
1603
|
targetRoot: mount.sourcePath,
|
|
1515
1604
|
sourceMode: "all",
|
|
1605
|
+
// The outer worktree shows a submodule as an empty directory; the tree
|
|
1606
|
+
// carries its pointer, which a Git checkout applies to its index (D4).
|
|
1607
|
+
sourceGitlinks: outerSubtreeGitlinks(workspacePath, mount),
|
|
1516
1608
|
// A missing checkout has no Git index to define git-only deletions. Its
|
|
1517
1609
|
// newly created directory is empty, so all-mode is equivalent and lets
|
|
1518
1610
|
// bootstrap hydration preserve the fetched tree before Git initializes.
|
|
@@ -1528,7 +1620,7 @@ async function selectMountsWithUnchangedOuterSubtree(workspacePath, receipt, hea
|
|
|
1528
1620
|
return basisHead && basisHead !== head ? [{ mount, basisHead }] : [];
|
|
1529
1621
|
});
|
|
1530
1622
|
if (candidates.length === 0) return /* @__PURE__ */ new Set();
|
|
1531
|
-
const objects = await
|
|
1623
|
+
const objects = await resolveGitObjectsAsync(
|
|
1532
1624
|
workspacePath,
|
|
1533
1625
|
candidates.flatMap(({ mount, basisHead }) => [
|
|
1534
1626
|
workspaceTreeObjectName(basisHead, mount.workspaceRelativePath),
|
|
@@ -1537,8 +1629,8 @@ async function selectMountsWithUnchangedOuterSubtree(workspacePath, receipt, hea
|
|
|
1537
1629
|
);
|
|
1538
1630
|
const unchanged = /* @__PURE__ */ new Set();
|
|
1539
1631
|
for (const { mount, basisHead } of candidates) {
|
|
1540
|
-
const before = objects.get(workspaceTreeObjectName(basisHead, mount.workspaceRelativePath)) ?? null;
|
|
1541
|
-
const after = objects.get(workspaceTreeObjectName(head, mount.workspaceRelativePath)) ?? null;
|
|
1632
|
+
const before = objects.get(workspaceTreeObjectName(basisHead, mount.workspaceRelativePath))?.objectId ?? null;
|
|
1633
|
+
const after = objects.get(workspaceTreeObjectName(head, mount.workspaceRelativePath))?.objectId ?? null;
|
|
1542
1634
|
if (before === after) unchanged.add(mount.id);
|
|
1543
1635
|
}
|
|
1544
1636
|
return unchanged;
|
|
@@ -1564,56 +1656,67 @@ async function hydrateWorkspaceGitMountsTransactionally(input) {
|
|
|
1564
1656
|
const busyMountIds = new Set(input.ignoreBusy ? [] : candidateMounts.filter((mount) => mount.busy?.()).map(({ id }) => id));
|
|
1565
1657
|
const requestedIds = new Set(input.mounts.map(({ id }) => id));
|
|
1566
1658
|
const requiredIds = new Set(requiredMounts.map(({ id }) => id));
|
|
1567
|
-
const
|
|
1568
|
-
const
|
|
1659
|
+
const hydrationMounts = input.mounts.filter(({ id }) => !busyMountIds.has(id));
|
|
1660
|
+
const advancedDurabilityMounts = durabilityMounts.filter(
|
|
1569
1661
|
({ id }) => !busyMountIds.has(id) && (requestedIds.has(id) || !requiredIds.has(id))
|
|
1570
1662
|
);
|
|
1571
1663
|
const unavailableRequiredIds = requiredMounts.filter(({ id }) => busyMountIds.has(id) || !requestedIds.has(id)).map(({ id }) => id);
|
|
1572
1664
|
const carriedMounts = carryMounts.filter(({ id }) => !busyMountIds.has(id));
|
|
1573
1665
|
return {
|
|
1574
1666
|
busyMountIds,
|
|
1575
|
-
hydrationMounts
|
|
1576
|
-
advancedDurabilityMounts
|
|
1667
|
+
hydrationMounts,
|
|
1668
|
+
advancedDurabilityMounts,
|
|
1577
1669
|
skippedMountIds: [.../* @__PURE__ */ new Set([...busyMountIds, ...unavailableRequiredIds])].sort(),
|
|
1578
|
-
targetReceipt: targetHead ? receiptWithMountsAtHead(receiptBeforeTransaction, targetHead, [...
|
|
1670
|
+
targetReceipt: targetHead ? receiptWithMountsAtHead(receiptBeforeTransaction, targetHead, [...advancedDurabilityMounts, ...carriedMounts], receiptMounts) : null
|
|
1579
1671
|
};
|
|
1580
1672
|
};
|
|
1581
1673
|
const noopCandidate = computeSelection();
|
|
1582
1674
|
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)) {
|
|
1583
1675
|
return { hydratedMountIds: [], skippedMountIds: noopCandidate.skippedMountIds };
|
|
1584
1676
|
}
|
|
1585
|
-
const
|
|
1586
|
-
const
|
|
1587
|
-
|
|
1677
|
+
const hooks = input.hydrationHooks;
|
|
1678
|
+
const context = {
|
|
1679
|
+
transactionId: crypto.randomUUID(),
|
|
1680
|
+
workspacePath,
|
|
1681
|
+
mountIds: [...new Set([...noopCandidate.hydrationMounts, ...noopCandidate.advancedDurabilityMounts].map(({ id }) => id))].sort()
|
|
1682
|
+
};
|
|
1683
|
+
if (hooks) await hooks.beforeTransaction(context);
|
|
1588
1684
|
try {
|
|
1589
|
-
const
|
|
1590
|
-
const
|
|
1591
|
-
|
|
1592
|
-
throw new Error("Workspace hydration did not include every required mount");
|
|
1593
|
-
}
|
|
1594
|
-
fsyncHydratedWorkspaceMounts(advancedDurabilityMounts, durabilityScopes);
|
|
1595
|
-
markHydrationTransactionDurable(workspacePath);
|
|
1596
|
-
if (targetReceipt) updateHydratedWorkspaceReceipt(workspacePath, targetReceipt);
|
|
1597
|
-
removeHydrationTransaction(workspacePath);
|
|
1598
|
-
return {
|
|
1599
|
-
hydratedMountIds: hydration.hydratedMountIds,
|
|
1600
|
-
skippedMountIds: selection.skippedMountIds
|
|
1601
|
-
};
|
|
1602
|
-
} catch (error) {
|
|
1603
|
-
const transactionPath = hydrationTransactionPath(workspacePath);
|
|
1604
|
-
if (lstatIfExists(transactionPath) && hydrationTransactionCommitted(transactionPath)) {
|
|
1605
|
-
throw error;
|
|
1606
|
-
}
|
|
1685
|
+
const selection = computeSelection();
|
|
1686
|
+
const { hydrationMounts, advancedDurabilityMounts, targetReceipt } = selection;
|
|
1687
|
+
const { manifest, durabilityScopes } = beginHydrationTransaction(workspacePath, hydrationMounts, targetReceipt);
|
|
1607
1688
|
try {
|
|
1608
|
-
|
|
1689
|
+
const hydration = hydrateWorkspaceGitMountsRaw(workspacePath, hydrationMounts, { ignoreBusy: true });
|
|
1690
|
+
const expectedHydratedIds = hydrationMounts.map(({ id }) => id).sort();
|
|
1691
|
+
if (hydration.skippedMountIds.length > 0 || hydration.hydratedMountIds.length !== expectedHydratedIds.length || hydration.hydratedMountIds.some((id, index) => id !== expectedHydratedIds[index])) {
|
|
1692
|
+
throw new Error("Workspace hydration did not include every required mount");
|
|
1693
|
+
}
|
|
1694
|
+
fsyncHydratedWorkspaceMounts(advancedDurabilityMounts, durabilityScopes);
|
|
1695
|
+
markHydrationTransactionDurable(workspacePath);
|
|
1696
|
+
if (targetReceipt) updateHydratedWorkspaceReceipt(workspacePath, targetReceipt);
|
|
1609
1697
|
removeHydrationTransaction(workspacePath);
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1698
|
+
return {
|
|
1699
|
+
hydratedMountIds: hydration.hydratedMountIds,
|
|
1700
|
+
skippedMountIds: selection.skippedMountIds
|
|
1701
|
+
};
|
|
1702
|
+
} catch (error) {
|
|
1703
|
+
const transactionPath = hydrationTransactionPath(workspacePath);
|
|
1704
|
+
if (lstatIfExists(transactionPath) && hydrationTransactionCommitted(transactionPath)) {
|
|
1705
|
+
throw error;
|
|
1706
|
+
}
|
|
1707
|
+
try {
|
|
1708
|
+
restoreHydrationTransaction(workspacePath, manifest);
|
|
1709
|
+
removeHydrationTransaction(workspacePath);
|
|
1710
|
+
} catch (restoreError) {
|
|
1711
|
+
throw new AggregateError(
|
|
1712
|
+
[error, restoreError],
|
|
1713
|
+
`Workspace hydration failed and its durable snapshot could not be restored: ${error instanceof Error ? error.message : String(error)}`
|
|
1714
|
+
);
|
|
1715
|
+
}
|
|
1716
|
+
throw error;
|
|
1615
1717
|
}
|
|
1616
|
-
|
|
1718
|
+
} finally {
|
|
1719
|
+
hooks?.afterTransaction(context);
|
|
1617
1720
|
}
|
|
1618
1721
|
}
|
|
1619
1722
|
async function hydrateWorkspaceGitMounts(workspacePath, mounts, options = {}) {
|
|
@@ -1625,7 +1728,8 @@ async function hydrateWorkspaceGitMounts(workspacePath, mounts, options = {}) {
|
|
|
1625
1728
|
return hydrateWorkspaceGitMountsTransactionally({
|
|
1626
1729
|
workspacePath,
|
|
1627
1730
|
mounts,
|
|
1628
|
-
ignoreBusy: options.ignoreBusy
|
|
1731
|
+
ignoreBusy: options.ignoreBusy,
|
|
1732
|
+
hydrationHooks: options.hydrationHooks
|
|
1629
1733
|
});
|
|
1630
1734
|
}
|
|
1631
1735
|
async function recoverWorkspaceGitHydration(workspacePath, mounts, options = {}) {
|
|
@@ -1665,7 +1769,8 @@ async function recoverWorkspaceGitHydration(workspacePath, mounts, options = {})
|
|
|
1665
1769
|
receiptMounts: configuredBasisMounts,
|
|
1666
1770
|
requiredMounts: hydrationTargets,
|
|
1667
1771
|
ignoreBusy: options.ignoreBusy,
|
|
1668
|
-
recordCurrentHead: true
|
|
1772
|
+
recordCurrentHead: true,
|
|
1773
|
+
hydrationHooks: options.hydrationHooks
|
|
1669
1774
|
});
|
|
1670
1775
|
void hydration;
|
|
1671
1776
|
}
|
|
@@ -1716,7 +1821,8 @@ async function resetWorkspaceGit(input) {
|
|
|
1716
1821
|
durabilityMounts: [...requiredLiveMounts, ...selected.tombstones, ...durablyAbsentMounts],
|
|
1717
1822
|
receiptMounts: input.mounts,
|
|
1718
1823
|
requiredMounts: requiredLiveMounts,
|
|
1719
|
-
recordCurrentHead: true
|
|
1824
|
+
recordCurrentHead: true,
|
|
1825
|
+
hydrationHooks: input.hydrationHooks
|
|
1720
1826
|
});
|
|
1721
1827
|
const requiredLiveIds = requiredLiveMounts.map(({ id }) => id).sort();
|
|
1722
1828
|
const resetHead = revParse(workspacePath, "HEAD");
|
|
@@ -1790,7 +1896,7 @@ async function diffSizeBytes(workspacePath, baseRevision, headRevision, limit, p
|
|
|
1790
1896
|
if (exitCode !== 0) throw new Error(`Measure workspace diff: ${stderr || `git exited ${exitCode}`}`);
|
|
1791
1897
|
return total;
|
|
1792
1898
|
}
|
|
1793
|
-
async function
|
|
1899
|
+
async function resolveGitObjectsAsync(workspacePath, names) {
|
|
1794
1900
|
const resolved = /* @__PURE__ */ new Map();
|
|
1795
1901
|
const unique = [...new Set(names)];
|
|
1796
1902
|
if (unique.length === 0) return resolved;
|
|
@@ -1813,21 +1919,43 @@ async function resolveGitObjectIdsAsync(workspacePath, names) {
|
|
|
1813
1919
|
throw new Error(`Resolve workspace objects: expected ${unique.length} results, received ${lines.length}`);
|
|
1814
1920
|
}
|
|
1815
1921
|
unique.forEach((name, index) => {
|
|
1816
|
-
const match = /^([0-9a-f]{40,64}) \S+ \d+$/.exec(lines[index]);
|
|
1817
|
-
resolved.set(name, match ? match[1] : null);
|
|
1922
|
+
const match = /^([0-9a-f]{40,64}) (\S+) \d+$/.exec(lines[index]);
|
|
1923
|
+
resolved.set(name, match ? { objectId: match[1], type: match[2] } : null);
|
|
1818
1924
|
});
|
|
1819
1925
|
return resolved;
|
|
1820
1926
|
}
|
|
1821
1927
|
function workspaceTreeObjectName(revision, workspaceRelativePath) {
|
|
1822
1928
|
return `${revision}:${normalizedWorkspaceMountPath(workspaceRelativePath)}`;
|
|
1823
1929
|
}
|
|
1930
|
+
async function pathsReplacedByGitlinks(workspacePath, baseRevision, headRevision) {
|
|
1931
|
+
const base = baseRevision ?? emptyTreeHash(workspacePath);
|
|
1932
|
+
const raw = await gitOutputAsync(
|
|
1933
|
+
workspacePath,
|
|
1934
|
+
["diff", "--raw", "--no-renames", "-z", base, headRevision],
|
|
1935
|
+
"inspect workspace gitlink changes"
|
|
1936
|
+
);
|
|
1937
|
+
const records = raw.toString().split("\0");
|
|
1938
|
+
const addedGitlinks = [];
|
|
1939
|
+
for (let index = 0; index + 1 < records.length; index += 2) {
|
|
1940
|
+
if (/^:[0-7]{6} 160000 \S+ \S+ A$/u.test(records[index])) addedGitlinks.push(records[index + 1]);
|
|
1941
|
+
}
|
|
1942
|
+
if (addedGitlinks.length === 0) return [];
|
|
1943
|
+
const objects = await resolveGitObjectsAsync(
|
|
1944
|
+
workspacePath,
|
|
1945
|
+
addedGitlinks.map((relativePath) => `${base}:${relativePath}`)
|
|
1946
|
+
);
|
|
1947
|
+
return addedGitlinks.filter((relativePath) => objects.get(`${base}:${relativePath}`)?.type === "tree").sort();
|
|
1948
|
+
}
|
|
1949
|
+
function gitlinkMigrationPathspecs(paths) {
|
|
1950
|
+
return paths.map((relativePath) => `:(exclude,literal)${relativePath}`);
|
|
1951
|
+
}
|
|
1824
1952
|
async function measureInitialPublicationDiffSize(input) {
|
|
1825
1953
|
const candidates = input.mounts.filter(
|
|
1826
1954
|
(mount) => typeof mount.initialPublicationSourceRelativePath === "string"
|
|
1827
1955
|
);
|
|
1828
1956
|
if (candidates.length === 0) return null;
|
|
1829
1957
|
const revisions = [input.localHead, ...input.remoteHead ? [input.remoteHead] : []];
|
|
1830
|
-
const objects = await
|
|
1958
|
+
const objects = await resolveGitObjectsAsync(
|
|
1831
1959
|
input.workspacePath,
|
|
1832
1960
|
candidates.flatMap(
|
|
1833
1961
|
(mount) => revisions.flatMap((revision) => [
|
|
@@ -1836,7 +1964,7 @@ async function measureInitialPublicationDiffSize(input) {
|
|
|
1836
1964
|
])
|
|
1837
1965
|
)
|
|
1838
1966
|
);
|
|
1839
|
-
const lookup = (revision, workspaceRelativePath) => revision ? objects.get(workspaceTreeObjectName(revision, workspaceRelativePath)) ?? null : null;
|
|
1967
|
+
const lookup = (revision, workspaceRelativePath) => revision ? objects.get(workspaceTreeObjectName(revision, workspaceRelativePath))?.objectId ?? null : null;
|
|
1840
1968
|
const initialPublications = candidates.flatMap((mount) => {
|
|
1841
1969
|
const published = lookup(input.localHead, mount.workspaceRelativePath);
|
|
1842
1970
|
if (!published || lookup(input.remoteHead, mount.workspaceRelativePath)) return [];
|
|
@@ -1844,16 +1972,14 @@ async function measureInitialPublicationDiffSize(input) {
|
|
|
1844
1972
|
return [{ mount, published, source }];
|
|
1845
1973
|
});
|
|
1846
1974
|
if (initialPublications.length === 0) return null;
|
|
1847
|
-
let size = await input.measure(
|
|
1848
|
-
|
|
1849
|
-
input.
|
|
1850
|
-
|
|
1851
|
-
input.limit,
|
|
1852
|
-
initialPublications.map(({ mount }) => `:(exclude,literal)${normalizedWorkspaceMountPath(mount.workspaceRelativePath)}`)
|
|
1853
|
-
);
|
|
1975
|
+
let size = await input.measure(input.workspacePath, input.remoteHead, input.localHead, input.limit, [
|
|
1976
|
+
...initialPublications.map(({ mount }) => `:(exclude,literal)${normalizedWorkspaceMountPath(mount.workspaceRelativePath)}`),
|
|
1977
|
+
...input.excludePathspecs ?? []
|
|
1978
|
+
]);
|
|
1854
1979
|
for (const { published, source } of initialPublications) {
|
|
1855
1980
|
if (size > input.limit) break;
|
|
1856
|
-
|
|
1981
|
+
const migrations = source ? await pathsReplacedByGitlinks(input.workspacePath, source, published) : [];
|
|
1982
|
+
size += await input.measure(input.workspacePath, source, published, input.limit - size, gitlinkMigrationPathspecs(migrations));
|
|
1857
1983
|
}
|
|
1858
1984
|
return { size, mountIds: initialPublications.map(({ mount }) => mount.id).sort() };
|
|
1859
1985
|
}
|
|
@@ -1971,7 +2097,8 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
1971
2097
|
await recoverWorkspaceGitHydration(workspacePath, input.mounts, {
|
|
1972
2098
|
...deferredMountIds.size > 0 ? { deferMountIds: deferredMountIds } : {},
|
|
1973
2099
|
preserveResolutionInProgress,
|
|
1974
|
-
preserveStaleBases: true
|
|
2100
|
+
preserveStaleBases: true,
|
|
2101
|
+
hydrationHooks: input.hydrationHooks
|
|
1975
2102
|
});
|
|
1976
2103
|
const selected = activeMounts(input.mounts);
|
|
1977
2104
|
const projectionMutationTokens = new Map(
|
|
@@ -2097,7 +2224,8 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
2097
2224
|
carryMounts: uncoveredCompletionMounts.filter(({ id }) => unchangedMountIds.has(id)),
|
|
2098
2225
|
receiptMounts: completionReceiptMounts,
|
|
2099
2226
|
requiredMounts: uncoveredRequiredLiveMounts.filter(({ id }) => !unchangedMountIds.has(id)),
|
|
2100
|
-
recordCurrentHead: true
|
|
2227
|
+
recordCurrentHead: true,
|
|
2228
|
+
hydrationHooks: input.hydrationHooks
|
|
2101
2229
|
});
|
|
2102
2230
|
for (const id of hydration.skippedMountIds) cycleSkippedMountIds.add(id);
|
|
2103
2231
|
refreshCycleSkippedMounts(completionReceiptMounts);
|
|
@@ -2115,10 +2243,12 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
2115
2243
|
return classifyMounts([...selected.active, ...selected.tombstones], hydration.skippedMountIds);
|
|
2116
2244
|
};
|
|
2117
2245
|
let newlyStagedPaths = [];
|
|
2246
|
+
const projectedGitlinks = [];
|
|
2118
2247
|
const stageAndCommitWorkspace = async () => {
|
|
2119
2248
|
if (workspaceResolutionInProgress(workspacePath) || await workspaceHasUnmergedEntries(workspacePath)) {
|
|
2120
2249
|
throw new Error("Outer workspace clone has an unfinished merge/rebase; finish or abort it before synchronizing");
|
|
2121
2250
|
}
|
|
2251
|
+
await stageWorkspaceGitlinks(workspacePath, projectedGitlinks);
|
|
2122
2252
|
await gitAsync(workspacePath, ["add", "-A", "--", "."], "stage workspace working trees");
|
|
2123
2253
|
newlyStagedPaths = await stagedPaths(workspacePath);
|
|
2124
2254
|
if (newlyStagedPaths.length > 0) {
|
|
@@ -2148,7 +2278,7 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
2148
2278
|
cycleSkippedMountIds.add(mount.id);
|
|
2149
2279
|
continue;
|
|
2150
2280
|
}
|
|
2151
|
-
mirrorMountsToWorkspace(workspacePath, [mount]);
|
|
2281
|
+
projectedGitlinks.push(...mirrorMountsToWorkspace(workspacePath, [mount]));
|
|
2152
2282
|
projectedFastMounts.push(mount);
|
|
2153
2283
|
await yieldToWorkerEventLoop();
|
|
2154
2284
|
}
|
|
@@ -2247,7 +2377,9 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
2247
2377
|
}
|
|
2248
2378
|
const paths = await changedPaths(workspacePath, remoteHead, localHead);
|
|
2249
2379
|
const measureDiffSize = input.measureDiffSize ?? diffSizeBytes;
|
|
2250
|
-
|
|
2380
|
+
const gitlinkMigrations = paths.length > 0 ? await pathsReplacedByGitlinks(workspacePath, remoteHead, localHead) : [];
|
|
2381
|
+
const gitlinkMigrationExcludes = gitlinkMigrationPathspecs(gitlinkMigrations);
|
|
2382
|
+
let size = paths.length > 0 ? await measureDiffSize(workspacePath, remoteHead, localHead, maxDiffBytes, gitlinkMigrationExcludes) : 0;
|
|
2251
2383
|
input.assertStillAdmitted?.();
|
|
2252
2384
|
let initialPublicationMountIds;
|
|
2253
2385
|
if (paths.length > 0 && size > maxDiffBytes && !input.allowLargeDiff) {
|
|
@@ -2257,7 +2389,8 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
2257
2389
|
localHead,
|
|
2258
2390
|
mounts: input.mounts,
|
|
2259
2391
|
limit: maxDiffBytes,
|
|
2260
|
-
measure: measureDiffSize
|
|
2392
|
+
measure: measureDiffSize,
|
|
2393
|
+
excludePathspecs: gitlinkMigrationExcludes
|
|
2261
2394
|
});
|
|
2262
2395
|
input.assertStillAdmitted?.();
|
|
2263
2396
|
if (initialPublication && initialPublication.size <= maxDiffBytes) {
|
|
@@ -2265,6 +2398,11 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
2265
2398
|
initialPublicationMountIds = initialPublication.mountIds;
|
|
2266
2399
|
}
|
|
2267
2400
|
}
|
|
2401
|
+
let gitlinkMigrationConfirmed = false;
|
|
2402
|
+
if (gitlinkMigrations.length > 0 && size <= maxDiffBytes && !input.allowLargeDiff) {
|
|
2403
|
+
gitlinkMigrationConfirmed = await measureDiffSize(workspacePath, remoteHead, localHead, maxDiffBytes) > maxDiffBytes;
|
|
2404
|
+
input.assertStillAdmitted?.();
|
|
2405
|
+
}
|
|
2268
2406
|
if (paths.length > 0 && size > maxDiffBytes && !input.allowLargeDiff) {
|
|
2269
2407
|
const classifiedMounts = classifyMounts([...selected.active, ...selected.tombstones]);
|
|
2270
2408
|
return {
|
|
@@ -2309,7 +2447,7 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
2309
2447
|
"push",
|
|
2310
2448
|
"--no-recurse-submodules"
|
|
2311
2449
|
];
|
|
2312
|
-
if (input.allowLargeDiff || initialPublicationMountIds) {
|
|
2450
|
+
if (input.allowLargeDiff || initialPublicationMountIds || gitlinkMigrationConfirmed) {
|
|
2313
2451
|
pushArgs.push(`--push-option=${WORKSPACE_GIT_CONFIRMED_LARGE_DIFF_PUSH_OPTION}`);
|
|
2314
2452
|
}
|
|
2315
2453
|
pushArgs.push("origin", `HEAD:refs/heads/${WORKSPACE_GIT_BRANCH}`);
|
|
@@ -170,14 +170,6 @@ function indexMode(entry) {
|
|
|
170
170
|
function indexInfoRecord(mode, objectId, relativePath) {
|
|
171
171
|
return Buffer.concat([Buffer.from(`${mode} ${objectId} `), Buffer.from(relativePath), Buffer.from([0])]);
|
|
172
172
|
}
|
|
173
|
-
function basisGitlinks(workspacePath, basis) {
|
|
174
|
-
const listed = gitResult(workspacePath, ["ls-tree", "-r", "-z", `${basis.basisHead}:${basis.workspaceRelativePath}`]);
|
|
175
|
-
if (listed.exitCode !== 0) return [];
|
|
176
|
-
return nulRecords(listed.stdout).flatMap((record) => {
|
|
177
|
-
const match = /^160000 commit ([0-9a-f]{40,64})\t(.+)$/su.exec(record.toString());
|
|
178
|
-
return match ? [{ objectId: match[1], relativePath: match[2] }] : [];
|
|
179
|
-
});
|
|
180
|
-
}
|
|
181
173
|
function synthesizeMountTree(input) {
|
|
182
174
|
const sourceRoot = path.resolve(input.sourcePath);
|
|
183
175
|
const entries = inspectWorkingTree(sourceRoot, input.sourceMode);
|
|
@@ -188,19 +180,14 @@ function synthesizeMountTree(input) {
|
|
|
188
180
|
const records = [];
|
|
189
181
|
for (const [relativePath, entry] of [...entries.entries()].sort(([left], [right]) => left.localeCompare(right))) {
|
|
190
182
|
if (entry.kind === "directory") continue;
|
|
183
|
+
if (entry.kind === "gitlink") {
|
|
184
|
+
records.push(indexInfoRecord("160000", entry.objectId, relativePath));
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
191
187
|
const sourcePath = sourceEntryPath(sourceRoot, relativePath);
|
|
192
188
|
const objectId = entry.kind === "file" ? hashRegularFile(input.workspacePath, sourcePath, entry) : hashSymlink(input.workspacePath, sourcePath, entry);
|
|
193
189
|
records.push(indexInfoRecord(indexMode(entry), objectId, relativePath));
|
|
194
190
|
}
|
|
195
|
-
if (input.gitlinkBasis && input.sourceMode === "git") {
|
|
196
|
-
for (const gitlink of basisGitlinks(input.workspacePath, input.gitlinkBasis)) {
|
|
197
|
-
const populatedPrefix = `${gitlink.relativePath}/`;
|
|
198
|
-
const populated = [...entries.keys()].some((relativePath) => relativePath.startsWith(populatedPrefix));
|
|
199
|
-
if (entries.get(gitlink.relativePath)?.kind === "directory" && !populated) {
|
|
200
|
-
records.push(indexInfoRecord("160000", gitlink.objectId, gitlink.relativePath));
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
191
|
if (records.length > 0) {
|
|
205
192
|
git(input.workspacePath, ["update-index", "-z", "--index-info"], "populate workspace projection index", {
|
|
206
193
|
environment,
|
|
@@ -265,8 +252,7 @@ function synthesizeOursCommit(input) {
|
|
|
265
252
|
const mountTree = synthesizeMountTree({
|
|
266
253
|
workspacePath: input.workspacePath,
|
|
267
254
|
sourcePath: input.mount.sourcePath,
|
|
268
|
-
sourceMode: input.mount.sourceMode
|
|
269
|
-
gitlinkBasis: { basisHead: input.basisHead, workspaceRelativePath: input.workspaceRelativePath }
|
|
255
|
+
sourceMode: input.mount.sourceMode
|
|
270
256
|
});
|
|
271
257
|
const rootTree = graftMountTree({
|
|
272
258
|
workspacePath: input.workspacePath,
|