@ricsam/r5d-worker 0.0.92 → 0.0.94
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 +49 -22
- package/dist/cjs/package.json +1 -1
- package/dist/cjs/project-worktrees.cjs +131 -47
- package/dist/mjs/main.mjs +49 -22
- package/dist/mjs/package.json +1 -1
- package/dist/mjs/project-worktrees.mjs +131 -47
- package/dist/types/project-worktrees.d.ts +9 -1
- package/package.json +1 -1
package/dist/cjs/main.cjs
CHANGED
|
@@ -4067,7 +4067,7 @@ async function startWorker(options) {
|
|
|
4067
4067
|
}
|
|
4068
4068
|
return heads;
|
|
4069
4069
|
};
|
|
4070
|
-
const pushChangedProjectHeads = async (activeMountIds, publishedHead, publicationHeads, suppressedMountIds = /* @__PURE__ */ new Set()) => {
|
|
4070
|
+
const pushChangedProjectHeads = async (activeMountIds, publishedHead, publicationHeads, expectedMirrorHeads, suppressedMountIds = /* @__PURE__ */ new Set()) => {
|
|
4071
4071
|
const active = new Set(activeMountIds);
|
|
4072
4072
|
for (const project of projectConfigById.values()) {
|
|
4073
4073
|
if (!readyProjectIds.has(project.projectId) || !(0, import_repository_transition_policy.repositoryMirrorWritesAllowed)(project)) continue;
|
|
@@ -4082,6 +4082,12 @@ async function startWorker(options) {
|
|
|
4082
4082
|
}
|
|
4083
4083
|
}
|
|
4084
4084
|
if (changed.size === 0) continue;
|
|
4085
|
+
const branchLeases = /* @__PURE__ */ new Map();
|
|
4086
|
+
for (const branchName of changed.keys()) {
|
|
4087
|
+
const mountId = projectMountId(project.projectId, branchName);
|
|
4088
|
+
if (!expectedMirrorHeads.has(mountId)) throw new Error(`Missing observed hidden-mirror lease for active project mount ${mountId}`);
|
|
4089
|
+
branchLeases.set(branchName, expectedMirrorHeads.get(mountId) ?? null);
|
|
4090
|
+
}
|
|
4085
4091
|
const connection = projectConnection(project);
|
|
4086
4092
|
const results = await (0, import_project_worktrees.pushProjectMirrorHeads)({
|
|
4087
4093
|
projectRoot: configuredProjectRoot(projectsRoot, project),
|
|
@@ -4090,7 +4096,9 @@ async function startWorker(options) {
|
|
|
4090
4096
|
credentialHelper: connection.credentialHelper,
|
|
4091
4097
|
credentialUsername: connection.mirrorCredentialUsername,
|
|
4092
4098
|
onlyBranches: new Set(changed.keys()),
|
|
4093
|
-
publicationHeads: changed
|
|
4099
|
+
publicationHeads: changed,
|
|
4100
|
+
expectedMirrorHeads: branchLeases,
|
|
4101
|
+
branchIds: new Map(project.branches.map(({ branchId, branchName }) => [branchName, branchId]))
|
|
4094
4102
|
});
|
|
4095
4103
|
(0, import_project_worktrees.assertProjectMirrorHeadsPushed)(results);
|
|
4096
4104
|
for (const result of results) {
|
|
@@ -4154,7 +4162,8 @@ async function startWorker(options) {
|
|
|
4154
4162
|
mirrorUrl: connection.mirrorUrl,
|
|
4155
4163
|
credentialHelper: connection.credentialHelper,
|
|
4156
4164
|
credentialUsername: connection.mirrorCredentialUsername,
|
|
4157
|
-
allowNonFastForward: input.allowNonFastForward
|
|
4165
|
+
allowNonFastForward: input.allowNonFastForward,
|
|
4166
|
+
branchIds: new Map(project.branches.map(({ branchId, branchName }) => [branchName, branchId]))
|
|
4158
4167
|
})
|
|
4159
4168
|
});
|
|
4160
4169
|
} catch (error) {
|
|
@@ -4167,7 +4176,7 @@ async function startWorker(options) {
|
|
|
4167
4176
|
}
|
|
4168
4177
|
return bundles;
|
|
4169
4178
|
};
|
|
4170
|
-
const applyObservedProjectHeadsAfterInboundWorkspace = async (bundles, activeMountIds, requireObservedHeads) => {
|
|
4179
|
+
const applyObservedProjectHeadsAfterInboundWorkspace = async (bundles, activeMountIds, requireObservedHeads, integratedWorkspaceHead) => {
|
|
4171
4180
|
const active = new Set(activeMountIds);
|
|
4172
4181
|
for (const bundle of bundles) {
|
|
4173
4182
|
const project = projectConfigById.get(bundle.projectId);
|
|
@@ -4179,7 +4188,16 @@ async function startWorker(options) {
|
|
|
4179
4188
|
projectRoot: bundle.projectRoot,
|
|
4180
4189
|
primaryBranchName: bundle.primaryBranchName,
|
|
4181
4190
|
observations,
|
|
4182
|
-
branchMutationAllowed: (branchName, checkoutPath) => !projectBranchMountBusy(project.projectId, branchName, checkoutPath)
|
|
4191
|
+
branchMutationAllowed: (branchName, checkoutPath) => !projectBranchMountBusy(project.projectId, branchName, checkoutPath),
|
|
4192
|
+
serverSyncedWorkspaceCovered: async (observation) => {
|
|
4193
|
+
if (!integratedWorkspaceHead) return false;
|
|
4194
|
+
for (const workspaceHead of observation.serverSyncedWorkspaceHeads) {
|
|
4195
|
+
if (await tryGitAsync(["merge-base", "--is-ancestor", workspaceHead, integratedWorkspaceHead], { cwd: workspaceShadowRoot })) {
|
|
4196
|
+
return true;
|
|
4197
|
+
}
|
|
4198
|
+
}
|
|
4199
|
+
return false;
|
|
4200
|
+
}
|
|
4183
4201
|
});
|
|
4184
4202
|
if (requireObservedHeads) {
|
|
4185
4203
|
for (const observation of observations) {
|
|
@@ -4304,6 +4322,12 @@ async function startWorker(options) {
|
|
|
4304
4322
|
const outerRemediation = input.trigger.type === "remediation" || input.trigger.type === "remediation_confirm";
|
|
4305
4323
|
const mounts = outerRemediation ? (0, import_workspace_git_sync.workspaceGitMountsForRemediation)(configuredMounts) : configuredMounts;
|
|
4306
4324
|
const observedProjectHeads = outerRemediation ? [] : await observeProjectHeadsBeforeOuterWorkspace({ allowNonFastForward: false, failClosed: false });
|
|
4325
|
+
const expectedMirrorHeads = /* @__PURE__ */ new Map();
|
|
4326
|
+
for (const bundle of observedProjectHeads) {
|
|
4327
|
+
for (const observation of bundle.observations) {
|
|
4328
|
+
expectedMirrorHeads.set(projectMountId(bundle.projectId, observation.branchName), observation.mirrorHead);
|
|
4329
|
+
}
|
|
4330
|
+
}
|
|
4307
4331
|
const inboundMoveMountIds = new Set(
|
|
4308
4332
|
observedProjectHeads.flatMap(
|
|
4309
4333
|
({ projectId, observations }) => observations.filter(({ shouldMove }) => shouldMove).map(({ branchName }) => projectMountId(projectId, branchName))
|
|
@@ -4325,12 +4349,17 @@ async function startWorker(options) {
|
|
|
4325
4349
|
requiredAncestorHeads: input.requiredAncestorHeads,
|
|
4326
4350
|
assertStillAdmitted: input.assertStillAdmitted,
|
|
4327
4351
|
afterWorkspacePublished: outerRemediation ? void 0 : async ({ activeMountIds, publishedHead }) => {
|
|
4328
|
-
await pushChangedProjectHeads(activeMountIds, publishedHead, publicationHeads, inboundMoveMountIds);
|
|
4352
|
+
await pushChangedProjectHeads(activeMountIds, publishedHead, publicationHeads, expectedMirrorHeads, inboundMoveMountIds);
|
|
4329
4353
|
}
|
|
4330
4354
|
});
|
|
4331
4355
|
input.assertStillAdmitted?.();
|
|
4332
4356
|
if (["no_change", "updated", "pushed"].includes(result.outcome) && !outerRemediation) {
|
|
4333
|
-
await applyObservedProjectHeadsAfterInboundWorkspace(
|
|
4357
|
+
await applyObservedProjectHeadsAfterInboundWorkspace(
|
|
4358
|
+
observedProjectHeads,
|
|
4359
|
+
result.activeMountIds,
|
|
4360
|
+
false,
|
|
4361
|
+
result.localHead ?? result.remoteHead
|
|
4362
|
+
);
|
|
4334
4363
|
}
|
|
4335
4364
|
return mapWorkspaceGitResult(input.attemptId, input.trigger, result);
|
|
4336
4365
|
};
|
|
@@ -4425,11 +4454,12 @@ async function startWorker(options) {
|
|
|
4425
4454
|
}
|
|
4426
4455
|
try {
|
|
4427
4456
|
assertStillAdmitted();
|
|
4457
|
+
const hasDurableBranchDeletion = projectWorkspaceState.tombstones.some((tombstone) => tombstone.kind === "branch");
|
|
4428
4458
|
return await performWorkspaceSync({
|
|
4429
4459
|
attemptId,
|
|
4430
4460
|
trigger: input.trigger,
|
|
4431
|
-
confirmedLargeDiff: input.confirmedLargeDiff,
|
|
4432
|
-
confirmationReason: input.confirmationReason,
|
|
4461
|
+
confirmedLargeDiff: input.confirmedLargeDiff === true || hasDurableBranchDeletion,
|
|
4462
|
+
confirmationReason: input.confirmationReason ?? (hasDurableBranchDeletion ? "durable project branch deletion" : void 0),
|
|
4433
4463
|
resetToCanonical: input.resetToCanonical,
|
|
4434
4464
|
requiredAncestorHeads: input.requiredAncestorHeads,
|
|
4435
4465
|
assertStillAdmitted
|
|
@@ -4966,9 +4996,12 @@ async function startWorker(options) {
|
|
|
4966
4996
|
);
|
|
4967
4997
|
}
|
|
4968
4998
|
};
|
|
4999
|
+
const hasDurableBranchDeletion = projectWorkspaceState.tombstones.some((tombstone) => tombstone.kind === "branch");
|
|
4969
5000
|
const result = await performWorkspaceSync({
|
|
4970
5001
|
attemptId: crypto.randomUUID(),
|
|
4971
5002
|
trigger: { type: "connect" },
|
|
5003
|
+
confirmedLargeDiff: hasDurableBranchDeletion,
|
|
5004
|
+
confirmationReason: hasDurableBranchDeletion ? "durable project branch deletion" : void 0,
|
|
4972
5005
|
resetToCanonical: workspaceConfigurationResetToCanonicalIsAllowed(message.resetToCanonical, incidentDeferral.incidentId),
|
|
4973
5006
|
assertStillAdmitted: assertConfigurationSyncStillAdmitted
|
|
4974
5007
|
});
|
|
@@ -5210,7 +5243,9 @@ async function startWorker(options) {
|
|
|
5210
5243
|
ptyEnvFilesV1: true,
|
|
5211
5244
|
workspaceRemediationAncestorGuardV1: true,
|
|
5212
5245
|
workspaceIncidentConfigDeferralV1: true,
|
|
5213
|
-
workspaceConfigResetToCanonicalV1: true
|
|
5246
|
+
workspaceConfigResetToCanonicalV1: true,
|
|
5247
|
+
projectBranchDeletionFastAckV1: true,
|
|
5248
|
+
projectMirrorLeaseV1: true
|
|
5214
5249
|
},
|
|
5215
5250
|
projectRoot: projectsRoot,
|
|
5216
5251
|
artifactRoot,
|
|
@@ -5464,23 +5499,15 @@ async function startWorker(options) {
|
|
|
5464
5499
|
return true;
|
|
5465
5500
|
});
|
|
5466
5501
|
if (deletionNeedsSync) {
|
|
5467
|
-
|
|
5468
|
-
|
|
5502
|
+
scheduleAutomaticWorkspaceSync(
|
|
5503
|
+
{
|
|
5469
5504
|
type: "manual",
|
|
5470
5505
|
projectId: message.projectId,
|
|
5471
5506
|
branchName: message.branchName,
|
|
5472
5507
|
detail: "deleted project branch"
|
|
5473
5508
|
},
|
|
5474
|
-
|
|
5475
|
-
|
|
5476
|
-
sendResult: false
|
|
5477
|
-
});
|
|
5478
|
-
if (result.outcome === "failed" || result.outcome === "conflict_blocked" || result.outcome === "large_diff_blocked") {
|
|
5479
|
-
throw new Error(result.error ?? `Project branch deletion workspace sync ended with ${result.outcome}`);
|
|
5480
|
-
}
|
|
5481
|
-
if (pendingMirrorDeletes.has(deletionKey)) {
|
|
5482
|
-
throw new Error("Project branch deletion did not remove the hidden mirror ref");
|
|
5483
|
-
}
|
|
5509
|
+
0
|
|
5510
|
+
);
|
|
5484
5511
|
}
|
|
5485
5512
|
sendWorkerMessage(ws, {
|
|
5486
5513
|
type: "operation_result",
|
package/dist/cjs/package.json
CHANGED
|
@@ -64,6 +64,7 @@ const PROJECT_WORKTREE_SNAPSHOT_MANIFEST_VERSION = 1;
|
|
|
64
64
|
const MAX_PROJECT_WORKTREE_SNAPSHOT_MANIFEST_BYTES = 64 * 1024;
|
|
65
65
|
const PROJECT_WORKTREE_SNAPSHOT_OWNER_SESSION_ID = (0, import_node_crypto.randomUUID)();
|
|
66
66
|
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;
|
|
67
|
+
const PROJECT_GIT_TRANSPORT_TIMEOUT_MS = 3e4;
|
|
67
68
|
const PROJECT_WORKTREE_SNAPSHOT_NAME = new RegExp(
|
|
68
69
|
`^${PROJECT_WORKTREE_SNAPSHOT_PREFIX.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}([1-9]\\d*)-([A-Za-z0-9]{6})$`
|
|
69
70
|
);
|
|
@@ -133,7 +134,7 @@ function git(cwd, args, action) {
|
|
|
133
134
|
function tryGit(cwd, args) {
|
|
134
135
|
return gitResult(cwd, args).exitCode === 0;
|
|
135
136
|
}
|
|
136
|
-
async function gitResultAsync(cwd, args) {
|
|
137
|
+
async function gitResultAsync(cwd, args, timeoutMs) {
|
|
137
138
|
const subprocess = Bun.spawn(gitCommandArgs(args), {
|
|
138
139
|
cwd,
|
|
139
140
|
stdin: "ignore",
|
|
@@ -141,12 +142,22 @@ async function gitResultAsync(cwd, args) {
|
|
|
141
142
|
stderr: "pipe",
|
|
142
143
|
env: (0, import_git_process_environment.workerGitProcessEnvironment)()
|
|
143
144
|
});
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
subprocess.
|
|
148
|
-
|
|
149
|
-
|
|
145
|
+
let timedOut = false;
|
|
146
|
+
const timeout = timeoutMs ? setTimeout(() => {
|
|
147
|
+
timedOut = true;
|
|
148
|
+
subprocess.kill("SIGKILL");
|
|
149
|
+
}, timeoutMs) : void 0;
|
|
150
|
+
try {
|
|
151
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
152
|
+
new Response(subprocess.stdout).text(),
|
|
153
|
+
new Response(subprocess.stderr).text(),
|
|
154
|
+
subprocess.exited
|
|
155
|
+
]);
|
|
156
|
+
if (timedOut) throw new Error(`Git transport timed out after ${timeoutMs}ms`);
|
|
157
|
+
return { exitCode, stdout: stdout.trim(), stderr: stderr.trim() };
|
|
158
|
+
} finally {
|
|
159
|
+
if (timeout) clearTimeout(timeout);
|
|
160
|
+
}
|
|
150
161
|
}
|
|
151
162
|
async function gitAsync(cwd, args, action) {
|
|
152
163
|
const result = await gitResultAsync(cwd, args);
|
|
@@ -861,25 +872,33 @@ async function configureRepository(input) {
|
|
|
861
872
|
}
|
|
862
873
|
}
|
|
863
874
|
async function fetchProjectHeads(input) {
|
|
864
|
-
const originFetch = await gitResultAsync(
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
875
|
+
const originFetch = await gitResultAsync(
|
|
876
|
+
input.primaryPath,
|
|
877
|
+
[
|
|
878
|
+
...(0, import_git_process_environment.gitTransportSecurityArgs)(input.originUrl, input.credentialHelper, input.originCredentialUsername),
|
|
879
|
+
"fetch",
|
|
880
|
+
"--no-recurse-submodules",
|
|
881
|
+
"--prune",
|
|
882
|
+
"origin",
|
|
883
|
+
"+refs/heads/*:refs/remotes/origin/*"
|
|
884
|
+
],
|
|
885
|
+
PROJECT_GIT_TRANSPORT_TIMEOUT_MS
|
|
886
|
+
);
|
|
872
887
|
if (originFetch.exitCode !== 0) {
|
|
873
888
|
throw new Error(`Fetch project origin: ${originFetch.stderr || originFetch.stdout || `git exited ${originFetch.exitCode}`}`);
|
|
874
889
|
}
|
|
875
|
-
const mirrorFetch = await gitResultAsync(
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
890
|
+
const mirrorFetch = await gitResultAsync(
|
|
891
|
+
input.primaryPath,
|
|
892
|
+
[
|
|
893
|
+
...(0, import_git_process_environment.gitTransportSecurityArgs)(input.mirrorUrl, input.credentialHelper, input.mirrorCredentialUsername),
|
|
894
|
+
"fetch",
|
|
895
|
+
"--no-recurse-submodules",
|
|
896
|
+
"--prune",
|
|
897
|
+
input.mirrorUrl,
|
|
898
|
+
"+refs/heads/*:refs/r5d/mirror/*"
|
|
899
|
+
],
|
|
900
|
+
PROJECT_GIT_TRANSPORT_TIMEOUT_MS
|
|
901
|
+
);
|
|
883
902
|
if (mirrorFetch.exitCode !== 0 && !/(couldn't find remote ref|does not have any commits|remote repository is empty|no such ref)/i.test(
|
|
884
903
|
`${mirrorFetch.stderr}
|
|
885
904
|
${mirrorFetch.stdout}`
|
|
@@ -1258,13 +1277,17 @@ function removeProjectWorktrees(input) {
|
|
|
1258
1277
|
}
|
|
1259
1278
|
async function deleteProjectMirrorBranch(input) {
|
|
1260
1279
|
(0, import_managed_paths.validateManagedBranchName)(input.branchName);
|
|
1261
|
-
const deleted = await gitResultAsync(
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1280
|
+
const deleted = await gitResultAsync(
|
|
1281
|
+
input.gitDirectory,
|
|
1282
|
+
[
|
|
1283
|
+
...(0, import_git_process_environment.gitTransportSecurityArgs)(input.mirrorUrl, input.credentialHelper, input.credentialUsername),
|
|
1284
|
+
"push",
|
|
1285
|
+
"--no-recurse-submodules",
|
|
1286
|
+
input.mirrorUrl,
|
|
1287
|
+
`:refs/heads/${input.branchName}`
|
|
1288
|
+
],
|
|
1289
|
+
PROJECT_GIT_TRANSPORT_TIMEOUT_MS
|
|
1290
|
+
);
|
|
1268
1291
|
if (deleted.exitCode !== 0 && !/(remote ref does not exist|unable to delete|no such ref)/i.test(`${deleted.stderr}
|
|
1269
1292
|
${deleted.stdout}`)) {
|
|
1270
1293
|
throw new Error(
|
|
@@ -1284,21 +1307,50 @@ async function pushProjectMirrorHeads(input) {
|
|
|
1284
1307
|
if (!await tryGitAsync(checkoutPath, ["cat-file", "-e", `${head}^{commit}`])) {
|
|
1285
1308
|
throw new Error(`Exact publication head ${head} is unavailable for project branch ${branchName}`);
|
|
1286
1309
|
}
|
|
1310
|
+
if (!input.expectedMirrorHeads.has(branchName)) {
|
|
1311
|
+
throw new Error(`Missing hidden-mirror lease for project branch ${branchName}`);
|
|
1312
|
+
}
|
|
1313
|
+
const expectedMirrorHead = input.expectedMirrorHeads.get(branchName) ?? null;
|
|
1314
|
+
if (expectedMirrorHead !== null && !/^[0-9a-f]{40}(?:[0-9a-f]{24})?$/.test(expectedMirrorHead)) {
|
|
1315
|
+
throw new Error(`Invalid hidden-mirror lease for project branch ${branchName}`);
|
|
1316
|
+
}
|
|
1317
|
+
const branchId = input.branchIds.get(branchName);
|
|
1318
|
+
if (!branchId || !/^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/.test(branchId)) {
|
|
1319
|
+
throw new Error(`Missing valid branch incarnation for project branch ${branchName}`);
|
|
1320
|
+
}
|
|
1321
|
+
if (expectedMirrorHead) {
|
|
1322
|
+
const handoffPrefix = `refs/r5d/server-synced/${branchId}/`;
|
|
1323
|
+
const markedCommits = (await gitAsync(
|
|
1324
|
+
checkoutPath,
|
|
1325
|
+
["for-each-ref", "--format=%(objectname)", handoffPrefix],
|
|
1326
|
+
`inspect synchronized server commits before publishing ${branchName}`
|
|
1327
|
+
)).split(/\r?\n/).filter((value) => /^[0-9a-f]{40}(?:[0-9a-f]{24})?$/.test(value));
|
|
1328
|
+
for (const markedCommit of markedCommits) {
|
|
1329
|
+
const markerIsInLease = await tryGitAsync(checkoutPath, ["merge-base", "--is-ancestor", markedCommit, expectedMirrorHead]);
|
|
1330
|
+
if (markerIsInLease && !await tryGitAsync(checkoutPath, ["merge-base", "--is-ancestor", markedCommit, head])) {
|
|
1331
|
+
throw new Error(`Refusing to overwrite synchronized server commit history for project branch ${branchName}`);
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1287
1335
|
if (projectWorktreeOperationInProgress(checkoutPath)) {
|
|
1288
1336
|
results.push({ branchName, head, pushed: false, reason: "git_operation_in_progress" });
|
|
1289
1337
|
continue;
|
|
1290
1338
|
}
|
|
1291
|
-
await
|
|
1339
|
+
const pushed = await gitResultAsync(
|
|
1292
1340
|
checkoutPath,
|
|
1293
1341
|
[
|
|
1294
1342
|
...(0, import_git_process_environment.gitTransportSecurityArgs)(input.mirrorUrl, input.credentialHelper, input.credentialUsername),
|
|
1295
1343
|
"push",
|
|
1296
1344
|
"--no-recurse-submodules",
|
|
1345
|
+
`--force-with-lease=refs/heads/${branchName}:${expectedMirrorHead ?? ""}`,
|
|
1297
1346
|
input.mirrorUrl,
|
|
1298
|
-
|
|
1347
|
+
`${head}:refs/heads/${branchName}`
|
|
1299
1348
|
],
|
|
1300
|
-
|
|
1349
|
+
PROJECT_GIT_TRANSPORT_TIMEOUT_MS
|
|
1301
1350
|
);
|
|
1351
|
+
if (pushed.exitCode !== 0) {
|
|
1352
|
+
throw new Error(`Push hidden project mirror for ${branchName}: ${pushed.stderr || pushed.stdout || `git exited ${pushed.exitCode}`}`);
|
|
1353
|
+
}
|
|
1302
1354
|
results.push({ branchName, head, pushed: true });
|
|
1303
1355
|
}
|
|
1304
1356
|
return results;
|
|
@@ -1311,14 +1363,19 @@ function assertProjectMirrorHeadsPushed(results) {
|
|
|
1311
1363
|
}
|
|
1312
1364
|
async function observeProjectMirrorHeads(input) {
|
|
1313
1365
|
const primaryPath = branchPath(input.projectRoot, input.primaryBranchName);
|
|
1314
|
-
const fetch = await gitResultAsync(
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1366
|
+
const fetch = await gitResultAsync(
|
|
1367
|
+
primaryPath,
|
|
1368
|
+
[
|
|
1369
|
+
...(0, import_git_process_environment.gitTransportSecurityArgs)(input.mirrorUrl, input.credentialHelper, input.credentialUsername),
|
|
1370
|
+
"fetch",
|
|
1371
|
+
"--no-recurse-submodules",
|
|
1372
|
+
"--prune",
|
|
1373
|
+
input.mirrorUrl,
|
|
1374
|
+
"+refs/heads/*:refs/r5d/mirror/*",
|
|
1375
|
+
"+refs/r5d/server-synced/*:refs/r5d/server-synced/*"
|
|
1376
|
+
],
|
|
1377
|
+
PROJECT_GIT_TRANSPORT_TIMEOUT_MS
|
|
1378
|
+
);
|
|
1322
1379
|
if (fetch.exitCode !== 0)
|
|
1323
1380
|
throw new Error(`Refresh project head mirror: ${fetch.stderr || fetch.stdout || `git exited ${fetch.exitCode}`}`);
|
|
1324
1381
|
const observations = [];
|
|
@@ -1327,14 +1384,40 @@ async function observeProjectMirrorHeads(input) {
|
|
|
1327
1384
|
const previousHead = await gitAsync(checkoutPath, ["rev-parse", "HEAD"], `resolve local project head ${branchName}`);
|
|
1328
1385
|
const mirrorRef = `refs/r5d/mirror/${branchName}`;
|
|
1329
1386
|
const mirrorHead = await tryGitAsync(primaryPath, ["show-ref", "--verify", "--quiet", mirrorRef]) ? await gitAsync(primaryPath, ["rev-parse", mirrorRef], `resolve refreshed mirror head ${branchName}`) : null;
|
|
1330
|
-
const
|
|
1331
|
-
|
|
1387
|
+
const branchId = input.branchIds?.get(branchName);
|
|
1388
|
+
if (branchId && !/^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/.test(branchId)) throw new Error(`Invalid branch id for ${branchName}`);
|
|
1389
|
+
const handoffPrefix = branchId ? `refs/r5d/server-synced/${branchId}/` : null;
|
|
1390
|
+
const handoffRows = handoffPrefix ? await gitAsync(
|
|
1391
|
+
primaryPath,
|
|
1392
|
+
["for-each-ref", "--format=%(objectname) %(refname)", handoffPrefix],
|
|
1393
|
+
`inspect synchronized server commits for ${branchName}`
|
|
1394
|
+
) : "";
|
|
1395
|
+
const serverSyncedWorkspaceHeads = (await Promise.all(
|
|
1396
|
+
handoffRows.split(/\r?\n/).map(async (row) => {
|
|
1397
|
+
const separator = row.indexOf(" ");
|
|
1398
|
+
if (separator < 0 || !mirrorHead) return null;
|
|
1399
|
+
const markedCommit = row.slice(0, separator);
|
|
1400
|
+
const ref = row.slice(separator + 1);
|
|
1401
|
+
if (!/^[0-9a-f]{40}(?:[0-9a-f]{24})?$/.test(markedCommit) || !handoffPrefix || !ref.startsWith(handoffPrefix)) {
|
|
1402
|
+
return null;
|
|
1403
|
+
}
|
|
1404
|
+
const markerIsIntegrated = markedCommit === mirrorHead || await tryGitAsync(primaryPath, ["merge-base", "--is-ancestor", markedCommit, mirrorHead]);
|
|
1405
|
+
if (!markerIsIntegrated) return null;
|
|
1406
|
+
const workspaceHead = ref.slice(handoffPrefix.length);
|
|
1407
|
+
return /^[0-9a-f]{40}(?:[0-9a-f]{24})?$/.test(workspaceHead) ? workspaceHead : null;
|
|
1408
|
+
})
|
|
1409
|
+
)).filter((workspaceHead) => workspaceHead !== null).sort();
|
|
1410
|
+
const fastForward = Boolean(
|
|
1411
|
+
mirrorHead && mirrorHead !== previousHead && await tryGitAsync(checkoutPath, ["merge-base", "--is-ancestor", previousHead, mirrorHead])
|
|
1332
1412
|
);
|
|
1413
|
+
const serverSynced = Boolean(mirrorHead && mirrorHead !== previousHead && serverSyncedWorkspaceHeads.length > 0);
|
|
1414
|
+
const shouldMove = Boolean(mirrorHead && mirrorHead !== previousHead && (input.allowNonFastForward || fastForward || serverSynced));
|
|
1333
1415
|
observations.push({
|
|
1334
1416
|
branchName,
|
|
1335
1417
|
previousHead,
|
|
1336
1418
|
mirrorHead,
|
|
1337
|
-
mode: input.allowNonFastForward ? "reseed" : "fast_forward",
|
|
1419
|
+
mode: input.allowNonFastForward ? "reseed" : serverSynced && !fastForward ? "server_synced" : "fast_forward",
|
|
1420
|
+
serverSyncedWorkspaceHeads,
|
|
1338
1421
|
shouldMove
|
|
1339
1422
|
});
|
|
1340
1423
|
}
|
|
@@ -1346,7 +1429,8 @@ async function applyObservedProjectMirrorHeads(input) {
|
|
|
1346
1429
|
const { branchName, mirrorHead } = observation;
|
|
1347
1430
|
const checkoutPath = branchPath(input.projectRoot, branchName);
|
|
1348
1431
|
const currentHead = await gitAsync(checkoutPath, ["rev-parse", "HEAD"], `resolve local project head ${branchName}`);
|
|
1349
|
-
|
|
1432
|
+
const serverSyncedCovered = observation.mode !== "server_synced" || (input.serverSyncedWorkspaceCovered ? await input.serverSyncedWorkspaceCovered(observation) : false);
|
|
1433
|
+
if (!observation.shouldMove || !serverSyncedCovered || !mirrorHead || currentHead === mirrorHead || currentHead !== observation.previousHead) {
|
|
1350
1434
|
results.push({ branchName, previousHead: currentHead, mirrorHead, moved: false });
|
|
1351
1435
|
continue;
|
|
1352
1436
|
}
|
|
@@ -1358,14 +1442,14 @@ async function applyObservedProjectMirrorHeads(input) {
|
|
|
1358
1442
|
results.push({ branchName, previousHead: currentHead, mirrorHead, moved: false });
|
|
1359
1443
|
continue;
|
|
1360
1444
|
}
|
|
1361
|
-
if (observation.mode === "reseed") {
|
|
1445
|
+
if (observation.mode === "reseed" || observation.mode === "server_synced") {
|
|
1362
1446
|
await preserveHead(checkoutPath);
|
|
1363
1447
|
if (!mutationAllowed()) {
|
|
1364
1448
|
results.push({ branchName, previousHead: currentHead, mirrorHead, moved: false });
|
|
1365
1449
|
continue;
|
|
1366
1450
|
}
|
|
1367
1451
|
}
|
|
1368
|
-
const action = observation.mode === "reseed" ? "reseed" : "fast-forward";
|
|
1452
|
+
const action = observation.mode === "reseed" ? "reseed" : observation.mode === "server_synced" ? "adopt server commit" : "fast-forward";
|
|
1369
1453
|
await gitAsync(checkoutPath, ["read-tree", "--reset", mirrorHead], `${action} project index ${branchName}`);
|
|
1370
1454
|
await gitAsync(
|
|
1371
1455
|
checkoutPath,
|
package/dist/mjs/main.mjs
CHANGED
|
@@ -4073,7 +4073,7 @@ async function startWorker(options) {
|
|
|
4073
4073
|
}
|
|
4074
4074
|
return heads;
|
|
4075
4075
|
};
|
|
4076
|
-
const pushChangedProjectHeads = async (activeMountIds, publishedHead, publicationHeads, suppressedMountIds = /* @__PURE__ */ new Set()) => {
|
|
4076
|
+
const pushChangedProjectHeads = async (activeMountIds, publishedHead, publicationHeads, expectedMirrorHeads, suppressedMountIds = /* @__PURE__ */ new Set()) => {
|
|
4077
4077
|
const active = new Set(activeMountIds);
|
|
4078
4078
|
for (const project of projectConfigById.values()) {
|
|
4079
4079
|
if (!readyProjectIds.has(project.projectId) || !repositoryMirrorWritesAllowed(project)) continue;
|
|
@@ -4088,6 +4088,12 @@ async function startWorker(options) {
|
|
|
4088
4088
|
}
|
|
4089
4089
|
}
|
|
4090
4090
|
if (changed.size === 0) continue;
|
|
4091
|
+
const branchLeases = /* @__PURE__ */ new Map();
|
|
4092
|
+
for (const branchName of changed.keys()) {
|
|
4093
|
+
const mountId = projectMountId(project.projectId, branchName);
|
|
4094
|
+
if (!expectedMirrorHeads.has(mountId)) throw new Error(`Missing observed hidden-mirror lease for active project mount ${mountId}`);
|
|
4095
|
+
branchLeases.set(branchName, expectedMirrorHeads.get(mountId) ?? null);
|
|
4096
|
+
}
|
|
4091
4097
|
const connection = projectConnection(project);
|
|
4092
4098
|
const results = await pushProjectMirrorHeads({
|
|
4093
4099
|
projectRoot: configuredProjectRoot(projectsRoot, project),
|
|
@@ -4096,7 +4102,9 @@ async function startWorker(options) {
|
|
|
4096
4102
|
credentialHelper: connection.credentialHelper,
|
|
4097
4103
|
credentialUsername: connection.mirrorCredentialUsername,
|
|
4098
4104
|
onlyBranches: new Set(changed.keys()),
|
|
4099
|
-
publicationHeads: changed
|
|
4105
|
+
publicationHeads: changed,
|
|
4106
|
+
expectedMirrorHeads: branchLeases,
|
|
4107
|
+
branchIds: new Map(project.branches.map(({ branchId, branchName }) => [branchName, branchId]))
|
|
4100
4108
|
});
|
|
4101
4109
|
assertProjectMirrorHeadsPushed(results);
|
|
4102
4110
|
for (const result of results) {
|
|
@@ -4160,7 +4168,8 @@ async function startWorker(options) {
|
|
|
4160
4168
|
mirrorUrl: connection.mirrorUrl,
|
|
4161
4169
|
credentialHelper: connection.credentialHelper,
|
|
4162
4170
|
credentialUsername: connection.mirrorCredentialUsername,
|
|
4163
|
-
allowNonFastForward: input.allowNonFastForward
|
|
4171
|
+
allowNonFastForward: input.allowNonFastForward,
|
|
4172
|
+
branchIds: new Map(project.branches.map(({ branchId, branchName }) => [branchName, branchId]))
|
|
4164
4173
|
})
|
|
4165
4174
|
});
|
|
4166
4175
|
} catch (error) {
|
|
@@ -4173,7 +4182,7 @@ async function startWorker(options) {
|
|
|
4173
4182
|
}
|
|
4174
4183
|
return bundles;
|
|
4175
4184
|
};
|
|
4176
|
-
const applyObservedProjectHeadsAfterInboundWorkspace = async (bundles, activeMountIds, requireObservedHeads) => {
|
|
4185
|
+
const applyObservedProjectHeadsAfterInboundWorkspace = async (bundles, activeMountIds, requireObservedHeads, integratedWorkspaceHead) => {
|
|
4177
4186
|
const active = new Set(activeMountIds);
|
|
4178
4187
|
for (const bundle of bundles) {
|
|
4179
4188
|
const project = projectConfigById.get(bundle.projectId);
|
|
@@ -4185,7 +4194,16 @@ async function startWorker(options) {
|
|
|
4185
4194
|
projectRoot: bundle.projectRoot,
|
|
4186
4195
|
primaryBranchName: bundle.primaryBranchName,
|
|
4187
4196
|
observations,
|
|
4188
|
-
branchMutationAllowed: (branchName, checkoutPath) => !projectBranchMountBusy(project.projectId, branchName, checkoutPath)
|
|
4197
|
+
branchMutationAllowed: (branchName, checkoutPath) => !projectBranchMountBusy(project.projectId, branchName, checkoutPath),
|
|
4198
|
+
serverSyncedWorkspaceCovered: async (observation) => {
|
|
4199
|
+
if (!integratedWorkspaceHead) return false;
|
|
4200
|
+
for (const workspaceHead of observation.serverSyncedWorkspaceHeads) {
|
|
4201
|
+
if (await tryGitAsync(["merge-base", "--is-ancestor", workspaceHead, integratedWorkspaceHead], { cwd: workspaceShadowRoot })) {
|
|
4202
|
+
return true;
|
|
4203
|
+
}
|
|
4204
|
+
}
|
|
4205
|
+
return false;
|
|
4206
|
+
}
|
|
4189
4207
|
});
|
|
4190
4208
|
if (requireObservedHeads) {
|
|
4191
4209
|
for (const observation of observations) {
|
|
@@ -4310,6 +4328,12 @@ async function startWorker(options) {
|
|
|
4310
4328
|
const outerRemediation = input.trigger.type === "remediation" || input.trigger.type === "remediation_confirm";
|
|
4311
4329
|
const mounts = outerRemediation ? workspaceGitMountsForRemediation(configuredMounts) : configuredMounts;
|
|
4312
4330
|
const observedProjectHeads = outerRemediation ? [] : await observeProjectHeadsBeforeOuterWorkspace({ allowNonFastForward: false, failClosed: false });
|
|
4331
|
+
const expectedMirrorHeads = /* @__PURE__ */ new Map();
|
|
4332
|
+
for (const bundle of observedProjectHeads) {
|
|
4333
|
+
for (const observation of bundle.observations) {
|
|
4334
|
+
expectedMirrorHeads.set(projectMountId(bundle.projectId, observation.branchName), observation.mirrorHead);
|
|
4335
|
+
}
|
|
4336
|
+
}
|
|
4313
4337
|
const inboundMoveMountIds = new Set(
|
|
4314
4338
|
observedProjectHeads.flatMap(
|
|
4315
4339
|
({ projectId, observations }) => observations.filter(({ shouldMove }) => shouldMove).map(({ branchName }) => projectMountId(projectId, branchName))
|
|
@@ -4331,12 +4355,17 @@ async function startWorker(options) {
|
|
|
4331
4355
|
requiredAncestorHeads: input.requiredAncestorHeads,
|
|
4332
4356
|
assertStillAdmitted: input.assertStillAdmitted,
|
|
4333
4357
|
afterWorkspacePublished: outerRemediation ? void 0 : async ({ activeMountIds, publishedHead }) => {
|
|
4334
|
-
await pushChangedProjectHeads(activeMountIds, publishedHead, publicationHeads, inboundMoveMountIds);
|
|
4358
|
+
await pushChangedProjectHeads(activeMountIds, publishedHead, publicationHeads, expectedMirrorHeads, inboundMoveMountIds);
|
|
4335
4359
|
}
|
|
4336
4360
|
});
|
|
4337
4361
|
input.assertStillAdmitted?.();
|
|
4338
4362
|
if (["no_change", "updated", "pushed"].includes(result.outcome) && !outerRemediation) {
|
|
4339
|
-
await applyObservedProjectHeadsAfterInboundWorkspace(
|
|
4363
|
+
await applyObservedProjectHeadsAfterInboundWorkspace(
|
|
4364
|
+
observedProjectHeads,
|
|
4365
|
+
result.activeMountIds,
|
|
4366
|
+
false,
|
|
4367
|
+
result.localHead ?? result.remoteHead
|
|
4368
|
+
);
|
|
4340
4369
|
}
|
|
4341
4370
|
return mapWorkspaceGitResult(input.attemptId, input.trigger, result);
|
|
4342
4371
|
};
|
|
@@ -4431,11 +4460,12 @@ async function startWorker(options) {
|
|
|
4431
4460
|
}
|
|
4432
4461
|
try {
|
|
4433
4462
|
assertStillAdmitted();
|
|
4463
|
+
const hasDurableBranchDeletion = projectWorkspaceState.tombstones.some((tombstone) => tombstone.kind === "branch");
|
|
4434
4464
|
return await performWorkspaceSync({
|
|
4435
4465
|
attemptId,
|
|
4436
4466
|
trigger: input.trigger,
|
|
4437
|
-
confirmedLargeDiff: input.confirmedLargeDiff,
|
|
4438
|
-
confirmationReason: input.confirmationReason,
|
|
4467
|
+
confirmedLargeDiff: input.confirmedLargeDiff === true || hasDurableBranchDeletion,
|
|
4468
|
+
confirmationReason: input.confirmationReason ?? (hasDurableBranchDeletion ? "durable project branch deletion" : void 0),
|
|
4439
4469
|
resetToCanonical: input.resetToCanonical,
|
|
4440
4470
|
requiredAncestorHeads: input.requiredAncestorHeads,
|
|
4441
4471
|
assertStillAdmitted
|
|
@@ -4972,9 +5002,12 @@ async function startWorker(options) {
|
|
|
4972
5002
|
);
|
|
4973
5003
|
}
|
|
4974
5004
|
};
|
|
5005
|
+
const hasDurableBranchDeletion = projectWorkspaceState.tombstones.some((tombstone) => tombstone.kind === "branch");
|
|
4975
5006
|
const result = await performWorkspaceSync({
|
|
4976
5007
|
attemptId: crypto.randomUUID(),
|
|
4977
5008
|
trigger: { type: "connect" },
|
|
5009
|
+
confirmedLargeDiff: hasDurableBranchDeletion,
|
|
5010
|
+
confirmationReason: hasDurableBranchDeletion ? "durable project branch deletion" : void 0,
|
|
4978
5011
|
resetToCanonical: workspaceConfigurationResetToCanonicalIsAllowed(message.resetToCanonical, incidentDeferral.incidentId),
|
|
4979
5012
|
assertStillAdmitted: assertConfigurationSyncStillAdmitted
|
|
4980
5013
|
});
|
|
@@ -5216,7 +5249,9 @@ async function startWorker(options) {
|
|
|
5216
5249
|
ptyEnvFilesV1: true,
|
|
5217
5250
|
workspaceRemediationAncestorGuardV1: true,
|
|
5218
5251
|
workspaceIncidentConfigDeferralV1: true,
|
|
5219
|
-
workspaceConfigResetToCanonicalV1: true
|
|
5252
|
+
workspaceConfigResetToCanonicalV1: true,
|
|
5253
|
+
projectBranchDeletionFastAckV1: true,
|
|
5254
|
+
projectMirrorLeaseV1: true
|
|
5220
5255
|
},
|
|
5221
5256
|
projectRoot: projectsRoot,
|
|
5222
5257
|
artifactRoot,
|
|
@@ -5470,23 +5505,15 @@ async function startWorker(options) {
|
|
|
5470
5505
|
return true;
|
|
5471
5506
|
});
|
|
5472
5507
|
if (deletionNeedsSync) {
|
|
5473
|
-
|
|
5474
|
-
|
|
5508
|
+
scheduleAutomaticWorkspaceSync(
|
|
5509
|
+
{
|
|
5475
5510
|
type: "manual",
|
|
5476
5511
|
projectId: message.projectId,
|
|
5477
5512
|
branchName: message.branchName,
|
|
5478
5513
|
detail: "deleted project branch"
|
|
5479
5514
|
},
|
|
5480
|
-
|
|
5481
|
-
|
|
5482
|
-
sendResult: false
|
|
5483
|
-
});
|
|
5484
|
-
if (result.outcome === "failed" || result.outcome === "conflict_blocked" || result.outcome === "large_diff_blocked") {
|
|
5485
|
-
throw new Error(result.error ?? `Project branch deletion workspace sync ended with ${result.outcome}`);
|
|
5486
|
-
}
|
|
5487
|
-
if (pendingMirrorDeletes.has(deletionKey)) {
|
|
5488
|
-
throw new Error("Project branch deletion did not remove the hidden mirror ref");
|
|
5489
|
-
}
|
|
5515
|
+
0
|
|
5516
|
+
);
|
|
5490
5517
|
}
|
|
5491
5518
|
sendWorkerMessage(ws, {
|
|
5492
5519
|
type: "operation_result",
|
package/dist/mjs/package.json
CHANGED
|
@@ -11,6 +11,7 @@ const PROJECT_WORKTREE_SNAPSHOT_MANIFEST_VERSION = 1;
|
|
|
11
11
|
const MAX_PROJECT_WORKTREE_SNAPSHOT_MANIFEST_BYTES = 64 * 1024;
|
|
12
12
|
const PROJECT_WORKTREE_SNAPSHOT_OWNER_SESSION_ID = randomUUID();
|
|
13
13
|
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;
|
|
14
|
+
const PROJECT_GIT_TRANSPORT_TIMEOUT_MS = 3e4;
|
|
14
15
|
const PROJECT_WORKTREE_SNAPSHOT_NAME = new RegExp(
|
|
15
16
|
`^${PROJECT_WORKTREE_SNAPSHOT_PREFIX.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}([1-9]\\d*)-([A-Za-z0-9]{6})$`
|
|
16
17
|
);
|
|
@@ -80,7 +81,7 @@ function git(cwd, args, action) {
|
|
|
80
81
|
function tryGit(cwd, args) {
|
|
81
82
|
return gitResult(cwd, args).exitCode === 0;
|
|
82
83
|
}
|
|
83
|
-
async function gitResultAsync(cwd, args) {
|
|
84
|
+
async function gitResultAsync(cwd, args, timeoutMs) {
|
|
84
85
|
const subprocess = Bun.spawn(gitCommandArgs(args), {
|
|
85
86
|
cwd,
|
|
86
87
|
stdin: "ignore",
|
|
@@ -88,12 +89,22 @@ async function gitResultAsync(cwd, args) {
|
|
|
88
89
|
stderr: "pipe",
|
|
89
90
|
env: workerGitProcessEnvironment()
|
|
90
91
|
});
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
subprocess.
|
|
95
|
-
|
|
96
|
-
|
|
92
|
+
let timedOut = false;
|
|
93
|
+
const timeout = timeoutMs ? setTimeout(() => {
|
|
94
|
+
timedOut = true;
|
|
95
|
+
subprocess.kill("SIGKILL");
|
|
96
|
+
}, timeoutMs) : void 0;
|
|
97
|
+
try {
|
|
98
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
99
|
+
new Response(subprocess.stdout).text(),
|
|
100
|
+
new Response(subprocess.stderr).text(),
|
|
101
|
+
subprocess.exited
|
|
102
|
+
]);
|
|
103
|
+
if (timedOut) throw new Error(`Git transport timed out after ${timeoutMs}ms`);
|
|
104
|
+
return { exitCode, stdout: stdout.trim(), stderr: stderr.trim() };
|
|
105
|
+
} finally {
|
|
106
|
+
if (timeout) clearTimeout(timeout);
|
|
107
|
+
}
|
|
97
108
|
}
|
|
98
109
|
async function gitAsync(cwd, args, action) {
|
|
99
110
|
const result = await gitResultAsync(cwd, args);
|
|
@@ -808,25 +819,33 @@ async function configureRepository(input) {
|
|
|
808
819
|
}
|
|
809
820
|
}
|
|
810
821
|
async function fetchProjectHeads(input) {
|
|
811
|
-
const originFetch = await gitResultAsync(
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
822
|
+
const originFetch = await gitResultAsync(
|
|
823
|
+
input.primaryPath,
|
|
824
|
+
[
|
|
825
|
+
...gitTransportSecurityArgs(input.originUrl, input.credentialHelper, input.originCredentialUsername),
|
|
826
|
+
"fetch",
|
|
827
|
+
"--no-recurse-submodules",
|
|
828
|
+
"--prune",
|
|
829
|
+
"origin",
|
|
830
|
+
"+refs/heads/*:refs/remotes/origin/*"
|
|
831
|
+
],
|
|
832
|
+
PROJECT_GIT_TRANSPORT_TIMEOUT_MS
|
|
833
|
+
);
|
|
819
834
|
if (originFetch.exitCode !== 0) {
|
|
820
835
|
throw new Error(`Fetch project origin: ${originFetch.stderr || originFetch.stdout || `git exited ${originFetch.exitCode}`}`);
|
|
821
836
|
}
|
|
822
|
-
const mirrorFetch = await gitResultAsync(
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
837
|
+
const mirrorFetch = await gitResultAsync(
|
|
838
|
+
input.primaryPath,
|
|
839
|
+
[
|
|
840
|
+
...gitTransportSecurityArgs(input.mirrorUrl, input.credentialHelper, input.mirrorCredentialUsername),
|
|
841
|
+
"fetch",
|
|
842
|
+
"--no-recurse-submodules",
|
|
843
|
+
"--prune",
|
|
844
|
+
input.mirrorUrl,
|
|
845
|
+
"+refs/heads/*:refs/r5d/mirror/*"
|
|
846
|
+
],
|
|
847
|
+
PROJECT_GIT_TRANSPORT_TIMEOUT_MS
|
|
848
|
+
);
|
|
830
849
|
if (mirrorFetch.exitCode !== 0 && !/(couldn't find remote ref|does not have any commits|remote repository is empty|no such ref)/i.test(
|
|
831
850
|
`${mirrorFetch.stderr}
|
|
832
851
|
${mirrorFetch.stdout}`
|
|
@@ -1205,13 +1224,17 @@ function removeProjectWorktrees(input) {
|
|
|
1205
1224
|
}
|
|
1206
1225
|
async function deleteProjectMirrorBranch(input) {
|
|
1207
1226
|
validateManagedBranchName(input.branchName);
|
|
1208
|
-
const deleted = await gitResultAsync(
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1227
|
+
const deleted = await gitResultAsync(
|
|
1228
|
+
input.gitDirectory,
|
|
1229
|
+
[
|
|
1230
|
+
...gitTransportSecurityArgs(input.mirrorUrl, input.credentialHelper, input.credentialUsername),
|
|
1231
|
+
"push",
|
|
1232
|
+
"--no-recurse-submodules",
|
|
1233
|
+
input.mirrorUrl,
|
|
1234
|
+
`:refs/heads/${input.branchName}`
|
|
1235
|
+
],
|
|
1236
|
+
PROJECT_GIT_TRANSPORT_TIMEOUT_MS
|
|
1237
|
+
);
|
|
1215
1238
|
if (deleted.exitCode !== 0 && !/(remote ref does not exist|unable to delete|no such ref)/i.test(`${deleted.stderr}
|
|
1216
1239
|
${deleted.stdout}`)) {
|
|
1217
1240
|
throw new Error(
|
|
@@ -1231,21 +1254,50 @@ async function pushProjectMirrorHeads(input) {
|
|
|
1231
1254
|
if (!await tryGitAsync(checkoutPath, ["cat-file", "-e", `${head}^{commit}`])) {
|
|
1232
1255
|
throw new Error(`Exact publication head ${head} is unavailable for project branch ${branchName}`);
|
|
1233
1256
|
}
|
|
1257
|
+
if (!input.expectedMirrorHeads.has(branchName)) {
|
|
1258
|
+
throw new Error(`Missing hidden-mirror lease for project branch ${branchName}`);
|
|
1259
|
+
}
|
|
1260
|
+
const expectedMirrorHead = input.expectedMirrorHeads.get(branchName) ?? null;
|
|
1261
|
+
if (expectedMirrorHead !== null && !/^[0-9a-f]{40}(?:[0-9a-f]{24})?$/.test(expectedMirrorHead)) {
|
|
1262
|
+
throw new Error(`Invalid hidden-mirror lease for project branch ${branchName}`);
|
|
1263
|
+
}
|
|
1264
|
+
const branchId = input.branchIds.get(branchName);
|
|
1265
|
+
if (!branchId || !/^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/.test(branchId)) {
|
|
1266
|
+
throw new Error(`Missing valid branch incarnation for project branch ${branchName}`);
|
|
1267
|
+
}
|
|
1268
|
+
if (expectedMirrorHead) {
|
|
1269
|
+
const handoffPrefix = `refs/r5d/server-synced/${branchId}/`;
|
|
1270
|
+
const markedCommits = (await gitAsync(
|
|
1271
|
+
checkoutPath,
|
|
1272
|
+
["for-each-ref", "--format=%(objectname)", handoffPrefix],
|
|
1273
|
+
`inspect synchronized server commits before publishing ${branchName}`
|
|
1274
|
+
)).split(/\r?\n/).filter((value) => /^[0-9a-f]{40}(?:[0-9a-f]{24})?$/.test(value));
|
|
1275
|
+
for (const markedCommit of markedCommits) {
|
|
1276
|
+
const markerIsInLease = await tryGitAsync(checkoutPath, ["merge-base", "--is-ancestor", markedCommit, expectedMirrorHead]);
|
|
1277
|
+
if (markerIsInLease && !await tryGitAsync(checkoutPath, ["merge-base", "--is-ancestor", markedCommit, head])) {
|
|
1278
|
+
throw new Error(`Refusing to overwrite synchronized server commit history for project branch ${branchName}`);
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1234
1282
|
if (projectWorktreeOperationInProgress(checkoutPath)) {
|
|
1235
1283
|
results.push({ branchName, head, pushed: false, reason: "git_operation_in_progress" });
|
|
1236
1284
|
continue;
|
|
1237
1285
|
}
|
|
1238
|
-
await
|
|
1286
|
+
const pushed = await gitResultAsync(
|
|
1239
1287
|
checkoutPath,
|
|
1240
1288
|
[
|
|
1241
1289
|
...gitTransportSecurityArgs(input.mirrorUrl, input.credentialHelper, input.credentialUsername),
|
|
1242
1290
|
"push",
|
|
1243
1291
|
"--no-recurse-submodules",
|
|
1292
|
+
`--force-with-lease=refs/heads/${branchName}:${expectedMirrorHead ?? ""}`,
|
|
1244
1293
|
input.mirrorUrl,
|
|
1245
|
-
|
|
1294
|
+
`${head}:refs/heads/${branchName}`
|
|
1246
1295
|
],
|
|
1247
|
-
|
|
1296
|
+
PROJECT_GIT_TRANSPORT_TIMEOUT_MS
|
|
1248
1297
|
);
|
|
1298
|
+
if (pushed.exitCode !== 0) {
|
|
1299
|
+
throw new Error(`Push hidden project mirror for ${branchName}: ${pushed.stderr || pushed.stdout || `git exited ${pushed.exitCode}`}`);
|
|
1300
|
+
}
|
|
1249
1301
|
results.push({ branchName, head, pushed: true });
|
|
1250
1302
|
}
|
|
1251
1303
|
return results;
|
|
@@ -1258,14 +1310,19 @@ function assertProjectMirrorHeadsPushed(results) {
|
|
|
1258
1310
|
}
|
|
1259
1311
|
async function observeProjectMirrorHeads(input) {
|
|
1260
1312
|
const primaryPath = branchPath(input.projectRoot, input.primaryBranchName);
|
|
1261
|
-
const fetch = await gitResultAsync(
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1313
|
+
const fetch = await gitResultAsync(
|
|
1314
|
+
primaryPath,
|
|
1315
|
+
[
|
|
1316
|
+
...gitTransportSecurityArgs(input.mirrorUrl, input.credentialHelper, input.credentialUsername),
|
|
1317
|
+
"fetch",
|
|
1318
|
+
"--no-recurse-submodules",
|
|
1319
|
+
"--prune",
|
|
1320
|
+
input.mirrorUrl,
|
|
1321
|
+
"+refs/heads/*:refs/r5d/mirror/*",
|
|
1322
|
+
"+refs/r5d/server-synced/*:refs/r5d/server-synced/*"
|
|
1323
|
+
],
|
|
1324
|
+
PROJECT_GIT_TRANSPORT_TIMEOUT_MS
|
|
1325
|
+
);
|
|
1269
1326
|
if (fetch.exitCode !== 0)
|
|
1270
1327
|
throw new Error(`Refresh project head mirror: ${fetch.stderr || fetch.stdout || `git exited ${fetch.exitCode}`}`);
|
|
1271
1328
|
const observations = [];
|
|
@@ -1274,14 +1331,40 @@ async function observeProjectMirrorHeads(input) {
|
|
|
1274
1331
|
const previousHead = await gitAsync(checkoutPath, ["rev-parse", "HEAD"], `resolve local project head ${branchName}`);
|
|
1275
1332
|
const mirrorRef = `refs/r5d/mirror/${branchName}`;
|
|
1276
1333
|
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
|
-
|
|
1334
|
+
const branchId = input.branchIds?.get(branchName);
|
|
1335
|
+
if (branchId && !/^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/.test(branchId)) throw new Error(`Invalid branch id for ${branchName}`);
|
|
1336
|
+
const handoffPrefix = branchId ? `refs/r5d/server-synced/${branchId}/` : null;
|
|
1337
|
+
const handoffRows = handoffPrefix ? await gitAsync(
|
|
1338
|
+
primaryPath,
|
|
1339
|
+
["for-each-ref", "--format=%(objectname) %(refname)", handoffPrefix],
|
|
1340
|
+
`inspect synchronized server commits for ${branchName}`
|
|
1341
|
+
) : "";
|
|
1342
|
+
const serverSyncedWorkspaceHeads = (await Promise.all(
|
|
1343
|
+
handoffRows.split(/\r?\n/).map(async (row) => {
|
|
1344
|
+
const separator = row.indexOf(" ");
|
|
1345
|
+
if (separator < 0 || !mirrorHead) return null;
|
|
1346
|
+
const markedCommit = row.slice(0, separator);
|
|
1347
|
+
const ref = row.slice(separator + 1);
|
|
1348
|
+
if (!/^[0-9a-f]{40}(?:[0-9a-f]{24})?$/.test(markedCommit) || !handoffPrefix || !ref.startsWith(handoffPrefix)) {
|
|
1349
|
+
return null;
|
|
1350
|
+
}
|
|
1351
|
+
const markerIsIntegrated = markedCommit === mirrorHead || await tryGitAsync(primaryPath, ["merge-base", "--is-ancestor", markedCommit, mirrorHead]);
|
|
1352
|
+
if (!markerIsIntegrated) return null;
|
|
1353
|
+
const workspaceHead = ref.slice(handoffPrefix.length);
|
|
1354
|
+
return /^[0-9a-f]{40}(?:[0-9a-f]{24})?$/.test(workspaceHead) ? workspaceHead : null;
|
|
1355
|
+
})
|
|
1356
|
+
)).filter((workspaceHead) => workspaceHead !== null).sort();
|
|
1357
|
+
const fastForward = Boolean(
|
|
1358
|
+
mirrorHead && mirrorHead !== previousHead && await tryGitAsync(checkoutPath, ["merge-base", "--is-ancestor", previousHead, mirrorHead])
|
|
1279
1359
|
);
|
|
1360
|
+
const serverSynced = Boolean(mirrorHead && mirrorHead !== previousHead && serverSyncedWorkspaceHeads.length > 0);
|
|
1361
|
+
const shouldMove = Boolean(mirrorHead && mirrorHead !== previousHead && (input.allowNonFastForward || fastForward || serverSynced));
|
|
1280
1362
|
observations.push({
|
|
1281
1363
|
branchName,
|
|
1282
1364
|
previousHead,
|
|
1283
1365
|
mirrorHead,
|
|
1284
|
-
mode: input.allowNonFastForward ? "reseed" : "fast_forward",
|
|
1366
|
+
mode: input.allowNonFastForward ? "reseed" : serverSynced && !fastForward ? "server_synced" : "fast_forward",
|
|
1367
|
+
serverSyncedWorkspaceHeads,
|
|
1285
1368
|
shouldMove
|
|
1286
1369
|
});
|
|
1287
1370
|
}
|
|
@@ -1293,7 +1376,8 @@ async function applyObservedProjectMirrorHeads(input) {
|
|
|
1293
1376
|
const { branchName, mirrorHead } = observation;
|
|
1294
1377
|
const checkoutPath = branchPath(input.projectRoot, branchName);
|
|
1295
1378
|
const currentHead = await gitAsync(checkoutPath, ["rev-parse", "HEAD"], `resolve local project head ${branchName}`);
|
|
1296
|
-
|
|
1379
|
+
const serverSyncedCovered = observation.mode !== "server_synced" || (input.serverSyncedWorkspaceCovered ? await input.serverSyncedWorkspaceCovered(observation) : false);
|
|
1380
|
+
if (!observation.shouldMove || !serverSyncedCovered || !mirrorHead || currentHead === mirrorHead || currentHead !== observation.previousHead) {
|
|
1297
1381
|
results.push({ branchName, previousHead: currentHead, mirrorHead, moved: false });
|
|
1298
1382
|
continue;
|
|
1299
1383
|
}
|
|
@@ -1305,14 +1389,14 @@ async function applyObservedProjectMirrorHeads(input) {
|
|
|
1305
1389
|
results.push({ branchName, previousHead: currentHead, mirrorHead, moved: false });
|
|
1306
1390
|
continue;
|
|
1307
1391
|
}
|
|
1308
|
-
if (observation.mode === "reseed") {
|
|
1392
|
+
if (observation.mode === "reseed" || observation.mode === "server_synced") {
|
|
1309
1393
|
await preserveHead(checkoutPath);
|
|
1310
1394
|
if (!mutationAllowed()) {
|
|
1311
1395
|
results.push({ branchName, previousHead: currentHead, mirrorHead, moved: false });
|
|
1312
1396
|
continue;
|
|
1313
1397
|
}
|
|
1314
1398
|
}
|
|
1315
|
-
const action = observation.mode === "reseed" ? "reseed" : "fast-forward";
|
|
1399
|
+
const action = observation.mode === "reseed" ? "reseed" : observation.mode === "server_synced" ? "adopt server commit" : "fast-forward";
|
|
1316
1400
|
await gitAsync(checkoutPath, ["read-tree", "--reset", mirrorHead], `${action} project index ${branchName}`);
|
|
1317
1401
|
await gitAsync(
|
|
1318
1402
|
checkoutPath,
|
|
@@ -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,7 +204,8 @@ 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
|
/**
|
|
@@ -210,6 +215,7 @@ export type ProjectMirrorHeadObservation = {
|
|
|
210
215
|
*/
|
|
211
216
|
export declare function observeProjectMirrorHeads(input: Omit<ProjectMirrorHeadUpdateInput, "branchMutationAllowed"> & {
|
|
212
217
|
allowNonFastForward: boolean;
|
|
218
|
+
branchIds?: ReadonlyMap<string, string>;
|
|
213
219
|
}): Promise<ProjectMirrorHeadObservation[]>;
|
|
214
220
|
/** Apply only previously observed object ids; this function never refetches. */
|
|
215
221
|
export declare function applyObservedProjectMirrorHeads(input: {
|
|
@@ -218,6 +224,8 @@ export declare function applyObservedProjectMirrorHeads(input: {
|
|
|
218
224
|
observations: readonly ProjectMirrorHeadObservation[];
|
|
219
225
|
/** Rechecked immediately before refs or the index move. */
|
|
220
226
|
branchMutationAllowed?: (branchName: string, checkoutPath: string) => boolean;
|
|
227
|
+
/** Proves the visible outer workspace has hydrated the server commit's exact source generation. */
|
|
228
|
+
serverSyncedWorkspaceCovered?: (observation: ProjectMirrorHeadObservation) => Promise<boolean>;
|
|
221
229
|
}): Promise<Array<{
|
|
222
230
|
branchName: string;
|
|
223
231
|
previousHead: string;
|