@ricsam/r5d-worker 0.0.103 → 0.0.104
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 +78 -15
- package/dist/cjs/package.json +1 -1
- package/dist/cjs/project-worktrees.cjs +123 -31
- package/dist/mjs/main.mjs +79 -15
- package/dist/mjs/package.json +1 -1
- package/dist/mjs/project-worktrees.mjs +120 -31
- package/dist/types/main.d.ts +6 -0
- package/dist/types/project-worktrees.d.ts +47 -0
- package/package.json +1 -1
package/dist/cjs/main.cjs
CHANGED
|
@@ -3509,6 +3509,7 @@ async function startWorker(options) {
|
|
|
3509
3509
|
let terminalReplayTimer;
|
|
3510
3510
|
let workspaceSyncRequestsInFlight = 0;
|
|
3511
3511
|
let lastWorkspaceSyncMirrorObservationMs = 0;
|
|
3512
|
+
let lastInventoriedMissingCheckouts;
|
|
3512
3513
|
let sessionArtifactSyncRequestsInFlight = 0;
|
|
3513
3514
|
let cliUpdateInProgress = false;
|
|
3514
3515
|
let reloadAfterClose = false;
|
|
@@ -3566,8 +3567,15 @@ async function startWorker(options) {
|
|
|
3566
3567
|
const primaryBranchName = primaryProjectBranch(project);
|
|
3567
3568
|
const connection = projectConnection(project);
|
|
3568
3569
|
const sourcePath = configuredProjectBranchPath(projectsRoot, project, branchName);
|
|
3569
|
-
|
|
3570
|
-
|
|
3570
|
+
const primaryPath = configuredProjectBranchPath(projectsRoot, project, primaryBranchName);
|
|
3571
|
+
if (requireLocalBranch || import_node_fs.default.existsSync(sourcePath) && hasProjectWorktree(primaryPath)) {
|
|
3572
|
+
const deleted = (0, import_project_worktrees.deleteLinkedProjectBranch)({ projectRoot, primaryBranchName, branchName });
|
|
3573
|
+
if (deleted.movedAsidePath) {
|
|
3574
|
+
process.stderr.write(
|
|
3575
|
+
`[r5d-worker] set aside ${sourcePath}, which was not a linked worktree of ${project.projectPath}, at ${deleted.movedAsidePath}
|
|
3576
|
+
`
|
|
3577
|
+
);
|
|
3578
|
+
}
|
|
3571
3579
|
}
|
|
3572
3580
|
const planSourcePath = import_node_path.default.join(planRoot, project.projectId, ...branchName.split("/"));
|
|
3573
3581
|
import_node_fs.default.rmSync(planSourcePath, { recursive: true, force: true });
|
|
@@ -3640,11 +3648,17 @@ async function startWorker(options) {
|
|
|
3640
3648
|
if (branchName === primaryBranchName) {
|
|
3641
3649
|
throw new Error(`Cannot hide preserve-only project branch ${project.projectId}/${branchName} because it is the active anchor`);
|
|
3642
3650
|
}
|
|
3643
|
-
(0, import_project_worktrees.deleteLinkedProjectBranch)({
|
|
3651
|
+
const deleted = (0, import_project_worktrees.deleteLinkedProjectBranch)({
|
|
3644
3652
|
projectRoot: configuredProjectRoot(projectsRoot, project),
|
|
3645
3653
|
primaryBranchName,
|
|
3646
3654
|
branchName
|
|
3647
3655
|
});
|
|
3656
|
+
if (deleted.movedAsidePath) {
|
|
3657
|
+
process.stderr.write(
|
|
3658
|
+
`[r5d-worker] set aside ${branchPath}, which was not a linked worktree of ${project.projectPath}, at ${deleted.movedAsidePath}
|
|
3659
|
+
`
|
|
3660
|
+
);
|
|
3661
|
+
}
|
|
3648
3662
|
import_node_fs.default.rmSync(import_node_path.default.join(planRoot, project.projectId, ...branchName.split("/")), { recursive: true, force: true });
|
|
3649
3663
|
readyProjectIds.delete(project.projectId);
|
|
3650
3664
|
reconciledProjectConfigFingerprints.delete(project.projectId);
|
|
@@ -3813,6 +3827,40 @@ async function startWorker(options) {
|
|
|
3813
3827
|
});
|
|
3814
3828
|
};
|
|
3815
3829
|
const projectBranchMountBusy = (projectId, branchName, branchPath) => !readyProjectIds.has(projectId) || projectBranchMountActivityBusy(projectId, branchName, branchPath);
|
|
3830
|
+
const inventoryConfiguredProjectCheckouts = () => {
|
|
3831
|
+
const inventory = { projects: [], missingCheckouts: [] };
|
|
3832
|
+
for (const project of projectConfigById.values()) {
|
|
3833
|
+
if (project.executionDisabled || !readyProjectIds.has(project.projectId) || project.branches.length === 0) {
|
|
3834
|
+
inventory.projects.push(project);
|
|
3835
|
+
continue;
|
|
3836
|
+
}
|
|
3837
|
+
const checkouts = (0, import_project_worktrees.inventoryProjectCheckouts)({
|
|
3838
|
+
projectRoot: configuredProjectRoot(projectsRoot, project),
|
|
3839
|
+
primaryBranchName: primaryProjectBranch(project),
|
|
3840
|
+
branchNames: project.branches.map(({ branchName }) => branchName)
|
|
3841
|
+
});
|
|
3842
|
+
if (checkouts.missingCheckouts.length === 0) {
|
|
3843
|
+
inventory.projects.push(project);
|
|
3844
|
+
continue;
|
|
3845
|
+
}
|
|
3846
|
+
for (const missing of checkouts.missingCheckouts) inventory.missingCheckouts.push({ projectId: project.projectId, ...missing });
|
|
3847
|
+
const linked = new Set(checkouts.linkedBranchNames);
|
|
3848
|
+
inventory.projects.push({ ...project, branches: project.branches.filter(({ branchName }) => linked.has(branchName)) });
|
|
3849
|
+
}
|
|
3850
|
+
inventory.missingCheckouts.sort(
|
|
3851
|
+
(left, right) => left.projectId.localeCompare(right.projectId) || left.branchName.localeCompare(right.branchName)
|
|
3852
|
+
);
|
|
3853
|
+
return inventory;
|
|
3854
|
+
};
|
|
3855
|
+
const reportedMissingCheckouts = (inventory) => inventory.missingCheckouts.map(({ projectId, branchName, reason }) => ({ projectId, branchName, reason }));
|
|
3856
|
+
const logMissingCheckouts = (inventory) => {
|
|
3857
|
+
if (inventory.missingCheckouts.length === 0) return;
|
|
3858
|
+
const described = inventory.missingCheckouts.map(({ checkoutPath, reason }) => `${checkoutPath} (${reason === "missing" ? "directory missing" : "not a linked worktree of the project"})`).join(", ");
|
|
3859
|
+
process.stderr.write(
|
|
3860
|
+
`[r5d-worker] skipping ${inventory.missingCheckouts.length} configured project checkout(s) this cycle: ${described}; delete the branch through the platform or reconnect this worker to recreate the checkout
|
|
3861
|
+
`
|
|
3862
|
+
);
|
|
3863
|
+
};
|
|
3816
3864
|
const buildWorkspaceMounts = (projects = projectConfigById.values()) => {
|
|
3817
3865
|
const mounts = [];
|
|
3818
3866
|
const creatorLocalIncarnations = new Map(
|
|
@@ -4091,9 +4139,9 @@ async function startWorker(options) {
|
|
|
4091
4139
|
(left, right) => left.projectId.localeCompare(right.projectId) || left.branchName.localeCompare(right.branchName)
|
|
4092
4140
|
);
|
|
4093
4141
|
};
|
|
4094
|
-
const captureProjectHeadsForWorkspacePublication = async () => {
|
|
4142
|
+
const captureProjectHeadsForWorkspacePublication = async (projects = projectConfigById.values()) => {
|
|
4095
4143
|
const heads = /* @__PURE__ */ new Map();
|
|
4096
|
-
for (const project of
|
|
4144
|
+
for (const project of projects) {
|
|
4097
4145
|
if (!readyProjectIds.has(project.projectId)) continue;
|
|
4098
4146
|
for (const branch of project.branches) {
|
|
4099
4147
|
const branchPath = configuredProjectBranchPath(projectsRoot, project, branch.branchName);
|
|
@@ -4103,9 +4151,9 @@ async function startWorker(options) {
|
|
|
4103
4151
|
}
|
|
4104
4152
|
return heads;
|
|
4105
4153
|
};
|
|
4106
|
-
const pushChangedProjectHeads = async (activeMountIds, publishedHead, publicationHeads, expectedMirrorHeads, suppressedMountIds = /* @__PURE__ */ new Set()) => {
|
|
4154
|
+
const pushChangedProjectHeads = async (activeMountIds, publishedHead, publicationHeads, expectedMirrorHeads, suppressedMountIds = /* @__PURE__ */ new Set(), projects = projectConfigById.values()) => {
|
|
4107
4155
|
const active = new Set(activeMountIds);
|
|
4108
|
-
for (const project of
|
|
4156
|
+
for (const project of projects) {
|
|
4109
4157
|
if (!readyProjectIds.has(project.projectId) || !(0, import_repository_transition_policy.repositoryMirrorWritesAllowed)(project)) continue;
|
|
4110
4158
|
const changed = /* @__PURE__ */ new Map();
|
|
4111
4159
|
for (const branch of project.branches) {
|
|
@@ -4205,7 +4253,7 @@ async function startWorker(options) {
|
|
|
4205
4253
|
};
|
|
4206
4254
|
const observeProjectHeadsBeforeOuterWorkspace = async (input) => {
|
|
4207
4255
|
const observationStartedAt = Date.now();
|
|
4208
|
-
const eligibleProjects = [...projectConfigById.values()].filter(
|
|
4256
|
+
const eligibleProjects = [...input.projects ?? projectConfigById.values()].filter(
|
|
4209
4257
|
(project) => !project.executionDisabled && readyProjectIds.has(project.projectId) && project.branches.length > 0
|
|
4210
4258
|
);
|
|
4211
4259
|
try {
|
|
@@ -4315,7 +4363,7 @@ async function startWorker(options) {
|
|
|
4315
4363
|
}
|
|
4316
4364
|
}
|
|
4317
4365
|
};
|
|
4318
|
-
const mapWorkspaceGitResult = async (attemptId, trigger, result) => {
|
|
4366
|
+
const mapWorkspaceGitResult = async (attemptId, trigger, result, missingCheckouts) => {
|
|
4319
4367
|
const activeProjectBranchPublications = (0, import_workspace_publication_evidence.activeProjectBranchPublicationEvidence)([...projectConfigById.values()], result.activeMountIds);
|
|
4320
4368
|
return {
|
|
4321
4369
|
type: "workspace_sync",
|
|
@@ -4333,6 +4381,7 @@ async function startWorker(options) {
|
|
|
4333
4381
|
affectedPaths: result.affectedPaths,
|
|
4334
4382
|
activeMountIds: result.activeMountIds,
|
|
4335
4383
|
skippedMountIds: result.skippedMountIds,
|
|
4384
|
+
...missingCheckouts ? { missingCheckouts } : {},
|
|
4336
4385
|
activeProjectBranchPublications,
|
|
4337
4386
|
discardedPaths: [],
|
|
4338
4387
|
localChangesDiscarded: false,
|
|
@@ -4360,6 +4409,7 @@ async function startWorker(options) {
|
|
|
4360
4409
|
affectedPaths: [],
|
|
4361
4410
|
discardedPaths: [],
|
|
4362
4411
|
localChangesDiscarded: false,
|
|
4412
|
+
...lastInventoriedMissingCheckouts ? { missingCheckouts: lastInventoriedMissingCheckouts } : {},
|
|
4363
4413
|
error: error instanceof Error ? error.message : String(error)
|
|
4364
4414
|
};
|
|
4365
4415
|
};
|
|
@@ -4368,7 +4418,11 @@ async function startWorker(options) {
|
|
|
4368
4418
|
if (!workspaceRemoteUrl || !workspaceCredentialHelper || !workspaceCredentialUsername || !workspaceGitIdentity) {
|
|
4369
4419
|
throw new Error("Worker workspace configuration has not been received");
|
|
4370
4420
|
}
|
|
4371
|
-
const
|
|
4421
|
+
const checkoutInventory = inventoryConfiguredProjectCheckouts();
|
|
4422
|
+
logMissingCheckouts(checkoutInventory);
|
|
4423
|
+
const missingCheckouts = reportedMissingCheckouts(checkoutInventory);
|
|
4424
|
+
lastInventoriedMissingCheckouts = missingCheckouts;
|
|
4425
|
+
const configuredMounts = buildWorkspaceMounts(checkoutInventory.projects);
|
|
4372
4426
|
if (input.resetToCanonical) {
|
|
4373
4427
|
workspaceProjectionLedger.reset();
|
|
4374
4428
|
const resetHasBusyLiveMount = configuredMounts.some(
|
|
@@ -4379,7 +4433,8 @@ async function startWorker(options) {
|
|
|
4379
4433
|
}
|
|
4380
4434
|
const observedProjectHeads2 = await observeProjectHeadsBeforeOuterWorkspace({
|
|
4381
4435
|
allowNonFastForward: true,
|
|
4382
|
-
failClosed: true
|
|
4436
|
+
failClosed: true,
|
|
4437
|
+
projects: checkoutInventory.projects
|
|
4383
4438
|
});
|
|
4384
4439
|
const reset = await (0, import_workspace_git_sync.resetWorkspaceGit)({
|
|
4385
4440
|
workspacePath: workspaceShadowRoot,
|
|
@@ -4407,6 +4462,7 @@ async function startWorker(options) {
|
|
|
4407
4462
|
affectedPaths: reset.discardedPaths,
|
|
4408
4463
|
activeMountIds: reset.activeMountIds,
|
|
4409
4464
|
skippedMountIds: reset.skippedMountIds,
|
|
4465
|
+
missingCheckouts,
|
|
4410
4466
|
discardedPaths: reset.discardedPaths,
|
|
4411
4467
|
localChangesDiscarded: reset.discardedPaths.length > 0 || reset.startingHead !== reset.remoteHead
|
|
4412
4468
|
};
|
|
@@ -4414,7 +4470,7 @@ async function startWorker(options) {
|
|
|
4414
4470
|
const outerRemediation = input.trigger.type === "remediation" || input.trigger.type === "remediation_confirm";
|
|
4415
4471
|
const mounts = outerRemediation ? (0, import_workspace_git_sync.workspaceGitMountsForRemediation)(configuredMounts) : configuredMounts;
|
|
4416
4472
|
if (outerRemediation) workspaceProjectionLedger.reset();
|
|
4417
|
-
const observedProjectHeads = outerRemediation ? [] : await observeProjectHeadsBeforeOuterWorkspace({ allowNonFastForward: false, failClosed: false });
|
|
4473
|
+
const observedProjectHeads = outerRemediation ? [] : await observeProjectHeadsBeforeOuterWorkspace({ allowNonFastForward: false, failClosed: false, projects: checkoutInventory.projects });
|
|
4418
4474
|
const expectedMirrorHeads = /* @__PURE__ */ new Map();
|
|
4419
4475
|
for (const bundle of observedProjectHeads) {
|
|
4420
4476
|
for (const observation of bundle.observations) {
|
|
@@ -4426,7 +4482,7 @@ async function startWorker(options) {
|
|
|
4426
4482
|
({ projectId, observations }) => observations.filter(({ shouldMove }) => shouldMove).map(({ branchName }) => projectMountId(projectId, branchName))
|
|
4427
4483
|
)
|
|
4428
4484
|
);
|
|
4429
|
-
const publicationHeads = outerRemediation ? /* @__PURE__ */ new Map() : await captureProjectHeadsForWorkspacePublication();
|
|
4485
|
+
const publicationHeads = outerRemediation ? /* @__PURE__ */ new Map() : await captureProjectHeadsForWorkspacePublication(checkoutInventory.projects);
|
|
4430
4486
|
const result = await (0, import_workspace_git_sync.synchronizeWorkspaceGit)({
|
|
4431
4487
|
attemptId: input.attemptId,
|
|
4432
4488
|
workerLabel: label,
|
|
@@ -4446,7 +4502,14 @@ async function startWorker(options) {
|
|
|
4446
4502
|
advertisedRemoteHead: projectMirrorFetchSkippingEnabled && !outerRemediation ? advertisedWorkspaceHead : void 0,
|
|
4447
4503
|
assertStillAdmitted: input.assertStillAdmitted,
|
|
4448
4504
|
afterWorkspacePublished: outerRemediation ? void 0 : async ({ activeMountIds, publishedHead }) => {
|
|
4449
|
-
await pushChangedProjectHeads(
|
|
4505
|
+
await pushChangedProjectHeads(
|
|
4506
|
+
activeMountIds,
|
|
4507
|
+
publishedHead,
|
|
4508
|
+
publicationHeads,
|
|
4509
|
+
expectedMirrorHeads,
|
|
4510
|
+
inboundMoveMountIds,
|
|
4511
|
+
checkoutInventory.projects
|
|
4512
|
+
);
|
|
4450
4513
|
},
|
|
4451
4514
|
hydrationHooks: workspaceHydrationTransactionHooks
|
|
4452
4515
|
});
|
|
@@ -4465,7 +4528,7 @@ async function startWorker(options) {
|
|
|
4465
4528
|
result.localHead ?? result.remoteHead
|
|
4466
4529
|
);
|
|
4467
4530
|
}
|
|
4468
|
-
const mapped = await mapWorkspaceGitResult(input.attemptId, input.trigger, result);
|
|
4531
|
+
const mapped = await mapWorkspaceGitResult(input.attemptId, input.trigger, result, missingCheckouts);
|
|
4469
4532
|
if (!outerRemediation) {
|
|
4470
4533
|
workspaceProjectionLedger.recordCycle({ result: mapped, mounts, projectedMutationTokens: result.projectedMutationTokens });
|
|
4471
4534
|
}
|
package/dist/cjs/package.json
CHANGED
|
@@ -28,10 +28,12 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
28
28
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
29
|
var project_worktrees_exports = {};
|
|
30
30
|
__export(project_worktrees_exports, {
|
|
31
|
+
PROJECT_REMOVED_CHECKOUTS_DIRECTORY: () => PROJECT_REMOVED_CHECKOUTS_DIRECTORY,
|
|
31
32
|
PROJECT_WORKTREE_SNAPSHOT_PREFIX: () => PROJECT_WORKTREE_SNAPSHOT_PREFIX,
|
|
32
33
|
ProjectBranchCreationRollbackIncompleteError: () => ProjectBranchCreationRollbackIncompleteError,
|
|
33
34
|
applyObservedProjectMirrorHeads: () => applyObservedProjectMirrorHeads,
|
|
34
35
|
assertProjectMirrorHeadsPushed: () => assertProjectMirrorHeadsPushed,
|
|
36
|
+
classifyProjectCheckout: () => classifyProjectCheckout,
|
|
35
37
|
computeLocalProjectMirrorRefsTokens: () => computeLocalProjectMirrorRefsTokens,
|
|
36
38
|
createLinkedProjectBranch: () => createLinkedProjectBranch,
|
|
37
39
|
createOrRetryLinkedProjectBranch: () => createOrRetryLinkedProjectBranch,
|
|
@@ -40,6 +42,7 @@ __export(project_worktrees_exports, {
|
|
|
40
42
|
ensureProjectWorktrees: () => ensureProjectWorktrees,
|
|
41
43
|
fastForwardProjectHeadsFromMirror: () => fastForwardProjectHeadsFromMirror,
|
|
42
44
|
hasLinkedProjectWorktreeLayout: () => hasLinkedProjectWorktreeLayout,
|
|
45
|
+
inventoryProjectCheckouts: () => inventoryProjectCheckouts,
|
|
43
46
|
observeProjectMirrorHeads: () => observeProjectMirrorHeads,
|
|
44
47
|
projectBranchMayExistAfterCreateFailure: () => projectBranchMayExistAfterCreateFailure,
|
|
45
48
|
projectOriginBranchName: () => projectOriginBranchName,
|
|
@@ -334,6 +337,8 @@ function normalizeSnapshotManifest(value) {
|
|
|
334
337
|
if (record.state !== "building" && record.state !== "prepared" && record.state !== "restoring" && record.state !== "consumed") {
|
|
335
338
|
throw new Error("Project worktree snapshot manifest has an invalid state");
|
|
336
339
|
}
|
|
340
|
+
const scope = record.scope === void 0 ? "project" : record.scope;
|
|
341
|
+
if (scope !== "project" && scope !== "branches") throw new Error("Project worktree snapshot manifest has an invalid scope");
|
|
337
342
|
if (!Number.isSafeInteger(record.ownerProcessId) || record.ownerProcessId <= 0) {
|
|
338
343
|
throw new Error("Project worktree snapshot manifest has an invalid owner process");
|
|
339
344
|
}
|
|
@@ -369,6 +374,7 @@ function normalizeSnapshotManifest(value) {
|
|
|
369
374
|
return {
|
|
370
375
|
version: PROJECT_WORKTREE_SNAPSHOT_MANIFEST_VERSION,
|
|
371
376
|
state: record.state,
|
|
377
|
+
scope,
|
|
372
378
|
ownerProcessId: record.ownerProcessId,
|
|
373
379
|
ownerSessionId: record.ownerSessionId,
|
|
374
380
|
projectRoot: record.projectRoot,
|
|
@@ -405,7 +411,7 @@ function writeSnapshotManifest(snapshotRoot, manifest) {
|
|
|
405
411
|
return readSnapshotManifest(snapshotRoot);
|
|
406
412
|
}
|
|
407
413
|
function assertSameSnapshotIdentity(expected, actual) {
|
|
408
|
-
if (expected.ownerProcessId !== actual.ownerProcessId || expected.ownerSessionId !== actual.ownerSessionId || expected.projectRoot !== actual.projectRoot || expected.createdAt !== actual.createdAt || JSON.stringify(expected.branches) !== JSON.stringify(actual.branches)) {
|
|
414
|
+
if (expected.scope !== actual.scope || expected.ownerProcessId !== actual.ownerProcessId || expected.ownerSessionId !== actual.ownerSessionId || expected.projectRoot !== actual.projectRoot || expected.createdAt !== actual.createdAt || JSON.stringify(expected.branches) !== JSON.stringify(actual.branches)) {
|
|
409
415
|
throw new Error("Project worktree snapshot identity changed");
|
|
410
416
|
}
|
|
411
417
|
}
|
|
@@ -472,7 +478,7 @@ function assertNoOutstandingSnapshotForProject(temporaryRoot, projectRoot) {
|
|
|
472
478
|
}
|
|
473
479
|
}
|
|
474
480
|
}
|
|
475
|
-
function snapshotBranchTrees(projectRoot, branches, temporaryRoot, onProgress) {
|
|
481
|
+
function snapshotBranchTrees(projectRoot, branches, temporaryRoot, onProgress, scope = "project") {
|
|
476
482
|
const resolvedProjectRoot = import_node_path.default.resolve(projectRoot);
|
|
477
483
|
const resolvedTemporaryRoot = import_node_path.default.resolve(temporaryRoot ?? import_node_os.default.tmpdir());
|
|
478
484
|
assertNoOutstandingSnapshotForProject(resolvedTemporaryRoot, resolvedProjectRoot);
|
|
@@ -493,6 +499,7 @@ function snapshotBranchTrees(projectRoot, branches, temporaryRoot, onProgress) {
|
|
|
493
499
|
let manifest = writeSnapshotManifest(root, {
|
|
494
500
|
version: PROJECT_WORKTREE_SNAPSHOT_MANIFEST_VERSION,
|
|
495
501
|
state: "building",
|
|
502
|
+
scope,
|
|
496
503
|
ownerProcessId: process.pid,
|
|
497
504
|
ownerSessionId: PROJECT_WORKTREE_SNAPSHOT_OWNER_SESSION_ID,
|
|
498
505
|
projectRoot: resolvedProjectRoot,
|
|
@@ -599,7 +606,7 @@ function restoreProjectWorktreeSnapshot(snapshotRoot, manifest, onProgress) {
|
|
|
599
606
|
const presentBranches = current.branches.filter(({ snapshotPresent }) => snapshotPresent);
|
|
600
607
|
const destinationParent = import_node_path.default.dirname(current.projectRoot);
|
|
601
608
|
import_node_fs.default.mkdirSync(destinationParent, { recursive: true });
|
|
602
|
-
const canMoveWholeSnapshot = presentBranches.length > 0 && (current.state === "restoring" || treesRootStat !== null && treesRootStat.dev === import_node_fs.default.lstatSync(destinationParent).dev);
|
|
609
|
+
const canMoveWholeSnapshot = current.scope === "project" && presentBranches.length > 0 && (current.state === "restoring" || treesRootStat !== null && treesRootStat.dev === import_node_fs.default.lstatSync(destinationParent).dev);
|
|
603
610
|
if (canMoveWholeSnapshot) {
|
|
604
611
|
if (current.state === "prepared") current = advanceSnapshotState(snapshotRoot, current, "restoring");
|
|
605
612
|
onProgress?.({
|
|
@@ -690,8 +697,12 @@ function consumeAndRemoveProjectWorktreeSnapshot(snapshotRoot, manifest) {
|
|
|
690
697
|
} catch {
|
|
691
698
|
}
|
|
692
699
|
}
|
|
693
|
-
function persistReconciledProjectAndConsumeSnapshot(snapshot, afterProjectTreeFsync) {
|
|
694
|
-
|
|
700
|
+
function persistReconciledProjectAndConsumeSnapshot(snapshot, afterProjectTreeFsync, durabilityPaths) {
|
|
701
|
+
if (durabilityPaths) {
|
|
702
|
+
for (const durabilityPath of durabilityPaths) if (import_node_fs.default.existsSync(durabilityPath)) fsyncTree(durabilityPath);
|
|
703
|
+
} else {
|
|
704
|
+
fsyncTree(snapshot.manifest.projectRoot);
|
|
705
|
+
}
|
|
695
706
|
fsyncManagedProjectRootEntry(snapshot.manifest.projectRoot);
|
|
696
707
|
afterProjectTreeFsync?.();
|
|
697
708
|
consumeAndRemoveProjectWorktreeSnapshot(snapshot.root, snapshot.manifest);
|
|
@@ -989,8 +1000,10 @@ async function reconcileLinkedLayout(input) {
|
|
|
989
1000
|
await configureRepository({ ...input, primaryPath });
|
|
990
1001
|
await tryGitAsync(primaryPath, ["worktree", "prune"]);
|
|
991
1002
|
await fetchProjectHeads({ ...input, primaryPath });
|
|
1003
|
+
const primaryCommonDir = await commonGitDirectoryAsync(primaryPath);
|
|
1004
|
+
if (!primaryCommonDir) throw new Error("Primary project checkout is unavailable");
|
|
992
1005
|
const mirrorHeads = /* @__PURE__ */ new Map();
|
|
993
|
-
const
|
|
1006
|
+
const plan = [];
|
|
994
1007
|
for (const branch of input.branches) {
|
|
995
1008
|
const checkoutPath = branchPath(input.projectRoot, branch.branchName);
|
|
996
1009
|
const resolved = await seedForBranch({
|
|
@@ -1000,24 +1013,30 @@ async function reconcileLinkedLayout(input) {
|
|
|
1000
1013
|
requireMirrorHead: input.exactMirrorBranches?.has(branch.branchName)
|
|
1001
1014
|
});
|
|
1002
1015
|
mirrorHeads.set(branch.branchName, resolved.mirrorHead);
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
input.beforeDestructiveMutation();
|
|
1007
|
-
mutatedBranches.add(branch.branchName);
|
|
1008
|
-
await preserveHead(checkoutPath);
|
|
1009
|
-
await gitAsync(checkoutPath, ["reset", "--hard", resolved.seed], `anchor linked worktree ${branch.branchName}`);
|
|
1016
|
+
const linked = branch.branchName === input.primaryBranchName || gitDirectoryKind(checkoutPath) !== null && await commonGitDirectoryAsync(checkoutPath) === primaryCommonDir;
|
|
1017
|
+
if (!linked) {
|
|
1018
|
+
plan.push({ branchName: branch.branchName, checkoutPath, seed: resolved.seed, action: "recreate" });
|
|
1010
1019
|
continue;
|
|
1011
1020
|
}
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1021
|
+
const currentHead = await gitAsync(checkoutPath, ["rev-parse", "HEAD"], `resolve linked worktree head ${branch.branchName}`);
|
|
1022
|
+
plan.push({ branchName: branch.branchName, checkoutPath, seed: resolved.seed, action: currentHead === resolved.seed ? "keep" : "reset" });
|
|
1023
|
+
}
|
|
1024
|
+
const mutatedBranches = new Set(plan.filter(({ action }) => action !== "keep").map(({ branchName }) => branchName));
|
|
1025
|
+
if (mutatedBranches.size > 0) input.beforeDestructiveMutation(mutatedBranches);
|
|
1026
|
+
for (const entry of plan) {
|
|
1027
|
+
if (entry.action === "keep") continue;
|
|
1028
|
+
if (entry.action === "reset") {
|
|
1029
|
+
await preserveHead(entry.checkoutPath);
|
|
1030
|
+
await gitAsync(entry.checkoutPath, ["reset", "--hard", entry.seed], `anchor linked worktree ${entry.branchName}`);
|
|
1031
|
+
continue;
|
|
1032
|
+
}
|
|
1033
|
+
import_node_fs.default.rmSync(entry.checkoutPath, { recursive: true, force: true });
|
|
1034
|
+
await gitAsync(primaryPath, ["branch", "-f", entry.branchName, entry.seed], `anchor project branch ${entry.branchName}`);
|
|
1035
|
+
import_node_fs.default.mkdirSync(import_node_path.default.dirname(entry.checkoutPath), { recursive: true });
|
|
1017
1036
|
await gitAsync(
|
|
1018
1037
|
primaryPath,
|
|
1019
|
-
["worktree", "add", "--force", checkoutPath,
|
|
1020
|
-
`create linked worktree ${
|
|
1038
|
+
["worktree", "add", "--force", entry.checkoutPath, entry.branchName],
|
|
1039
|
+
`create linked worktree ${entry.branchName}`
|
|
1021
1040
|
);
|
|
1022
1041
|
}
|
|
1023
1042
|
return { mirrorHeads, mutatedBranches };
|
|
@@ -1044,8 +1063,14 @@ async function ensureProjectWorktrees(input) {
|
|
|
1044
1063
|
const snapshotStorageRoot = import_node_path.default.resolve(input.snapshotRoot ?? import_node_os.default.tmpdir());
|
|
1045
1064
|
assertNoOutstandingSnapshotForProject(snapshotStorageRoot, projectRoot);
|
|
1046
1065
|
let snapshot;
|
|
1047
|
-
const ensureSnapshot = () => {
|
|
1048
|
-
snapshot ??= snapshotBranchTrees(
|
|
1066
|
+
const ensureSnapshot = (branchNames) => {
|
|
1067
|
+
snapshot ??= snapshotBranchTrees(
|
|
1068
|
+
projectRoot,
|
|
1069
|
+
branchNames ? input.branches.filter(({ branchName }) => branchNames.has(branchName)) : input.branches,
|
|
1070
|
+
snapshotStorageRoot,
|
|
1071
|
+
input.onSnapshotProgress,
|
|
1072
|
+
branchNames ? "branches" : "project"
|
|
1073
|
+
);
|
|
1049
1074
|
return snapshot;
|
|
1050
1075
|
};
|
|
1051
1076
|
try {
|
|
@@ -1097,7 +1122,21 @@ async function ensureProjectWorktrees(input) {
|
|
|
1097
1122
|
});
|
|
1098
1123
|
}
|
|
1099
1124
|
states.sort((left, right) => left.branchName.localeCompare(right.branchName));
|
|
1100
|
-
if (snapshot)
|
|
1125
|
+
if (snapshot) {
|
|
1126
|
+
const primaryGitDirectory = import_node_path.default.join(branchPath(projectRoot, input.primaryBranchName), ".git");
|
|
1127
|
+
persistReconciledProjectAndConsumeSnapshot(
|
|
1128
|
+
snapshot,
|
|
1129
|
+
void 0,
|
|
1130
|
+
snapshot.manifest.scope === "branches" ? [
|
|
1131
|
+
...snapshot.manifest.branches.map(({ branchName }) => branchPath(projectRoot, branchName)),
|
|
1132
|
+
// Branch refs and worktree registrations moved in the shared
|
|
1133
|
+
// administrative directory; objects are fsynced by git itself.
|
|
1134
|
+
import_node_path.default.join(primaryGitDirectory, "refs"),
|
|
1135
|
+
import_node_path.default.join(primaryGitDirectory, "packed-refs"),
|
|
1136
|
+
import_node_path.default.join(primaryGitDirectory, "worktrees")
|
|
1137
|
+
] : void 0
|
|
1138
|
+
);
|
|
1139
|
+
}
|
|
1101
1140
|
return states;
|
|
1102
1141
|
} catch (error) {
|
|
1103
1142
|
if (!snapshot) throw error;
|
|
@@ -1243,6 +1282,53 @@ function createOrRetryLinkedProjectBranch(input) {
|
|
|
1243
1282
|
}
|
|
1244
1283
|
return createLinkedProjectBranch(input);
|
|
1245
1284
|
}
|
|
1285
|
+
function classifyProjectCheckout(input) {
|
|
1286
|
+
const checkoutPath = branchPath(input.projectRoot, input.branchName);
|
|
1287
|
+
let stat;
|
|
1288
|
+
try {
|
|
1289
|
+
stat = import_node_fs.default.lstatSync(checkoutPath);
|
|
1290
|
+
} catch {
|
|
1291
|
+
return "missing";
|
|
1292
|
+
}
|
|
1293
|
+
if (!stat.isDirectory()) return "not_a_worktree";
|
|
1294
|
+
if (input.branchName === input.primaryBranchName) return gitDirectoryKind(checkoutPath) === "directory" ? "linked" : "not_a_worktree";
|
|
1295
|
+
if (gitDirectoryKind(checkoutPath) !== "file") return "not_a_worktree";
|
|
1296
|
+
const gitDirectory = perWorktreeGitDirectory(checkoutPath);
|
|
1297
|
+
if (!gitDirectory) return "not_a_worktree";
|
|
1298
|
+
let registry;
|
|
1299
|
+
let registered;
|
|
1300
|
+
try {
|
|
1301
|
+
registry = import_node_fs.default.realpathSync(import_node_path.default.join(branchPath(input.projectRoot, input.primaryBranchName), ".git", "worktrees"));
|
|
1302
|
+
registered = import_node_fs.default.realpathSync(gitDirectory);
|
|
1303
|
+
} catch {
|
|
1304
|
+
return "not_a_worktree";
|
|
1305
|
+
}
|
|
1306
|
+
const relative = import_node_path.default.relative(registry, registered);
|
|
1307
|
+
if (!relative || relative === ".." || relative.startsWith(`..${import_node_path.default.sep}`) || import_node_path.default.isAbsolute(relative)) return "not_a_worktree";
|
|
1308
|
+
return "linked";
|
|
1309
|
+
}
|
|
1310
|
+
function inventoryProjectCheckouts(input) {
|
|
1311
|
+
const states = input.branchNames.map((branchName) => ({
|
|
1312
|
+
branchName,
|
|
1313
|
+
checkoutPath: branchPath(input.projectRoot, branchName),
|
|
1314
|
+
state: classifyProjectCheckout({ ...input, branchName })
|
|
1315
|
+
}));
|
|
1316
|
+
const primaryLinked = states.some(({ branchName, state }) => branchName === input.primaryBranchName && state === "linked");
|
|
1317
|
+
const inventory = { linkedBranchNames: [], missingCheckouts: [] };
|
|
1318
|
+
for (const { branchName, checkoutPath, state } of states) {
|
|
1319
|
+
if (primaryLinked && state === "linked") inventory.linkedBranchNames.push(branchName);
|
|
1320
|
+
else inventory.missingCheckouts.push({ branchName, checkoutPath, reason: state === "linked" ? "not_a_worktree" : state });
|
|
1321
|
+
}
|
|
1322
|
+
return inventory;
|
|
1323
|
+
}
|
|
1324
|
+
const PROJECT_REMOVED_CHECKOUTS_DIRECTORY = ".r5d-removed";
|
|
1325
|
+
function moveProjectCheckoutAside(projectRoot, branchName, checkoutPath) {
|
|
1326
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
1327
|
+
const target = import_node_path.default.join(projectRoot, PROJECT_REMOVED_CHECKOUTS_DIRECTORY, ...`${branchName}-${timestamp}`.split("/"));
|
|
1328
|
+
import_node_fs.default.mkdirSync(import_node_path.default.dirname(target), { recursive: true });
|
|
1329
|
+
import_node_fs.default.renameSync(checkoutPath, target);
|
|
1330
|
+
return target;
|
|
1331
|
+
}
|
|
1246
1332
|
function deleteLinkedProjectBranch(input) {
|
|
1247
1333
|
(0, import_managed_paths.validateManagedBranchName)(input.primaryBranchName);
|
|
1248
1334
|
(0, import_managed_paths.validateManagedBranchName)(input.branchName);
|
|
@@ -1251,15 +1337,18 @@ function deleteLinkedProjectBranch(input) {
|
|
|
1251
1337
|
const checkoutPath = branchPath(input.projectRoot, input.branchName);
|
|
1252
1338
|
const primaryCommonDir = commonGitDirectory(primaryPath);
|
|
1253
1339
|
if (!primaryCommonDir) throw new Error("Primary project checkout is unavailable");
|
|
1340
|
+
let movedAsidePath;
|
|
1254
1341
|
if (import_node_fs.default.existsSync(checkoutPath)) {
|
|
1255
|
-
const checkoutCommonDir = commonGitDirectory(checkoutPath);
|
|
1342
|
+
const checkoutCommonDir = import_node_fs.default.lstatSync(checkoutPath).isDirectory() ? commonGitDirectory(checkoutPath) : null;
|
|
1256
1343
|
if (checkoutCommonDir !== primaryCommonDir) {
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1344
|
+
movedAsidePath = moveProjectCheckoutAside(input.projectRoot, input.branchName, checkoutPath);
|
|
1345
|
+
tryGit(primaryPath, ["worktree", "prune"]);
|
|
1346
|
+
} else {
|
|
1347
|
+
if (projectWorktreeOperationInProgress(checkoutPath)) {
|
|
1348
|
+
throw new Error(`Project branch ${input.branchName} has an in-progress Git operation`);
|
|
1349
|
+
}
|
|
1350
|
+
git(primaryPath, ["worktree", "remove", "--force", checkoutPath], `remove linked worktree ${input.branchName}`);
|
|
1261
1351
|
}
|
|
1262
|
-
git(primaryPath, ["worktree", "remove", "--force", checkoutPath], `remove linked worktree ${input.branchName}`);
|
|
1263
1352
|
} else {
|
|
1264
1353
|
tryGit(primaryPath, ["worktree", "prune"]);
|
|
1265
1354
|
}
|
|
@@ -1267,7 +1356,7 @@ function deleteLinkedProjectBranch(input) {
|
|
|
1267
1356
|
if (tryGit(primaryPath, ["show-ref", "--verify", "--quiet", branchRef])) {
|
|
1268
1357
|
git(primaryPath, ["branch", "-D", input.branchName], `delete project branch ${input.branchName}`);
|
|
1269
1358
|
}
|
|
1270
|
-
return { branchName: input.branchName };
|
|
1359
|
+
return { branchName: input.branchName, ...movedAsidePath ? { movedAsidePath } : {} };
|
|
1271
1360
|
}
|
|
1272
1361
|
function removeProjectWorktrees(input) {
|
|
1273
1362
|
const projectRoot = import_node_path.default.resolve(input.projectRoot);
|
|
@@ -1526,7 +1615,7 @@ const projectWorktreesTestHarness = {
|
|
|
1526
1615
|
commandArgs: gitCommandArgs,
|
|
1527
1616
|
configureRepository,
|
|
1528
1617
|
createPreparedSnapshot(input) {
|
|
1529
|
-
return { root: snapshotBranchTrees(input.projectRoot, input.branches, input.temporaryRoot).root };
|
|
1618
|
+
return { root: snapshotBranchTrees(input.projectRoot, input.branches, input.temporaryRoot, void 0, input.scope).root };
|
|
1530
1619
|
},
|
|
1531
1620
|
consumeSnapshot(snapshotRoot) {
|
|
1532
1621
|
const manifest = readSnapshotManifest(snapshotRoot);
|
|
@@ -1542,10 +1631,12 @@ const projectWorktreesTestHarness = {
|
|
|
1542
1631
|
};
|
|
1543
1632
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1544
1633
|
0 && (module.exports = {
|
|
1634
|
+
PROJECT_REMOVED_CHECKOUTS_DIRECTORY,
|
|
1545
1635
|
PROJECT_WORKTREE_SNAPSHOT_PREFIX,
|
|
1546
1636
|
ProjectBranchCreationRollbackIncompleteError,
|
|
1547
1637
|
applyObservedProjectMirrorHeads,
|
|
1548
1638
|
assertProjectMirrorHeadsPushed,
|
|
1639
|
+
classifyProjectCheckout,
|
|
1549
1640
|
computeLocalProjectMirrorRefsTokens,
|
|
1550
1641
|
createLinkedProjectBranch,
|
|
1551
1642
|
createOrRetryLinkedProjectBranch,
|
|
@@ -1554,6 +1645,7 @@ const projectWorktreesTestHarness = {
|
|
|
1554
1645
|
ensureProjectWorktrees,
|
|
1555
1646
|
fastForwardProjectHeadsFromMirror,
|
|
1556
1647
|
hasLinkedProjectWorktreeLayout,
|
|
1648
|
+
inventoryProjectCheckouts,
|
|
1557
1649
|
observeProjectMirrorHeads,
|
|
1558
1650
|
projectBranchMayExistAfterCreateFailure,
|
|
1559
1651
|
projectOriginBranchName,
|
package/dist/mjs/main.mjs
CHANGED
|
@@ -65,6 +65,7 @@ import {
|
|
|
65
65
|
deleteLinkedProjectBranch,
|
|
66
66
|
deleteProjectMirrorBranch,
|
|
67
67
|
ensureProjectWorktrees,
|
|
68
|
+
inventoryProjectCheckouts,
|
|
68
69
|
observeProjectMirrorHeads,
|
|
69
70
|
projectBranchMayExistAfterCreateFailure,
|
|
70
71
|
projectOriginBranchName,
|
|
@@ -3519,6 +3520,7 @@ async function startWorker(options) {
|
|
|
3519
3520
|
let terminalReplayTimer;
|
|
3520
3521
|
let workspaceSyncRequestsInFlight = 0;
|
|
3521
3522
|
let lastWorkspaceSyncMirrorObservationMs = 0;
|
|
3523
|
+
let lastInventoriedMissingCheckouts;
|
|
3522
3524
|
let sessionArtifactSyncRequestsInFlight = 0;
|
|
3523
3525
|
let cliUpdateInProgress = false;
|
|
3524
3526
|
let reloadAfterClose = false;
|
|
@@ -3576,8 +3578,15 @@ async function startWorker(options) {
|
|
|
3576
3578
|
const primaryBranchName = primaryProjectBranch(project);
|
|
3577
3579
|
const connection = projectConnection(project);
|
|
3578
3580
|
const sourcePath = configuredProjectBranchPath(projectsRoot, project, branchName);
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
+
const primaryPath = configuredProjectBranchPath(projectsRoot, project, primaryBranchName);
|
|
3582
|
+
if (requireLocalBranch || fs.existsSync(sourcePath) && hasProjectWorktree(primaryPath)) {
|
|
3583
|
+
const deleted = deleteLinkedProjectBranch({ projectRoot, primaryBranchName, branchName });
|
|
3584
|
+
if (deleted.movedAsidePath) {
|
|
3585
|
+
process.stderr.write(
|
|
3586
|
+
`[r5d-worker] set aside ${sourcePath}, which was not a linked worktree of ${project.projectPath}, at ${deleted.movedAsidePath}
|
|
3587
|
+
`
|
|
3588
|
+
);
|
|
3589
|
+
}
|
|
3581
3590
|
}
|
|
3582
3591
|
const planSourcePath = path.join(planRoot, project.projectId, ...branchName.split("/"));
|
|
3583
3592
|
fs.rmSync(planSourcePath, { recursive: true, force: true });
|
|
@@ -3650,11 +3659,17 @@ async function startWorker(options) {
|
|
|
3650
3659
|
if (branchName === primaryBranchName) {
|
|
3651
3660
|
throw new Error(`Cannot hide preserve-only project branch ${project.projectId}/${branchName} because it is the active anchor`);
|
|
3652
3661
|
}
|
|
3653
|
-
deleteLinkedProjectBranch({
|
|
3662
|
+
const deleted = deleteLinkedProjectBranch({
|
|
3654
3663
|
projectRoot: configuredProjectRoot(projectsRoot, project),
|
|
3655
3664
|
primaryBranchName,
|
|
3656
3665
|
branchName
|
|
3657
3666
|
});
|
|
3667
|
+
if (deleted.movedAsidePath) {
|
|
3668
|
+
process.stderr.write(
|
|
3669
|
+
`[r5d-worker] set aside ${branchPath}, which was not a linked worktree of ${project.projectPath}, at ${deleted.movedAsidePath}
|
|
3670
|
+
`
|
|
3671
|
+
);
|
|
3672
|
+
}
|
|
3658
3673
|
fs.rmSync(path.join(planRoot, project.projectId, ...branchName.split("/")), { recursive: true, force: true });
|
|
3659
3674
|
readyProjectIds.delete(project.projectId);
|
|
3660
3675
|
reconciledProjectConfigFingerprints.delete(project.projectId);
|
|
@@ -3823,6 +3838,40 @@ async function startWorker(options) {
|
|
|
3823
3838
|
});
|
|
3824
3839
|
};
|
|
3825
3840
|
const projectBranchMountBusy = (projectId, branchName, branchPath) => !readyProjectIds.has(projectId) || projectBranchMountActivityBusy(projectId, branchName, branchPath);
|
|
3841
|
+
const inventoryConfiguredProjectCheckouts = () => {
|
|
3842
|
+
const inventory = { projects: [], missingCheckouts: [] };
|
|
3843
|
+
for (const project of projectConfigById.values()) {
|
|
3844
|
+
if (project.executionDisabled || !readyProjectIds.has(project.projectId) || project.branches.length === 0) {
|
|
3845
|
+
inventory.projects.push(project);
|
|
3846
|
+
continue;
|
|
3847
|
+
}
|
|
3848
|
+
const checkouts = inventoryProjectCheckouts({
|
|
3849
|
+
projectRoot: configuredProjectRoot(projectsRoot, project),
|
|
3850
|
+
primaryBranchName: primaryProjectBranch(project),
|
|
3851
|
+
branchNames: project.branches.map(({ branchName }) => branchName)
|
|
3852
|
+
});
|
|
3853
|
+
if (checkouts.missingCheckouts.length === 0) {
|
|
3854
|
+
inventory.projects.push(project);
|
|
3855
|
+
continue;
|
|
3856
|
+
}
|
|
3857
|
+
for (const missing of checkouts.missingCheckouts) inventory.missingCheckouts.push({ projectId: project.projectId, ...missing });
|
|
3858
|
+
const linked = new Set(checkouts.linkedBranchNames);
|
|
3859
|
+
inventory.projects.push({ ...project, branches: project.branches.filter(({ branchName }) => linked.has(branchName)) });
|
|
3860
|
+
}
|
|
3861
|
+
inventory.missingCheckouts.sort(
|
|
3862
|
+
(left, right) => left.projectId.localeCompare(right.projectId) || left.branchName.localeCompare(right.branchName)
|
|
3863
|
+
);
|
|
3864
|
+
return inventory;
|
|
3865
|
+
};
|
|
3866
|
+
const reportedMissingCheckouts = (inventory) => inventory.missingCheckouts.map(({ projectId, branchName, reason }) => ({ projectId, branchName, reason }));
|
|
3867
|
+
const logMissingCheckouts = (inventory) => {
|
|
3868
|
+
if (inventory.missingCheckouts.length === 0) return;
|
|
3869
|
+
const described = inventory.missingCheckouts.map(({ checkoutPath, reason }) => `${checkoutPath} (${reason === "missing" ? "directory missing" : "not a linked worktree of the project"})`).join(", ");
|
|
3870
|
+
process.stderr.write(
|
|
3871
|
+
`[r5d-worker] skipping ${inventory.missingCheckouts.length} configured project checkout(s) this cycle: ${described}; delete the branch through the platform or reconnect this worker to recreate the checkout
|
|
3872
|
+
`
|
|
3873
|
+
);
|
|
3874
|
+
};
|
|
3826
3875
|
const buildWorkspaceMounts = (projects = projectConfigById.values()) => {
|
|
3827
3876
|
const mounts = [];
|
|
3828
3877
|
const creatorLocalIncarnations = new Map(
|
|
@@ -4101,9 +4150,9 @@ async function startWorker(options) {
|
|
|
4101
4150
|
(left, right) => left.projectId.localeCompare(right.projectId) || left.branchName.localeCompare(right.branchName)
|
|
4102
4151
|
);
|
|
4103
4152
|
};
|
|
4104
|
-
const captureProjectHeadsForWorkspacePublication = async () => {
|
|
4153
|
+
const captureProjectHeadsForWorkspacePublication = async (projects = projectConfigById.values()) => {
|
|
4105
4154
|
const heads = /* @__PURE__ */ new Map();
|
|
4106
|
-
for (const project of
|
|
4155
|
+
for (const project of projects) {
|
|
4107
4156
|
if (!readyProjectIds.has(project.projectId)) continue;
|
|
4108
4157
|
for (const branch of project.branches) {
|
|
4109
4158
|
const branchPath = configuredProjectBranchPath(projectsRoot, project, branch.branchName);
|
|
@@ -4113,9 +4162,9 @@ async function startWorker(options) {
|
|
|
4113
4162
|
}
|
|
4114
4163
|
return heads;
|
|
4115
4164
|
};
|
|
4116
|
-
const pushChangedProjectHeads = async (activeMountIds, publishedHead, publicationHeads, expectedMirrorHeads, suppressedMountIds = /* @__PURE__ */ new Set()) => {
|
|
4165
|
+
const pushChangedProjectHeads = async (activeMountIds, publishedHead, publicationHeads, expectedMirrorHeads, suppressedMountIds = /* @__PURE__ */ new Set(), projects = projectConfigById.values()) => {
|
|
4117
4166
|
const active = new Set(activeMountIds);
|
|
4118
|
-
for (const project of
|
|
4167
|
+
for (const project of projects) {
|
|
4119
4168
|
if (!readyProjectIds.has(project.projectId) || !repositoryMirrorWritesAllowed(project)) continue;
|
|
4120
4169
|
const changed = /* @__PURE__ */ new Map();
|
|
4121
4170
|
for (const branch of project.branches) {
|
|
@@ -4215,7 +4264,7 @@ async function startWorker(options) {
|
|
|
4215
4264
|
};
|
|
4216
4265
|
const observeProjectHeadsBeforeOuterWorkspace = async (input) => {
|
|
4217
4266
|
const observationStartedAt = Date.now();
|
|
4218
|
-
const eligibleProjects = [...projectConfigById.values()].filter(
|
|
4267
|
+
const eligibleProjects = [...input.projects ?? projectConfigById.values()].filter(
|
|
4219
4268
|
(project) => !project.executionDisabled && readyProjectIds.has(project.projectId) && project.branches.length > 0
|
|
4220
4269
|
);
|
|
4221
4270
|
try {
|
|
@@ -4325,7 +4374,7 @@ async function startWorker(options) {
|
|
|
4325
4374
|
}
|
|
4326
4375
|
}
|
|
4327
4376
|
};
|
|
4328
|
-
const mapWorkspaceGitResult = async (attemptId, trigger, result) => {
|
|
4377
|
+
const mapWorkspaceGitResult = async (attemptId, trigger, result, missingCheckouts) => {
|
|
4329
4378
|
const activeProjectBranchPublications = activeProjectBranchPublicationEvidence([...projectConfigById.values()], result.activeMountIds);
|
|
4330
4379
|
return {
|
|
4331
4380
|
type: "workspace_sync",
|
|
@@ -4343,6 +4392,7 @@ async function startWorker(options) {
|
|
|
4343
4392
|
affectedPaths: result.affectedPaths,
|
|
4344
4393
|
activeMountIds: result.activeMountIds,
|
|
4345
4394
|
skippedMountIds: result.skippedMountIds,
|
|
4395
|
+
...missingCheckouts ? { missingCheckouts } : {},
|
|
4346
4396
|
activeProjectBranchPublications,
|
|
4347
4397
|
discardedPaths: [],
|
|
4348
4398
|
localChangesDiscarded: false,
|
|
@@ -4370,6 +4420,7 @@ async function startWorker(options) {
|
|
|
4370
4420
|
affectedPaths: [],
|
|
4371
4421
|
discardedPaths: [],
|
|
4372
4422
|
localChangesDiscarded: false,
|
|
4423
|
+
...lastInventoriedMissingCheckouts ? { missingCheckouts: lastInventoriedMissingCheckouts } : {},
|
|
4373
4424
|
error: error instanceof Error ? error.message : String(error)
|
|
4374
4425
|
};
|
|
4375
4426
|
};
|
|
@@ -4378,7 +4429,11 @@ async function startWorker(options) {
|
|
|
4378
4429
|
if (!workspaceRemoteUrl || !workspaceCredentialHelper || !workspaceCredentialUsername || !workspaceGitIdentity) {
|
|
4379
4430
|
throw new Error("Worker workspace configuration has not been received");
|
|
4380
4431
|
}
|
|
4381
|
-
const
|
|
4432
|
+
const checkoutInventory = inventoryConfiguredProjectCheckouts();
|
|
4433
|
+
logMissingCheckouts(checkoutInventory);
|
|
4434
|
+
const missingCheckouts = reportedMissingCheckouts(checkoutInventory);
|
|
4435
|
+
lastInventoriedMissingCheckouts = missingCheckouts;
|
|
4436
|
+
const configuredMounts = buildWorkspaceMounts(checkoutInventory.projects);
|
|
4382
4437
|
if (input.resetToCanonical) {
|
|
4383
4438
|
workspaceProjectionLedger.reset();
|
|
4384
4439
|
const resetHasBusyLiveMount = configuredMounts.some(
|
|
@@ -4389,7 +4444,8 @@ async function startWorker(options) {
|
|
|
4389
4444
|
}
|
|
4390
4445
|
const observedProjectHeads2 = await observeProjectHeadsBeforeOuterWorkspace({
|
|
4391
4446
|
allowNonFastForward: true,
|
|
4392
|
-
failClosed: true
|
|
4447
|
+
failClosed: true,
|
|
4448
|
+
projects: checkoutInventory.projects
|
|
4393
4449
|
});
|
|
4394
4450
|
const reset = await resetWorkspaceGit({
|
|
4395
4451
|
workspacePath: workspaceShadowRoot,
|
|
@@ -4417,6 +4473,7 @@ async function startWorker(options) {
|
|
|
4417
4473
|
affectedPaths: reset.discardedPaths,
|
|
4418
4474
|
activeMountIds: reset.activeMountIds,
|
|
4419
4475
|
skippedMountIds: reset.skippedMountIds,
|
|
4476
|
+
missingCheckouts,
|
|
4420
4477
|
discardedPaths: reset.discardedPaths,
|
|
4421
4478
|
localChangesDiscarded: reset.discardedPaths.length > 0 || reset.startingHead !== reset.remoteHead
|
|
4422
4479
|
};
|
|
@@ -4424,7 +4481,7 @@ async function startWorker(options) {
|
|
|
4424
4481
|
const outerRemediation = input.trigger.type === "remediation" || input.trigger.type === "remediation_confirm";
|
|
4425
4482
|
const mounts = outerRemediation ? workspaceGitMountsForRemediation(configuredMounts) : configuredMounts;
|
|
4426
4483
|
if (outerRemediation) workspaceProjectionLedger.reset();
|
|
4427
|
-
const observedProjectHeads = outerRemediation ? [] : await observeProjectHeadsBeforeOuterWorkspace({ allowNonFastForward: false, failClosed: false });
|
|
4484
|
+
const observedProjectHeads = outerRemediation ? [] : await observeProjectHeadsBeforeOuterWorkspace({ allowNonFastForward: false, failClosed: false, projects: checkoutInventory.projects });
|
|
4428
4485
|
const expectedMirrorHeads = /* @__PURE__ */ new Map();
|
|
4429
4486
|
for (const bundle of observedProjectHeads) {
|
|
4430
4487
|
for (const observation of bundle.observations) {
|
|
@@ -4436,7 +4493,7 @@ async function startWorker(options) {
|
|
|
4436
4493
|
({ projectId, observations }) => observations.filter(({ shouldMove }) => shouldMove).map(({ branchName }) => projectMountId(projectId, branchName))
|
|
4437
4494
|
)
|
|
4438
4495
|
);
|
|
4439
|
-
const publicationHeads = outerRemediation ? /* @__PURE__ */ new Map() : await captureProjectHeadsForWorkspacePublication();
|
|
4496
|
+
const publicationHeads = outerRemediation ? /* @__PURE__ */ new Map() : await captureProjectHeadsForWorkspacePublication(checkoutInventory.projects);
|
|
4440
4497
|
const result = await synchronizeWorkspaceGit({
|
|
4441
4498
|
attemptId: input.attemptId,
|
|
4442
4499
|
workerLabel: label,
|
|
@@ -4456,7 +4513,14 @@ async function startWorker(options) {
|
|
|
4456
4513
|
advertisedRemoteHead: projectMirrorFetchSkippingEnabled && !outerRemediation ? advertisedWorkspaceHead : void 0,
|
|
4457
4514
|
assertStillAdmitted: input.assertStillAdmitted,
|
|
4458
4515
|
afterWorkspacePublished: outerRemediation ? void 0 : async ({ activeMountIds, publishedHead }) => {
|
|
4459
|
-
await pushChangedProjectHeads(
|
|
4516
|
+
await pushChangedProjectHeads(
|
|
4517
|
+
activeMountIds,
|
|
4518
|
+
publishedHead,
|
|
4519
|
+
publicationHeads,
|
|
4520
|
+
expectedMirrorHeads,
|
|
4521
|
+
inboundMoveMountIds,
|
|
4522
|
+
checkoutInventory.projects
|
|
4523
|
+
);
|
|
4460
4524
|
},
|
|
4461
4525
|
hydrationHooks: workspaceHydrationTransactionHooks
|
|
4462
4526
|
});
|
|
@@ -4475,7 +4539,7 @@ async function startWorker(options) {
|
|
|
4475
4539
|
result.localHead ?? result.remoteHead
|
|
4476
4540
|
);
|
|
4477
4541
|
}
|
|
4478
|
-
const mapped = await mapWorkspaceGitResult(input.attemptId, input.trigger, result);
|
|
4542
|
+
const mapped = await mapWorkspaceGitResult(input.attemptId, input.trigger, result, missingCheckouts);
|
|
4479
4543
|
if (!outerRemediation) {
|
|
4480
4544
|
workspaceProjectionLedger.recordCycle({ result: mapped, mounts, projectedMutationTokens: result.projectedMutationTokens });
|
|
4481
4545
|
}
|
package/dist/mjs/package.json
CHANGED
|
@@ -287,6 +287,8 @@ function normalizeSnapshotManifest(value) {
|
|
|
287
287
|
if (record.state !== "building" && record.state !== "prepared" && record.state !== "restoring" && record.state !== "consumed") {
|
|
288
288
|
throw new Error("Project worktree snapshot manifest has an invalid state");
|
|
289
289
|
}
|
|
290
|
+
const scope = record.scope === void 0 ? "project" : record.scope;
|
|
291
|
+
if (scope !== "project" && scope !== "branches") throw new Error("Project worktree snapshot manifest has an invalid scope");
|
|
290
292
|
if (!Number.isSafeInteger(record.ownerProcessId) || record.ownerProcessId <= 0) {
|
|
291
293
|
throw new Error("Project worktree snapshot manifest has an invalid owner process");
|
|
292
294
|
}
|
|
@@ -322,6 +324,7 @@ function normalizeSnapshotManifest(value) {
|
|
|
322
324
|
return {
|
|
323
325
|
version: PROJECT_WORKTREE_SNAPSHOT_MANIFEST_VERSION,
|
|
324
326
|
state: record.state,
|
|
327
|
+
scope,
|
|
325
328
|
ownerProcessId: record.ownerProcessId,
|
|
326
329
|
ownerSessionId: record.ownerSessionId,
|
|
327
330
|
projectRoot: record.projectRoot,
|
|
@@ -358,7 +361,7 @@ function writeSnapshotManifest(snapshotRoot, manifest) {
|
|
|
358
361
|
return readSnapshotManifest(snapshotRoot);
|
|
359
362
|
}
|
|
360
363
|
function assertSameSnapshotIdentity(expected, actual) {
|
|
361
|
-
if (expected.ownerProcessId !== actual.ownerProcessId || expected.ownerSessionId !== actual.ownerSessionId || expected.projectRoot !== actual.projectRoot || expected.createdAt !== actual.createdAt || JSON.stringify(expected.branches) !== JSON.stringify(actual.branches)) {
|
|
364
|
+
if (expected.scope !== actual.scope || expected.ownerProcessId !== actual.ownerProcessId || expected.ownerSessionId !== actual.ownerSessionId || expected.projectRoot !== actual.projectRoot || expected.createdAt !== actual.createdAt || JSON.stringify(expected.branches) !== JSON.stringify(actual.branches)) {
|
|
362
365
|
throw new Error("Project worktree snapshot identity changed");
|
|
363
366
|
}
|
|
364
367
|
}
|
|
@@ -425,7 +428,7 @@ function assertNoOutstandingSnapshotForProject(temporaryRoot, projectRoot) {
|
|
|
425
428
|
}
|
|
426
429
|
}
|
|
427
430
|
}
|
|
428
|
-
function snapshotBranchTrees(projectRoot, branches, temporaryRoot, onProgress) {
|
|
431
|
+
function snapshotBranchTrees(projectRoot, branches, temporaryRoot, onProgress, scope = "project") {
|
|
429
432
|
const resolvedProjectRoot = path.resolve(projectRoot);
|
|
430
433
|
const resolvedTemporaryRoot = path.resolve(temporaryRoot ?? os.tmpdir());
|
|
431
434
|
assertNoOutstandingSnapshotForProject(resolvedTemporaryRoot, resolvedProjectRoot);
|
|
@@ -446,6 +449,7 @@ function snapshotBranchTrees(projectRoot, branches, temporaryRoot, onProgress) {
|
|
|
446
449
|
let manifest = writeSnapshotManifest(root, {
|
|
447
450
|
version: PROJECT_WORKTREE_SNAPSHOT_MANIFEST_VERSION,
|
|
448
451
|
state: "building",
|
|
452
|
+
scope,
|
|
449
453
|
ownerProcessId: process.pid,
|
|
450
454
|
ownerSessionId: PROJECT_WORKTREE_SNAPSHOT_OWNER_SESSION_ID,
|
|
451
455
|
projectRoot: resolvedProjectRoot,
|
|
@@ -552,7 +556,7 @@ function restoreProjectWorktreeSnapshot(snapshotRoot, manifest, onProgress) {
|
|
|
552
556
|
const presentBranches = current.branches.filter(({ snapshotPresent }) => snapshotPresent);
|
|
553
557
|
const destinationParent = path.dirname(current.projectRoot);
|
|
554
558
|
fs.mkdirSync(destinationParent, { recursive: true });
|
|
555
|
-
const canMoveWholeSnapshot = presentBranches.length > 0 && (current.state === "restoring" || treesRootStat !== null && treesRootStat.dev === fs.lstatSync(destinationParent).dev);
|
|
559
|
+
const canMoveWholeSnapshot = current.scope === "project" && presentBranches.length > 0 && (current.state === "restoring" || treesRootStat !== null && treesRootStat.dev === fs.lstatSync(destinationParent).dev);
|
|
556
560
|
if (canMoveWholeSnapshot) {
|
|
557
561
|
if (current.state === "prepared") current = advanceSnapshotState(snapshotRoot, current, "restoring");
|
|
558
562
|
onProgress?.({
|
|
@@ -643,8 +647,12 @@ function consumeAndRemoveProjectWorktreeSnapshot(snapshotRoot, manifest) {
|
|
|
643
647
|
} catch {
|
|
644
648
|
}
|
|
645
649
|
}
|
|
646
|
-
function persistReconciledProjectAndConsumeSnapshot(snapshot, afterProjectTreeFsync) {
|
|
647
|
-
|
|
650
|
+
function persistReconciledProjectAndConsumeSnapshot(snapshot, afterProjectTreeFsync, durabilityPaths) {
|
|
651
|
+
if (durabilityPaths) {
|
|
652
|
+
for (const durabilityPath of durabilityPaths) if (fs.existsSync(durabilityPath)) fsyncTree(durabilityPath);
|
|
653
|
+
} else {
|
|
654
|
+
fsyncTree(snapshot.manifest.projectRoot);
|
|
655
|
+
}
|
|
648
656
|
fsyncManagedProjectRootEntry(snapshot.manifest.projectRoot);
|
|
649
657
|
afterProjectTreeFsync?.();
|
|
650
658
|
consumeAndRemoveProjectWorktreeSnapshot(snapshot.root, snapshot.manifest);
|
|
@@ -942,8 +950,10 @@ async function reconcileLinkedLayout(input) {
|
|
|
942
950
|
await configureRepository({ ...input, primaryPath });
|
|
943
951
|
await tryGitAsync(primaryPath, ["worktree", "prune"]);
|
|
944
952
|
await fetchProjectHeads({ ...input, primaryPath });
|
|
953
|
+
const primaryCommonDir = await commonGitDirectoryAsync(primaryPath);
|
|
954
|
+
if (!primaryCommonDir) throw new Error("Primary project checkout is unavailable");
|
|
945
955
|
const mirrorHeads = /* @__PURE__ */ new Map();
|
|
946
|
-
const
|
|
956
|
+
const plan = [];
|
|
947
957
|
for (const branch of input.branches) {
|
|
948
958
|
const checkoutPath = branchPath(input.projectRoot, branch.branchName);
|
|
949
959
|
const resolved = await seedForBranch({
|
|
@@ -953,24 +963,30 @@ async function reconcileLinkedLayout(input) {
|
|
|
953
963
|
requireMirrorHead: input.exactMirrorBranches?.has(branch.branchName)
|
|
954
964
|
});
|
|
955
965
|
mirrorHeads.set(branch.branchName, resolved.mirrorHead);
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
input.beforeDestructiveMutation();
|
|
960
|
-
mutatedBranches.add(branch.branchName);
|
|
961
|
-
await preserveHead(checkoutPath);
|
|
962
|
-
await gitAsync(checkoutPath, ["reset", "--hard", resolved.seed], `anchor linked worktree ${branch.branchName}`);
|
|
966
|
+
const linked = branch.branchName === input.primaryBranchName || gitDirectoryKind(checkoutPath) !== null && await commonGitDirectoryAsync(checkoutPath) === primaryCommonDir;
|
|
967
|
+
if (!linked) {
|
|
968
|
+
plan.push({ branchName: branch.branchName, checkoutPath, seed: resolved.seed, action: "recreate" });
|
|
963
969
|
continue;
|
|
964
970
|
}
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
971
|
+
const currentHead = await gitAsync(checkoutPath, ["rev-parse", "HEAD"], `resolve linked worktree head ${branch.branchName}`);
|
|
972
|
+
plan.push({ branchName: branch.branchName, checkoutPath, seed: resolved.seed, action: currentHead === resolved.seed ? "keep" : "reset" });
|
|
973
|
+
}
|
|
974
|
+
const mutatedBranches = new Set(plan.filter(({ action }) => action !== "keep").map(({ branchName }) => branchName));
|
|
975
|
+
if (mutatedBranches.size > 0) input.beforeDestructiveMutation(mutatedBranches);
|
|
976
|
+
for (const entry of plan) {
|
|
977
|
+
if (entry.action === "keep") continue;
|
|
978
|
+
if (entry.action === "reset") {
|
|
979
|
+
await preserveHead(entry.checkoutPath);
|
|
980
|
+
await gitAsync(entry.checkoutPath, ["reset", "--hard", entry.seed], `anchor linked worktree ${entry.branchName}`);
|
|
981
|
+
continue;
|
|
982
|
+
}
|
|
983
|
+
fs.rmSync(entry.checkoutPath, { recursive: true, force: true });
|
|
984
|
+
await gitAsync(primaryPath, ["branch", "-f", entry.branchName, entry.seed], `anchor project branch ${entry.branchName}`);
|
|
985
|
+
fs.mkdirSync(path.dirname(entry.checkoutPath), { recursive: true });
|
|
970
986
|
await gitAsync(
|
|
971
987
|
primaryPath,
|
|
972
|
-
["worktree", "add", "--force", checkoutPath,
|
|
973
|
-
`create linked worktree ${
|
|
988
|
+
["worktree", "add", "--force", entry.checkoutPath, entry.branchName],
|
|
989
|
+
`create linked worktree ${entry.branchName}`
|
|
974
990
|
);
|
|
975
991
|
}
|
|
976
992
|
return { mirrorHeads, mutatedBranches };
|
|
@@ -997,8 +1013,14 @@ async function ensureProjectWorktrees(input) {
|
|
|
997
1013
|
const snapshotStorageRoot = path.resolve(input.snapshotRoot ?? os.tmpdir());
|
|
998
1014
|
assertNoOutstandingSnapshotForProject(snapshotStorageRoot, projectRoot);
|
|
999
1015
|
let snapshot;
|
|
1000
|
-
const ensureSnapshot = () => {
|
|
1001
|
-
snapshot ??= snapshotBranchTrees(
|
|
1016
|
+
const ensureSnapshot = (branchNames) => {
|
|
1017
|
+
snapshot ??= snapshotBranchTrees(
|
|
1018
|
+
projectRoot,
|
|
1019
|
+
branchNames ? input.branches.filter(({ branchName }) => branchNames.has(branchName)) : input.branches,
|
|
1020
|
+
snapshotStorageRoot,
|
|
1021
|
+
input.onSnapshotProgress,
|
|
1022
|
+
branchNames ? "branches" : "project"
|
|
1023
|
+
);
|
|
1002
1024
|
return snapshot;
|
|
1003
1025
|
};
|
|
1004
1026
|
try {
|
|
@@ -1050,7 +1072,21 @@ async function ensureProjectWorktrees(input) {
|
|
|
1050
1072
|
});
|
|
1051
1073
|
}
|
|
1052
1074
|
states.sort((left, right) => left.branchName.localeCompare(right.branchName));
|
|
1053
|
-
if (snapshot)
|
|
1075
|
+
if (snapshot) {
|
|
1076
|
+
const primaryGitDirectory = path.join(branchPath(projectRoot, input.primaryBranchName), ".git");
|
|
1077
|
+
persistReconciledProjectAndConsumeSnapshot(
|
|
1078
|
+
snapshot,
|
|
1079
|
+
void 0,
|
|
1080
|
+
snapshot.manifest.scope === "branches" ? [
|
|
1081
|
+
...snapshot.manifest.branches.map(({ branchName }) => branchPath(projectRoot, branchName)),
|
|
1082
|
+
// Branch refs and worktree registrations moved in the shared
|
|
1083
|
+
// administrative directory; objects are fsynced by git itself.
|
|
1084
|
+
path.join(primaryGitDirectory, "refs"),
|
|
1085
|
+
path.join(primaryGitDirectory, "packed-refs"),
|
|
1086
|
+
path.join(primaryGitDirectory, "worktrees")
|
|
1087
|
+
] : void 0
|
|
1088
|
+
);
|
|
1089
|
+
}
|
|
1054
1090
|
return states;
|
|
1055
1091
|
} catch (error) {
|
|
1056
1092
|
if (!snapshot) throw error;
|
|
@@ -1196,6 +1232,53 @@ function createOrRetryLinkedProjectBranch(input) {
|
|
|
1196
1232
|
}
|
|
1197
1233
|
return createLinkedProjectBranch(input);
|
|
1198
1234
|
}
|
|
1235
|
+
function classifyProjectCheckout(input) {
|
|
1236
|
+
const checkoutPath = branchPath(input.projectRoot, input.branchName);
|
|
1237
|
+
let stat;
|
|
1238
|
+
try {
|
|
1239
|
+
stat = fs.lstatSync(checkoutPath);
|
|
1240
|
+
} catch {
|
|
1241
|
+
return "missing";
|
|
1242
|
+
}
|
|
1243
|
+
if (!stat.isDirectory()) return "not_a_worktree";
|
|
1244
|
+
if (input.branchName === input.primaryBranchName) return gitDirectoryKind(checkoutPath) === "directory" ? "linked" : "not_a_worktree";
|
|
1245
|
+
if (gitDirectoryKind(checkoutPath) !== "file") return "not_a_worktree";
|
|
1246
|
+
const gitDirectory = perWorktreeGitDirectory(checkoutPath);
|
|
1247
|
+
if (!gitDirectory) return "not_a_worktree";
|
|
1248
|
+
let registry;
|
|
1249
|
+
let registered;
|
|
1250
|
+
try {
|
|
1251
|
+
registry = fs.realpathSync(path.join(branchPath(input.projectRoot, input.primaryBranchName), ".git", "worktrees"));
|
|
1252
|
+
registered = fs.realpathSync(gitDirectory);
|
|
1253
|
+
} catch {
|
|
1254
|
+
return "not_a_worktree";
|
|
1255
|
+
}
|
|
1256
|
+
const relative = path.relative(registry, registered);
|
|
1257
|
+
if (!relative || relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) return "not_a_worktree";
|
|
1258
|
+
return "linked";
|
|
1259
|
+
}
|
|
1260
|
+
function inventoryProjectCheckouts(input) {
|
|
1261
|
+
const states = input.branchNames.map((branchName) => ({
|
|
1262
|
+
branchName,
|
|
1263
|
+
checkoutPath: branchPath(input.projectRoot, branchName),
|
|
1264
|
+
state: classifyProjectCheckout({ ...input, branchName })
|
|
1265
|
+
}));
|
|
1266
|
+
const primaryLinked = states.some(({ branchName, state }) => branchName === input.primaryBranchName && state === "linked");
|
|
1267
|
+
const inventory = { linkedBranchNames: [], missingCheckouts: [] };
|
|
1268
|
+
for (const { branchName, checkoutPath, state } of states) {
|
|
1269
|
+
if (primaryLinked && state === "linked") inventory.linkedBranchNames.push(branchName);
|
|
1270
|
+
else inventory.missingCheckouts.push({ branchName, checkoutPath, reason: state === "linked" ? "not_a_worktree" : state });
|
|
1271
|
+
}
|
|
1272
|
+
return inventory;
|
|
1273
|
+
}
|
|
1274
|
+
const PROJECT_REMOVED_CHECKOUTS_DIRECTORY = ".r5d-removed";
|
|
1275
|
+
function moveProjectCheckoutAside(projectRoot, branchName, checkoutPath) {
|
|
1276
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
1277
|
+
const target = path.join(projectRoot, PROJECT_REMOVED_CHECKOUTS_DIRECTORY, ...`${branchName}-${timestamp}`.split("/"));
|
|
1278
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
1279
|
+
fs.renameSync(checkoutPath, target);
|
|
1280
|
+
return target;
|
|
1281
|
+
}
|
|
1199
1282
|
function deleteLinkedProjectBranch(input) {
|
|
1200
1283
|
validateManagedBranchName(input.primaryBranchName);
|
|
1201
1284
|
validateManagedBranchName(input.branchName);
|
|
@@ -1204,15 +1287,18 @@ function deleteLinkedProjectBranch(input) {
|
|
|
1204
1287
|
const checkoutPath = branchPath(input.projectRoot, input.branchName);
|
|
1205
1288
|
const primaryCommonDir = commonGitDirectory(primaryPath);
|
|
1206
1289
|
if (!primaryCommonDir) throw new Error("Primary project checkout is unavailable");
|
|
1290
|
+
let movedAsidePath;
|
|
1207
1291
|
if (fs.existsSync(checkoutPath)) {
|
|
1208
|
-
const checkoutCommonDir = commonGitDirectory(checkoutPath);
|
|
1292
|
+
const checkoutCommonDir = fs.lstatSync(checkoutPath).isDirectory() ? commonGitDirectory(checkoutPath) : null;
|
|
1209
1293
|
if (checkoutCommonDir !== primaryCommonDir) {
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1294
|
+
movedAsidePath = moveProjectCheckoutAside(input.projectRoot, input.branchName, checkoutPath);
|
|
1295
|
+
tryGit(primaryPath, ["worktree", "prune"]);
|
|
1296
|
+
} else {
|
|
1297
|
+
if (projectWorktreeOperationInProgress(checkoutPath)) {
|
|
1298
|
+
throw new Error(`Project branch ${input.branchName} has an in-progress Git operation`);
|
|
1299
|
+
}
|
|
1300
|
+
git(primaryPath, ["worktree", "remove", "--force", checkoutPath], `remove linked worktree ${input.branchName}`);
|
|
1214
1301
|
}
|
|
1215
|
-
git(primaryPath, ["worktree", "remove", "--force", checkoutPath], `remove linked worktree ${input.branchName}`);
|
|
1216
1302
|
} else {
|
|
1217
1303
|
tryGit(primaryPath, ["worktree", "prune"]);
|
|
1218
1304
|
}
|
|
@@ -1220,7 +1306,7 @@ function deleteLinkedProjectBranch(input) {
|
|
|
1220
1306
|
if (tryGit(primaryPath, ["show-ref", "--verify", "--quiet", branchRef])) {
|
|
1221
1307
|
git(primaryPath, ["branch", "-D", input.branchName], `delete project branch ${input.branchName}`);
|
|
1222
1308
|
}
|
|
1223
|
-
return { branchName: input.branchName };
|
|
1309
|
+
return { branchName: input.branchName, ...movedAsidePath ? { movedAsidePath } : {} };
|
|
1224
1310
|
}
|
|
1225
1311
|
function removeProjectWorktrees(input) {
|
|
1226
1312
|
const projectRoot = path.resolve(input.projectRoot);
|
|
@@ -1479,7 +1565,7 @@ const projectWorktreesTestHarness = {
|
|
|
1479
1565
|
commandArgs: gitCommandArgs,
|
|
1480
1566
|
configureRepository,
|
|
1481
1567
|
createPreparedSnapshot(input) {
|
|
1482
|
-
return { root: snapshotBranchTrees(input.projectRoot, input.branches, input.temporaryRoot).root };
|
|
1568
|
+
return { root: snapshotBranchTrees(input.projectRoot, input.branches, input.temporaryRoot, void 0, input.scope).root };
|
|
1483
1569
|
},
|
|
1484
1570
|
consumeSnapshot(snapshotRoot) {
|
|
1485
1571
|
const manifest = readSnapshotManifest(snapshotRoot);
|
|
@@ -1494,10 +1580,12 @@ const projectWorktreesTestHarness = {
|
|
|
1494
1580
|
}
|
|
1495
1581
|
};
|
|
1496
1582
|
export {
|
|
1583
|
+
PROJECT_REMOVED_CHECKOUTS_DIRECTORY,
|
|
1497
1584
|
PROJECT_WORKTREE_SNAPSHOT_PREFIX,
|
|
1498
1585
|
ProjectBranchCreationRollbackIncompleteError,
|
|
1499
1586
|
applyObservedProjectMirrorHeads,
|
|
1500
1587
|
assertProjectMirrorHeadsPushed,
|
|
1588
|
+
classifyProjectCheckout,
|
|
1501
1589
|
computeLocalProjectMirrorRefsTokens,
|
|
1502
1590
|
createLinkedProjectBranch,
|
|
1503
1591
|
createOrRetryLinkedProjectBranch,
|
|
@@ -1506,6 +1594,7 @@ export {
|
|
|
1506
1594
|
ensureProjectWorktrees,
|
|
1507
1595
|
fastForwardProjectHeadsFromMirror,
|
|
1508
1596
|
hasLinkedProjectWorktreeLayout,
|
|
1597
|
+
inventoryProjectCheckouts,
|
|
1509
1598
|
observeProjectMirrorHeads,
|
|
1510
1599
|
projectBranchMayExistAfterCreateFailure,
|
|
1511
1600
|
projectOriginBranchName,
|
package/dist/types/main.d.ts
CHANGED
|
@@ -93,6 +93,12 @@ type WorkspaceSyncResult = {
|
|
|
93
93
|
affectedPaths: string[];
|
|
94
94
|
activeMountIds?: string[];
|
|
95
95
|
skippedMountIds?: string[];
|
|
96
|
+
/** Configured checkouts this cycle excluded because their directory is absent or is not a linked worktree of the project. */
|
|
97
|
+
missingCheckouts?: Array<{
|
|
98
|
+
projectId: string;
|
|
99
|
+
branchName: string;
|
|
100
|
+
reason: "missing" | "not_a_worktree";
|
|
101
|
+
}>;
|
|
96
102
|
activeProjectBranchPublications?: Array<{
|
|
97
103
|
branchId: string;
|
|
98
104
|
projectId: string;
|
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
import { type ProjectMirrorMount, type ProjectMirrorRefsTokens } from "./project-mirror-refs-token";
|
|
2
2
|
export declare const PROJECT_WORKTREE_SNAPSHOT_PREFIX = "r5d-project-worktrees-";
|
|
3
3
|
declare const PROJECT_WORKTREE_SNAPSHOT_MANIFEST_VERSION: 1;
|
|
4
|
+
/**
|
|
5
|
+
* `project` snapshots cover every configured branch and restore by replacing
|
|
6
|
+
* the whole project root. `branches` snapshots cover only the checkouts one
|
|
7
|
+
* reconciliation rewrites and restore those paths individually, so a rollback
|
|
8
|
+
* never touches a checkout the transaction did not.
|
|
9
|
+
*/
|
|
10
|
+
type ProjectWorktreeSnapshotScope = "project" | "branches";
|
|
4
11
|
type ProjectWorktreeSnapshotManifest = {
|
|
5
12
|
version: typeof PROJECT_WORKTREE_SNAPSHOT_MANIFEST_VERSION;
|
|
6
13
|
state: "building" | "prepared" | "restoring" | "consumed";
|
|
14
|
+
scope: ProjectWorktreeSnapshotScope;
|
|
7
15
|
ownerProcessId: number;
|
|
8
16
|
ownerSessionId: string;
|
|
9
17
|
projectRoot: string;
|
|
@@ -157,12 +165,50 @@ export declare function createOrRetryLinkedProjectBranch(input: {
|
|
|
157
165
|
branchPath: string;
|
|
158
166
|
baseCommitHash: string;
|
|
159
167
|
};
|
|
168
|
+
export type ProjectCheckoutState = "linked" | "missing" | "not_a_worktree";
|
|
169
|
+
export type ProjectCheckoutInventory = {
|
|
170
|
+
/** Branches whose checkout is this project's primary clone or one of its linked worktrees. */
|
|
171
|
+
linkedBranchNames: string[];
|
|
172
|
+
missingCheckouts: Array<{
|
|
173
|
+
branchName: string;
|
|
174
|
+
checkoutPath: string;
|
|
175
|
+
reason: Exclude<ProjectCheckoutState, "linked">;
|
|
176
|
+
}>;
|
|
177
|
+
};
|
|
178
|
+
/**
|
|
179
|
+
* Classify one configured branch checkout without spawning git, cheap enough
|
|
180
|
+
* to run for every configured branch on every sync cycle. The primary
|
|
181
|
+
* checkout is `linked` when its `.git` is a directory; another branch is
|
|
182
|
+
* `linked` when its `.git` pointer file resolves into that primary clone's
|
|
183
|
+
* `.git/worktrees` registry and the registered directory still exists.
|
|
184
|
+
* `missing` means the checkout path is absent. Everything else (a hydrated
|
|
185
|
+
* directory that never became a checkout, a foreign clone, a pointer to a
|
|
186
|
+
* pruned registration) is `not_a_worktree`.
|
|
187
|
+
*/
|
|
188
|
+
export declare function classifyProjectCheckout(input: {
|
|
189
|
+
projectRoot: string;
|
|
190
|
+
primaryBranchName: string;
|
|
191
|
+
branchName: string;
|
|
192
|
+
}): ProjectCheckoutState;
|
|
193
|
+
/**
|
|
194
|
+
* Partition a ready project's configured branches into checkouts a sync cycle
|
|
195
|
+
* may use and checkouts it must skip. A primary clone that is not linked takes
|
|
196
|
+
* every branch with it: nothing can be observed or published through it.
|
|
197
|
+
*/
|
|
198
|
+
export declare function inventoryProjectCheckouts(input: {
|
|
199
|
+
projectRoot: string;
|
|
200
|
+
primaryBranchName: string;
|
|
201
|
+
branchNames: readonly string[];
|
|
202
|
+
}): ProjectCheckoutInventory;
|
|
203
|
+
export declare const PROJECT_REMOVED_CHECKOUTS_DIRECTORY = ".r5d-removed";
|
|
160
204
|
export declare function deleteLinkedProjectBranch(input: {
|
|
161
205
|
projectRoot: string;
|
|
162
206
|
primaryBranchName: string;
|
|
163
207
|
branchName: string;
|
|
164
208
|
}): {
|
|
165
209
|
branchName: string;
|
|
210
|
+
/** Where a path that was not this project's linked worktree was set aside instead of being deleted. */
|
|
211
|
+
movedAsidePath?: string;
|
|
166
212
|
};
|
|
167
213
|
export declare function removeProjectWorktrees(input: {
|
|
168
214
|
projectRoot: string;
|
|
@@ -290,6 +336,7 @@ export declare const projectWorktreesTestHarness: {
|
|
|
290
336
|
projectRoot: string;
|
|
291
337
|
branches: readonly ProjectWorktreeBranch[];
|
|
292
338
|
temporaryRoot: string;
|
|
339
|
+
scope?: ProjectWorktreeSnapshotScope;
|
|
293
340
|
}): {
|
|
294
341
|
root: string;
|
|
295
342
|
};
|