@ricsam/r5d-worker 0.0.103 → 0.0.105
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 +106 -30
- package/dist/cjs/package.json +1 -1
- package/dist/cjs/project-worktrees.cjs +126 -32
- package/dist/mjs/main.mjs +107 -30
- package/dist/mjs/package.json +1 -1
- package/dist/mjs/project-worktrees.mjs +123 -32
- 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(
|
|
@@ -3938,6 +3986,7 @@ async function startWorker(options) {
|
|
|
3938
3986
|
)
|
|
3939
3987
|
].sort();
|
|
3940
3988
|
const ensureConfiguredProjects = async (onProgress) => {
|
|
3989
|
+
const reconciledProjectIds = /* @__PURE__ */ new Set();
|
|
3941
3990
|
const projects = [...projectConfigById.values()];
|
|
3942
3991
|
for (const [projectIndex, project] of projects.entries()) {
|
|
3943
3992
|
onProgress?.({
|
|
@@ -4003,6 +4052,7 @@ async function startWorker(options) {
|
|
|
4003
4052
|
});
|
|
4004
4053
|
readyProjectIds.add(project.projectId);
|
|
4005
4054
|
reconciledProjectConfigFingerprints.set(project.projectId, configFingerprint);
|
|
4055
|
+
reconciledProjectIds.add(project.projectId);
|
|
4006
4056
|
for (const state of states) {
|
|
4007
4057
|
pendingCheckouts.delete(projectBranchKey(project.projectId, state.branchName));
|
|
4008
4058
|
const key = projectBranchKey(project.projectId, state.branchName);
|
|
@@ -4040,26 +4090,37 @@ async function startWorker(options) {
|
|
|
4040
4090
|
totalItems: projects.length
|
|
4041
4091
|
});
|
|
4042
4092
|
}
|
|
4093
|
+
return reconciledProjectIds;
|
|
4043
4094
|
};
|
|
4044
|
-
const
|
|
4095
|
+
const ORIGIN_DIVERGENCE_REFRESH_INTERVAL_MS = 5 * 60 * 1e3;
|
|
4096
|
+
const lastOriginDivergenceFetchMs = /* @__PURE__ */ new Map();
|
|
4097
|
+
const collectAheadOfOriginBranches = async (refetchOriginProjectIds) => {
|
|
4045
4098
|
const aheadBranches = [];
|
|
4099
|
+
for (const projectId of [...lastOriginDivergenceFetchMs.keys()]) {
|
|
4100
|
+
if (!projectConfigById.has(projectId)) lastOriginDivergenceFetchMs.delete(projectId);
|
|
4101
|
+
}
|
|
4046
4102
|
for (const project of projectConfigById.values()) {
|
|
4047
4103
|
if (project.executionDisabled || !readyProjectIds.has(project.projectId) || project.branches.length === 0) continue;
|
|
4048
4104
|
const connection = projectConnection(project);
|
|
4049
4105
|
const primaryPath = configuredProjectBranchPath(projectsRoot, project, primaryProjectBranch(project));
|
|
4050
|
-
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
|
|
4055
|
-
|
|
4056
|
-
|
|
4057
|
-
|
|
4058
|
-
|
|
4059
|
-
|
|
4060
|
-
|
|
4061
|
-
|
|
4106
|
+
const lastFetchedAtMs = lastOriginDivergenceFetchMs.get(project.projectId);
|
|
4107
|
+
if (refetchOriginProjectIds.has(project.projectId) || lastFetchedAtMs === void 0 || Date.now() - lastFetchedAtMs >= ORIGIN_DIVERGENCE_REFRESH_INTERVAL_MS) {
|
|
4108
|
+
if (await tryGitAsync(
|
|
4109
|
+
[
|
|
4110
|
+
...(0, import_git_process_environment.gitTransportSecurityArgs)(connection.originUrl, connection.credentialHelper, connection.originCredentialUsername),
|
|
4111
|
+
"fetch",
|
|
4112
|
+
"--no-recurse-submodules",
|
|
4113
|
+
"--prune",
|
|
4114
|
+
"origin",
|
|
4115
|
+
"+refs/heads/*:refs/remotes/origin/*"
|
|
4116
|
+
],
|
|
4117
|
+
{ cwd: primaryPath }
|
|
4118
|
+
)) {
|
|
4119
|
+
lastOriginDivergenceFetchMs.set(project.projectId, Date.now());
|
|
4120
|
+
} else {
|
|
4121
|
+
process.stderr.write(`[r5d-worker] could not refresh origin divergence for ${project.projectPath}
|
|
4062
4122
|
`);
|
|
4123
|
+
}
|
|
4063
4124
|
}
|
|
4064
4125
|
for (const branch of project.branches) {
|
|
4065
4126
|
const branchPath = configuredProjectBranchPath(projectsRoot, project, branch.branchName);
|
|
@@ -4091,9 +4152,9 @@ async function startWorker(options) {
|
|
|
4091
4152
|
(left, right) => left.projectId.localeCompare(right.projectId) || left.branchName.localeCompare(right.branchName)
|
|
4092
4153
|
);
|
|
4093
4154
|
};
|
|
4094
|
-
const captureProjectHeadsForWorkspacePublication = async () => {
|
|
4155
|
+
const captureProjectHeadsForWorkspacePublication = async (projects = projectConfigById.values()) => {
|
|
4095
4156
|
const heads = /* @__PURE__ */ new Map();
|
|
4096
|
-
for (const project of
|
|
4157
|
+
for (const project of projects) {
|
|
4097
4158
|
if (!readyProjectIds.has(project.projectId)) continue;
|
|
4098
4159
|
for (const branch of project.branches) {
|
|
4099
4160
|
const branchPath = configuredProjectBranchPath(projectsRoot, project, branch.branchName);
|
|
@@ -4103,9 +4164,9 @@ async function startWorker(options) {
|
|
|
4103
4164
|
}
|
|
4104
4165
|
return heads;
|
|
4105
4166
|
};
|
|
4106
|
-
const pushChangedProjectHeads = async (activeMountIds, publishedHead, publicationHeads, expectedMirrorHeads, suppressedMountIds = /* @__PURE__ */ new Set()) => {
|
|
4167
|
+
const pushChangedProjectHeads = async (activeMountIds, publishedHead, publicationHeads, expectedMirrorHeads, suppressedMountIds = /* @__PURE__ */ new Set(), projects = projectConfigById.values()) => {
|
|
4107
4168
|
const active = new Set(activeMountIds);
|
|
4108
|
-
for (const project of
|
|
4169
|
+
for (const project of projects) {
|
|
4109
4170
|
if (!readyProjectIds.has(project.projectId) || !(0, import_repository_transition_policy.repositoryMirrorWritesAllowed)(project)) continue;
|
|
4110
4171
|
const changed = /* @__PURE__ */ new Map();
|
|
4111
4172
|
for (const branch of project.branches) {
|
|
@@ -4205,7 +4266,7 @@ async function startWorker(options) {
|
|
|
4205
4266
|
};
|
|
4206
4267
|
const observeProjectHeadsBeforeOuterWorkspace = async (input) => {
|
|
4207
4268
|
const observationStartedAt = Date.now();
|
|
4208
|
-
const eligibleProjects = [...projectConfigById.values()].filter(
|
|
4269
|
+
const eligibleProjects = [...input.projects ?? projectConfigById.values()].filter(
|
|
4209
4270
|
(project) => !project.executionDisabled && readyProjectIds.has(project.projectId) && project.branches.length > 0
|
|
4210
4271
|
);
|
|
4211
4272
|
try {
|
|
@@ -4315,7 +4376,7 @@ async function startWorker(options) {
|
|
|
4315
4376
|
}
|
|
4316
4377
|
}
|
|
4317
4378
|
};
|
|
4318
|
-
const mapWorkspaceGitResult = async (attemptId, trigger, result) => {
|
|
4379
|
+
const mapWorkspaceGitResult = async (attemptId, trigger, result, missingCheckouts) => {
|
|
4319
4380
|
const activeProjectBranchPublications = (0, import_workspace_publication_evidence.activeProjectBranchPublicationEvidence)([...projectConfigById.values()], result.activeMountIds);
|
|
4320
4381
|
return {
|
|
4321
4382
|
type: "workspace_sync",
|
|
@@ -4333,6 +4394,7 @@ async function startWorker(options) {
|
|
|
4333
4394
|
affectedPaths: result.affectedPaths,
|
|
4334
4395
|
activeMountIds: result.activeMountIds,
|
|
4335
4396
|
skippedMountIds: result.skippedMountIds,
|
|
4397
|
+
...missingCheckouts ? { missingCheckouts } : {},
|
|
4336
4398
|
activeProjectBranchPublications,
|
|
4337
4399
|
discardedPaths: [],
|
|
4338
4400
|
localChangesDiscarded: false,
|
|
@@ -4360,6 +4422,7 @@ async function startWorker(options) {
|
|
|
4360
4422
|
affectedPaths: [],
|
|
4361
4423
|
discardedPaths: [],
|
|
4362
4424
|
localChangesDiscarded: false,
|
|
4425
|
+
...lastInventoriedMissingCheckouts ? { missingCheckouts: lastInventoriedMissingCheckouts } : {},
|
|
4363
4426
|
error: error instanceof Error ? error.message : String(error)
|
|
4364
4427
|
};
|
|
4365
4428
|
};
|
|
@@ -4368,7 +4431,11 @@ async function startWorker(options) {
|
|
|
4368
4431
|
if (!workspaceRemoteUrl || !workspaceCredentialHelper || !workspaceCredentialUsername || !workspaceGitIdentity) {
|
|
4369
4432
|
throw new Error("Worker workspace configuration has not been received");
|
|
4370
4433
|
}
|
|
4371
|
-
const
|
|
4434
|
+
const checkoutInventory = inventoryConfiguredProjectCheckouts();
|
|
4435
|
+
logMissingCheckouts(checkoutInventory);
|
|
4436
|
+
const missingCheckouts = reportedMissingCheckouts(checkoutInventory);
|
|
4437
|
+
lastInventoriedMissingCheckouts = missingCheckouts;
|
|
4438
|
+
const configuredMounts = buildWorkspaceMounts(checkoutInventory.projects);
|
|
4372
4439
|
if (input.resetToCanonical) {
|
|
4373
4440
|
workspaceProjectionLedger.reset();
|
|
4374
4441
|
const resetHasBusyLiveMount = configuredMounts.some(
|
|
@@ -4379,7 +4446,8 @@ async function startWorker(options) {
|
|
|
4379
4446
|
}
|
|
4380
4447
|
const observedProjectHeads2 = await observeProjectHeadsBeforeOuterWorkspace({
|
|
4381
4448
|
allowNonFastForward: true,
|
|
4382
|
-
failClosed: true
|
|
4449
|
+
failClosed: true,
|
|
4450
|
+
projects: checkoutInventory.projects
|
|
4383
4451
|
});
|
|
4384
4452
|
const reset = await (0, import_workspace_git_sync.resetWorkspaceGit)({
|
|
4385
4453
|
workspacePath: workspaceShadowRoot,
|
|
@@ -4407,6 +4475,7 @@ async function startWorker(options) {
|
|
|
4407
4475
|
affectedPaths: reset.discardedPaths,
|
|
4408
4476
|
activeMountIds: reset.activeMountIds,
|
|
4409
4477
|
skippedMountIds: reset.skippedMountIds,
|
|
4478
|
+
missingCheckouts,
|
|
4410
4479
|
discardedPaths: reset.discardedPaths,
|
|
4411
4480
|
localChangesDiscarded: reset.discardedPaths.length > 0 || reset.startingHead !== reset.remoteHead
|
|
4412
4481
|
};
|
|
@@ -4414,7 +4483,7 @@ async function startWorker(options) {
|
|
|
4414
4483
|
const outerRemediation = input.trigger.type === "remediation" || input.trigger.type === "remediation_confirm";
|
|
4415
4484
|
const mounts = outerRemediation ? (0, import_workspace_git_sync.workspaceGitMountsForRemediation)(configuredMounts) : configuredMounts;
|
|
4416
4485
|
if (outerRemediation) workspaceProjectionLedger.reset();
|
|
4417
|
-
const observedProjectHeads = outerRemediation ? [] : await observeProjectHeadsBeforeOuterWorkspace({ allowNonFastForward: false, failClosed: false });
|
|
4486
|
+
const observedProjectHeads = outerRemediation ? [] : await observeProjectHeadsBeforeOuterWorkspace({ allowNonFastForward: false, failClosed: false, projects: checkoutInventory.projects });
|
|
4418
4487
|
const expectedMirrorHeads = /* @__PURE__ */ new Map();
|
|
4419
4488
|
for (const bundle of observedProjectHeads) {
|
|
4420
4489
|
for (const observation of bundle.observations) {
|
|
@@ -4426,7 +4495,7 @@ async function startWorker(options) {
|
|
|
4426
4495
|
({ projectId, observations }) => observations.filter(({ shouldMove }) => shouldMove).map(({ branchName }) => projectMountId(projectId, branchName))
|
|
4427
4496
|
)
|
|
4428
4497
|
);
|
|
4429
|
-
const publicationHeads = outerRemediation ? /* @__PURE__ */ new Map() : await captureProjectHeadsForWorkspacePublication();
|
|
4498
|
+
const publicationHeads = outerRemediation ? /* @__PURE__ */ new Map() : await captureProjectHeadsForWorkspacePublication(checkoutInventory.projects);
|
|
4430
4499
|
const result = await (0, import_workspace_git_sync.synchronizeWorkspaceGit)({
|
|
4431
4500
|
attemptId: input.attemptId,
|
|
4432
4501
|
workerLabel: label,
|
|
@@ -4446,7 +4515,14 @@ async function startWorker(options) {
|
|
|
4446
4515
|
advertisedRemoteHead: projectMirrorFetchSkippingEnabled && !outerRemediation ? advertisedWorkspaceHead : void 0,
|
|
4447
4516
|
assertStillAdmitted: input.assertStillAdmitted,
|
|
4448
4517
|
afterWorkspacePublished: outerRemediation ? void 0 : async ({ activeMountIds, publishedHead }) => {
|
|
4449
|
-
await pushChangedProjectHeads(
|
|
4518
|
+
await pushChangedProjectHeads(
|
|
4519
|
+
activeMountIds,
|
|
4520
|
+
publishedHead,
|
|
4521
|
+
publicationHeads,
|
|
4522
|
+
expectedMirrorHeads,
|
|
4523
|
+
inboundMoveMountIds,
|
|
4524
|
+
checkoutInventory.projects
|
|
4525
|
+
);
|
|
4450
4526
|
},
|
|
4451
4527
|
hydrationHooks: workspaceHydrationTransactionHooks
|
|
4452
4528
|
});
|
|
@@ -4465,7 +4541,7 @@ async function startWorker(options) {
|
|
|
4465
4541
|
result.localHead ?? result.remoteHead
|
|
4466
4542
|
);
|
|
4467
4543
|
}
|
|
4468
|
-
const mapped = await mapWorkspaceGitResult(input.attemptId, input.trigger, result);
|
|
4544
|
+
const mapped = await mapWorkspaceGitResult(input.attemptId, input.trigger, result, missingCheckouts);
|
|
4469
4545
|
if (!outerRemediation) {
|
|
4470
4546
|
workspaceProjectionLedger.recordCycle({ result: mapped, mounts, projectedMutationTokens: result.projectedMutationTokens });
|
|
4471
4547
|
}
|
|
@@ -5175,7 +5251,7 @@ async function startWorker(options) {
|
|
|
5175
5251
|
// checkout initialization from erasing synchronized dirt.
|
|
5176
5252
|
{ ignoreBusy: true, hydrationHooks: workspaceHydrationTransactionHooks }
|
|
5177
5253
|
);
|
|
5178
|
-
await ensureConfiguredProjects((progress) => {
|
|
5254
|
+
const reconciledProjectIds = await ensureConfiguredProjects((progress) => {
|
|
5179
5255
|
sendLifecycleProgress({ phase: "configuring", operationId: configurationOperationId, ...progress });
|
|
5180
5256
|
process.stdout.write(
|
|
5181
5257
|
`[r5d-worker] ${progress.detail ?? "Configuring workspace"}${progress.completedBytes === void 0 ? "" : `: ${progress.completedBytes}/${progress.totalBytes ?? "?"} bytes`}${progress.completedItems === void 0 ? "" : `, ${progress.completedItems}/${progress.totalItems ?? "?"} item(s)`}
|
|
@@ -5231,7 +5307,7 @@ async function startWorker(options) {
|
|
|
5231
5307
|
const pending = [...pendingCheckouts.values()].sort(
|
|
5232
5308
|
(left, right) => left.projectId.localeCompare(right.projectId) || left.branchName.localeCompare(right.branchName)
|
|
5233
5309
|
);
|
|
5234
|
-
return { result, pending, aheadOfOriginBranches: await collectAheadOfOriginBranches() };
|
|
5310
|
+
return { result, pending, aheadOfOriginBranches: await collectAheadOfOriginBranches(reconciledProjectIds) };
|
|
5235
5311
|
});
|
|
5236
5312
|
} catch (error) {
|
|
5237
5313
|
if (receiptGeneration !== workspaceConfigurationReceiptGeneration) {
|
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,35 +1000,45 @@ 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);
|
|
1009
|
+
const exactMirror = input.exactMirrorBranches?.has(branch.branchName) === true;
|
|
996
1010
|
const resolved = await seedForBranch({
|
|
997
1011
|
...input,
|
|
998
1012
|
primaryPath,
|
|
999
1013
|
branch,
|
|
1000
|
-
requireMirrorHead:
|
|
1014
|
+
requireMirrorHead: exactMirror
|
|
1001
1015
|
});
|
|
1002
1016
|
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}`);
|
|
1017
|
+
const linked = branch.branchName === input.primaryBranchName || gitDirectoryKind(checkoutPath) !== null && await commonGitDirectoryAsync(checkoutPath) === primaryCommonDir;
|
|
1018
|
+
if (!linked) {
|
|
1019
|
+
plan.push({ branchName: branch.branchName, checkoutPath, seed: resolved.seed, action: "recreate" });
|
|
1010
1020
|
continue;
|
|
1011
1021
|
}
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1022
|
+
const currentHead = await gitAsync(checkoutPath, ["rev-parse", "HEAD"], `resolve linked worktree head ${branch.branchName}`);
|
|
1023
|
+
const keep = currentHead === resolved.seed || !exactMirror && await tryGitAsync(checkoutPath, ["merge-base", "--is-ancestor", resolved.seed, currentHead]);
|
|
1024
|
+
plan.push({ branchName: branch.branchName, checkoutPath, seed: resolved.seed, action: keep ? "keep" : "reset" });
|
|
1025
|
+
}
|
|
1026
|
+
const mutatedBranches = new Set(plan.filter(({ action }) => action !== "keep").map(({ branchName }) => branchName));
|
|
1027
|
+
if (mutatedBranches.size > 0) input.beforeDestructiveMutation(mutatedBranches);
|
|
1028
|
+
for (const entry of plan) {
|
|
1029
|
+
if (entry.action === "keep") continue;
|
|
1030
|
+
if (entry.action === "reset") {
|
|
1031
|
+
await preserveHead(entry.checkoutPath);
|
|
1032
|
+
await gitAsync(entry.checkoutPath, ["reset", "--hard", entry.seed], `anchor linked worktree ${entry.branchName}`);
|
|
1033
|
+
continue;
|
|
1034
|
+
}
|
|
1035
|
+
import_node_fs.default.rmSync(entry.checkoutPath, { recursive: true, force: true });
|
|
1036
|
+
await gitAsync(primaryPath, ["branch", "-f", entry.branchName, entry.seed], `anchor project branch ${entry.branchName}`);
|
|
1037
|
+
import_node_fs.default.mkdirSync(import_node_path.default.dirname(entry.checkoutPath), { recursive: true });
|
|
1017
1038
|
await gitAsync(
|
|
1018
1039
|
primaryPath,
|
|
1019
|
-
["worktree", "add", "--force", checkoutPath,
|
|
1020
|
-
`create linked worktree ${
|
|
1040
|
+
["worktree", "add", "--force", entry.checkoutPath, entry.branchName],
|
|
1041
|
+
`create linked worktree ${entry.branchName}`
|
|
1021
1042
|
);
|
|
1022
1043
|
}
|
|
1023
1044
|
return { mirrorHeads, mutatedBranches };
|
|
@@ -1044,8 +1065,14 @@ async function ensureProjectWorktrees(input) {
|
|
|
1044
1065
|
const snapshotStorageRoot = import_node_path.default.resolve(input.snapshotRoot ?? import_node_os.default.tmpdir());
|
|
1045
1066
|
assertNoOutstandingSnapshotForProject(snapshotStorageRoot, projectRoot);
|
|
1046
1067
|
let snapshot;
|
|
1047
|
-
const ensureSnapshot = () => {
|
|
1048
|
-
snapshot ??= snapshotBranchTrees(
|
|
1068
|
+
const ensureSnapshot = (branchNames) => {
|
|
1069
|
+
snapshot ??= snapshotBranchTrees(
|
|
1070
|
+
projectRoot,
|
|
1071
|
+
branchNames ? input.branches.filter(({ branchName }) => branchNames.has(branchName)) : input.branches,
|
|
1072
|
+
snapshotStorageRoot,
|
|
1073
|
+
input.onSnapshotProgress,
|
|
1074
|
+
branchNames ? "branches" : "project"
|
|
1075
|
+
);
|
|
1049
1076
|
return snapshot;
|
|
1050
1077
|
};
|
|
1051
1078
|
try {
|
|
@@ -1097,7 +1124,21 @@ async function ensureProjectWorktrees(input) {
|
|
|
1097
1124
|
});
|
|
1098
1125
|
}
|
|
1099
1126
|
states.sort((left, right) => left.branchName.localeCompare(right.branchName));
|
|
1100
|
-
if (snapshot)
|
|
1127
|
+
if (snapshot) {
|
|
1128
|
+
const primaryGitDirectory = import_node_path.default.join(branchPath(projectRoot, input.primaryBranchName), ".git");
|
|
1129
|
+
persistReconciledProjectAndConsumeSnapshot(
|
|
1130
|
+
snapshot,
|
|
1131
|
+
void 0,
|
|
1132
|
+
snapshot.manifest.scope === "branches" ? [
|
|
1133
|
+
...snapshot.manifest.branches.map(({ branchName }) => branchPath(projectRoot, branchName)),
|
|
1134
|
+
// Branch refs and worktree registrations moved in the shared
|
|
1135
|
+
// administrative directory; objects are fsynced by git itself.
|
|
1136
|
+
import_node_path.default.join(primaryGitDirectory, "refs"),
|
|
1137
|
+
import_node_path.default.join(primaryGitDirectory, "packed-refs"),
|
|
1138
|
+
import_node_path.default.join(primaryGitDirectory, "worktrees")
|
|
1139
|
+
] : void 0
|
|
1140
|
+
);
|
|
1141
|
+
}
|
|
1101
1142
|
return states;
|
|
1102
1143
|
} catch (error) {
|
|
1103
1144
|
if (!snapshot) throw error;
|
|
@@ -1243,6 +1284,53 @@ function createOrRetryLinkedProjectBranch(input) {
|
|
|
1243
1284
|
}
|
|
1244
1285
|
return createLinkedProjectBranch(input);
|
|
1245
1286
|
}
|
|
1287
|
+
function classifyProjectCheckout(input) {
|
|
1288
|
+
const checkoutPath = branchPath(input.projectRoot, input.branchName);
|
|
1289
|
+
let stat;
|
|
1290
|
+
try {
|
|
1291
|
+
stat = import_node_fs.default.lstatSync(checkoutPath);
|
|
1292
|
+
} catch {
|
|
1293
|
+
return "missing";
|
|
1294
|
+
}
|
|
1295
|
+
if (!stat.isDirectory()) return "not_a_worktree";
|
|
1296
|
+
if (input.branchName === input.primaryBranchName) return gitDirectoryKind(checkoutPath) === "directory" ? "linked" : "not_a_worktree";
|
|
1297
|
+
if (gitDirectoryKind(checkoutPath) !== "file") return "not_a_worktree";
|
|
1298
|
+
const gitDirectory = perWorktreeGitDirectory(checkoutPath);
|
|
1299
|
+
if (!gitDirectory) return "not_a_worktree";
|
|
1300
|
+
let registry;
|
|
1301
|
+
let registered;
|
|
1302
|
+
try {
|
|
1303
|
+
registry = import_node_fs.default.realpathSync(import_node_path.default.join(branchPath(input.projectRoot, input.primaryBranchName), ".git", "worktrees"));
|
|
1304
|
+
registered = import_node_fs.default.realpathSync(gitDirectory);
|
|
1305
|
+
} catch {
|
|
1306
|
+
return "not_a_worktree";
|
|
1307
|
+
}
|
|
1308
|
+
const relative = import_node_path.default.relative(registry, registered);
|
|
1309
|
+
if (!relative || relative === ".." || relative.startsWith(`..${import_node_path.default.sep}`) || import_node_path.default.isAbsolute(relative)) return "not_a_worktree";
|
|
1310
|
+
return "linked";
|
|
1311
|
+
}
|
|
1312
|
+
function inventoryProjectCheckouts(input) {
|
|
1313
|
+
const states = input.branchNames.map((branchName) => ({
|
|
1314
|
+
branchName,
|
|
1315
|
+
checkoutPath: branchPath(input.projectRoot, branchName),
|
|
1316
|
+
state: classifyProjectCheckout({ ...input, branchName })
|
|
1317
|
+
}));
|
|
1318
|
+
const primaryLinked = states.some(({ branchName, state }) => branchName === input.primaryBranchName && state === "linked");
|
|
1319
|
+
const inventory = { linkedBranchNames: [], missingCheckouts: [] };
|
|
1320
|
+
for (const { branchName, checkoutPath, state } of states) {
|
|
1321
|
+
if (primaryLinked && state === "linked") inventory.linkedBranchNames.push(branchName);
|
|
1322
|
+
else inventory.missingCheckouts.push({ branchName, checkoutPath, reason: state === "linked" ? "not_a_worktree" : state });
|
|
1323
|
+
}
|
|
1324
|
+
return inventory;
|
|
1325
|
+
}
|
|
1326
|
+
const PROJECT_REMOVED_CHECKOUTS_DIRECTORY = ".r5d-removed";
|
|
1327
|
+
function moveProjectCheckoutAside(projectRoot, branchName, checkoutPath) {
|
|
1328
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
1329
|
+
const target = import_node_path.default.join(projectRoot, PROJECT_REMOVED_CHECKOUTS_DIRECTORY, ...`${branchName}-${timestamp}`.split("/"));
|
|
1330
|
+
import_node_fs.default.mkdirSync(import_node_path.default.dirname(target), { recursive: true });
|
|
1331
|
+
import_node_fs.default.renameSync(checkoutPath, target);
|
|
1332
|
+
return target;
|
|
1333
|
+
}
|
|
1246
1334
|
function deleteLinkedProjectBranch(input) {
|
|
1247
1335
|
(0, import_managed_paths.validateManagedBranchName)(input.primaryBranchName);
|
|
1248
1336
|
(0, import_managed_paths.validateManagedBranchName)(input.branchName);
|
|
@@ -1251,15 +1339,18 @@ function deleteLinkedProjectBranch(input) {
|
|
|
1251
1339
|
const checkoutPath = branchPath(input.projectRoot, input.branchName);
|
|
1252
1340
|
const primaryCommonDir = commonGitDirectory(primaryPath);
|
|
1253
1341
|
if (!primaryCommonDir) throw new Error("Primary project checkout is unavailable");
|
|
1342
|
+
let movedAsidePath;
|
|
1254
1343
|
if (import_node_fs.default.existsSync(checkoutPath)) {
|
|
1255
|
-
const checkoutCommonDir = commonGitDirectory(checkoutPath);
|
|
1344
|
+
const checkoutCommonDir = import_node_fs.default.lstatSync(checkoutPath).isDirectory() ? commonGitDirectory(checkoutPath) : null;
|
|
1256
1345
|
if (checkoutCommonDir !== primaryCommonDir) {
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1346
|
+
movedAsidePath = moveProjectCheckoutAside(input.projectRoot, input.branchName, checkoutPath);
|
|
1347
|
+
tryGit(primaryPath, ["worktree", "prune"]);
|
|
1348
|
+
} else {
|
|
1349
|
+
if (projectWorktreeOperationInProgress(checkoutPath)) {
|
|
1350
|
+
throw new Error(`Project branch ${input.branchName} has an in-progress Git operation`);
|
|
1351
|
+
}
|
|
1352
|
+
git(primaryPath, ["worktree", "remove", "--force", checkoutPath], `remove linked worktree ${input.branchName}`);
|
|
1261
1353
|
}
|
|
1262
|
-
git(primaryPath, ["worktree", "remove", "--force", checkoutPath], `remove linked worktree ${input.branchName}`);
|
|
1263
1354
|
} else {
|
|
1264
1355
|
tryGit(primaryPath, ["worktree", "prune"]);
|
|
1265
1356
|
}
|
|
@@ -1267,7 +1358,7 @@ function deleteLinkedProjectBranch(input) {
|
|
|
1267
1358
|
if (tryGit(primaryPath, ["show-ref", "--verify", "--quiet", branchRef])) {
|
|
1268
1359
|
git(primaryPath, ["branch", "-D", input.branchName], `delete project branch ${input.branchName}`);
|
|
1269
1360
|
}
|
|
1270
|
-
return { branchName: input.branchName };
|
|
1361
|
+
return { branchName: input.branchName, ...movedAsidePath ? { movedAsidePath } : {} };
|
|
1271
1362
|
}
|
|
1272
1363
|
function removeProjectWorktrees(input) {
|
|
1273
1364
|
const projectRoot = import_node_path.default.resolve(input.projectRoot);
|
|
@@ -1526,7 +1617,7 @@ const projectWorktreesTestHarness = {
|
|
|
1526
1617
|
commandArgs: gitCommandArgs,
|
|
1527
1618
|
configureRepository,
|
|
1528
1619
|
createPreparedSnapshot(input) {
|
|
1529
|
-
return { root: snapshotBranchTrees(input.projectRoot, input.branches, input.temporaryRoot).root };
|
|
1620
|
+
return { root: snapshotBranchTrees(input.projectRoot, input.branches, input.temporaryRoot, void 0, input.scope).root };
|
|
1530
1621
|
},
|
|
1531
1622
|
consumeSnapshot(snapshotRoot) {
|
|
1532
1623
|
const manifest = readSnapshotManifest(snapshotRoot);
|
|
@@ -1542,10 +1633,12 @@ const projectWorktreesTestHarness = {
|
|
|
1542
1633
|
};
|
|
1543
1634
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1544
1635
|
0 && (module.exports = {
|
|
1636
|
+
PROJECT_REMOVED_CHECKOUTS_DIRECTORY,
|
|
1545
1637
|
PROJECT_WORKTREE_SNAPSHOT_PREFIX,
|
|
1546
1638
|
ProjectBranchCreationRollbackIncompleteError,
|
|
1547
1639
|
applyObservedProjectMirrorHeads,
|
|
1548
1640
|
assertProjectMirrorHeadsPushed,
|
|
1641
|
+
classifyProjectCheckout,
|
|
1549
1642
|
computeLocalProjectMirrorRefsTokens,
|
|
1550
1643
|
createLinkedProjectBranch,
|
|
1551
1644
|
createOrRetryLinkedProjectBranch,
|
|
@@ -1554,6 +1647,7 @@ const projectWorktreesTestHarness = {
|
|
|
1554
1647
|
ensureProjectWorktrees,
|
|
1555
1648
|
fastForwardProjectHeadsFromMirror,
|
|
1556
1649
|
hasLinkedProjectWorktreeLayout,
|
|
1650
|
+
inventoryProjectCheckouts,
|
|
1557
1651
|
observeProjectMirrorHeads,
|
|
1558
1652
|
projectBranchMayExistAfterCreateFailure,
|
|
1559
1653
|
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(
|
|
@@ -3948,6 +3997,7 @@ async function startWorker(options) {
|
|
|
3948
3997
|
)
|
|
3949
3998
|
].sort();
|
|
3950
3999
|
const ensureConfiguredProjects = async (onProgress) => {
|
|
4000
|
+
const reconciledProjectIds = /* @__PURE__ */ new Set();
|
|
3951
4001
|
const projects = [...projectConfigById.values()];
|
|
3952
4002
|
for (const [projectIndex, project] of projects.entries()) {
|
|
3953
4003
|
onProgress?.({
|
|
@@ -4013,6 +4063,7 @@ async function startWorker(options) {
|
|
|
4013
4063
|
});
|
|
4014
4064
|
readyProjectIds.add(project.projectId);
|
|
4015
4065
|
reconciledProjectConfigFingerprints.set(project.projectId, configFingerprint);
|
|
4066
|
+
reconciledProjectIds.add(project.projectId);
|
|
4016
4067
|
for (const state of states) {
|
|
4017
4068
|
pendingCheckouts.delete(projectBranchKey(project.projectId, state.branchName));
|
|
4018
4069
|
const key = projectBranchKey(project.projectId, state.branchName);
|
|
@@ -4050,26 +4101,37 @@ async function startWorker(options) {
|
|
|
4050
4101
|
totalItems: projects.length
|
|
4051
4102
|
});
|
|
4052
4103
|
}
|
|
4104
|
+
return reconciledProjectIds;
|
|
4053
4105
|
};
|
|
4054
|
-
const
|
|
4106
|
+
const ORIGIN_DIVERGENCE_REFRESH_INTERVAL_MS = 5 * 60 * 1e3;
|
|
4107
|
+
const lastOriginDivergenceFetchMs = /* @__PURE__ */ new Map();
|
|
4108
|
+
const collectAheadOfOriginBranches = async (refetchOriginProjectIds) => {
|
|
4055
4109
|
const aheadBranches = [];
|
|
4110
|
+
for (const projectId of [...lastOriginDivergenceFetchMs.keys()]) {
|
|
4111
|
+
if (!projectConfigById.has(projectId)) lastOriginDivergenceFetchMs.delete(projectId);
|
|
4112
|
+
}
|
|
4056
4113
|
for (const project of projectConfigById.values()) {
|
|
4057
4114
|
if (project.executionDisabled || !readyProjectIds.has(project.projectId) || project.branches.length === 0) continue;
|
|
4058
4115
|
const connection = projectConnection(project);
|
|
4059
4116
|
const primaryPath = configuredProjectBranchPath(projectsRoot, project, primaryProjectBranch(project));
|
|
4060
|
-
|
|
4061
|
-
|
|
4062
|
-
|
|
4063
|
-
|
|
4064
|
-
|
|
4065
|
-
|
|
4066
|
-
|
|
4067
|
-
|
|
4068
|
-
|
|
4069
|
-
|
|
4070
|
-
|
|
4071
|
-
|
|
4117
|
+
const lastFetchedAtMs = lastOriginDivergenceFetchMs.get(project.projectId);
|
|
4118
|
+
if (refetchOriginProjectIds.has(project.projectId) || lastFetchedAtMs === void 0 || Date.now() - lastFetchedAtMs >= ORIGIN_DIVERGENCE_REFRESH_INTERVAL_MS) {
|
|
4119
|
+
if (await tryGitAsync(
|
|
4120
|
+
[
|
|
4121
|
+
...gitTransportSecurityArgs(connection.originUrl, connection.credentialHelper, connection.originCredentialUsername),
|
|
4122
|
+
"fetch",
|
|
4123
|
+
"--no-recurse-submodules",
|
|
4124
|
+
"--prune",
|
|
4125
|
+
"origin",
|
|
4126
|
+
"+refs/heads/*:refs/remotes/origin/*"
|
|
4127
|
+
],
|
|
4128
|
+
{ cwd: primaryPath }
|
|
4129
|
+
)) {
|
|
4130
|
+
lastOriginDivergenceFetchMs.set(project.projectId, Date.now());
|
|
4131
|
+
} else {
|
|
4132
|
+
process.stderr.write(`[r5d-worker] could not refresh origin divergence for ${project.projectPath}
|
|
4072
4133
|
`);
|
|
4134
|
+
}
|
|
4073
4135
|
}
|
|
4074
4136
|
for (const branch of project.branches) {
|
|
4075
4137
|
const branchPath = configuredProjectBranchPath(projectsRoot, project, branch.branchName);
|
|
@@ -4101,9 +4163,9 @@ async function startWorker(options) {
|
|
|
4101
4163
|
(left, right) => left.projectId.localeCompare(right.projectId) || left.branchName.localeCompare(right.branchName)
|
|
4102
4164
|
);
|
|
4103
4165
|
};
|
|
4104
|
-
const captureProjectHeadsForWorkspacePublication = async () => {
|
|
4166
|
+
const captureProjectHeadsForWorkspacePublication = async (projects = projectConfigById.values()) => {
|
|
4105
4167
|
const heads = /* @__PURE__ */ new Map();
|
|
4106
|
-
for (const project of
|
|
4168
|
+
for (const project of projects) {
|
|
4107
4169
|
if (!readyProjectIds.has(project.projectId)) continue;
|
|
4108
4170
|
for (const branch of project.branches) {
|
|
4109
4171
|
const branchPath = configuredProjectBranchPath(projectsRoot, project, branch.branchName);
|
|
@@ -4113,9 +4175,9 @@ async function startWorker(options) {
|
|
|
4113
4175
|
}
|
|
4114
4176
|
return heads;
|
|
4115
4177
|
};
|
|
4116
|
-
const pushChangedProjectHeads = async (activeMountIds, publishedHead, publicationHeads, expectedMirrorHeads, suppressedMountIds = /* @__PURE__ */ new Set()) => {
|
|
4178
|
+
const pushChangedProjectHeads = async (activeMountIds, publishedHead, publicationHeads, expectedMirrorHeads, suppressedMountIds = /* @__PURE__ */ new Set(), projects = projectConfigById.values()) => {
|
|
4117
4179
|
const active = new Set(activeMountIds);
|
|
4118
|
-
for (const project of
|
|
4180
|
+
for (const project of projects) {
|
|
4119
4181
|
if (!readyProjectIds.has(project.projectId) || !repositoryMirrorWritesAllowed(project)) continue;
|
|
4120
4182
|
const changed = /* @__PURE__ */ new Map();
|
|
4121
4183
|
for (const branch of project.branches) {
|
|
@@ -4215,7 +4277,7 @@ async function startWorker(options) {
|
|
|
4215
4277
|
};
|
|
4216
4278
|
const observeProjectHeadsBeforeOuterWorkspace = async (input) => {
|
|
4217
4279
|
const observationStartedAt = Date.now();
|
|
4218
|
-
const eligibleProjects = [...projectConfigById.values()].filter(
|
|
4280
|
+
const eligibleProjects = [...input.projects ?? projectConfigById.values()].filter(
|
|
4219
4281
|
(project) => !project.executionDisabled && readyProjectIds.has(project.projectId) && project.branches.length > 0
|
|
4220
4282
|
);
|
|
4221
4283
|
try {
|
|
@@ -4325,7 +4387,7 @@ async function startWorker(options) {
|
|
|
4325
4387
|
}
|
|
4326
4388
|
}
|
|
4327
4389
|
};
|
|
4328
|
-
const mapWorkspaceGitResult = async (attemptId, trigger, result) => {
|
|
4390
|
+
const mapWorkspaceGitResult = async (attemptId, trigger, result, missingCheckouts) => {
|
|
4329
4391
|
const activeProjectBranchPublications = activeProjectBranchPublicationEvidence([...projectConfigById.values()], result.activeMountIds);
|
|
4330
4392
|
return {
|
|
4331
4393
|
type: "workspace_sync",
|
|
@@ -4343,6 +4405,7 @@ async function startWorker(options) {
|
|
|
4343
4405
|
affectedPaths: result.affectedPaths,
|
|
4344
4406
|
activeMountIds: result.activeMountIds,
|
|
4345
4407
|
skippedMountIds: result.skippedMountIds,
|
|
4408
|
+
...missingCheckouts ? { missingCheckouts } : {},
|
|
4346
4409
|
activeProjectBranchPublications,
|
|
4347
4410
|
discardedPaths: [],
|
|
4348
4411
|
localChangesDiscarded: false,
|
|
@@ -4370,6 +4433,7 @@ async function startWorker(options) {
|
|
|
4370
4433
|
affectedPaths: [],
|
|
4371
4434
|
discardedPaths: [],
|
|
4372
4435
|
localChangesDiscarded: false,
|
|
4436
|
+
...lastInventoriedMissingCheckouts ? { missingCheckouts: lastInventoriedMissingCheckouts } : {},
|
|
4373
4437
|
error: error instanceof Error ? error.message : String(error)
|
|
4374
4438
|
};
|
|
4375
4439
|
};
|
|
@@ -4378,7 +4442,11 @@ async function startWorker(options) {
|
|
|
4378
4442
|
if (!workspaceRemoteUrl || !workspaceCredentialHelper || !workspaceCredentialUsername || !workspaceGitIdentity) {
|
|
4379
4443
|
throw new Error("Worker workspace configuration has not been received");
|
|
4380
4444
|
}
|
|
4381
|
-
const
|
|
4445
|
+
const checkoutInventory = inventoryConfiguredProjectCheckouts();
|
|
4446
|
+
logMissingCheckouts(checkoutInventory);
|
|
4447
|
+
const missingCheckouts = reportedMissingCheckouts(checkoutInventory);
|
|
4448
|
+
lastInventoriedMissingCheckouts = missingCheckouts;
|
|
4449
|
+
const configuredMounts = buildWorkspaceMounts(checkoutInventory.projects);
|
|
4382
4450
|
if (input.resetToCanonical) {
|
|
4383
4451
|
workspaceProjectionLedger.reset();
|
|
4384
4452
|
const resetHasBusyLiveMount = configuredMounts.some(
|
|
@@ -4389,7 +4457,8 @@ async function startWorker(options) {
|
|
|
4389
4457
|
}
|
|
4390
4458
|
const observedProjectHeads2 = await observeProjectHeadsBeforeOuterWorkspace({
|
|
4391
4459
|
allowNonFastForward: true,
|
|
4392
|
-
failClosed: true
|
|
4460
|
+
failClosed: true,
|
|
4461
|
+
projects: checkoutInventory.projects
|
|
4393
4462
|
});
|
|
4394
4463
|
const reset = await resetWorkspaceGit({
|
|
4395
4464
|
workspacePath: workspaceShadowRoot,
|
|
@@ -4417,6 +4486,7 @@ async function startWorker(options) {
|
|
|
4417
4486
|
affectedPaths: reset.discardedPaths,
|
|
4418
4487
|
activeMountIds: reset.activeMountIds,
|
|
4419
4488
|
skippedMountIds: reset.skippedMountIds,
|
|
4489
|
+
missingCheckouts,
|
|
4420
4490
|
discardedPaths: reset.discardedPaths,
|
|
4421
4491
|
localChangesDiscarded: reset.discardedPaths.length > 0 || reset.startingHead !== reset.remoteHead
|
|
4422
4492
|
};
|
|
@@ -4424,7 +4494,7 @@ async function startWorker(options) {
|
|
|
4424
4494
|
const outerRemediation = input.trigger.type === "remediation" || input.trigger.type === "remediation_confirm";
|
|
4425
4495
|
const mounts = outerRemediation ? workspaceGitMountsForRemediation(configuredMounts) : configuredMounts;
|
|
4426
4496
|
if (outerRemediation) workspaceProjectionLedger.reset();
|
|
4427
|
-
const observedProjectHeads = outerRemediation ? [] : await observeProjectHeadsBeforeOuterWorkspace({ allowNonFastForward: false, failClosed: false });
|
|
4497
|
+
const observedProjectHeads = outerRemediation ? [] : await observeProjectHeadsBeforeOuterWorkspace({ allowNonFastForward: false, failClosed: false, projects: checkoutInventory.projects });
|
|
4428
4498
|
const expectedMirrorHeads = /* @__PURE__ */ new Map();
|
|
4429
4499
|
for (const bundle of observedProjectHeads) {
|
|
4430
4500
|
for (const observation of bundle.observations) {
|
|
@@ -4436,7 +4506,7 @@ async function startWorker(options) {
|
|
|
4436
4506
|
({ projectId, observations }) => observations.filter(({ shouldMove }) => shouldMove).map(({ branchName }) => projectMountId(projectId, branchName))
|
|
4437
4507
|
)
|
|
4438
4508
|
);
|
|
4439
|
-
const publicationHeads = outerRemediation ? /* @__PURE__ */ new Map() : await captureProjectHeadsForWorkspacePublication();
|
|
4509
|
+
const publicationHeads = outerRemediation ? /* @__PURE__ */ new Map() : await captureProjectHeadsForWorkspacePublication(checkoutInventory.projects);
|
|
4440
4510
|
const result = await synchronizeWorkspaceGit({
|
|
4441
4511
|
attemptId: input.attemptId,
|
|
4442
4512
|
workerLabel: label,
|
|
@@ -4456,7 +4526,14 @@ async function startWorker(options) {
|
|
|
4456
4526
|
advertisedRemoteHead: projectMirrorFetchSkippingEnabled && !outerRemediation ? advertisedWorkspaceHead : void 0,
|
|
4457
4527
|
assertStillAdmitted: input.assertStillAdmitted,
|
|
4458
4528
|
afterWorkspacePublished: outerRemediation ? void 0 : async ({ activeMountIds, publishedHead }) => {
|
|
4459
|
-
await pushChangedProjectHeads(
|
|
4529
|
+
await pushChangedProjectHeads(
|
|
4530
|
+
activeMountIds,
|
|
4531
|
+
publishedHead,
|
|
4532
|
+
publicationHeads,
|
|
4533
|
+
expectedMirrorHeads,
|
|
4534
|
+
inboundMoveMountIds,
|
|
4535
|
+
checkoutInventory.projects
|
|
4536
|
+
);
|
|
4460
4537
|
},
|
|
4461
4538
|
hydrationHooks: workspaceHydrationTransactionHooks
|
|
4462
4539
|
});
|
|
@@ -4475,7 +4552,7 @@ async function startWorker(options) {
|
|
|
4475
4552
|
result.localHead ?? result.remoteHead
|
|
4476
4553
|
);
|
|
4477
4554
|
}
|
|
4478
|
-
const mapped = await mapWorkspaceGitResult(input.attemptId, input.trigger, result);
|
|
4555
|
+
const mapped = await mapWorkspaceGitResult(input.attemptId, input.trigger, result, missingCheckouts);
|
|
4479
4556
|
if (!outerRemediation) {
|
|
4480
4557
|
workspaceProjectionLedger.recordCycle({ result: mapped, mounts, projectedMutationTokens: result.projectedMutationTokens });
|
|
4481
4558
|
}
|
|
@@ -5185,7 +5262,7 @@ async function startWorker(options) {
|
|
|
5185
5262
|
// checkout initialization from erasing synchronized dirt.
|
|
5186
5263
|
{ ignoreBusy: true, hydrationHooks: workspaceHydrationTransactionHooks }
|
|
5187
5264
|
);
|
|
5188
|
-
await ensureConfiguredProjects((progress) => {
|
|
5265
|
+
const reconciledProjectIds = await ensureConfiguredProjects((progress) => {
|
|
5189
5266
|
sendLifecycleProgress({ phase: "configuring", operationId: configurationOperationId, ...progress });
|
|
5190
5267
|
process.stdout.write(
|
|
5191
5268
|
`[r5d-worker] ${progress.detail ?? "Configuring workspace"}${progress.completedBytes === void 0 ? "" : `: ${progress.completedBytes}/${progress.totalBytes ?? "?"} bytes`}${progress.completedItems === void 0 ? "" : `, ${progress.completedItems}/${progress.totalItems ?? "?"} item(s)`}
|
|
@@ -5241,7 +5318,7 @@ async function startWorker(options) {
|
|
|
5241
5318
|
const pending = [...pendingCheckouts.values()].sort(
|
|
5242
5319
|
(left, right) => left.projectId.localeCompare(right.projectId) || left.branchName.localeCompare(right.branchName)
|
|
5243
5320
|
);
|
|
5244
|
-
return { result, pending, aheadOfOriginBranches: await collectAheadOfOriginBranches() };
|
|
5321
|
+
return { result, pending, aheadOfOriginBranches: await collectAheadOfOriginBranches(reconciledProjectIds) };
|
|
5245
5322
|
});
|
|
5246
5323
|
} catch (error) {
|
|
5247
5324
|
if (receiptGeneration !== workspaceConfigurationReceiptGeneration) {
|
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,35 +950,45 @@ 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);
|
|
959
|
+
const exactMirror = input.exactMirrorBranches?.has(branch.branchName) === true;
|
|
949
960
|
const resolved = await seedForBranch({
|
|
950
961
|
...input,
|
|
951
962
|
primaryPath,
|
|
952
963
|
branch,
|
|
953
|
-
requireMirrorHead:
|
|
964
|
+
requireMirrorHead: exactMirror
|
|
954
965
|
});
|
|
955
966
|
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}`);
|
|
967
|
+
const linked = branch.branchName === input.primaryBranchName || gitDirectoryKind(checkoutPath) !== null && await commonGitDirectoryAsync(checkoutPath) === primaryCommonDir;
|
|
968
|
+
if (!linked) {
|
|
969
|
+
plan.push({ branchName: branch.branchName, checkoutPath, seed: resolved.seed, action: "recreate" });
|
|
963
970
|
continue;
|
|
964
971
|
}
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
972
|
+
const currentHead = await gitAsync(checkoutPath, ["rev-parse", "HEAD"], `resolve linked worktree head ${branch.branchName}`);
|
|
973
|
+
const keep = currentHead === resolved.seed || !exactMirror && await tryGitAsync(checkoutPath, ["merge-base", "--is-ancestor", resolved.seed, currentHead]);
|
|
974
|
+
plan.push({ branchName: branch.branchName, checkoutPath, seed: resolved.seed, action: keep ? "keep" : "reset" });
|
|
975
|
+
}
|
|
976
|
+
const mutatedBranches = new Set(plan.filter(({ action }) => action !== "keep").map(({ branchName }) => branchName));
|
|
977
|
+
if (mutatedBranches.size > 0) input.beforeDestructiveMutation(mutatedBranches);
|
|
978
|
+
for (const entry of plan) {
|
|
979
|
+
if (entry.action === "keep") continue;
|
|
980
|
+
if (entry.action === "reset") {
|
|
981
|
+
await preserveHead(entry.checkoutPath);
|
|
982
|
+
await gitAsync(entry.checkoutPath, ["reset", "--hard", entry.seed], `anchor linked worktree ${entry.branchName}`);
|
|
983
|
+
continue;
|
|
984
|
+
}
|
|
985
|
+
fs.rmSync(entry.checkoutPath, { recursive: true, force: true });
|
|
986
|
+
await gitAsync(primaryPath, ["branch", "-f", entry.branchName, entry.seed], `anchor project branch ${entry.branchName}`);
|
|
987
|
+
fs.mkdirSync(path.dirname(entry.checkoutPath), { recursive: true });
|
|
970
988
|
await gitAsync(
|
|
971
989
|
primaryPath,
|
|
972
|
-
["worktree", "add", "--force", checkoutPath,
|
|
973
|
-
`create linked worktree ${
|
|
990
|
+
["worktree", "add", "--force", entry.checkoutPath, entry.branchName],
|
|
991
|
+
`create linked worktree ${entry.branchName}`
|
|
974
992
|
);
|
|
975
993
|
}
|
|
976
994
|
return { mirrorHeads, mutatedBranches };
|
|
@@ -997,8 +1015,14 @@ async function ensureProjectWorktrees(input) {
|
|
|
997
1015
|
const snapshotStorageRoot = path.resolve(input.snapshotRoot ?? os.tmpdir());
|
|
998
1016
|
assertNoOutstandingSnapshotForProject(snapshotStorageRoot, projectRoot);
|
|
999
1017
|
let snapshot;
|
|
1000
|
-
const ensureSnapshot = () => {
|
|
1001
|
-
snapshot ??= snapshotBranchTrees(
|
|
1018
|
+
const ensureSnapshot = (branchNames) => {
|
|
1019
|
+
snapshot ??= snapshotBranchTrees(
|
|
1020
|
+
projectRoot,
|
|
1021
|
+
branchNames ? input.branches.filter(({ branchName }) => branchNames.has(branchName)) : input.branches,
|
|
1022
|
+
snapshotStorageRoot,
|
|
1023
|
+
input.onSnapshotProgress,
|
|
1024
|
+
branchNames ? "branches" : "project"
|
|
1025
|
+
);
|
|
1002
1026
|
return snapshot;
|
|
1003
1027
|
};
|
|
1004
1028
|
try {
|
|
@@ -1050,7 +1074,21 @@ async function ensureProjectWorktrees(input) {
|
|
|
1050
1074
|
});
|
|
1051
1075
|
}
|
|
1052
1076
|
states.sort((left, right) => left.branchName.localeCompare(right.branchName));
|
|
1053
|
-
if (snapshot)
|
|
1077
|
+
if (snapshot) {
|
|
1078
|
+
const primaryGitDirectory = path.join(branchPath(projectRoot, input.primaryBranchName), ".git");
|
|
1079
|
+
persistReconciledProjectAndConsumeSnapshot(
|
|
1080
|
+
snapshot,
|
|
1081
|
+
void 0,
|
|
1082
|
+
snapshot.manifest.scope === "branches" ? [
|
|
1083
|
+
...snapshot.manifest.branches.map(({ branchName }) => branchPath(projectRoot, branchName)),
|
|
1084
|
+
// Branch refs and worktree registrations moved in the shared
|
|
1085
|
+
// administrative directory; objects are fsynced by git itself.
|
|
1086
|
+
path.join(primaryGitDirectory, "refs"),
|
|
1087
|
+
path.join(primaryGitDirectory, "packed-refs"),
|
|
1088
|
+
path.join(primaryGitDirectory, "worktrees")
|
|
1089
|
+
] : void 0
|
|
1090
|
+
);
|
|
1091
|
+
}
|
|
1054
1092
|
return states;
|
|
1055
1093
|
} catch (error) {
|
|
1056
1094
|
if (!snapshot) throw error;
|
|
@@ -1196,6 +1234,53 @@ function createOrRetryLinkedProjectBranch(input) {
|
|
|
1196
1234
|
}
|
|
1197
1235
|
return createLinkedProjectBranch(input);
|
|
1198
1236
|
}
|
|
1237
|
+
function classifyProjectCheckout(input) {
|
|
1238
|
+
const checkoutPath = branchPath(input.projectRoot, input.branchName);
|
|
1239
|
+
let stat;
|
|
1240
|
+
try {
|
|
1241
|
+
stat = fs.lstatSync(checkoutPath);
|
|
1242
|
+
} catch {
|
|
1243
|
+
return "missing";
|
|
1244
|
+
}
|
|
1245
|
+
if (!stat.isDirectory()) return "not_a_worktree";
|
|
1246
|
+
if (input.branchName === input.primaryBranchName) return gitDirectoryKind(checkoutPath) === "directory" ? "linked" : "not_a_worktree";
|
|
1247
|
+
if (gitDirectoryKind(checkoutPath) !== "file") return "not_a_worktree";
|
|
1248
|
+
const gitDirectory = perWorktreeGitDirectory(checkoutPath);
|
|
1249
|
+
if (!gitDirectory) return "not_a_worktree";
|
|
1250
|
+
let registry;
|
|
1251
|
+
let registered;
|
|
1252
|
+
try {
|
|
1253
|
+
registry = fs.realpathSync(path.join(branchPath(input.projectRoot, input.primaryBranchName), ".git", "worktrees"));
|
|
1254
|
+
registered = fs.realpathSync(gitDirectory);
|
|
1255
|
+
} catch {
|
|
1256
|
+
return "not_a_worktree";
|
|
1257
|
+
}
|
|
1258
|
+
const relative = path.relative(registry, registered);
|
|
1259
|
+
if (!relative || relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) return "not_a_worktree";
|
|
1260
|
+
return "linked";
|
|
1261
|
+
}
|
|
1262
|
+
function inventoryProjectCheckouts(input) {
|
|
1263
|
+
const states = input.branchNames.map((branchName) => ({
|
|
1264
|
+
branchName,
|
|
1265
|
+
checkoutPath: branchPath(input.projectRoot, branchName),
|
|
1266
|
+
state: classifyProjectCheckout({ ...input, branchName })
|
|
1267
|
+
}));
|
|
1268
|
+
const primaryLinked = states.some(({ branchName, state }) => branchName === input.primaryBranchName && state === "linked");
|
|
1269
|
+
const inventory = { linkedBranchNames: [], missingCheckouts: [] };
|
|
1270
|
+
for (const { branchName, checkoutPath, state } of states) {
|
|
1271
|
+
if (primaryLinked && state === "linked") inventory.linkedBranchNames.push(branchName);
|
|
1272
|
+
else inventory.missingCheckouts.push({ branchName, checkoutPath, reason: state === "linked" ? "not_a_worktree" : state });
|
|
1273
|
+
}
|
|
1274
|
+
return inventory;
|
|
1275
|
+
}
|
|
1276
|
+
const PROJECT_REMOVED_CHECKOUTS_DIRECTORY = ".r5d-removed";
|
|
1277
|
+
function moveProjectCheckoutAside(projectRoot, branchName, checkoutPath) {
|
|
1278
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
1279
|
+
const target = path.join(projectRoot, PROJECT_REMOVED_CHECKOUTS_DIRECTORY, ...`${branchName}-${timestamp}`.split("/"));
|
|
1280
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
1281
|
+
fs.renameSync(checkoutPath, target);
|
|
1282
|
+
return target;
|
|
1283
|
+
}
|
|
1199
1284
|
function deleteLinkedProjectBranch(input) {
|
|
1200
1285
|
validateManagedBranchName(input.primaryBranchName);
|
|
1201
1286
|
validateManagedBranchName(input.branchName);
|
|
@@ -1204,15 +1289,18 @@ function deleteLinkedProjectBranch(input) {
|
|
|
1204
1289
|
const checkoutPath = branchPath(input.projectRoot, input.branchName);
|
|
1205
1290
|
const primaryCommonDir = commonGitDirectory(primaryPath);
|
|
1206
1291
|
if (!primaryCommonDir) throw new Error("Primary project checkout is unavailable");
|
|
1292
|
+
let movedAsidePath;
|
|
1207
1293
|
if (fs.existsSync(checkoutPath)) {
|
|
1208
|
-
const checkoutCommonDir = commonGitDirectory(checkoutPath);
|
|
1294
|
+
const checkoutCommonDir = fs.lstatSync(checkoutPath).isDirectory() ? commonGitDirectory(checkoutPath) : null;
|
|
1209
1295
|
if (checkoutCommonDir !== primaryCommonDir) {
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1296
|
+
movedAsidePath = moveProjectCheckoutAside(input.projectRoot, input.branchName, checkoutPath);
|
|
1297
|
+
tryGit(primaryPath, ["worktree", "prune"]);
|
|
1298
|
+
} else {
|
|
1299
|
+
if (projectWorktreeOperationInProgress(checkoutPath)) {
|
|
1300
|
+
throw new Error(`Project branch ${input.branchName} has an in-progress Git operation`);
|
|
1301
|
+
}
|
|
1302
|
+
git(primaryPath, ["worktree", "remove", "--force", checkoutPath], `remove linked worktree ${input.branchName}`);
|
|
1214
1303
|
}
|
|
1215
|
-
git(primaryPath, ["worktree", "remove", "--force", checkoutPath], `remove linked worktree ${input.branchName}`);
|
|
1216
1304
|
} else {
|
|
1217
1305
|
tryGit(primaryPath, ["worktree", "prune"]);
|
|
1218
1306
|
}
|
|
@@ -1220,7 +1308,7 @@ function deleteLinkedProjectBranch(input) {
|
|
|
1220
1308
|
if (tryGit(primaryPath, ["show-ref", "--verify", "--quiet", branchRef])) {
|
|
1221
1309
|
git(primaryPath, ["branch", "-D", input.branchName], `delete project branch ${input.branchName}`);
|
|
1222
1310
|
}
|
|
1223
|
-
return { branchName: input.branchName };
|
|
1311
|
+
return { branchName: input.branchName, ...movedAsidePath ? { movedAsidePath } : {} };
|
|
1224
1312
|
}
|
|
1225
1313
|
function removeProjectWorktrees(input) {
|
|
1226
1314
|
const projectRoot = path.resolve(input.projectRoot);
|
|
@@ -1479,7 +1567,7 @@ const projectWorktreesTestHarness = {
|
|
|
1479
1567
|
commandArgs: gitCommandArgs,
|
|
1480
1568
|
configureRepository,
|
|
1481
1569
|
createPreparedSnapshot(input) {
|
|
1482
|
-
return { root: snapshotBranchTrees(input.projectRoot, input.branches, input.temporaryRoot).root };
|
|
1570
|
+
return { root: snapshotBranchTrees(input.projectRoot, input.branches, input.temporaryRoot, void 0, input.scope).root };
|
|
1483
1571
|
},
|
|
1484
1572
|
consumeSnapshot(snapshotRoot) {
|
|
1485
1573
|
const manifest = readSnapshotManifest(snapshotRoot);
|
|
@@ -1494,10 +1582,12 @@ const projectWorktreesTestHarness = {
|
|
|
1494
1582
|
}
|
|
1495
1583
|
};
|
|
1496
1584
|
export {
|
|
1585
|
+
PROJECT_REMOVED_CHECKOUTS_DIRECTORY,
|
|
1497
1586
|
PROJECT_WORKTREE_SNAPSHOT_PREFIX,
|
|
1498
1587
|
ProjectBranchCreationRollbackIncompleteError,
|
|
1499
1588
|
applyObservedProjectMirrorHeads,
|
|
1500
1589
|
assertProjectMirrorHeadsPushed,
|
|
1590
|
+
classifyProjectCheckout,
|
|
1501
1591
|
computeLocalProjectMirrorRefsTokens,
|
|
1502
1592
|
createLinkedProjectBranch,
|
|
1503
1593
|
createOrRetryLinkedProjectBranch,
|
|
@@ -1506,6 +1596,7 @@ export {
|
|
|
1506
1596
|
ensureProjectWorktrees,
|
|
1507
1597
|
fastForwardProjectHeadsFromMirror,
|
|
1508
1598
|
hasLinkedProjectWorktreeLayout,
|
|
1599
|
+
inventoryProjectCheckouts,
|
|
1509
1600
|
observeProjectMirrorHeads,
|
|
1510
1601
|
projectBranchMayExistAfterCreateFailure,
|
|
1511
1602
|
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
|
};
|