@ricsam/r5d-worker 0.0.94 → 0.0.96
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 +145 -28
- package/dist/cjs/package.json +1 -1
- package/dist/cjs/project-mirror-fetch-policy.cjs +41 -0
- package/dist/cjs/project-mirror-refs-token.cjs +51 -0
- package/dist/cjs/project-worktrees.cjs +40 -15
- package/dist/cjs/workspace-git-sync.cjs +146 -14
- package/dist/mjs/main.mjs +146 -28
- package/dist/mjs/package.json +1 -1
- package/dist/mjs/project-mirror-fetch-policy.mjs +16 -0
- package/dist/mjs/project-mirror-refs-token.mjs +24 -0
- package/dist/mjs/project-worktrees.mjs +44 -15
- package/dist/mjs/workspace-git-sync.mjs +146 -14
- package/dist/types/main.d.ts +9 -0
- package/dist/types/project-mirror-fetch-policy.d.ts +29 -0
- package/dist/types/project-mirror-refs-token.d.ts +19 -0
- package/dist/types/project-worktrees.d.ts +21 -0
- package/dist/types/workspace-git-sync.d.ts +14 -1
- package/package.json +1 -1
|
@@ -4,6 +4,12 @@ import os from "node:os";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { gitCredentialUsernameConfigKey, gitTransportSecurityArgs, workerGitProcessEnvironment } from "./git-process-environment.mjs";
|
|
6
6
|
import { validateManagedBranchName } from "./managed-paths.mjs";
|
|
7
|
+
import {
|
|
8
|
+
canonicalizeLocalProjectMirrorRefs,
|
|
9
|
+
hashProjectMirrorRefs,
|
|
10
|
+
PROJECT_MIRROR_REF_PREFIX,
|
|
11
|
+
PROJECT_SERVER_SYNCED_REF_PREFIX
|
|
12
|
+
} from "./project-mirror-refs-token.mjs";
|
|
7
13
|
import { mirrorWorkingTree } from "./working-tree-mirror.mjs";
|
|
8
14
|
const PROJECT_WORKTREE_SNAPSHOT_PREFIX = "r5d-project-worktrees-";
|
|
9
15
|
const PROJECT_WORKTREE_SNAPSHOT_MANIFEST = "transaction.json";
|
|
@@ -1241,6 +1247,7 @@ ${deleted.stdout}`)) {
|
|
|
1241
1247
|
`Delete hidden project mirror for ${input.branchName}: ${deleted.stderr || deleted.stdout || `git exited ${deleted.exitCode}`}`
|
|
1242
1248
|
);
|
|
1243
1249
|
}
|
|
1250
|
+
await gitResultAsync(input.gitDirectory, ["update-ref", "-d", `refs/r5d/mirror/${input.branchName}`]);
|
|
1244
1251
|
}
|
|
1245
1252
|
async function pushProjectMirrorHeads(input) {
|
|
1246
1253
|
const results = [];
|
|
@@ -1298,6 +1305,7 @@ async function pushProjectMirrorHeads(input) {
|
|
|
1298
1305
|
if (pushed.exitCode !== 0) {
|
|
1299
1306
|
throw new Error(`Push hidden project mirror for ${branchName}: ${pushed.stderr || pushed.stdout || `git exited ${pushed.exitCode}`}`);
|
|
1300
1307
|
}
|
|
1308
|
+
await gitAsync(checkoutPath, ["update-ref", `refs/r5d/mirror/${branchName}`, head], `record pushed mirror tip ${branchName}`);
|
|
1301
1309
|
results.push({ branchName, head, pushed: true });
|
|
1302
1310
|
}
|
|
1303
1311
|
return results;
|
|
@@ -1310,21 +1318,23 @@ function assertProjectMirrorHeadsPushed(results) {
|
|
|
1310
1318
|
}
|
|
1311
1319
|
async function observeProjectMirrorHeads(input) {
|
|
1312
1320
|
const primaryPath = branchPath(input.projectRoot, input.primaryBranchName);
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1321
|
+
if (input.refreshRemote !== false) {
|
|
1322
|
+
const fetch = await gitResultAsync(
|
|
1323
|
+
primaryPath,
|
|
1324
|
+
[
|
|
1325
|
+
...gitTransportSecurityArgs(input.mirrorUrl, input.credentialHelper, input.credentialUsername),
|
|
1326
|
+
"fetch",
|
|
1327
|
+
"--no-recurse-submodules",
|
|
1328
|
+
"--prune",
|
|
1329
|
+
input.mirrorUrl,
|
|
1330
|
+
"+refs/heads/*:refs/r5d/mirror/*",
|
|
1331
|
+
"+refs/r5d/server-synced/*:refs/r5d/server-synced/*"
|
|
1332
|
+
],
|
|
1333
|
+
PROJECT_GIT_TRANSPORT_TIMEOUT_MS
|
|
1334
|
+
);
|
|
1335
|
+
if (fetch.exitCode !== 0)
|
|
1336
|
+
throw new Error(`Refresh project head mirror: ${fetch.stderr || fetch.stdout || `git exited ${fetch.exitCode}`}`);
|
|
1337
|
+
}
|
|
1328
1338
|
const observations = [];
|
|
1329
1339
|
for (const branchName of [...input.branchNames].sort()) {
|
|
1330
1340
|
const checkoutPath = branchPath(input.projectRoot, branchName);
|
|
@@ -1370,6 +1380,24 @@ async function observeProjectMirrorHeads(input) {
|
|
|
1370
1380
|
}
|
|
1371
1381
|
return observations;
|
|
1372
1382
|
}
|
|
1383
|
+
async function computeLocalProjectMirrorRefsToken(input) {
|
|
1384
|
+
const primaryPath = branchPath(input.projectRoot, input.primaryBranchName);
|
|
1385
|
+
const rows = await gitAsync(
|
|
1386
|
+
primaryPath,
|
|
1387
|
+
["for-each-ref", "--format=%(objectname) %(refname)", PROJECT_MIRROR_REF_PREFIX, PROJECT_SERVER_SYNCED_REF_PREFIX],
|
|
1388
|
+
"list cached project mirror refs"
|
|
1389
|
+
);
|
|
1390
|
+
const refs = rows.split(/\r?\n/).filter(Boolean).map((row) => {
|
|
1391
|
+
const separator = row.indexOf(" ");
|
|
1392
|
+
const objectName = separator < 0 ? "" : row.slice(0, separator);
|
|
1393
|
+
const refname = separator < 0 ? "" : row.slice(separator + 1);
|
|
1394
|
+
if (!/^[0-9a-f]{40,64}$/.test(objectName) || refname.length === 0) {
|
|
1395
|
+
throw new Error(`Invalid cached mirror ref listing row: ${row}`);
|
|
1396
|
+
}
|
|
1397
|
+
return { refname, objectName };
|
|
1398
|
+
});
|
|
1399
|
+
return hashProjectMirrorRefs(canonicalizeLocalProjectMirrorRefs(refs));
|
|
1400
|
+
}
|
|
1373
1401
|
async function applyObservedProjectMirrorHeads(input) {
|
|
1374
1402
|
const results = [];
|
|
1375
1403
|
for (const observation of [...input.observations].sort((left, right) => left.branchName.localeCompare(right.branchName))) {
|
|
@@ -1451,6 +1479,7 @@ export {
|
|
|
1451
1479
|
ProjectBranchCreationRollbackIncompleteError,
|
|
1452
1480
|
applyObservedProjectMirrorHeads,
|
|
1453
1481
|
assertProjectMirrorHeadsPushed,
|
|
1482
|
+
computeLocalProjectMirrorRefsToken,
|
|
1454
1483
|
createLinkedProjectBranch,
|
|
1455
1484
|
createOrRetryLinkedProjectBranch,
|
|
1456
1485
|
deleteLinkedProjectBranch,
|
|
@@ -1438,13 +1438,38 @@ function hydrateWorkspaceGitMountsRaw(workspacePath, mounts, options = {}) {
|
|
|
1438
1438
|
}
|
|
1439
1439
|
return { hydratedMountIds: hydratedMountIds.sort(), skippedMountIds: skippedMountIds.sort() };
|
|
1440
1440
|
}
|
|
1441
|
+
async function selectMountsWithUnchangedOuterSubtree(workspacePath, receipt, head, mounts) {
|
|
1442
|
+
const candidates = mounts.flatMap((mount) => {
|
|
1443
|
+
const basisHead = workspaceHydrationReceiptMountBasisHead(receipt, mount);
|
|
1444
|
+
return basisHead && basisHead !== head ? [{ mount, basisHead }] : [];
|
|
1445
|
+
});
|
|
1446
|
+
if (candidates.length === 0) return /* @__PURE__ */ new Set();
|
|
1447
|
+
const objects = await resolveGitObjectIdsAsync(
|
|
1448
|
+
workspacePath,
|
|
1449
|
+
candidates.flatMap(({ mount, basisHead }) => [
|
|
1450
|
+
workspaceTreeObjectName(basisHead, mount.workspaceRelativePath),
|
|
1451
|
+
workspaceTreeObjectName(head, mount.workspaceRelativePath)
|
|
1452
|
+
])
|
|
1453
|
+
);
|
|
1454
|
+
const unchanged = /* @__PURE__ */ new Set();
|
|
1455
|
+
for (const { mount, basisHead } of candidates) {
|
|
1456
|
+
const before = objects.get(workspaceTreeObjectName(basisHead, mount.workspaceRelativePath)) ?? null;
|
|
1457
|
+
const after = objects.get(workspaceTreeObjectName(head, mount.workspaceRelativePath)) ?? null;
|
|
1458
|
+
if (before === after) unchanged.add(mount.id);
|
|
1459
|
+
}
|
|
1460
|
+
return unchanged;
|
|
1461
|
+
}
|
|
1441
1462
|
async function hydrateWorkspaceGitMountsTransactionally(input) {
|
|
1442
1463
|
const workspacePath = path.resolve(input.workspacePath);
|
|
1443
1464
|
const durabilityMounts = input.durabilityMounts ?? input.mounts;
|
|
1444
1465
|
const receiptMounts = input.receiptMounts ?? durabilityMounts;
|
|
1445
1466
|
const requiredMounts = input.requiredMounts ?? input.mounts;
|
|
1467
|
+
const carryMounts = input.carryMounts ?? [];
|
|
1468
|
+
if (carryMounts.length > 0 && !input.recordCurrentHead) {
|
|
1469
|
+
throw new Error("Workspace hydration can only carry unchanged mounts while recording the current head");
|
|
1470
|
+
}
|
|
1446
1471
|
const candidateMounts = [
|
|
1447
|
-
...new Map([...input.mounts, ...durabilityMounts, ...requiredMounts].map((mount) => [mount.id, mount])).values()
|
|
1472
|
+
...new Map([...input.mounts, ...durabilityMounts, ...requiredMounts, ...carryMounts].map((mount) => [mount.id, mount])).values()
|
|
1448
1473
|
];
|
|
1449
1474
|
if (input.recordCurrentHead && !await workspaceCheckoutDurabilityIsCurrentAsync(workspacePath)) {
|
|
1450
1475
|
throw new Error("Workspace checkout is not durably materialized at its current HEAD");
|
|
@@ -1460,12 +1485,13 @@ async function hydrateWorkspaceGitMountsTransactionally(input) {
|
|
|
1460
1485
|
({ id }) => !busyMountIds.has(id) && (requestedIds.has(id) || !requiredIds.has(id))
|
|
1461
1486
|
);
|
|
1462
1487
|
const unavailableRequiredIds = requiredMounts.filter(({ id }) => busyMountIds.has(id) || !requestedIds.has(id)).map(({ id }) => id);
|
|
1488
|
+
const carriedMounts = carryMounts.filter(({ id }) => !busyMountIds.has(id));
|
|
1463
1489
|
return {
|
|
1464
1490
|
busyMountIds,
|
|
1465
1491
|
hydrationMounts: hydrationMounts2,
|
|
1466
1492
|
advancedDurabilityMounts: advancedDurabilityMounts2,
|
|
1467
1493
|
skippedMountIds: [.../* @__PURE__ */ new Set([...busyMountIds, ...unavailableRequiredIds])].sort(),
|
|
1468
|
-
targetReceipt: targetHead ? receiptWithMountsAtHead(receiptBeforeTransaction, targetHead, advancedDurabilityMounts2, receiptMounts) : null
|
|
1494
|
+
targetReceipt: targetHead ? receiptWithMountsAtHead(receiptBeforeTransaction, targetHead, [...advancedDurabilityMounts2, ...carriedMounts], receiptMounts) : null
|
|
1469
1495
|
};
|
|
1470
1496
|
};
|
|
1471
1497
|
const noopCandidate = computeSelection();
|
|
@@ -1641,14 +1667,26 @@ async function changedPaths(workspacePath, baseRevision, headRevision) {
|
|
|
1641
1667
|
const base = baseRevision ?? emptyTreeHash(workspacePath);
|
|
1642
1668
|
return (await gitAsync(workspacePath, ["diff", "--name-only", "--no-renames", "-z", base, headRevision], "inspect workspace commit paths")).split("\0").filter(Boolean).sort();
|
|
1643
1669
|
}
|
|
1644
|
-
async function diffSizeBytes(workspacePath, baseRevision, headRevision, limit) {
|
|
1670
|
+
async function diffSizeBytes(workspacePath, baseRevision, headRevision, limit, pathspecs = []) {
|
|
1645
1671
|
const base = baseRevision ?? emptyTreeHash(workspacePath);
|
|
1646
|
-
const subprocess = Bun.spawn(
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1672
|
+
const subprocess = Bun.spawn(
|
|
1673
|
+
[
|
|
1674
|
+
"git",
|
|
1675
|
+
...NON_RECURSIVE_GIT_CONFIG,
|
|
1676
|
+
"diff",
|
|
1677
|
+
"--binary",
|
|
1678
|
+
"--no-ext-diff",
|
|
1679
|
+
base,
|
|
1680
|
+
headRevision,
|
|
1681
|
+
...pathspecs.length > 0 ? ["--", ...pathspecs] : []
|
|
1682
|
+
],
|
|
1683
|
+
{
|
|
1684
|
+
cwd: workspacePath,
|
|
1685
|
+
stdout: "pipe",
|
|
1686
|
+
stderr: "pipe",
|
|
1687
|
+
env: workerGitProcessEnvironment()
|
|
1688
|
+
}
|
|
1689
|
+
);
|
|
1652
1690
|
const stderrPromise = new Response(subprocess.stderr).text();
|
|
1653
1691
|
const reader = subprocess.stdout.getReader();
|
|
1654
1692
|
let total = 0;
|
|
@@ -1668,6 +1706,73 @@ async function diffSizeBytes(workspacePath, baseRevision, headRevision, limit) {
|
|
|
1668
1706
|
if (exitCode !== 0) throw new Error(`Measure workspace diff: ${stderr || `git exited ${exitCode}`}`);
|
|
1669
1707
|
return total;
|
|
1670
1708
|
}
|
|
1709
|
+
async function resolveGitObjectIdsAsync(workspacePath, names) {
|
|
1710
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
1711
|
+
const unique = [...new Set(names)];
|
|
1712
|
+
if (unique.length === 0) return resolved;
|
|
1713
|
+
const subprocess = Bun.spawn(gitCommandArgs(["cat-file", "--batch-check"]), {
|
|
1714
|
+
cwd: workspacePath,
|
|
1715
|
+
stdin: Buffer.from(`${unique.join("\n")}
|
|
1716
|
+
`),
|
|
1717
|
+
stdout: "pipe",
|
|
1718
|
+
stderr: "pipe",
|
|
1719
|
+
env: workerGitProcessEnvironment()
|
|
1720
|
+
});
|
|
1721
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
1722
|
+
new Response(subprocess.stdout).text(),
|
|
1723
|
+
new Response(subprocess.stderr).text(),
|
|
1724
|
+
subprocess.exited
|
|
1725
|
+
]);
|
|
1726
|
+
if (exitCode !== 0) throw new Error(`Resolve workspace objects: ${stderr.trim() || `git exited ${exitCode}`}`);
|
|
1727
|
+
const lines = stdout.split("\n").filter((line) => line.length > 0);
|
|
1728
|
+
if (lines.length !== unique.length) {
|
|
1729
|
+
throw new Error(`Resolve workspace objects: expected ${unique.length} results, received ${lines.length}`);
|
|
1730
|
+
}
|
|
1731
|
+
unique.forEach((name, index) => {
|
|
1732
|
+
const match = /^([0-9a-f]{40,64}) \S+ \d+$/.exec(lines[index]);
|
|
1733
|
+
resolved.set(name, match ? match[1] : null);
|
|
1734
|
+
});
|
|
1735
|
+
return resolved;
|
|
1736
|
+
}
|
|
1737
|
+
function workspaceTreeObjectName(revision, workspaceRelativePath) {
|
|
1738
|
+
return `${revision}:${normalizedWorkspaceMountPath(workspaceRelativePath)}`;
|
|
1739
|
+
}
|
|
1740
|
+
async function measureInitialPublicationDiffSize(input) {
|
|
1741
|
+
const candidates = input.mounts.filter(
|
|
1742
|
+
(mount) => typeof mount.initialPublicationSourceRelativePath === "string"
|
|
1743
|
+
);
|
|
1744
|
+
if (candidates.length === 0) return null;
|
|
1745
|
+
const revisions = [input.localHead, ...input.remoteHead ? [input.remoteHead] : []];
|
|
1746
|
+
const objects = await resolveGitObjectIdsAsync(
|
|
1747
|
+
input.workspacePath,
|
|
1748
|
+
candidates.flatMap(
|
|
1749
|
+
(mount) => revisions.flatMap((revision) => [
|
|
1750
|
+
workspaceTreeObjectName(revision, mount.workspaceRelativePath),
|
|
1751
|
+
workspaceTreeObjectName(revision, mount.initialPublicationSourceRelativePath)
|
|
1752
|
+
])
|
|
1753
|
+
)
|
|
1754
|
+
);
|
|
1755
|
+
const lookup = (revision, workspaceRelativePath) => revision ? objects.get(workspaceTreeObjectName(revision, workspaceRelativePath)) ?? null : null;
|
|
1756
|
+
const initialPublications = candidates.flatMap((mount) => {
|
|
1757
|
+
const published = lookup(input.localHead, mount.workspaceRelativePath);
|
|
1758
|
+
if (!published || lookup(input.remoteHead, mount.workspaceRelativePath)) return [];
|
|
1759
|
+
const source = lookup(input.localHead, mount.initialPublicationSourceRelativePath) ?? lookup(input.remoteHead, mount.initialPublicationSourceRelativePath);
|
|
1760
|
+
return [{ mount, published, source }];
|
|
1761
|
+
});
|
|
1762
|
+
if (initialPublications.length === 0) return null;
|
|
1763
|
+
let size = await input.measure(
|
|
1764
|
+
input.workspacePath,
|
|
1765
|
+
input.remoteHead,
|
|
1766
|
+
input.localHead,
|
|
1767
|
+
input.limit,
|
|
1768
|
+
initialPublications.map(({ mount }) => `:(exclude,literal)${normalizedWorkspaceMountPath(mount.workspaceRelativePath)}`)
|
|
1769
|
+
);
|
|
1770
|
+
for (const { published, source } of initialPublications) {
|
|
1771
|
+
if (size > input.limit) break;
|
|
1772
|
+
size += await input.measure(input.workspacePath, source, published, input.limit - size);
|
|
1773
|
+
}
|
|
1774
|
+
return { size, mountIds: initialPublications.map(({ mount }) => mount.id).sort() };
|
|
1775
|
+
}
|
|
1671
1776
|
function conflictPaths(workspacePath) {
|
|
1672
1777
|
const result = gitResult(workspacePath, ["diff", "--name-only", "--diff-filter=U", "-z"]);
|
|
1673
1778
|
return result.exitCode === 0 ? result.stdout.split("\0").filter(Boolean).sort() : [];
|
|
@@ -1895,12 +2000,19 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
1895
2000
|
const uncoveredCompletionBasisMounts = completionReceiptMounts.filter(
|
|
1896
2001
|
(mount) => !cycleSkippedMountIds.has(mount.id) && !projectionSkippedIds.has(mount.id) && !mountIsAlreadyCovered(mount)
|
|
1897
2002
|
);
|
|
2003
|
+
const unchangedMountIds = currentHeadBeforeHydration ? await selectMountsWithUnchangedOuterSubtree(
|
|
2004
|
+
workspacePath,
|
|
2005
|
+
currentReceiptBeforeHydration,
|
|
2006
|
+
currentHeadBeforeHydration,
|
|
2007
|
+
uncoveredCompletionMounts
|
|
2008
|
+
) : /* @__PURE__ */ new Set();
|
|
1898
2009
|
const hydration = await hydrateWorkspaceGitMountsTransactionally({
|
|
1899
2010
|
workspacePath,
|
|
1900
|
-
mounts: uncoveredCompletionMounts,
|
|
1901
|
-
durabilityMounts: uncoveredCompletionBasisMounts,
|
|
2011
|
+
mounts: uncoveredCompletionMounts.filter(({ id }) => !unchangedMountIds.has(id)),
|
|
2012
|
+
durabilityMounts: uncoveredCompletionBasisMounts.filter(({ id }) => !unchangedMountIds.has(id)),
|
|
2013
|
+
carryMounts: uncoveredCompletionMounts.filter(({ id }) => unchangedMountIds.has(id)),
|
|
1902
2014
|
receiptMounts: completionReceiptMounts,
|
|
1903
|
-
requiredMounts: uncoveredRequiredLiveMounts,
|
|
2015
|
+
requiredMounts: uncoveredRequiredLiveMounts.filter(({ id }) => !unchangedMountIds.has(id)),
|
|
1904
2016
|
recordCurrentHead: true
|
|
1905
2017
|
});
|
|
1906
2018
|
for (const id of hydration.skippedMountIds) cycleSkippedMountIds.add(id);
|
|
@@ -2050,8 +2162,25 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
2050
2162
|
};
|
|
2051
2163
|
}
|
|
2052
2164
|
const paths = await changedPaths(workspacePath, remoteHead, localHead);
|
|
2053
|
-
const
|
|
2165
|
+
const measureDiffSize = input.measureDiffSize ?? diffSizeBytes;
|
|
2166
|
+
let size = paths.length > 0 ? await measureDiffSize(workspacePath, remoteHead, localHead, maxDiffBytes) : 0;
|
|
2054
2167
|
input.assertStillAdmitted?.();
|
|
2168
|
+
let initialPublicationMountIds;
|
|
2169
|
+
if (paths.length > 0 && size > maxDiffBytes && !input.allowLargeDiff) {
|
|
2170
|
+
const initialPublication = await measureInitialPublicationDiffSize({
|
|
2171
|
+
workspacePath,
|
|
2172
|
+
remoteHead,
|
|
2173
|
+
localHead,
|
|
2174
|
+
mounts: input.mounts,
|
|
2175
|
+
limit: maxDiffBytes,
|
|
2176
|
+
measure: measureDiffSize
|
|
2177
|
+
});
|
|
2178
|
+
input.assertStillAdmitted?.();
|
|
2179
|
+
if (initialPublication && initialPublication.size <= maxDiffBytes) {
|
|
2180
|
+
size = initialPublication.size;
|
|
2181
|
+
initialPublicationMountIds = initialPublication.mountIds;
|
|
2182
|
+
}
|
|
2183
|
+
}
|
|
2055
2184
|
if (paths.length > 0 && size > maxDiffBytes && !input.allowLargeDiff) {
|
|
2056
2185
|
const classifiedMounts = classifyMounts([...selected.active, ...selected.tombstones]);
|
|
2057
2186
|
return {
|
|
@@ -2096,7 +2225,9 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
2096
2225
|
"push",
|
|
2097
2226
|
"--no-recurse-submodules"
|
|
2098
2227
|
];
|
|
2099
|
-
if (input.allowLargeDiff)
|
|
2228
|
+
if (input.allowLargeDiff || initialPublicationMountIds) {
|
|
2229
|
+
pushArgs.push(`--push-option=${WORKSPACE_GIT_CONFIRMED_LARGE_DIFF_PUSH_OPTION}`);
|
|
2230
|
+
}
|
|
2100
2231
|
pushArgs.push("origin", `HEAD:refs/heads/${WORKSPACE_GIT_BRANCH}`);
|
|
2101
2232
|
const push = await gitResultAsync(workspacePath, pushArgs);
|
|
2102
2233
|
if (push.exitCode === 0) {
|
|
@@ -2118,6 +2249,7 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
2118
2249
|
diffSizeBytes: size,
|
|
2119
2250
|
affectedPaths: paths,
|
|
2120
2251
|
...completedMounts,
|
|
2252
|
+
...initialPublicationMountIds ? { initialPublicationMountIds } : {},
|
|
2121
2253
|
...verifiedAncestorHeads ? { verifiedAncestorHeads } : {},
|
|
2122
2254
|
...projectionWarning ? { error: projectionWarning } : {}
|
|
2123
2255
|
};
|
package/dist/types/main.d.ts
CHANGED
|
@@ -125,6 +125,15 @@ type WorkerServerMessage = {
|
|
|
125
125
|
confirmationReason?: string;
|
|
126
126
|
resetToCanonical?: boolean;
|
|
127
127
|
requiredAncestorHeads?: string[];
|
|
128
|
+
} | {
|
|
129
|
+
/**
|
|
130
|
+
* Content-derived tokens naming each canonical project repository's
|
|
131
|
+
* observable ref state, keyed by project id. Replace-all: projects
|
|
132
|
+
* absent from a message have no advertised token and observation falls
|
|
133
|
+
* back to fetching. See `project-mirror-fetch-policy.ts`.
|
|
134
|
+
*/
|
|
135
|
+
type: "project_mirror_refs_tokens";
|
|
136
|
+
tokens: Record<string, string>;
|
|
128
137
|
} | {
|
|
129
138
|
type: "create_project_branch";
|
|
130
139
|
requestId: string;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decides whether a periodic observation must fetch a project's hidden mirror
|
|
3
|
+
* or may reuse the locally cached mirror refs. The server advertises a
|
|
4
|
+
* content-derived token per project (hash of the exact ref namespaces the
|
|
5
|
+
* observation fetch would transfer); when the advertised token equals the
|
|
6
|
+
* token the local refs already cover, the fetch would be a no-op.
|
|
7
|
+
*
|
|
8
|
+
* Skipping is fail-open: any missing or mismatched information fetches. The
|
|
9
|
+
* backstop bounds the damage of a server-side token bug (for example a token
|
|
10
|
+
* computed against the wrong repository path) to one interval; every
|
|
11
|
+
* git-transport writer is observed by the server's on-disk recompute, so the
|
|
12
|
+
* backstop is insurance against bugs, not against known unobserved writers.
|
|
13
|
+
*/
|
|
14
|
+
export declare const PROJECT_MIRROR_FETCH_BACKSTOP_MS: number;
|
|
15
|
+
export type ProjectMirrorFetchDecision = {
|
|
16
|
+
fetch: true;
|
|
17
|
+
reason: "skipping_disabled" | "no_advertised_token" | "never_fetched" | "token_changed" | "backstop_elapsed";
|
|
18
|
+
} | {
|
|
19
|
+
fetch: false;
|
|
20
|
+
reason: "token_match";
|
|
21
|
+
};
|
|
22
|
+
export declare function projectMirrorFetchDecision(input: {
|
|
23
|
+
advertisedToken: string | undefined;
|
|
24
|
+
fetchedToken: string | undefined;
|
|
25
|
+
lastFetchAtMs: number | undefined;
|
|
26
|
+
nowMs: number;
|
|
27
|
+
skippingEnabled: boolean;
|
|
28
|
+
backstopMs?: number;
|
|
29
|
+
}): ProjectMirrorFetchDecision;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export declare const PROJECT_MIRROR_REF_PREFIX = "refs/r5d/mirror/";
|
|
2
|
+
export declare const PROJECT_SERVER_SYNCED_REF_PREFIX = "refs/r5d/server-synced/";
|
|
3
|
+
export type ProjectMirrorRef = {
|
|
4
|
+
refname: string;
|
|
5
|
+
objectName: string;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Hash a ref listing into a mirror-refs token. Both sides sort here rather
|
|
9
|
+
* than trusting git's listing order: renaming `refs/r5d/mirror/*` back to
|
|
10
|
+
* `refs/heads/*` changes sort neighborhoods.
|
|
11
|
+
*/
|
|
12
|
+
export declare function hashProjectMirrorRefs(refs: readonly ProjectMirrorRef[]): string;
|
|
13
|
+
/**
|
|
14
|
+
* Rename locally fetched mirror refs into the namespaces the server hashes.
|
|
15
|
+
* Rows outside the two observed namespaces are dropped; the caller lists only
|
|
16
|
+
* those namespaces, so a dropped row means an unexpected listing, and dropping
|
|
17
|
+
* it can only make tokens differ — which fails open into a fetch.
|
|
18
|
+
*/
|
|
19
|
+
export declare function canonicalizeLocalProjectMirrorRefs(refs: readonly ProjectMirrorRef[]): ProjectMirrorRef[];
|
|
@@ -212,11 +212,32 @@ export type ProjectMirrorHeadObservation = {
|
|
|
212
212
|
* Fetch and pin the exact hidden-mirror tips that may be applied after a
|
|
213
213
|
* causally later outer-workspace fetch and hydration. Observation deliberately
|
|
214
214
|
* does not mutate a live branch or its index.
|
|
215
|
+
*
|
|
216
|
+
* With `refreshRemote: false` the network fetch is skipped and the tips come
|
|
217
|
+
* from the locally cached `refs/r5d/mirror/*` / `refs/r5d/server-synced/*`
|
|
218
|
+
* refs of an earlier fetch. That is safe in the stale direction (a newer
|
|
219
|
+
* mirror tip waits for a later cycle, exactly like a tip that lands mid-cycle
|
|
220
|
+
* today) and in the fresh direction (hidden-mirror publication is tree-first,
|
|
221
|
+
* so any locally cached tip's covering outer tree was published before the
|
|
222
|
+
* fetch that cached it, hence before this cycle's outer fetch).
|
|
215
223
|
*/
|
|
216
224
|
export declare function observeProjectMirrorHeads(input: Omit<ProjectMirrorHeadUpdateInput, "branchMutationAllowed"> & {
|
|
217
225
|
allowNonFastForward: boolean;
|
|
218
226
|
branchIds?: ReadonlyMap<string, string>;
|
|
227
|
+
refreshRemote?: boolean;
|
|
219
228
|
}): Promise<ProjectMirrorHeadObservation[]>;
|
|
229
|
+
/**
|
|
230
|
+
* Compute the mirror-refs token for the locally cached mirror state — the
|
|
231
|
+
* token the server will advertise once it hashes the same refs. After a
|
|
232
|
+
* successful lease push (which updates `refs/r5d/mirror/*` above), this
|
|
233
|
+
* matches the server's next advertisement unless something else also moved
|
|
234
|
+
* the canonical repository, in which case the mismatch fails open into a
|
|
235
|
+
* fetch. Formula twin: `server/git/project-mirror-refs-token.ts`.
|
|
236
|
+
*/
|
|
237
|
+
export declare function computeLocalProjectMirrorRefsToken(input: {
|
|
238
|
+
projectRoot: string;
|
|
239
|
+
primaryBranchName: string;
|
|
240
|
+
}): Promise<string>;
|
|
220
241
|
/** Apply only previously observed object ids; this function never refetches. */
|
|
221
242
|
export declare function applyObservedProjectMirrorHeads(input: {
|
|
222
243
|
projectRoot: string;
|
|
@@ -26,6 +26,13 @@ export type WorkspaceGitMount = {
|
|
|
26
26
|
* This is transient and must clear after that move is published.
|
|
27
27
|
*/
|
|
28
28
|
preserveLocalOnHydrationBasisChange?: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Workspace-relative path of the subtree this creator-local mount was copied
|
|
31
|
+
* from (its source branch checkout). While the mount's own outer subtree is
|
|
32
|
+
* still unpublished, the automatic diff-size guard measures it against that
|
|
33
|
+
* source subtree instead of counting the whole copied tree as new content.
|
|
34
|
+
*/
|
|
35
|
+
initialPublicationSourceRelativePath?: string;
|
|
29
36
|
/** Remove the outer-workspace subtree when the mounted source was intentionally deleted. */
|
|
30
37
|
deleteWhenSourceMissing?: boolean;
|
|
31
38
|
busy?: () => boolean;
|
|
@@ -51,10 +58,16 @@ export type WorkspaceGitSyncResult = {
|
|
|
51
58
|
remoteHead: string | null;
|
|
52
59
|
publishedHead: string | null;
|
|
53
60
|
rebaseCount: number;
|
|
61
|
+
/**
|
|
62
|
+
* Bytes the automatic publication guard evaluated. A creator-local branch's
|
|
63
|
+
* first publication counts only its divergence from the source subtree.
|
|
64
|
+
*/
|
|
54
65
|
diffSizeBytes: number;
|
|
55
66
|
affectedPaths: string[];
|
|
56
67
|
activeMountIds: string[];
|
|
57
68
|
skippedMountIds: string[];
|
|
69
|
+
/** Creator-local mounts whose first outer publication was measured against their source subtree. */
|
|
70
|
+
initialPublicationMountIds?: string[];
|
|
58
71
|
conflictPaths?: string[];
|
|
59
72
|
conflictSnapshotRefs?: {
|
|
60
73
|
local: string;
|
|
@@ -152,7 +165,7 @@ export declare function resetWorkspaceGit(input: {
|
|
|
152
165
|
activeMountIds: string[];
|
|
153
166
|
skippedMountIds: string[];
|
|
154
167
|
}>;
|
|
155
|
-
declare function diffSizeBytes(workspacePath: string, baseRevision: string | null, headRevision: string, limit: number): Promise<number>;
|
|
168
|
+
declare function diffSizeBytes(workspacePath: string, baseRevision: string | null, headRevision: string, limit: number, pathspecs?: readonly string[]): Promise<number>;
|
|
156
169
|
export declare function synchronizeWorkspaceGit(input: {
|
|
157
170
|
attemptId?: string;
|
|
158
171
|
workerLabel: string;
|