@ricsam/r5d-worker 0.0.93 → 0.0.95
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/main.cjs +171 -33
- 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 +158 -49
- package/dist/mjs/main.mjs +172 -33
- 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 +162 -49
- 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 +30 -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";
|
|
@@ -11,6 +17,7 @@ const PROJECT_WORKTREE_SNAPSHOT_MANIFEST_VERSION = 1;
|
|
|
11
17
|
const MAX_PROJECT_WORKTREE_SNAPSHOT_MANIFEST_BYTES = 64 * 1024;
|
|
12
18
|
const PROJECT_WORKTREE_SNAPSHOT_OWNER_SESSION_ID = randomUUID();
|
|
13
19
|
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
20
|
+
const PROJECT_GIT_TRANSPORT_TIMEOUT_MS = 3e4;
|
|
14
21
|
const PROJECT_WORKTREE_SNAPSHOT_NAME = new RegExp(
|
|
15
22
|
`^${PROJECT_WORKTREE_SNAPSHOT_PREFIX.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}([1-9]\\d*)-([A-Za-z0-9]{6})$`
|
|
16
23
|
);
|
|
@@ -80,7 +87,7 @@ function git(cwd, args, action) {
|
|
|
80
87
|
function tryGit(cwd, args) {
|
|
81
88
|
return gitResult(cwd, args).exitCode === 0;
|
|
82
89
|
}
|
|
83
|
-
async function gitResultAsync(cwd, args) {
|
|
90
|
+
async function gitResultAsync(cwd, args, timeoutMs) {
|
|
84
91
|
const subprocess = Bun.spawn(gitCommandArgs(args), {
|
|
85
92
|
cwd,
|
|
86
93
|
stdin: "ignore",
|
|
@@ -88,12 +95,22 @@ async function gitResultAsync(cwd, args) {
|
|
|
88
95
|
stderr: "pipe",
|
|
89
96
|
env: workerGitProcessEnvironment()
|
|
90
97
|
});
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
subprocess.
|
|
95
|
-
|
|
96
|
-
|
|
98
|
+
let timedOut = false;
|
|
99
|
+
const timeout = timeoutMs ? setTimeout(() => {
|
|
100
|
+
timedOut = true;
|
|
101
|
+
subprocess.kill("SIGKILL");
|
|
102
|
+
}, timeoutMs) : void 0;
|
|
103
|
+
try {
|
|
104
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
105
|
+
new Response(subprocess.stdout).text(),
|
|
106
|
+
new Response(subprocess.stderr).text(),
|
|
107
|
+
subprocess.exited
|
|
108
|
+
]);
|
|
109
|
+
if (timedOut) throw new Error(`Git transport timed out after ${timeoutMs}ms`);
|
|
110
|
+
return { exitCode, stdout: stdout.trim(), stderr: stderr.trim() };
|
|
111
|
+
} finally {
|
|
112
|
+
if (timeout) clearTimeout(timeout);
|
|
113
|
+
}
|
|
97
114
|
}
|
|
98
115
|
async function gitAsync(cwd, args, action) {
|
|
99
116
|
const result = await gitResultAsync(cwd, args);
|
|
@@ -808,25 +825,33 @@ async function configureRepository(input) {
|
|
|
808
825
|
}
|
|
809
826
|
}
|
|
810
827
|
async function fetchProjectHeads(input) {
|
|
811
|
-
const originFetch = await gitResultAsync(
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
828
|
+
const originFetch = await gitResultAsync(
|
|
829
|
+
input.primaryPath,
|
|
830
|
+
[
|
|
831
|
+
...gitTransportSecurityArgs(input.originUrl, input.credentialHelper, input.originCredentialUsername),
|
|
832
|
+
"fetch",
|
|
833
|
+
"--no-recurse-submodules",
|
|
834
|
+
"--prune",
|
|
835
|
+
"origin",
|
|
836
|
+
"+refs/heads/*:refs/remotes/origin/*"
|
|
837
|
+
],
|
|
838
|
+
PROJECT_GIT_TRANSPORT_TIMEOUT_MS
|
|
839
|
+
);
|
|
819
840
|
if (originFetch.exitCode !== 0) {
|
|
820
841
|
throw new Error(`Fetch project origin: ${originFetch.stderr || originFetch.stdout || `git exited ${originFetch.exitCode}`}`);
|
|
821
842
|
}
|
|
822
|
-
const mirrorFetch = await gitResultAsync(
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
843
|
+
const mirrorFetch = await gitResultAsync(
|
|
844
|
+
input.primaryPath,
|
|
845
|
+
[
|
|
846
|
+
...gitTransportSecurityArgs(input.mirrorUrl, input.credentialHelper, input.mirrorCredentialUsername),
|
|
847
|
+
"fetch",
|
|
848
|
+
"--no-recurse-submodules",
|
|
849
|
+
"--prune",
|
|
850
|
+
input.mirrorUrl,
|
|
851
|
+
"+refs/heads/*:refs/r5d/mirror/*"
|
|
852
|
+
],
|
|
853
|
+
PROJECT_GIT_TRANSPORT_TIMEOUT_MS
|
|
854
|
+
);
|
|
830
855
|
if (mirrorFetch.exitCode !== 0 && !/(couldn't find remote ref|does not have any commits|remote repository is empty|no such ref)/i.test(
|
|
831
856
|
`${mirrorFetch.stderr}
|
|
832
857
|
${mirrorFetch.stdout}`
|
|
@@ -1205,19 +1230,24 @@ function removeProjectWorktrees(input) {
|
|
|
1205
1230
|
}
|
|
1206
1231
|
async function deleteProjectMirrorBranch(input) {
|
|
1207
1232
|
validateManagedBranchName(input.branchName);
|
|
1208
|
-
const deleted = await gitResultAsync(
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1233
|
+
const deleted = await gitResultAsync(
|
|
1234
|
+
input.gitDirectory,
|
|
1235
|
+
[
|
|
1236
|
+
...gitTransportSecurityArgs(input.mirrorUrl, input.credentialHelper, input.credentialUsername),
|
|
1237
|
+
"push",
|
|
1238
|
+
"--no-recurse-submodules",
|
|
1239
|
+
input.mirrorUrl,
|
|
1240
|
+
`:refs/heads/${input.branchName}`
|
|
1241
|
+
],
|
|
1242
|
+
PROJECT_GIT_TRANSPORT_TIMEOUT_MS
|
|
1243
|
+
);
|
|
1215
1244
|
if (deleted.exitCode !== 0 && !/(remote ref does not exist|unable to delete|no such ref)/i.test(`${deleted.stderr}
|
|
1216
1245
|
${deleted.stdout}`)) {
|
|
1217
1246
|
throw new Error(
|
|
1218
1247
|
`Delete hidden project mirror for ${input.branchName}: ${deleted.stderr || deleted.stdout || `git exited ${deleted.exitCode}`}`
|
|
1219
1248
|
);
|
|
1220
1249
|
}
|
|
1250
|
+
await gitResultAsync(input.gitDirectory, ["update-ref", "-d", `refs/r5d/mirror/${input.branchName}`]);
|
|
1221
1251
|
}
|
|
1222
1252
|
async function pushProjectMirrorHeads(input) {
|
|
1223
1253
|
const results = [];
|
|
@@ -1231,21 +1261,51 @@ async function pushProjectMirrorHeads(input) {
|
|
|
1231
1261
|
if (!await tryGitAsync(checkoutPath, ["cat-file", "-e", `${head}^{commit}`])) {
|
|
1232
1262
|
throw new Error(`Exact publication head ${head} is unavailable for project branch ${branchName}`);
|
|
1233
1263
|
}
|
|
1264
|
+
if (!input.expectedMirrorHeads.has(branchName)) {
|
|
1265
|
+
throw new Error(`Missing hidden-mirror lease for project branch ${branchName}`);
|
|
1266
|
+
}
|
|
1267
|
+
const expectedMirrorHead = input.expectedMirrorHeads.get(branchName) ?? null;
|
|
1268
|
+
if (expectedMirrorHead !== null && !/^[0-9a-f]{40}(?:[0-9a-f]{24})?$/.test(expectedMirrorHead)) {
|
|
1269
|
+
throw new Error(`Invalid hidden-mirror lease for project branch ${branchName}`);
|
|
1270
|
+
}
|
|
1271
|
+
const branchId = input.branchIds.get(branchName);
|
|
1272
|
+
if (!branchId || !/^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/.test(branchId)) {
|
|
1273
|
+
throw new Error(`Missing valid branch incarnation for project branch ${branchName}`);
|
|
1274
|
+
}
|
|
1275
|
+
if (expectedMirrorHead) {
|
|
1276
|
+
const handoffPrefix = `refs/r5d/server-synced/${branchId}/`;
|
|
1277
|
+
const markedCommits = (await gitAsync(
|
|
1278
|
+
checkoutPath,
|
|
1279
|
+
["for-each-ref", "--format=%(objectname)", handoffPrefix],
|
|
1280
|
+
`inspect synchronized server commits before publishing ${branchName}`
|
|
1281
|
+
)).split(/\r?\n/).filter((value) => /^[0-9a-f]{40}(?:[0-9a-f]{24})?$/.test(value));
|
|
1282
|
+
for (const markedCommit of markedCommits) {
|
|
1283
|
+
const markerIsInLease = await tryGitAsync(checkoutPath, ["merge-base", "--is-ancestor", markedCommit, expectedMirrorHead]);
|
|
1284
|
+
if (markerIsInLease && !await tryGitAsync(checkoutPath, ["merge-base", "--is-ancestor", markedCommit, head])) {
|
|
1285
|
+
throw new Error(`Refusing to overwrite synchronized server commit history for project branch ${branchName}`);
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1234
1289
|
if (projectWorktreeOperationInProgress(checkoutPath)) {
|
|
1235
1290
|
results.push({ branchName, head, pushed: false, reason: "git_operation_in_progress" });
|
|
1236
1291
|
continue;
|
|
1237
1292
|
}
|
|
1238
|
-
await
|
|
1293
|
+
const pushed = await gitResultAsync(
|
|
1239
1294
|
checkoutPath,
|
|
1240
1295
|
[
|
|
1241
1296
|
...gitTransportSecurityArgs(input.mirrorUrl, input.credentialHelper, input.credentialUsername),
|
|
1242
1297
|
"push",
|
|
1243
1298
|
"--no-recurse-submodules",
|
|
1299
|
+
`--force-with-lease=refs/heads/${branchName}:${expectedMirrorHead ?? ""}`,
|
|
1244
1300
|
input.mirrorUrl,
|
|
1245
|
-
|
|
1301
|
+
`${head}:refs/heads/${branchName}`
|
|
1246
1302
|
],
|
|
1247
|
-
|
|
1303
|
+
PROJECT_GIT_TRANSPORT_TIMEOUT_MS
|
|
1248
1304
|
);
|
|
1305
|
+
if (pushed.exitCode !== 0) {
|
|
1306
|
+
throw new Error(`Push hidden project mirror for ${branchName}: ${pushed.stderr || pushed.stdout || `git exited ${pushed.exitCode}`}`);
|
|
1307
|
+
}
|
|
1308
|
+
await gitAsync(checkoutPath, ["update-ref", `refs/r5d/mirror/${branchName}`, head], `record pushed mirror tip ${branchName}`);
|
|
1249
1309
|
results.push({ branchName, head, pushed: true });
|
|
1250
1310
|
}
|
|
1251
1311
|
return results;
|
|
@@ -1258,42 +1318,94 @@ function assertProjectMirrorHeadsPushed(results) {
|
|
|
1258
1318
|
}
|
|
1259
1319
|
async function observeProjectMirrorHeads(input) {
|
|
1260
1320
|
const primaryPath = branchPath(input.projectRoot, input.primaryBranchName);
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
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
|
+
}
|
|
1271
1338
|
const observations = [];
|
|
1272
1339
|
for (const branchName of [...input.branchNames].sort()) {
|
|
1273
1340
|
const checkoutPath = branchPath(input.projectRoot, branchName);
|
|
1274
1341
|
const previousHead = await gitAsync(checkoutPath, ["rev-parse", "HEAD"], `resolve local project head ${branchName}`);
|
|
1275
1342
|
const mirrorRef = `refs/r5d/mirror/${branchName}`;
|
|
1276
1343
|
const mirrorHead = await tryGitAsync(primaryPath, ["show-ref", "--verify", "--quiet", mirrorRef]) ? await gitAsync(primaryPath, ["rev-parse", mirrorRef], `resolve refreshed mirror head ${branchName}`) : null;
|
|
1277
|
-
const
|
|
1278
|
-
|
|
1344
|
+
const branchId = input.branchIds?.get(branchName);
|
|
1345
|
+
if (branchId && !/^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/.test(branchId)) throw new Error(`Invalid branch id for ${branchName}`);
|
|
1346
|
+
const handoffPrefix = branchId ? `refs/r5d/server-synced/${branchId}/` : null;
|
|
1347
|
+
const handoffRows = handoffPrefix ? await gitAsync(
|
|
1348
|
+
primaryPath,
|
|
1349
|
+
["for-each-ref", "--format=%(objectname) %(refname)", handoffPrefix],
|
|
1350
|
+
`inspect synchronized server commits for ${branchName}`
|
|
1351
|
+
) : "";
|
|
1352
|
+
const serverSyncedWorkspaceHeads = (await Promise.all(
|
|
1353
|
+
handoffRows.split(/\r?\n/).map(async (row) => {
|
|
1354
|
+
const separator = row.indexOf(" ");
|
|
1355
|
+
if (separator < 0 || !mirrorHead) return null;
|
|
1356
|
+
const markedCommit = row.slice(0, separator);
|
|
1357
|
+
const ref = row.slice(separator + 1);
|
|
1358
|
+
if (!/^[0-9a-f]{40}(?:[0-9a-f]{24})?$/.test(markedCommit) || !handoffPrefix || !ref.startsWith(handoffPrefix)) {
|
|
1359
|
+
return null;
|
|
1360
|
+
}
|
|
1361
|
+
const markerIsIntegrated = markedCommit === mirrorHead || await tryGitAsync(primaryPath, ["merge-base", "--is-ancestor", markedCommit, mirrorHead]);
|
|
1362
|
+
if (!markerIsIntegrated) return null;
|
|
1363
|
+
const workspaceHead = ref.slice(handoffPrefix.length);
|
|
1364
|
+
return /^[0-9a-f]{40}(?:[0-9a-f]{24})?$/.test(workspaceHead) ? workspaceHead : null;
|
|
1365
|
+
})
|
|
1366
|
+
)).filter((workspaceHead) => workspaceHead !== null).sort();
|
|
1367
|
+
const fastForward = Boolean(
|
|
1368
|
+
mirrorHead && mirrorHead !== previousHead && await tryGitAsync(checkoutPath, ["merge-base", "--is-ancestor", previousHead, mirrorHead])
|
|
1279
1369
|
);
|
|
1370
|
+
const serverSynced = Boolean(mirrorHead && mirrorHead !== previousHead && serverSyncedWorkspaceHeads.length > 0);
|
|
1371
|
+
const shouldMove = Boolean(mirrorHead && mirrorHead !== previousHead && (input.allowNonFastForward || fastForward || serverSynced));
|
|
1280
1372
|
observations.push({
|
|
1281
1373
|
branchName,
|
|
1282
1374
|
previousHead,
|
|
1283
1375
|
mirrorHead,
|
|
1284
|
-
mode: input.allowNonFastForward ? "reseed" : "fast_forward",
|
|
1376
|
+
mode: input.allowNonFastForward ? "reseed" : serverSynced && !fastForward ? "server_synced" : "fast_forward",
|
|
1377
|
+
serverSyncedWorkspaceHeads,
|
|
1285
1378
|
shouldMove
|
|
1286
1379
|
});
|
|
1287
1380
|
}
|
|
1288
1381
|
return observations;
|
|
1289
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
|
+
}
|
|
1290
1401
|
async function applyObservedProjectMirrorHeads(input) {
|
|
1291
1402
|
const results = [];
|
|
1292
1403
|
for (const observation of [...input.observations].sort((left, right) => left.branchName.localeCompare(right.branchName))) {
|
|
1293
1404
|
const { branchName, mirrorHead } = observation;
|
|
1294
1405
|
const checkoutPath = branchPath(input.projectRoot, branchName);
|
|
1295
1406
|
const currentHead = await gitAsync(checkoutPath, ["rev-parse", "HEAD"], `resolve local project head ${branchName}`);
|
|
1296
|
-
|
|
1407
|
+
const serverSyncedCovered = observation.mode !== "server_synced" || (input.serverSyncedWorkspaceCovered ? await input.serverSyncedWorkspaceCovered(observation) : false);
|
|
1408
|
+
if (!observation.shouldMove || !serverSyncedCovered || !mirrorHead || currentHead === mirrorHead || currentHead !== observation.previousHead) {
|
|
1297
1409
|
results.push({ branchName, previousHead: currentHead, mirrorHead, moved: false });
|
|
1298
1410
|
continue;
|
|
1299
1411
|
}
|
|
@@ -1305,14 +1417,14 @@ async function applyObservedProjectMirrorHeads(input) {
|
|
|
1305
1417
|
results.push({ branchName, previousHead: currentHead, mirrorHead, moved: false });
|
|
1306
1418
|
continue;
|
|
1307
1419
|
}
|
|
1308
|
-
if (observation.mode === "reseed") {
|
|
1420
|
+
if (observation.mode === "reseed" || observation.mode === "server_synced") {
|
|
1309
1421
|
await preserveHead(checkoutPath);
|
|
1310
1422
|
if (!mutationAllowed()) {
|
|
1311
1423
|
results.push({ branchName, previousHead: currentHead, mirrorHead, moved: false });
|
|
1312
1424
|
continue;
|
|
1313
1425
|
}
|
|
1314
1426
|
}
|
|
1315
|
-
const action = observation.mode === "reseed" ? "reseed" : "fast-forward";
|
|
1427
|
+
const action = observation.mode === "reseed" ? "reseed" : observation.mode === "server_synced" ? "adopt server commit" : "fast-forward";
|
|
1316
1428
|
await gitAsync(checkoutPath, ["read-tree", "--reset", mirrorHead], `${action} project index ${branchName}`);
|
|
1317
1429
|
await gitAsync(
|
|
1318
1430
|
checkoutPath,
|
|
@@ -1367,6 +1479,7 @@ export {
|
|
|
1367
1479
|
ProjectBranchCreationRollbackIncompleteError,
|
|
1368
1480
|
applyObservedProjectMirrorHeads,
|
|
1369
1481
|
assertProjectMirrorHeadsPushed,
|
|
1482
|
+
computeLocalProjectMirrorRefsToken,
|
|
1370
1483
|
createLinkedProjectBranch,
|
|
1371
1484
|
createOrRetryLinkedProjectBranch,
|
|
1372
1485
|
deleteLinkedProjectBranch,
|
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[];
|
|
@@ -175,6 +175,10 @@ export declare function pushProjectMirrorHeads(input: {
|
|
|
175
175
|
onlyBranches?: ReadonlySet<string>;
|
|
176
176
|
/** Exact pre-projection commits to publish after the outer tree is reachable. */
|
|
177
177
|
publicationHeads?: ReadonlyMap<string, string>;
|
|
178
|
+
/** Hidden-mirror tips observed before the corresponding outer projection. */
|
|
179
|
+
expectedMirrorHeads: ReadonlyMap<string, string | null>;
|
|
180
|
+
/** Durable branch incarnations used to scope server-authored handoff refs. */
|
|
181
|
+
branchIds: ReadonlyMap<string, string>;
|
|
178
182
|
}): Promise<Array<{
|
|
179
183
|
branchName: string;
|
|
180
184
|
head: string;
|
|
@@ -200,17 +204,40 @@ export type ProjectMirrorHeadObservation = {
|
|
|
200
204
|
branchName: string;
|
|
201
205
|
previousHead: string;
|
|
202
206
|
mirrorHead: string | null;
|
|
203
|
-
mode: "fast_forward" | "reseed";
|
|
207
|
+
mode: "fast_forward" | "reseed" | "server_synced";
|
|
208
|
+
serverSyncedWorkspaceHeads: string[];
|
|
204
209
|
shouldMove: boolean;
|
|
205
210
|
};
|
|
206
211
|
/**
|
|
207
212
|
* Fetch and pin the exact hidden-mirror tips that may be applied after a
|
|
208
213
|
* causally later outer-workspace fetch and hydration. Observation deliberately
|
|
209
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).
|
|
210
223
|
*/
|
|
211
224
|
export declare function observeProjectMirrorHeads(input: Omit<ProjectMirrorHeadUpdateInput, "branchMutationAllowed"> & {
|
|
212
225
|
allowNonFastForward: boolean;
|
|
226
|
+
branchIds?: ReadonlyMap<string, string>;
|
|
227
|
+
refreshRemote?: boolean;
|
|
213
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>;
|
|
214
241
|
/** Apply only previously observed object ids; this function never refetches. */
|
|
215
242
|
export declare function applyObservedProjectMirrorHeads(input: {
|
|
216
243
|
projectRoot: string;
|
|
@@ -218,6 +245,8 @@ export declare function applyObservedProjectMirrorHeads(input: {
|
|
|
218
245
|
observations: readonly ProjectMirrorHeadObservation[];
|
|
219
246
|
/** Rechecked immediately before refs or the index move. */
|
|
220
247
|
branchMutationAllowed?: (branchName: string, checkoutPath: string) => boolean;
|
|
248
|
+
/** Proves the visible outer workspace has hydrated the server commit's exact source generation. */
|
|
249
|
+
serverSyncedWorkspaceCovered?: (observation: ProjectMirrorHeadObservation) => Promise<boolean>;
|
|
221
250
|
}): Promise<Array<{
|
|
222
251
|
branchName: string;
|
|
223
252
|
previousHead: string;
|