@ricsam/r5d-worker 0.0.100 → 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/package.json +1 -1
- package/dist/cjs/working-tree-mirror.cjs +347 -99
- package/dist/cjs/workspace-git-sync.cjs +144 -22
- package/dist/cjs/workspace-merge-projection.cjs +5 -19
- package/dist/mjs/package.json +1 -1
- package/dist/mjs/working-tree-mirror.mjs +347 -99
- package/dist/mjs/workspace-git-sync.mjs +144 -22
- package/dist/mjs/workspace-merge-projection.mjs +5 -19
- package/dist/types/working-tree-mirror.d.ts +56 -11
- package/dist/types/workspace-git-sync.d.ts +17 -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;
|
|
@@ -1804,7 +1896,7 @@ async function diffSizeBytes(workspacePath, baseRevision, headRevision, limit, p
|
|
|
1804
1896
|
if (exitCode !== 0) throw new Error(`Measure workspace diff: ${stderr || `git exited ${exitCode}`}`);
|
|
1805
1897
|
return total;
|
|
1806
1898
|
}
|
|
1807
|
-
async function
|
|
1899
|
+
async function resolveGitObjectsAsync(workspacePath, names) {
|
|
1808
1900
|
const resolved = /* @__PURE__ */ new Map();
|
|
1809
1901
|
const unique = [...new Set(names)];
|
|
1810
1902
|
if (unique.length === 0) return resolved;
|
|
@@ -1827,21 +1919,43 @@ async function resolveGitObjectIdsAsync(workspacePath, names) {
|
|
|
1827
1919
|
throw new Error(`Resolve workspace objects: expected ${unique.length} results, received ${lines.length}`);
|
|
1828
1920
|
}
|
|
1829
1921
|
unique.forEach((name, index) => {
|
|
1830
|
-
const match = /^([0-9a-f]{40,64}) \S+ \d+$/.exec(lines[index]);
|
|
1831
|
-
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);
|
|
1832
1924
|
});
|
|
1833
1925
|
return resolved;
|
|
1834
1926
|
}
|
|
1835
1927
|
function workspaceTreeObjectName(revision, workspaceRelativePath) {
|
|
1836
1928
|
return `${revision}:${normalizedWorkspaceMountPath(workspaceRelativePath)}`;
|
|
1837
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
|
+
}
|
|
1838
1952
|
async function measureInitialPublicationDiffSize(input) {
|
|
1839
1953
|
const candidates = input.mounts.filter(
|
|
1840
1954
|
(mount) => typeof mount.initialPublicationSourceRelativePath === "string"
|
|
1841
1955
|
);
|
|
1842
1956
|
if (candidates.length === 0) return null;
|
|
1843
1957
|
const revisions = [input.localHead, ...input.remoteHead ? [input.remoteHead] : []];
|
|
1844
|
-
const objects = await
|
|
1958
|
+
const objects = await resolveGitObjectsAsync(
|
|
1845
1959
|
input.workspacePath,
|
|
1846
1960
|
candidates.flatMap(
|
|
1847
1961
|
(mount) => revisions.flatMap((revision) => [
|
|
@@ -1850,7 +1964,7 @@ async function measureInitialPublicationDiffSize(input) {
|
|
|
1850
1964
|
])
|
|
1851
1965
|
)
|
|
1852
1966
|
);
|
|
1853
|
-
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;
|
|
1854
1968
|
const initialPublications = candidates.flatMap((mount) => {
|
|
1855
1969
|
const published = lookup(input.localHead, mount.workspaceRelativePath);
|
|
1856
1970
|
if (!published || lookup(input.remoteHead, mount.workspaceRelativePath)) return [];
|
|
@@ -1858,16 +1972,14 @@ async function measureInitialPublicationDiffSize(input) {
|
|
|
1858
1972
|
return [{ mount, published, source }];
|
|
1859
1973
|
});
|
|
1860
1974
|
if (initialPublications.length === 0) return null;
|
|
1861
|
-
let size = await input.measure(
|
|
1862
|
-
|
|
1863
|
-
input.
|
|
1864
|
-
|
|
1865
|
-
input.limit,
|
|
1866
|
-
initialPublications.map(({ mount }) => `:(exclude,literal)${normalizedWorkspaceMountPath(mount.workspaceRelativePath)}`)
|
|
1867
|
-
);
|
|
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
|
+
]);
|
|
1868
1979
|
for (const { published, source } of initialPublications) {
|
|
1869
1980
|
if (size > input.limit) break;
|
|
1870
|
-
|
|
1981
|
+
const migrations = source ? await pathsReplacedByGitlinks(input.workspacePath, source, published) : [];
|
|
1982
|
+
size += await input.measure(input.workspacePath, source, published, input.limit - size, gitlinkMigrationPathspecs(migrations));
|
|
1871
1983
|
}
|
|
1872
1984
|
return { size, mountIds: initialPublications.map(({ mount }) => mount.id).sort() };
|
|
1873
1985
|
}
|
|
@@ -2131,10 +2243,12 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
2131
2243
|
return classifyMounts([...selected.active, ...selected.tombstones], hydration.skippedMountIds);
|
|
2132
2244
|
};
|
|
2133
2245
|
let newlyStagedPaths = [];
|
|
2246
|
+
const projectedGitlinks = [];
|
|
2134
2247
|
const stageAndCommitWorkspace = async () => {
|
|
2135
2248
|
if (workspaceResolutionInProgress(workspacePath) || await workspaceHasUnmergedEntries(workspacePath)) {
|
|
2136
2249
|
throw new Error("Outer workspace clone has an unfinished merge/rebase; finish or abort it before synchronizing");
|
|
2137
2250
|
}
|
|
2251
|
+
await stageWorkspaceGitlinks(workspacePath, projectedGitlinks);
|
|
2138
2252
|
await gitAsync(workspacePath, ["add", "-A", "--", "."], "stage workspace working trees");
|
|
2139
2253
|
newlyStagedPaths = await stagedPaths(workspacePath);
|
|
2140
2254
|
if (newlyStagedPaths.length > 0) {
|
|
@@ -2164,7 +2278,7 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
2164
2278
|
cycleSkippedMountIds.add(mount.id);
|
|
2165
2279
|
continue;
|
|
2166
2280
|
}
|
|
2167
|
-
mirrorMountsToWorkspace(workspacePath, [mount]);
|
|
2281
|
+
projectedGitlinks.push(...mirrorMountsToWorkspace(workspacePath, [mount]));
|
|
2168
2282
|
projectedFastMounts.push(mount);
|
|
2169
2283
|
await yieldToWorkerEventLoop();
|
|
2170
2284
|
}
|
|
@@ -2263,7 +2377,9 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
2263
2377
|
}
|
|
2264
2378
|
const paths = await changedPaths(workspacePath, remoteHead, localHead);
|
|
2265
2379
|
const measureDiffSize = input.measureDiffSize ?? diffSizeBytes;
|
|
2266
|
-
|
|
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;
|
|
2267
2383
|
input.assertStillAdmitted?.();
|
|
2268
2384
|
let initialPublicationMountIds;
|
|
2269
2385
|
if (paths.length > 0 && size > maxDiffBytes && !input.allowLargeDiff) {
|
|
@@ -2273,7 +2389,8 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
2273
2389
|
localHead,
|
|
2274
2390
|
mounts: input.mounts,
|
|
2275
2391
|
limit: maxDiffBytes,
|
|
2276
|
-
measure: measureDiffSize
|
|
2392
|
+
measure: measureDiffSize,
|
|
2393
|
+
excludePathspecs: gitlinkMigrationExcludes
|
|
2277
2394
|
});
|
|
2278
2395
|
input.assertStillAdmitted?.();
|
|
2279
2396
|
if (initialPublication && initialPublication.size <= maxDiffBytes) {
|
|
@@ -2281,6 +2398,11 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
2281
2398
|
initialPublicationMountIds = initialPublication.mountIds;
|
|
2282
2399
|
}
|
|
2283
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
|
+
}
|
|
2284
2406
|
if (paths.length > 0 && size > maxDiffBytes && !input.allowLargeDiff) {
|
|
2285
2407
|
const classifiedMounts = classifyMounts([...selected.active, ...selected.tombstones]);
|
|
2286
2408
|
return {
|
|
@@ -2325,7 +2447,7 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
2325
2447
|
"push",
|
|
2326
2448
|
"--no-recurse-submodules"
|
|
2327
2449
|
];
|
|
2328
|
-
if (input.allowLargeDiff || initialPublicationMountIds) {
|
|
2450
|
+
if (input.allowLargeDiff || initialPublicationMountIds || gitlinkMigrationConfirmed) {
|
|
2329
2451
|
pushArgs.push(`--push-option=${WORKSPACE_GIT_CONFIRMED_LARGE_DIFF_PUSH_OPTION}`);
|
|
2330
2452
|
}
|
|
2331
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,
|
|
@@ -12,6 +12,44 @@ export type TreeEntry = {
|
|
|
12
12
|
kind: "symlink";
|
|
13
13
|
mode: number;
|
|
14
14
|
target: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* A submodule (D4): the enclosing repository records a commit at this path
|
|
18
|
+
* and never its contents. A leaf for every operation here: nothing beneath
|
|
19
|
+
* it is inspected, copied, captured, removed, or restored.
|
|
20
|
+
*/
|
|
21
|
+
| {
|
|
22
|
+
kind: "gitlink";
|
|
23
|
+
objectId: string;
|
|
24
|
+
};
|
|
25
|
+
export type WorkingTreeGitlink = {
|
|
26
|
+
relativePath: string;
|
|
27
|
+
objectId: string;
|
|
28
|
+
};
|
|
29
|
+
/** One index record of a Git checkout, in the form `git update-index --index-info` accepts back. */
|
|
30
|
+
export type WorkingTreeIndexRecord = {
|
|
31
|
+
mode: string;
|
|
32
|
+
objectId: string;
|
|
33
|
+
stage: number;
|
|
34
|
+
relativePath: string;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Index entries a Git-target mirror changes: the records it removes or
|
|
38
|
+
* overwrites (a restore puts them back) and the gitlinks it writes (a restore
|
|
39
|
+
* drops them first).
|
|
40
|
+
*/
|
|
41
|
+
export type WorkingTreeIndexChanges = {
|
|
42
|
+
removed: WorkingTreeIndexRecord[];
|
|
43
|
+
written: WorkingTreeGitlink[];
|
|
44
|
+
};
|
|
45
|
+
export type WorkingTreeInspectionOptions = {
|
|
46
|
+
/**
|
|
47
|
+
* Gitlinks the enclosing repository records under an all-mode root, keyed
|
|
48
|
+
* by relative path. Git checks a submodule out as an empty directory, so
|
|
49
|
+
* the filesystem alone cannot show one; the entry replaces whatever is on
|
|
50
|
+
* disk at that path. Ignored in Git mode, where the index is read directly.
|
|
51
|
+
*/
|
|
52
|
+
gitlinks?: ReadonlyMap<string, string>;
|
|
15
53
|
};
|
|
16
54
|
/**
|
|
17
55
|
* Destructive native-tree moves cannot silently apply the Git-target filter:
|
|
@@ -20,19 +58,14 @@ export type TreeEntry = {
|
|
|
20
58
|
* every portable alias must be remediated before the old tree is retired.
|
|
21
59
|
*/
|
|
22
60
|
export declare function assertWorkingTreeHasNoPortableGitMetadataAliases(root: string): void;
|
|
23
|
-
export declare function inspectWorkingTree(root: string, mode: WorkingTreeSourceMode, options?:
|
|
24
|
-
/**
|
|
25
|
-
* Leave an uninitialized submodule (index gitlink whose directory is
|
|
26
|
-
* empty) out of a Git-mode inspection. Its empty directory is what keeps
|
|
27
|
-
* `git status` clean, so a deletion set must never remove it.
|
|
28
|
-
*/
|
|
29
|
-
omitUnpopulatedGitlinks?: boolean;
|
|
30
|
-
}): Map<string, TreeEntry>;
|
|
61
|
+
export declare function inspectWorkingTree(root: string, mode: WorkingTreeSourceMode, options?: WorkingTreeInspectionOptions): Map<string, TreeEntry>;
|
|
31
62
|
export declare function mirrorWorkingTree(input: {
|
|
32
63
|
sourceRoot: string;
|
|
33
64
|
targetRoot: string;
|
|
34
65
|
sourceMode: WorkingTreeSourceMode;
|
|
35
66
|
deletionMode: WorkingTreeDeletionMode;
|
|
67
|
+
/** Gitlinks the enclosing repository records for an all-mode source (see `inspectWorkingTree`). */
|
|
68
|
+
sourceGitlinks?: ReadonlyMap<string, string>;
|
|
36
69
|
/**
|
|
37
70
|
* Defer destination fsync only while building an unpublished private tree.
|
|
38
71
|
* The caller must durably fsync that complete tree before publishing it or
|
|
@@ -41,20 +74,28 @@ export declare function mirrorWorkingTree(input: {
|
|
|
41
74
|
durability?: WorkingTreeMirrorDurability;
|
|
42
75
|
}): {
|
|
43
76
|
paths: string[];
|
|
77
|
+
gitlinks: WorkingTreeGitlink[];
|
|
44
78
|
};
|
|
45
79
|
/** Canonical relative form used by mirror plans and scopes, or null when the value is unsafe. */
|
|
46
80
|
export declare function normalizeWorkingTreeRelativePath(value: string): string | null;
|
|
47
81
|
export type WorkingTreeMirrorPlan = {
|
|
48
|
-
/**
|
|
82
|
+
/**
|
|
83
|
+
* Every target path the mirror creates, rewrites, or verifies, ancestor
|
|
84
|
+
* directories included. A gitlink is verified as a directory; paths a
|
|
85
|
+
* submodule directory on the target shields are left out.
|
|
86
|
+
*/
|
|
49
87
|
desired: Map<string, TreeEntry>;
|
|
50
88
|
/**
|
|
51
89
|
* Target entries the mirror removes or overwrites, as they are before it
|
|
52
90
|
* runs. A directory entry means the whole subtree goes, ignored files
|
|
53
|
-
* included; entries beneath it are folded into it.
|
|
91
|
+
* included; entries beneath it are folded into it. Never a gitlink: a
|
|
92
|
+
* submodule directory stays, and only its index entry can change.
|
|
54
93
|
*/
|
|
55
94
|
replaced: Map<string, TreeEntry>;
|
|
56
95
|
/** Desired paths that have no target entry yet. */
|
|
57
96
|
absent: string[];
|
|
97
|
+
/** Index entries the mirror changes on a Git target; empty for any other target. */
|
|
98
|
+
index: WorkingTreeIndexChanges;
|
|
58
99
|
};
|
|
59
100
|
/**
|
|
60
101
|
* Compute exactly which target entries `mirrorWorkingTree` would remove,
|
|
@@ -63,19 +104,23 @@ export type WorkingTreeMirrorPlan = {
|
|
|
63
104
|
* A desired path replaces its target entry when the kinds differ, a symlink
|
|
64
105
|
* points elsewhere, or a file's bytes or mode differ; an equal entry is left
|
|
65
106
|
* alone, so the plan covers only what the mirror actually touches. A Git
|
|
66
|
-
* target's ignored trees appear only where a desired path lands on them
|
|
107
|
+
* target's ignored trees appear only where a desired path lands on them, and
|
|
108
|
+
* its submodule directories only as index changes.
|
|
67
109
|
*/
|
|
68
110
|
export declare function planWorkingTreeMirror(input: {
|
|
69
111
|
sourceRoot: string;
|
|
70
112
|
targetRoot: string;
|
|
71
113
|
sourceMode: WorkingTreeSourceMode;
|
|
72
114
|
deletionMode: WorkingTreeDeletionMode;
|
|
115
|
+
sourceGitlinks?: ReadonlyMap<string, string>;
|
|
73
116
|
}): WorkingTreeMirrorPlan;
|
|
74
117
|
export type WorkingTreeMirrorScope = {
|
|
75
118
|
/** Desired paths that did not exist before the mirror ran; a restore removes them. */
|
|
76
119
|
absent: string[];
|
|
77
120
|
/** Directories captured whole; a restore makes them identical to the snapshot again. */
|
|
78
121
|
trees: string[];
|
|
122
|
+
/** Index entries the mirror changed on a Git target; a restore drops the written gitlinks and puts the removed records back. */
|
|
123
|
+
index?: WorkingTreeIndexChanges;
|
|
79
124
|
};
|
|
80
125
|
/**
|
|
81
126
|
* Copy the entries a mirror plan replaces into `snapshotRoot` at their
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type WorkingTreeSourceMode } from "./working-tree-mirror";
|
|
1
|
+
import { type WorkingTreeGitlink, type WorkingTreeSourceMode } from "./working-tree-mirror";
|
|
2
2
|
export declare const WORKSPACE_GIT_BRANCH = "main";
|
|
3
3
|
export declare const MAX_WORKSPACE_GIT_DIFF_BYTES: number;
|
|
4
4
|
export declare const WORKSPACE_GIT_CONFIRMED_LARGE_DIFF_PUSH_OPTION = "r5d-confirm-large-diff-v1";
|
|
@@ -154,7 +154,16 @@ export declare function ensureWorkspaceGitClone(input: {
|
|
|
154
154
|
localHead: string | null;
|
|
155
155
|
remoteHead: string | null;
|
|
156
156
|
}>;
|
|
157
|
-
|
|
157
|
+
type ProjectedMountGitlinks = {
|
|
158
|
+
mount: WorkspaceGitMount;
|
|
159
|
+
gitlinks: WorkingTreeGitlink[];
|
|
160
|
+
};
|
|
161
|
+
/**
|
|
162
|
+
* Mirror each mount's visible tree into the outer worktree. A submodule
|
|
163
|
+
* arrives as an empty directory plus a gitlink the caller must stage with
|
|
164
|
+
* `stageWorkspaceGitlinks` before `git add -A`, which can never create one.
|
|
165
|
+
*/
|
|
166
|
+
declare function mirrorMountsToWorkspace(workspacePath: string, mounts: readonly WorkspaceGitMount[]): ProjectedMountGitlinks[];
|
|
158
167
|
export declare function hydrateWorkspaceGitMounts(workspacePath: string, mounts: readonly WorkspaceGitMount[], options?: {
|
|
159
168
|
ignoreBusy?: boolean;
|
|
160
169
|
hydrationHooks?: WorkspaceHydrationTransactionHooks;
|
|
@@ -188,6 +197,12 @@ export declare function resetWorkspaceGit(input: {
|
|
|
188
197
|
activeMountIds: string[];
|
|
189
198
|
skippedMountIds: string[];
|
|
190
199
|
}>;
|
|
200
|
+
/**
|
|
201
|
+
* Bytes of the raw `git diff --binary` between two revisions, capped at
|
|
202
|
+
* `limit + 1`. `pathspecs` narrows what is measured; callers pass
|
|
203
|
+
* `:(exclude,literal)<path>` for paths whose text is not publication content,
|
|
204
|
+
* such as a subtree a gitlink replaces (see `pathsReplacedByGitlinks`).
|
|
205
|
+
*/
|
|
191
206
|
declare function diffSizeBytes(workspacePath: string, baseRevision: string | null, headRevision: string, limit: number, pathspecs?: readonly string[]): Promise<number>;
|
|
192
207
|
export declare function synchronizeWorkspaceGit(input: {
|
|
193
208
|
attemptId?: string;
|