@ricsam/r5d-worker 0.0.137 → 0.0.139
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/README.md +4 -94
- package/dist/cjs/package.json +1 -1
- package/dist/mjs/main.mjs +46 -7543
- package/dist/mjs/package.json +1 -1
- package/dist/mjs/personal/action-scheduler.mjs +31 -0
- package/dist/mjs/personal/client.mjs +215 -0
- package/dist/mjs/personal/runtime.mjs +359 -0
- package/dist/mjs/personal/tcp.mjs +217 -0
- package/dist/mjs/runtime/adapter.mjs +42 -0
- package/dist/mjs/runtime/client.mjs +95 -0
- package/dist/mjs/runtime/daemon.mjs +131 -0
- package/dist/mjs/runtime/executor.mjs +436 -0
- package/dist/mjs/runtime/index.mjs +15 -0
- package/dist/mjs/runtime/protocol.mjs +139 -0
- package/dist/mjs/runtime/pty-host.mjs +156 -0
- package/dist/mjs/runtime/pty.mjs +127 -0
- package/dist/mjs/runtime/releases/artifacts.mjs +201 -0
- package/dist/mjs/runtime/releases/grants.mjs +93 -0
- package/dist/mjs/runtime/releases/main.mjs +101 -0
- package/dist/mjs/runtime/releases/manager.mjs +541 -0
- package/dist/mjs/runtime/releases/process.mjs +88 -0
- package/dist/mjs/runtime/releases/retirement.mjs +18 -0
- package/dist/mjs/runtime/releases/rpc-client.mjs +184 -0
- package/dist/mjs/runtime/releases/rpc-main.mjs +213 -0
- package/dist/mjs/runtime/releases/rpc-protocol.mjs +103 -0
- package/dist/mjs/runtime/storage.mjs +156 -0
- package/dist/mjs/runtime/workspace/artifacts.mjs +121 -0
- package/dist/mjs/runtime/workspace/authority.mjs +1355 -0
- package/dist/mjs/runtime/workspace/contracts.mjs +60 -0
- package/dist/mjs/runtime/workspace/file-write.mjs +23 -0
- package/dist/mjs/runtime/workspace/files.mjs +308 -0
- package/dist/mjs/runtime/workspace/http-body.mjs +45 -0
- package/dist/mjs/runtime/workspace/storage-client.mjs +59 -0
- package/dist/mjs/runtime/workspace/storage-wire.mjs +53 -0
- package/dist/types/main.d.ts +0 -1167
- package/dist/types/personal/action-scheduler.d.ts +23 -0
- package/dist/types/personal/client.d.ts +14 -0
- package/dist/types/personal/runtime.d.ts +121 -0
- package/dist/types/personal/tcp.d.ts +14 -0
- package/dist/types/runtime/client.d.ts +3 -1
- package/dist/types/runtime/executor.d.ts +6 -1
- package/dist/types/runtime/protocol.d.ts +176 -4
- package/dist/types/runtime/pty.d.ts +1 -0
- package/dist/types/runtime/releases/artifacts.d.ts +51 -0
- package/dist/types/runtime/releases/grants.d.ts +16 -0
- package/dist/types/runtime/releases/manager.d.ts +145 -0
- package/dist/types/runtime/releases/process.d.ts +14 -0
- package/dist/types/runtime/releases/retirement.d.ts +15 -0
- package/dist/types/runtime/releases/rpc-client.d.ts +52 -0
- package/dist/types/runtime/releases/rpc-main.d.ts +10 -0
- package/dist/types/runtime/releases/rpc-protocol.d.ts +1139 -0
- package/dist/types/runtime/workspace/artifacts.d.ts +30 -0
- package/dist/types/runtime/workspace/authority.d.ts +338 -0
- package/dist/types/runtime/workspace/contracts.d.ts +86 -0
- package/dist/types/runtime/workspace/file-write.d.ts +14 -0
- package/dist/types/runtime/workspace/files.d.ts +21 -0
- package/dist/types/runtime/workspace/http-body.d.ts +9 -0
- package/dist/types/runtime/workspace/storage-client.d.ts +23 -0
- package/dist/types/runtime/workspace/storage-wire.d.ts +138 -0
- package/package.json +8 -6
- package/dist/cjs/atomic-rename.cjs +0 -303
- package/dist/cjs/browser-upload.cjs +0 -73
- package/dist/cjs/cli-update.cjs +0 -108
- package/dist/cjs/command-launcher.cjs +0 -423
- package/dist/cjs/control-command-policy.cjs +0 -177
- package/dist/cjs/git-blob-hash.cjs +0 -41
- package/dist/cjs/git-identity.cjs +0 -39
- package/dist/cjs/git-process-environment.cjs +0 -133
- package/dist/cjs/inline-output.cjs +0 -62
- package/dist/cjs/main.cjs +0 -7517
- package/dist/cjs/managed-paths.cjs +0 -215
- package/dist/cjs/plan-lint.cjs +0 -165
- package/dist/cjs/plan-parser.cjs +0 -127
- package/dist/cjs/plan-paths.cjs +0 -53
- package/dist/cjs/port-forward-client.cjs +0 -206
- package/dist/cjs/process-tree.cjs +0 -117
- package/dist/cjs/project-checkout-garbage.cjs +0 -219
- package/dist/cjs/project-mirror-fetch-policy.cjs +0 -82
- package/dist/cjs/project-mirror-refs-token.cjs +0 -85
- package/dist/cjs/project-workspace-state.cjs +0 -972
- package/dist/cjs/project-worktrees.cjs +0 -1713
- package/dist/cjs/pty-output-coalescer.cjs +0 -87
- package/dist/cjs/recovery-journal-protocol.cjs +0 -56
- package/dist/cjs/recovery-journal-runtime.cjs +0 -133
- package/dist/cjs/recovery-journal-thread.cjs +0 -9
- package/dist/cjs/recovery-journal.cjs +0 -742
- package/dist/cjs/recovery-output-window.cjs +0 -77
- package/dist/cjs/recovery-protocol.cjs +0 -31
- package/dist/cjs/recovery-store.cjs +0 -252
- package/dist/cjs/registry-auth.cjs +0 -310
- package/dist/cjs/repository-transition-policy.cjs +0 -49
- package/dist/cjs/runtime-version.cjs +0 -88
- package/dist/cjs/session-file-mutations.cjs +0 -61
- package/dist/cjs/supervisor.cjs +0 -147
- package/dist/cjs/three-way-merge.cjs +0 -346
- package/dist/cjs/working-tree-mirror.cjs +0 -1872
- package/dist/cjs/workspace-automatic-sync-policy.cjs +0 -79
- package/dist/cjs/workspace-branch-incarnation-policy.cjs +0 -37
- package/dist/cjs/workspace-command-sync-policy.cjs +0 -112
- package/dist/cjs/workspace-command-targets.cjs +0 -63
- package/dist/cjs/workspace-filesystem-executor-thread.cjs +0 -36
- package/dist/cjs/workspace-filesystem-executor.cjs +0 -327
- package/dist/cjs/workspace-filesystem-job-types.cjs +0 -144
- package/dist/cjs/workspace-filesystem-jobs.cjs +0 -59
- package/dist/cjs/workspace-git-sync.cjs +0 -3595
- package/dist/cjs/workspace-hydration-ledger.cjs +0 -83
- package/dist/cjs/workspace-hydration-merge.cjs +0 -448
- package/dist/cjs/workspace-hydration-recovery-state.cjs +0 -53
- package/dist/cjs/workspace-incident-state.cjs +0 -37
- package/dist/cjs/workspace-merge-projection.cjs +0 -628
- package/dist/cjs/workspace-mount-boundary.cjs +0 -71
- package/dist/cjs/workspace-mount-hold-fence.cjs +0 -120
- package/dist/cjs/workspace-mutation-gate.cjs +0 -127
- package/dist/cjs/workspace-path-move.cjs +0 -195
- package/dist/cjs/workspace-preserve-only-policy.cjs +0 -36
- package/dist/cjs/workspace-project-config-policy.cjs +0 -53
- package/dist/cjs/workspace-projection-ledger.cjs +0 -81
- package/dist/cjs/workspace-publication-evidence.cjs +0 -46
- package/dist/cjs/workspace-sync-coalescer.cjs +0 -58
- package/dist/mjs/atomic-rename.mjs +0 -261
- package/dist/mjs/browser-upload.mjs +0 -42
- package/dist/mjs/command-launcher.mjs +0 -393
- package/dist/mjs/control-command-policy.mjs +0 -146
- package/dist/mjs/git-blob-hash.mjs +0 -16
- package/dist/mjs/git-identity.mjs +0 -15
- package/dist/mjs/git-process-environment.mjs +0 -105
- package/dist/mjs/inline-output.mjs +0 -37
- package/dist/mjs/managed-paths.mjs +0 -174
- package/dist/mjs/plan-lint.mjs +0 -129
- package/dist/mjs/plan-parser.mjs +0 -94
- package/dist/mjs/plan-paths.mjs +0 -25
- package/dist/mjs/port-forward-client.mjs +0 -172
- package/dist/mjs/project-checkout-garbage.mjs +0 -178
- package/dist/mjs/project-mirror-fetch-policy.mjs +0 -56
- package/dist/mjs/project-mirror-refs-token.mjs +0 -53
- package/dist/mjs/project-workspace-state.mjs +0 -937
- package/dist/mjs/project-worktrees.mjs +0 -1657
- package/dist/mjs/pty-output-coalescer.mjs +0 -61
- package/dist/mjs/recovery-journal-protocol.mjs +0 -30
- package/dist/mjs/recovery-journal-runtime.mjs +0 -112
- package/dist/mjs/recovery-journal-thread.mjs +0 -8
- package/dist/mjs/recovery-journal.mjs +0 -694
- package/dist/mjs/recovery-output-window.mjs +0 -53
- package/dist/mjs/recovery-protocol.mjs +0 -6
- package/dist/mjs/recovery-store.mjs +0 -217
- package/dist/mjs/registry-auth.mjs +0 -269
- package/dist/mjs/repository-transition-policy.mjs +0 -22
- package/dist/mjs/runtime-version.mjs +0 -54
- package/dist/mjs/session-file-mutations.mjs +0 -37
- package/dist/mjs/supervisor.mjs +0 -114
- package/dist/mjs/three-way-merge.mjs +0 -318
- package/dist/mjs/working-tree-mirror.mjs +0 -1817
- package/dist/mjs/workspace-automatic-sync-policy.mjs +0 -48
- package/dist/mjs/workspace-branch-incarnation-policy.mjs +0 -13
- package/dist/mjs/workspace-command-sync-policy.mjs +0 -84
- package/dist/mjs/workspace-command-targets.mjs +0 -37
- package/dist/mjs/workspace-filesystem-executor-thread.mjs +0 -38
- package/dist/mjs/workspace-filesystem-executor.mjs +0 -287
- package/dist/mjs/workspace-filesystem-job-types.mjs +0 -116
- package/dist/mjs/workspace-filesystem-jobs.mjs +0 -55
- package/dist/mjs/workspace-git-sync.mjs +0 -3553
- package/dist/mjs/workspace-hydration-ledger.mjs +0 -58
- package/dist/mjs/workspace-hydration-merge.mjs +0 -414
- package/dist/mjs/workspace-hydration-recovery-state.mjs +0 -29
- package/dist/mjs/workspace-incident-state.mjs +0 -12
- package/dist/mjs/workspace-merge-projection.mjs +0 -594
- package/dist/mjs/workspace-mount-boundary.mjs +0 -37
- package/dist/mjs/workspace-mount-hold-fence.mjs +0 -95
- package/dist/mjs/workspace-mutation-gate.mjs +0 -103
- package/dist/mjs/workspace-path-move.mjs +0 -160
- package/dist/mjs/workspace-preserve-only-policy.mjs +0 -11
- package/dist/mjs/workspace-project-config-policy.mjs +0 -27
- package/dist/mjs/workspace-projection-ledger.mjs +0 -57
- package/dist/mjs/workspace-publication-evidence.mjs +0 -21
- package/dist/mjs/workspace-sync-coalescer.mjs +0 -34
- package/dist/types/atomic-rename.d.ts +0 -78
- package/dist/types/browser-upload.d.ts +0 -7
- package/dist/types/command-launcher.d.ts +0 -104
- package/dist/types/control-command-policy.d.ts +0 -53
- package/dist/types/git-blob-hash.d.ts +0 -10
- package/dist/types/git-identity.d.ts +0 -9
- package/dist/types/git-process-environment.d.ts +0 -9
- package/dist/types/inline-output.d.ts +0 -3
- package/dist/types/managed-paths.d.ts +0 -19
- package/dist/types/plan-lint.d.ts +0 -16
- package/dist/types/plan-parser.d.ts +0 -38
- package/dist/types/plan-paths.d.ts +0 -36
- package/dist/types/port-forward-client.d.ts +0 -8
- package/dist/types/project-checkout-garbage.d.ts +0 -86
- package/dist/types/project-mirror-fetch-policy.d.ts +0 -84
- package/dist/types/project-mirror-refs-token.d.ts +0 -53
- package/dist/types/project-workspace-state.d.ts +0 -188
- package/dist/types/project-worktrees.d.ts +0 -397
- package/dist/types/pty-output-coalescer.d.ts +0 -20
- package/dist/types/recovery-journal-protocol.d.ts +0 -35
- package/dist/types/recovery-journal-runtime.d.ts +0 -12
- package/dist/types/recovery-journal-stall-fixture.d.ts +0 -1
- package/dist/types/recovery-journal-thread.d.ts +0 -1
- package/dist/types/recovery-journal.d.ts +0 -248
- package/dist/types/recovery-output-window.d.ts +0 -19
- package/dist/types/recovery-protocol.d.ts +0 -59
- package/dist/types/recovery-store.d.ts +0 -52
- package/dist/types/registry-auth.d.ts +0 -47
- package/dist/types/repository-transition-policy.d.ts +0 -26
- package/dist/types/runtime-version.d.ts +0 -1
- package/dist/types/session-file-mutations.d.ts +0 -22
- package/dist/types/supervisor-daemonized-fixture.d.ts +0 -1
- package/dist/types/supervisor-launcher-cleanup-fixture.d.ts +0 -1
- package/dist/types/supervisor-signal-fixture.d.ts +0 -1
- package/dist/types/supervisor.d.ts +0 -23
- package/dist/types/three-way-merge.d.ts +0 -77
- package/dist/types/working-tree-mirror.d.ts +0 -424
- package/dist/types/workspace-automatic-sync-policy.d.ts +0 -57
- package/dist/types/workspace-branch-incarnation-policy.d.ts +0 -14
- package/dist/types/workspace-command-sync-policy.d.ts +0 -57
- package/dist/types/workspace-command-targets.d.ts +0 -37
- package/dist/types/workspace-filesystem-executor-thread.d.ts +0 -1
- package/dist/types/workspace-filesystem-executor.d.ts +0 -123
- package/dist/types/workspace-filesystem-job-types.d.ts +0 -302
- package/dist/types/workspace-filesystem-jobs.d.ts +0 -7
- package/dist/types/workspace-git-sync.d.ts +0 -514
- package/dist/types/workspace-hydration-ledger.d.ts +0 -76
- package/dist/types/workspace-hydration-merge.d.ts +0 -103
- package/dist/types/workspace-hydration-recovery-state.d.ts +0 -10
- package/dist/types/workspace-incident-state.d.ts +0 -19
- package/dist/types/workspace-merge-projection.d.ts +0 -69
- package/dist/types/workspace-mount-boundary.d.ts +0 -10
- package/dist/types/workspace-mount-hold-fence.d.ts +0 -42
- package/dist/types/workspace-mutation-gate.d.ts +0 -42
- package/dist/types/workspace-path-move.d.ts +0 -21
- package/dist/types/workspace-preserve-only-policy.d.ts +0 -15
- package/dist/types/workspace-project-config-policy.d.ts +0 -48
- package/dist/types/workspace-projection-ledger.d.ts +0 -59
- package/dist/types/workspace-publication-evidence.d.ts +0 -17
- package/dist/types/workspace-sync-coalescer.d.ts +0 -28
- /package/dist/types/{credential-authority-lock-fixture.d.ts → runtime/releases/main.d.ts} +0 -0
|
@@ -1,937 +0,0 @@
|
|
|
1
|
-
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
-
import fs from "node:fs";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import {
|
|
5
|
-
managedBranchPath,
|
|
6
|
-
managedProjectRoot,
|
|
7
|
-
validateCheckoutPathSegments,
|
|
8
|
-
validateManagedBranchName
|
|
9
|
-
} from "./managed-paths.mjs";
|
|
10
|
-
const PROJECT_WORKSPACE_STATE_FILE = "project-workspace-state.json";
|
|
11
|
-
const PROJECT_WORKSPACE_STATE_VERSION = 1;
|
|
12
|
-
const PROJECT_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
13
|
-
const GIT_OBJECT_ID_PATTERN = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;
|
|
14
|
-
class ProjectWorkspacePendingBranchPathChangeError extends Error {
|
|
15
|
-
projectId;
|
|
16
|
-
branchName;
|
|
17
|
-
currentCheckoutPathSegments;
|
|
18
|
-
incomingCheckoutPathSegments;
|
|
19
|
-
constructor(input) {
|
|
20
|
-
super(`Cannot move project ${input.projectId} while creator-local branch ${input.branchName} is pending publication`);
|
|
21
|
-
this.name = "ProjectWorkspacePendingBranchPathChangeError";
|
|
22
|
-
this.projectId = input.projectId;
|
|
23
|
-
this.branchName = input.branchName;
|
|
24
|
-
this.currentCheckoutPathSegments = [...input.currentCheckoutPathSegments];
|
|
25
|
-
this.incomingCheckoutPathSegments = [...input.incomingCheckoutPathSegments];
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
class ProjectWorkspaceCheckoutPathCollisionError extends Error {
|
|
29
|
-
projectId;
|
|
30
|
-
occupyingProjectId;
|
|
31
|
-
checkoutPathSegments;
|
|
32
|
-
constructor(input) {
|
|
33
|
-
super(
|
|
34
|
-
`Cannot assign checkout path ${input.checkoutPathSegments.join("/")} to project ${input.projectId} while project ${input.occupyingProjectId} still owns a durable checkout or deletion tombstone there`
|
|
35
|
-
);
|
|
36
|
-
this.name = "ProjectWorkspaceCheckoutPathCollisionError";
|
|
37
|
-
this.projectId = input.projectId;
|
|
38
|
-
this.occupyingProjectId = input.occupyingProjectId;
|
|
39
|
-
this.checkoutPathSegments = [...input.checkoutPathSegments];
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
const EMPTY_STATE = {
|
|
43
|
-
version: PROJECT_WORKSPACE_STATE_VERSION,
|
|
44
|
-
desiredProjects: [],
|
|
45
|
-
locallyPendingCreatedBranches: [],
|
|
46
|
-
enabledRepositoryTransitions: [],
|
|
47
|
-
tombstones: []
|
|
48
|
-
};
|
|
49
|
-
function isRecord(value) {
|
|
50
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
51
|
-
}
|
|
52
|
-
function validateProjectId(projectId) {
|
|
53
|
-
if (typeof projectId !== "string" || projectId !== "local" && projectId !== "local-code" && !PROJECT_ID_PATTERN.test(projectId)) {
|
|
54
|
-
throw new Error(`Invalid project workspace state project id: ${String(projectId)}`);
|
|
55
|
-
}
|
|
56
|
-
return projectId;
|
|
57
|
-
}
|
|
58
|
-
function comparePathSegments(left, right) {
|
|
59
|
-
return left[0].localeCompare(right[0]) || left[1].localeCompare(right[1]);
|
|
60
|
-
}
|
|
61
|
-
function samePathSegments(left, right) {
|
|
62
|
-
return left[0] === right[0] && left[1] === right[1];
|
|
63
|
-
}
|
|
64
|
-
function pathsOverlap(left, right) {
|
|
65
|
-
const relative = path.relative(path.resolve(left), path.resolve(right));
|
|
66
|
-
const reverse = path.relative(path.resolve(right), path.resolve(left));
|
|
67
|
-
return relative === "" || relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative) || reverse !== ".." && !reverse.startsWith(`..${path.sep}`) && !path.isAbsolute(reverse);
|
|
68
|
-
}
|
|
69
|
-
function normalizeBranchNames(branches, label) {
|
|
70
|
-
const branchNames = branches.map(({ branchName }) => {
|
|
71
|
-
validateManagedBranchName(branchName);
|
|
72
|
-
return branchName;
|
|
73
|
-
});
|
|
74
|
-
if (new Set(branchNames).size !== branchNames.length) throw new Error(`${label} contains duplicate branches`);
|
|
75
|
-
const sorted = [...branchNames].sort();
|
|
76
|
-
for (const [index, branchName] of sorted.entries()) {
|
|
77
|
-
const overlapping = sorted.slice(index + 1).find((candidate) => candidate.startsWith(`${branchName}/`));
|
|
78
|
-
if (overlapping) throw new Error(`${label} contains overlapping branch paths ${branchName} and ${overlapping}`);
|
|
79
|
-
}
|
|
80
|
-
return sorted;
|
|
81
|
-
}
|
|
82
|
-
function normalizeDesiredProjects(projects) {
|
|
83
|
-
const normalized = projects.map((project) => {
|
|
84
|
-
const projectId = validateProjectId(project.projectId);
|
|
85
|
-
const checkoutPathSegments = validateCheckoutPathSegments(project.checkoutPathSegments);
|
|
86
|
-
validateManagedBranchName(project.defaultBranch);
|
|
87
|
-
const branchNames = normalizeBranchNames(project.branches, `Project ${projectId}`);
|
|
88
|
-
return {
|
|
89
|
-
projectId,
|
|
90
|
-
checkoutPathSegments,
|
|
91
|
-
defaultBranch: project.defaultBranch,
|
|
92
|
-
branches: branchNames.map((branchName) => ({ branchName }))
|
|
93
|
-
};
|
|
94
|
-
});
|
|
95
|
-
if (new Set(normalized.map(({ projectId }) => projectId)).size !== normalized.length) {
|
|
96
|
-
throw new Error("Project workspace state contains duplicate project ids");
|
|
97
|
-
}
|
|
98
|
-
const checkoutPaths = /* @__PURE__ */ new Map();
|
|
99
|
-
for (const project of normalized) {
|
|
100
|
-
const checkoutPath = project.checkoutPathSegments.join("/");
|
|
101
|
-
const existing = checkoutPaths.get(checkoutPath);
|
|
102
|
-
if (existing) throw new Error(`Projects ${existing} and ${project.projectId} share managed checkout path ${checkoutPath}`);
|
|
103
|
-
checkoutPaths.set(checkoutPath, project.projectId);
|
|
104
|
-
}
|
|
105
|
-
return normalized.sort(
|
|
106
|
-
(left, right) => left.projectId.localeCompare(right.projectId) || comparePathSegments(left.checkoutPathSegments, right.checkoutPathSegments)
|
|
107
|
-
);
|
|
108
|
-
}
|
|
109
|
-
function normalizePendingCreatedBranches(value) {
|
|
110
|
-
if (!Array.isArray(value)) throw new Error("Invalid durable locally pending project branches");
|
|
111
|
-
const pending = value.map((entry) => {
|
|
112
|
-
if (!isRecord(entry) || typeof entry.branchName !== "string") {
|
|
113
|
-
throw new Error("Invalid durable locally pending project branch");
|
|
114
|
-
}
|
|
115
|
-
const projectId = validateProjectId(entry.projectId);
|
|
116
|
-
validateManagedBranchName(entry.branchName);
|
|
117
|
-
if (entry.branchId !== void 0 && (typeof entry.branchId !== "string" || !/^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/.test(entry.branchId))) {
|
|
118
|
-
throw new Error("Invalid durable locally pending project branch id");
|
|
119
|
-
}
|
|
120
|
-
return { ...entry.branchId ? { branchId: entry.branchId } : {}, projectId, branchName: entry.branchName };
|
|
121
|
-
});
|
|
122
|
-
const keys = pending.map(({ projectId, branchName }) => `${projectId}\0${branchName}`);
|
|
123
|
-
if (new Set(keys).size !== keys.length) throw new Error("Duplicate durable locally pending project branch");
|
|
124
|
-
return pending.sort((left, right) => left.projectId.localeCompare(right.projectId) || left.branchName.localeCompare(right.branchName));
|
|
125
|
-
}
|
|
126
|
-
function normalizePreserveOnlyBranches(value) {
|
|
127
|
-
const normalized = normalizePendingCreatedBranches(value);
|
|
128
|
-
if (normalized.some(({ branchId }) => !branchId)) throw new Error("Preserve-only branch is missing its durable branch id");
|
|
129
|
-
return normalized;
|
|
130
|
-
}
|
|
131
|
-
function normalizeEnabledRepositoryTransitions(value) {
|
|
132
|
-
if (value === void 0) return [];
|
|
133
|
-
if (!Array.isArray(value)) throw new Error("Invalid durable enabled repository transitions");
|
|
134
|
-
const transitions = value.map((entry) => {
|
|
135
|
-
if (!isRecord(entry)) throw new Error("Invalid durable enabled repository transition");
|
|
136
|
-
const projectId = validateProjectId(entry.projectId);
|
|
137
|
-
if (entry.transitionId !== null && (typeof entry.transitionId !== "string" || !/^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/.test(entry.transitionId))) {
|
|
138
|
-
throw new Error("Invalid durable enabled repository transition id");
|
|
139
|
-
}
|
|
140
|
-
return { projectId, transitionId: entry.transitionId };
|
|
141
|
-
});
|
|
142
|
-
if (new Set(transitions.map(({ projectId }) => projectId)).size !== transitions.length) {
|
|
143
|
-
throw new Error("Duplicate durable enabled repository transition project");
|
|
144
|
-
}
|
|
145
|
-
return transitions.sort((left, right) => left.projectId.localeCompare(right.projectId));
|
|
146
|
-
}
|
|
147
|
-
function tombstoneBranchNames(tombstone) {
|
|
148
|
-
return tombstone.kind === "branch" ? [tombstone.branchName] : tombstone.branchNames;
|
|
149
|
-
}
|
|
150
|
-
function deletionId(input) {
|
|
151
|
-
const digest = createHash("sha256").update(
|
|
152
|
-
JSON.stringify([input.kind, input.projectId, input.checkoutPathSegments[0], input.checkoutPathSegments[1], input.branchName ?? null])
|
|
153
|
-
).digest("hex");
|
|
154
|
-
return `${input.kind}:${digest}`;
|
|
155
|
-
}
|
|
156
|
-
function normalizeDeletedMirrorRefs(value, branchNames, label) {
|
|
157
|
-
if (!Array.isArray(value) || value.some((branchName) => typeof branchName !== "string")) {
|
|
158
|
-
throw new Error(`${label} has invalid deleted mirror refs`);
|
|
159
|
-
}
|
|
160
|
-
const normalized = [...new Set(value)].sort();
|
|
161
|
-
if (normalized.some((branchName) => !branchNames.includes(branchName))) {
|
|
162
|
-
throw new Error(`${label} records a mirror ref outside its branch set`);
|
|
163
|
-
}
|
|
164
|
-
return normalized;
|
|
165
|
-
}
|
|
166
|
-
function normalizeTreePublishedHead(value, label) {
|
|
167
|
-
if (value === null) return null;
|
|
168
|
-
if (typeof value !== "string" || !GIT_OBJECT_ID_PATTERN.test(value)) {
|
|
169
|
-
throw new Error(`${label} has an invalid published workspace head`);
|
|
170
|
-
}
|
|
171
|
-
return value;
|
|
172
|
-
}
|
|
173
|
-
function normalizeTombstone(value) {
|
|
174
|
-
if (!isRecord(value)) throw new Error("Invalid project workspace deletion tombstone");
|
|
175
|
-
const projectId = validateProjectId(value.projectId);
|
|
176
|
-
const checkoutPathSegments = validateCheckoutPathSegments(value.checkoutPathSegments);
|
|
177
|
-
if (value.kind === "branch") {
|
|
178
|
-
if (typeof value.branchName !== "string") throw new Error("Invalid project workspace branch deletion tombstone");
|
|
179
|
-
validateManagedBranchName(value.branchName);
|
|
180
|
-
const expectedId2 = deletionId({ kind: "branch", projectId, checkoutPathSegments, branchName: value.branchName });
|
|
181
|
-
if (value.id !== expectedId2) throw new Error("Project workspace branch deletion tombstone id does not match its target");
|
|
182
|
-
return {
|
|
183
|
-
id: expectedId2,
|
|
184
|
-
kind: "branch",
|
|
185
|
-
projectId,
|
|
186
|
-
checkoutPathSegments,
|
|
187
|
-
branchName: value.branchName,
|
|
188
|
-
treePublishedHead: normalizeTreePublishedHead(value.treePublishedHead, `Deletion ${expectedId2}`),
|
|
189
|
-
mirrorRefsDeleted: normalizeDeletedMirrorRefs(value.mirrorRefsDeleted, [value.branchName], `Deletion ${expectedId2}`)
|
|
190
|
-
};
|
|
191
|
-
}
|
|
192
|
-
if (value.kind !== "project" || !Array.isArray(value.branchNames)) {
|
|
193
|
-
throw new Error("Invalid project workspace project deletion tombstone");
|
|
194
|
-
}
|
|
195
|
-
const branchNames = normalizeBranchNames(
|
|
196
|
-
value.branchNames.map((branchName) => ({ branchName: String(branchName) })),
|
|
197
|
-
`Deletion ${String(value.id)}`
|
|
198
|
-
);
|
|
199
|
-
const expectedId = deletionId({ kind: "project", projectId, checkoutPathSegments });
|
|
200
|
-
if (value.id !== expectedId) throw new Error("Project workspace project deletion tombstone id does not match its target");
|
|
201
|
-
return {
|
|
202
|
-
id: expectedId,
|
|
203
|
-
kind: "project",
|
|
204
|
-
projectId,
|
|
205
|
-
checkoutPathSegments,
|
|
206
|
-
branchNames,
|
|
207
|
-
treePublishedHead: normalizeTreePublishedHead(value.treePublishedHead, `Deletion ${expectedId}`),
|
|
208
|
-
mirrorRefsDeleted: normalizeDeletedMirrorRefs(value.mirrorRefsDeleted, branchNames, `Deletion ${expectedId}`)
|
|
209
|
-
};
|
|
210
|
-
}
|
|
211
|
-
function normalizeState(value) {
|
|
212
|
-
if (!isRecord(value) || value.version !== PROJECT_WORKSPACE_STATE_VERSION) {
|
|
213
|
-
throw new Error("Unsupported or invalid project workspace state file");
|
|
214
|
-
}
|
|
215
|
-
if (!Array.isArray(value.desiredProjects) || !Array.isArray(value.locallyPendingCreatedBranches) || !Array.isArray(value.tombstones)) {
|
|
216
|
-
throw new Error("Invalid project workspace state file");
|
|
217
|
-
}
|
|
218
|
-
const desiredProjects = normalizeDesiredProjects(value.desiredProjects);
|
|
219
|
-
const locallyPendingCreatedBranches = normalizePendingCreatedBranches(value.locallyPendingCreatedBranches);
|
|
220
|
-
const enabledRepositoryTransitions = normalizeEnabledRepositoryTransitions(value.enabledRepositoryTransitions);
|
|
221
|
-
const desiredById = new Map(desiredProjects.map((project) => [project.projectId, project]));
|
|
222
|
-
for (const pending of locallyPendingCreatedBranches) {
|
|
223
|
-
const project = desiredById.get(pending.projectId);
|
|
224
|
-
if (!project || !project.branches.some(({ branchName }) => branchName === pending.branchName)) {
|
|
225
|
-
throw new Error(`Durable pending branch ${pending.projectId}/${pending.branchName} is missing from desired project state`);
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
const tombstones = value.tombstones.map(normalizeTombstone);
|
|
229
|
-
if (new Set(tombstones.map(({ id }) => id)).size !== tombstones.length) {
|
|
230
|
-
throw new Error("Project workspace state contains duplicate deletion tombstones");
|
|
231
|
-
}
|
|
232
|
-
return {
|
|
233
|
-
version: PROJECT_WORKSPACE_STATE_VERSION,
|
|
234
|
-
desiredProjects,
|
|
235
|
-
locallyPendingCreatedBranches,
|
|
236
|
-
enabledRepositoryTransitions,
|
|
237
|
-
tombstones: tombstones.sort((left, right) => left.id.localeCompare(right.id))
|
|
238
|
-
};
|
|
239
|
-
}
|
|
240
|
-
function cloneDesiredProjects(projects) {
|
|
241
|
-
return projects.map((project) => ({
|
|
242
|
-
...project,
|
|
243
|
-
checkoutPathSegments: [...project.checkoutPathSegments],
|
|
244
|
-
branches: project.branches.map((branch) => ({ ...branch }))
|
|
245
|
-
}));
|
|
246
|
-
}
|
|
247
|
-
function cloneTombstones(tombstones) {
|
|
248
|
-
return tombstones.map((tombstone) => ({
|
|
249
|
-
...tombstone,
|
|
250
|
-
checkoutPathSegments: [...tombstone.checkoutPathSegments],
|
|
251
|
-
mirrorRefsDeleted: [...tombstone.mirrorRefsDeleted],
|
|
252
|
-
...tombstone.kind === "project" ? { branchNames: [...tombstone.branchNames] } : {}
|
|
253
|
-
}));
|
|
254
|
-
}
|
|
255
|
-
function readState(statePath) {
|
|
256
|
-
try {
|
|
257
|
-
return normalizeState(JSON.parse(fs.readFileSync(statePath, "utf8")));
|
|
258
|
-
} catch (error) {
|
|
259
|
-
if (error.code === "ENOENT") return normalizeState(EMPTY_STATE);
|
|
260
|
-
throw new Error(
|
|
261
|
-
`Could not read durable project workspace state at ${statePath}: ${error instanceof Error ? error.message : String(error)}`,
|
|
262
|
-
{ cause: error }
|
|
263
|
-
);
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
function fsyncDirectory(directory) {
|
|
267
|
-
let descriptor;
|
|
268
|
-
try {
|
|
269
|
-
descriptor = fs.openSync(directory, "r");
|
|
270
|
-
fs.fsyncSync(descriptor);
|
|
271
|
-
} finally {
|
|
272
|
-
if (descriptor !== void 0) fs.closeSync(descriptor);
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
function lstatIfExists(targetPath) {
|
|
276
|
-
try {
|
|
277
|
-
return fs.lstatSync(targetPath);
|
|
278
|
-
} catch (error) {
|
|
279
|
-
if (error.code === "ENOENT") return null;
|
|
280
|
-
throw error;
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
async function fsyncDirectoryAsync(directory) {
|
|
284
|
-
const handle = await fs.promises.open(directory, "r");
|
|
285
|
-
try {
|
|
286
|
-
await handle.sync();
|
|
287
|
-
} finally {
|
|
288
|
-
await handle.close();
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
async function lstatIfExistsAsync(targetPath) {
|
|
292
|
-
try {
|
|
293
|
-
return await fs.promises.lstat(targetPath);
|
|
294
|
-
} catch (error) {
|
|
295
|
-
if (error.code === "ENOENT") return null;
|
|
296
|
-
throw error;
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
async function ensureDurableStateRoot(stateRoot) {
|
|
300
|
-
const resolvedStateRoot = path.resolve(stateRoot);
|
|
301
|
-
let existingAncestor = resolvedStateRoot;
|
|
302
|
-
while (!await lstatIfExistsAsync(existingAncestor)) {
|
|
303
|
-
const parent = path.dirname(existingAncestor);
|
|
304
|
-
if (parent === existingAncestor) throw new Error(`Project workspace state has no existing ancestor: ${resolvedStateRoot}`);
|
|
305
|
-
existingAncestor = parent;
|
|
306
|
-
}
|
|
307
|
-
const ancestorStat = await lstatIfExistsAsync(existingAncestor);
|
|
308
|
-
if (!ancestorStat || ancestorStat.isSymbolicLink() || !ancestorStat.isDirectory()) {
|
|
309
|
-
throw new Error(`Project workspace state must descend from a real directory: ${resolvedStateRoot}`);
|
|
310
|
-
}
|
|
311
|
-
await fs.promises.mkdir(resolvedStateRoot, { recursive: true, mode: 448 });
|
|
312
|
-
let current = resolvedStateRoot;
|
|
313
|
-
while (true) {
|
|
314
|
-
const stat = await lstatIfExistsAsync(current);
|
|
315
|
-
if (!stat || stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
316
|
-
throw new Error(`Project workspace state path must be a real directory: ${current}`);
|
|
317
|
-
}
|
|
318
|
-
await fsyncDirectoryAsync(current);
|
|
319
|
-
if (current === existingAncestor) break;
|
|
320
|
-
current = path.dirname(current);
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
async function writeStateAtomically(stateRoot, statePath, state, afterStateRootFsync) {
|
|
324
|
-
await ensureDurableStateRoot(stateRoot);
|
|
325
|
-
afterStateRootFsync?.();
|
|
326
|
-
const temporaryPath = path.join(stateRoot, `.${PROJECT_WORKSPACE_STATE_FILE}.${process.pid}.${randomUUID()}.tmp`);
|
|
327
|
-
let handle;
|
|
328
|
-
try {
|
|
329
|
-
handle = await fs.promises.open(temporaryPath, "wx", 384);
|
|
330
|
-
await handle.writeFile(`${JSON.stringify(normalizeState(state), null, 2)}
|
|
331
|
-
`, "utf8");
|
|
332
|
-
await handle.sync();
|
|
333
|
-
await handle.close();
|
|
334
|
-
handle = void 0;
|
|
335
|
-
await fs.promises.rename(temporaryPath, statePath);
|
|
336
|
-
await fsyncDirectoryAsync(stateRoot);
|
|
337
|
-
} finally {
|
|
338
|
-
if (handle !== void 0) await handle.close();
|
|
339
|
-
await fs.promises.rm(temporaryPath, { force: true });
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
function createBranchTombstone(project, branchName) {
|
|
343
|
-
return {
|
|
344
|
-
id: deletionId({ kind: "branch", projectId: project.projectId, checkoutPathSegments: project.checkoutPathSegments, branchName }),
|
|
345
|
-
kind: "branch",
|
|
346
|
-
projectId: project.projectId,
|
|
347
|
-
checkoutPathSegments: [...project.checkoutPathSegments],
|
|
348
|
-
branchName,
|
|
349
|
-
treePublishedHead: null,
|
|
350
|
-
mirrorRefsDeleted: []
|
|
351
|
-
};
|
|
352
|
-
}
|
|
353
|
-
function createProjectTombstone(project) {
|
|
354
|
-
return {
|
|
355
|
-
id: deletionId({ kind: "project", projectId: project.projectId, checkoutPathSegments: project.checkoutPathSegments }),
|
|
356
|
-
kind: "project",
|
|
357
|
-
projectId: project.projectId,
|
|
358
|
-
checkoutPathSegments: [...project.checkoutPathSegments],
|
|
359
|
-
branchNames: project.branches.map(({ branchName }) => branchName).sort(),
|
|
360
|
-
treePublishedHead: null,
|
|
361
|
-
mirrorRefsDeleted: []
|
|
362
|
-
};
|
|
363
|
-
}
|
|
364
|
-
function addBranchTombstone(tombstones, project, branchName) {
|
|
365
|
-
const projectTombstone = tombstones.find(
|
|
366
|
-
(tombstone) => tombstone.kind === "project" && tombstone.projectId === project.projectId && samePathSegments(tombstone.checkoutPathSegments, project.checkoutPathSegments)
|
|
367
|
-
);
|
|
368
|
-
if (projectTombstone) {
|
|
369
|
-
if (!projectTombstone.branchNames.includes(branchName)) {
|
|
370
|
-
projectTombstone.branchNames.push(branchName);
|
|
371
|
-
projectTombstone.branchNames.sort();
|
|
372
|
-
projectTombstone.treePublishedHead = null;
|
|
373
|
-
}
|
|
374
|
-
return;
|
|
375
|
-
}
|
|
376
|
-
const created = createBranchTombstone(project, branchName);
|
|
377
|
-
if (!tombstones.some(({ id }) => id === created.id)) tombstones.push(created);
|
|
378
|
-
}
|
|
379
|
-
function addProjectTombstone(tombstones, project) {
|
|
380
|
-
const created = createProjectTombstone(project);
|
|
381
|
-
const existing = tombstones.find(({ id }) => id === created.id);
|
|
382
|
-
if (existing) return;
|
|
383
|
-
const supersededBranches = tombstones.filter(
|
|
384
|
-
(tombstone) => tombstone.kind === "branch" && tombstone.projectId === project.projectId && samePathSegments(tombstone.checkoutPathSegments, project.checkoutPathSegments)
|
|
385
|
-
);
|
|
386
|
-
const alreadyDeleted = new Set(supersededBranches.flatMap(({ mirrorRefsDeleted }) => mirrorRefsDeleted));
|
|
387
|
-
created.mirrorRefsDeleted = created.branchNames.filter((branchName) => alreadyDeleted.has(branchName));
|
|
388
|
-
tombstones.splice(
|
|
389
|
-
0,
|
|
390
|
-
tombstones.length,
|
|
391
|
-
...tombstones.filter((tombstone) => !supersededBranches.some(({ id }) => id === tombstone.id)),
|
|
392
|
-
created
|
|
393
|
-
);
|
|
394
|
-
}
|
|
395
|
-
function applyPendingCreatedBranches(desiredProjects, pendingCreatedBranches) {
|
|
396
|
-
const seen = /* @__PURE__ */ new Set();
|
|
397
|
-
for (const pending of pendingCreatedBranches) {
|
|
398
|
-
const projectId = validateProjectId(pending.projectId);
|
|
399
|
-
validateManagedBranchName(pending.branchName);
|
|
400
|
-
const key = `${projectId}\0${pending.branchName}`;
|
|
401
|
-
if (seen.has(key)) continue;
|
|
402
|
-
seen.add(key);
|
|
403
|
-
const project = desiredProjects.find((candidate) => candidate.projectId === projectId);
|
|
404
|
-
if (!project) throw new Error(`Pending local branch ${projectId}/${pending.branchName} has no desired project`);
|
|
405
|
-
if (!project.branches.some(({ branchName }) => branchName === pending.branchName)) {
|
|
406
|
-
project.branches.push({ branchName: pending.branchName });
|
|
407
|
-
project.branches = normalizeBranchNames(project.branches, `Project ${projectId}`).map((branchName) => ({ branchName }));
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
function pendingCreatedBranchKey(input) {
|
|
412
|
-
return `${input.projectId}\0${input.branchName}`;
|
|
413
|
-
}
|
|
414
|
-
function pruneAuthoritativelyDesiredBranchDeletions(pendingDeletions, desiredProjects) {
|
|
415
|
-
const desiredKeys = new Set(
|
|
416
|
-
desiredProjects.flatMap(
|
|
417
|
-
(project) => project.branches.map(({ branchName }) => pendingCreatedBranchKey({ projectId: project.projectId, branchName }))
|
|
418
|
-
)
|
|
419
|
-
);
|
|
420
|
-
for (const [key, deletion] of pendingDeletions) {
|
|
421
|
-
if (key === pendingCreatedBranchKey(deletion) && desiredKeys.has(key)) pendingDeletions.delete(key);
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
function desiredProjectHasBranch(desiredProjects, pending) {
|
|
425
|
-
return Boolean(
|
|
426
|
-
desiredProjects.find(({ projectId }) => projectId === pending.projectId)?.branches.some(({ branchName }) => branchName === pending.branchName)
|
|
427
|
-
);
|
|
428
|
-
}
|
|
429
|
-
function mergePendingCreatedBranches(...groups) {
|
|
430
|
-
const byKey = /* @__PURE__ */ new Map();
|
|
431
|
-
for (const pending of groups.flat()) {
|
|
432
|
-
const normalized = normalizePendingCreatedBranches([pending])[0];
|
|
433
|
-
const key = pendingCreatedBranchKey(normalized);
|
|
434
|
-
const existing = byKey.get(key);
|
|
435
|
-
if (existing?.branchId && normalized.branchId && existing.branchId !== normalized.branchId) {
|
|
436
|
-
throw new Error(`Pending local branch ${normalized.projectId}/${normalized.branchName} changed durable branch id`);
|
|
437
|
-
}
|
|
438
|
-
byKey.set(key, normalized.branchId ? normalized : existing ?? normalized);
|
|
439
|
-
}
|
|
440
|
-
return [...byKey.values()].sort(
|
|
441
|
-
(left, right) => left.projectId.localeCompare(right.projectId) || left.branchName.localeCompare(right.branchName)
|
|
442
|
-
);
|
|
443
|
-
}
|
|
444
|
-
function primaryBranch(project) {
|
|
445
|
-
if (project.branches.some(({ branchName }) => branchName === "main")) return "main";
|
|
446
|
-
if (project.branches.some(({ branchName }) => branchName === project.defaultBranch)) return project.defaultBranch;
|
|
447
|
-
return project.branches[0]?.branchName ?? null;
|
|
448
|
-
}
|
|
449
|
-
function branchIsCurrentlyDesired(desiredProjects, projectId, branchName) {
|
|
450
|
-
return Boolean(
|
|
451
|
-
desiredProjects.find((project) => project.projectId === projectId)?.branches.some((branch) => branch.branchName === branchName)
|
|
452
|
-
);
|
|
453
|
-
}
|
|
454
|
-
function tombstoneTargetIsCurrentlyDesired(desiredProjects, tombstone) {
|
|
455
|
-
const project = desiredProjects.find(
|
|
456
|
-
(candidate) => candidate.projectId === tombstone.projectId && samePathSegments(candidate.checkoutPathSegments, tombstone.checkoutPathSegments)
|
|
457
|
-
);
|
|
458
|
-
if (!project) return false;
|
|
459
|
-
return tombstone.kind === "project" || project.branches.some(({ branchName }) => branchName === tombstone.branchName);
|
|
460
|
-
}
|
|
461
|
-
function tombstoneManagedPath(projectsRoot, tombstone) {
|
|
462
|
-
return tombstone.kind === "branch" ? managedBranchPath(projectsRoot, tombstone.checkoutPathSegments, tombstone.branchName) : managedProjectRoot(projectsRoot, tombstone.checkoutPathSegments);
|
|
463
|
-
}
|
|
464
|
-
function effectiveDesiredProjects(projectsRoot, desiredProjects, tombstones) {
|
|
465
|
-
const blockedPaths = tombstones.map((tombstone) => tombstoneManagedPath(projectsRoot, tombstone));
|
|
466
|
-
return desiredProjects.flatMap((project) => {
|
|
467
|
-
const projectRoot = managedProjectRoot(projectsRoot, project.checkoutPathSegments);
|
|
468
|
-
if (blockedPaths.some((blockedPath) => blockedPath === projectRoot)) return [];
|
|
469
|
-
return [
|
|
470
|
-
{
|
|
471
|
-
...project,
|
|
472
|
-
checkoutPathSegments: [...project.checkoutPathSegments],
|
|
473
|
-
branches: project.branches.filter(({ branchName }) => {
|
|
474
|
-
const branchPath = managedBranchPath(projectsRoot, project.checkoutPathSegments, branchName);
|
|
475
|
-
return !blockedPaths.some((blockedPath) => pathsOverlap(blockedPath, branchPath));
|
|
476
|
-
})
|
|
477
|
-
}
|
|
478
|
-
];
|
|
479
|
-
});
|
|
480
|
-
}
|
|
481
|
-
function stateView(projectsRoot, state) {
|
|
482
|
-
const effectiveProjects = effectiveDesiredProjects(projectsRoot, state.desiredProjects, state.tombstones);
|
|
483
|
-
const effectiveByPath = new Map(effectiveProjects.map((project) => [project.checkoutPathSegments.join("/"), project]));
|
|
484
|
-
const pendingTreeDeletions = state.tombstones.flatMap((tombstone) => {
|
|
485
|
-
if (tombstone.treePublishedHead) return [];
|
|
486
|
-
if (tombstone.kind === "project") {
|
|
487
|
-
return [
|
|
488
|
-
{
|
|
489
|
-
tombstoneId: tombstone.id,
|
|
490
|
-
kind: "project",
|
|
491
|
-
projectId: tombstone.projectId,
|
|
492
|
-
checkoutPathSegments: [...tombstone.checkoutPathSegments],
|
|
493
|
-
branchNames: [...tombstone.branchNames],
|
|
494
|
-
managedPath: managedProjectRoot(projectsRoot, tombstone.checkoutPathSegments),
|
|
495
|
-
projectDeleted: !state.desiredProjects.some(({ projectId }) => projectId === tombstone.projectId)
|
|
496
|
-
}
|
|
497
|
-
];
|
|
498
|
-
}
|
|
499
|
-
return [
|
|
500
|
-
{
|
|
501
|
-
tombstoneId: tombstone.id,
|
|
502
|
-
kind: "branch",
|
|
503
|
-
projectId: tombstone.projectId,
|
|
504
|
-
checkoutPathSegments: [...tombstone.checkoutPathSegments],
|
|
505
|
-
branchName: tombstone.branchName,
|
|
506
|
-
managedPath: managedBranchPath(projectsRoot, tombstone.checkoutPathSegments, tombstone.branchName),
|
|
507
|
-
primaryBranchName: primaryBranch(
|
|
508
|
-
effectiveByPath.get(tombstone.checkoutPathSegments.join("/")) ?? {
|
|
509
|
-
projectId: tombstone.projectId,
|
|
510
|
-
checkoutPathSegments: tombstone.checkoutPathSegments,
|
|
511
|
-
defaultBranch: "main",
|
|
512
|
-
branches: []
|
|
513
|
-
}
|
|
514
|
-
)
|
|
515
|
-
}
|
|
516
|
-
];
|
|
517
|
-
});
|
|
518
|
-
const pendingMirrorRefDeletions = state.tombstones.flatMap((tombstone) => {
|
|
519
|
-
if (!tombstone.treePublishedHead || tombstone.kind === "project" || branchIsCurrentlyDesired(state.desiredProjects, tombstone.projectId, tombstone.branchName)) {
|
|
520
|
-
return [];
|
|
521
|
-
}
|
|
522
|
-
return tombstoneBranchNames(tombstone).filter((branchName) => !tombstone.mirrorRefsDeleted.includes(branchName)).map((branchName) => ({
|
|
523
|
-
tombstoneId: tombstone.id,
|
|
524
|
-
projectId: tombstone.projectId,
|
|
525
|
-
branchName,
|
|
526
|
-
checkoutPathSegments: [...tombstone.checkoutPathSegments],
|
|
527
|
-
managedPath: managedBranchPath(projectsRoot, tombstone.checkoutPathSegments, branchName),
|
|
528
|
-
primaryBranchName: primaryBranch(
|
|
529
|
-
effectiveByPath.get(tombstone.checkoutPathSegments.join("/")) ?? {
|
|
530
|
-
projectId: tombstone.projectId,
|
|
531
|
-
checkoutPathSegments: tombstone.checkoutPathSegments,
|
|
532
|
-
defaultBranch: "main",
|
|
533
|
-
branches: []
|
|
534
|
-
}
|
|
535
|
-
),
|
|
536
|
-
treePublishedHead: tombstone.treePublishedHead
|
|
537
|
-
}));
|
|
538
|
-
});
|
|
539
|
-
return {
|
|
540
|
-
desiredProjects: cloneDesiredProjects(state.desiredProjects),
|
|
541
|
-
effectiveDesiredProjects: effectiveProjects,
|
|
542
|
-
locallyPendingCreatedBranches: state.locallyPendingCreatedBranches.map((pending) => ({ ...pending })),
|
|
543
|
-
enabledRepositoryTransitions: state.enabledRepositoryTransitions.map((transition) => ({ ...transition })),
|
|
544
|
-
tombstones: cloneTombstones(state.tombstones),
|
|
545
|
-
pendingTreeDeletions,
|
|
546
|
-
pendingMirrorRefDeletions
|
|
547
|
-
};
|
|
548
|
-
}
|
|
549
|
-
function isRealDirectory(candidate) {
|
|
550
|
-
try {
|
|
551
|
-
return fs.lstatSync(candidate).isDirectory();
|
|
552
|
-
} catch {
|
|
553
|
-
return false;
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
function directoryEntries(directory) {
|
|
557
|
-
if (!isRealDirectory(directory)) return null;
|
|
558
|
-
try {
|
|
559
|
-
return fs.readdirSync(directory, { withFileTypes: true });
|
|
560
|
-
} catch {
|
|
561
|
-
return null;
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
function canonicalEncodedBranchName(entryName) {
|
|
565
|
-
try {
|
|
566
|
-
const branchName = decodeURIComponent(entryName);
|
|
567
|
-
validateManagedBranchName(branchName);
|
|
568
|
-
return encodeURIComponent(branchName) === entryName ? branchName : null;
|
|
569
|
-
} catch {
|
|
570
|
-
return null;
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
function validOuterProjectId(entryName) {
|
|
574
|
-
try {
|
|
575
|
-
return validateProjectId(entryName);
|
|
576
|
-
} catch {
|
|
577
|
-
return null;
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
function discoveredOuterProject(projects, projectId) {
|
|
581
|
-
const existing = projects.get(projectId);
|
|
582
|
-
if (existing) return existing;
|
|
583
|
-
const created = {
|
|
584
|
-
projectId,
|
|
585
|
-
projectTreePath: null,
|
|
586
|
-
projectTreeRootIsWhollyManaged: false,
|
|
587
|
-
planTreePath: null,
|
|
588
|
-
planTreeRootIsWhollyManaged: false,
|
|
589
|
-
branches: /* @__PURE__ */ new Map()
|
|
590
|
-
};
|
|
591
|
-
projects.set(projectId, created);
|
|
592
|
-
return created;
|
|
593
|
-
}
|
|
594
|
-
function discoveredOuterBranch(project, branchName) {
|
|
595
|
-
const existing = project.branches.get(branchName);
|
|
596
|
-
if (existing) return existing;
|
|
597
|
-
const created = { branchName, projectTreePath: null, planTreePath: null };
|
|
598
|
-
project.branches.set(branchName, created);
|
|
599
|
-
return created;
|
|
600
|
-
}
|
|
601
|
-
function discoverStaleOuterProjectWorkspaceEntries(input) {
|
|
602
|
-
const workspaceShadowRoot = path.resolve(input.workspaceShadowRoot);
|
|
603
|
-
const desiredProjects = normalizeDesiredProjects(input.desiredProjects);
|
|
604
|
-
applyPendingCreatedBranches(desiredProjects, input.locallyPendingCreatedBranches ?? []);
|
|
605
|
-
const desiredProjectIds = new Set(desiredProjects.map(({ projectId }) => projectId));
|
|
606
|
-
const desiredBranchKeys = new Set(
|
|
607
|
-
desiredProjects.flatMap((project) => project.branches.map(({ branchName }) => `${project.projectId}\0${branchName}`))
|
|
608
|
-
);
|
|
609
|
-
const discovered = /* @__PURE__ */ new Map();
|
|
610
|
-
const projectsContainer = path.join(workspaceShadowRoot, "projects");
|
|
611
|
-
for (const projectEntry of directoryEntries(projectsContainer) ?? []) {
|
|
612
|
-
const projectId = validOuterProjectId(projectEntry.name);
|
|
613
|
-
const projectTreePath = path.join(projectsContainer, projectEntry.name);
|
|
614
|
-
if (!projectId || !projectEntry.isDirectory() || !isRealDirectory(projectTreePath)) continue;
|
|
615
|
-
const project = discoveredOuterProject(discovered, projectId);
|
|
616
|
-
project.projectTreePath = projectTreePath;
|
|
617
|
-
const projectEntries = directoryEntries(projectTreePath);
|
|
618
|
-
const branchesPath = path.join(projectTreePath, "branches");
|
|
619
|
-
const branchEntries = directoryEntries(branchesPath);
|
|
620
|
-
project.projectTreeRootIsWhollyManaged = Boolean(
|
|
621
|
-
projectEntries && projectEntries.every((entry) => entry.name === "branches" && entry.isDirectory()) && branchEntries
|
|
622
|
-
);
|
|
623
|
-
if (!branchEntries) continue;
|
|
624
|
-
for (const branchEntry of branchEntries) {
|
|
625
|
-
const branchName = canonicalEncodedBranchName(branchEntry.name);
|
|
626
|
-
const branchTreePath = path.join(branchesPath, branchEntry.name);
|
|
627
|
-
if (!branchName || !branchEntry.isDirectory() || !isRealDirectory(branchTreePath)) {
|
|
628
|
-
project.projectTreeRootIsWhollyManaged = false;
|
|
629
|
-
continue;
|
|
630
|
-
}
|
|
631
|
-
discoveredOuterBranch(project, branchName).projectTreePath = branchTreePath;
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
const plansContainer = path.join(workspaceShadowRoot, "plans");
|
|
635
|
-
for (const projectEntry of directoryEntries(plansContainer) ?? []) {
|
|
636
|
-
const projectId = validOuterProjectId(projectEntry.name);
|
|
637
|
-
const planTreePath = path.join(plansContainer, projectEntry.name);
|
|
638
|
-
if (!projectId || !projectEntry.isDirectory() || !isRealDirectory(planTreePath)) continue;
|
|
639
|
-
const project = discoveredOuterProject(discovered, projectId);
|
|
640
|
-
project.planTreePath = planTreePath;
|
|
641
|
-
const planEntries = directoryEntries(planTreePath);
|
|
642
|
-
project.planTreeRootIsWhollyManaged = Boolean(planEntries);
|
|
643
|
-
if (!planEntries) continue;
|
|
644
|
-
const desiredBranchNames = desiredProjects.find((desiredProject) => desiredProject.projectId === projectId)?.branches.map(({ branchName }) => branchName) ?? [];
|
|
645
|
-
for (const planEntry of planEntries) {
|
|
646
|
-
const branchPlanPath = path.join(planTreePath, planEntry.name);
|
|
647
|
-
const containsDesiredLegacyNestedPlan = desiredBranchNames.some((desiredBranchName) => {
|
|
648
|
-
const segments = desiredBranchName.split("/");
|
|
649
|
-
return segments.length > 1 && segments[0] === planEntry.name && isRealDirectory(path.join(planTreePath, ...segments));
|
|
650
|
-
});
|
|
651
|
-
if (containsDesiredLegacyNestedPlan) continue;
|
|
652
|
-
const branchName = canonicalEncodedBranchName(planEntry.name);
|
|
653
|
-
if (!branchName || !planEntry.isDirectory() || !isRealDirectory(branchPlanPath)) {
|
|
654
|
-
project.planTreeRootIsWhollyManaged = false;
|
|
655
|
-
continue;
|
|
656
|
-
}
|
|
657
|
-
discoveredOuterBranch(project, branchName).planTreePath = branchPlanPath;
|
|
658
|
-
}
|
|
659
|
-
}
|
|
660
|
-
const stale = [];
|
|
661
|
-
for (const project of [...discovered.values()].sort((left, right) => left.projectId.localeCompare(right.projectId))) {
|
|
662
|
-
const branches = [...project.branches.values()].sort((left, right) => left.branchName.localeCompare(right.branchName));
|
|
663
|
-
if (!desiredProjectIds.has(project.projectId)) {
|
|
664
|
-
const projectTreePath = project.projectTreeRootIsWhollyManaged ? project.projectTreePath : null;
|
|
665
|
-
const planTreePath = project.planTreeRootIsWhollyManaged ? project.planTreePath : null;
|
|
666
|
-
const individuallySafeBranches = branches.map((branch) => ({
|
|
667
|
-
branchName: branch.branchName,
|
|
668
|
-
projectTreePath: projectTreePath ? null : branch.projectTreePath,
|
|
669
|
-
planTreePath: planTreePath ? null : branch.planTreePath
|
|
670
|
-
}));
|
|
671
|
-
if (projectTreePath || planTreePath || individuallySafeBranches.some((branch) => branch.projectTreePath || branch.planTreePath)) {
|
|
672
|
-
stale.push({
|
|
673
|
-
kind: "project",
|
|
674
|
-
projectId: project.projectId,
|
|
675
|
-
projectTreePath,
|
|
676
|
-
planTreePath,
|
|
677
|
-
branches: individuallySafeBranches
|
|
678
|
-
});
|
|
679
|
-
}
|
|
680
|
-
continue;
|
|
681
|
-
}
|
|
682
|
-
for (const branch of branches) {
|
|
683
|
-
if (desiredBranchKeys.has(`${project.projectId}\0${branch.branchName}`)) continue;
|
|
684
|
-
stale.push({
|
|
685
|
-
kind: "branch",
|
|
686
|
-
projectId: project.projectId,
|
|
687
|
-
branchName: branch.branchName,
|
|
688
|
-
projectTreePath: branch.projectTreePath,
|
|
689
|
-
planTreePath: branch.planTreePath
|
|
690
|
-
});
|
|
691
|
-
}
|
|
692
|
-
}
|
|
693
|
-
return stale;
|
|
694
|
-
}
|
|
695
|
-
class ProjectWorkspaceStateStore {
|
|
696
|
-
stateRoot;
|
|
697
|
-
projectsRoot;
|
|
698
|
-
statePath;
|
|
699
|
-
constructor(input) {
|
|
700
|
-
this.stateRoot = path.resolve(input.stateRoot);
|
|
701
|
-
this.projectsRoot = path.resolve(input.projectsRoot);
|
|
702
|
-
this.statePath = path.join(this.stateRoot, PROJECT_WORKSPACE_STATE_FILE);
|
|
703
|
-
}
|
|
704
|
-
read() {
|
|
705
|
-
return stateView(this.projectsRoot, readState(this.statePath));
|
|
706
|
-
}
|
|
707
|
-
discoverStaleOuterEntries(workspaceShadowRoot) {
|
|
708
|
-
const state = readState(this.statePath);
|
|
709
|
-
return discoverStaleOuterProjectWorkspaceEntries({
|
|
710
|
-
workspaceShadowRoot,
|
|
711
|
-
desiredProjects: state.desiredProjects,
|
|
712
|
-
locallyPendingCreatedBranches: state.locallyPendingCreatedBranches
|
|
713
|
-
});
|
|
714
|
-
}
|
|
715
|
-
async reconcile(input) {
|
|
716
|
-
const previous = readState(this.statePath);
|
|
717
|
-
const authoritativeDesiredProjects = normalizeDesiredProjects(input.desiredProjects);
|
|
718
|
-
for (const project of authoritativeDesiredProjects) {
|
|
719
|
-
const occupyingProjectId = previous.desiredProjects.find(
|
|
720
|
-
(candidate) => candidate.projectId !== project.projectId && samePathSegments(candidate.checkoutPathSegments, project.checkoutPathSegments)
|
|
721
|
-
)?.projectId ?? previous.tombstones.find(
|
|
722
|
-
(candidate) => candidate.projectId !== project.projectId && samePathSegments(candidate.checkoutPathSegments, project.checkoutPathSegments)
|
|
723
|
-
)?.projectId;
|
|
724
|
-
if (occupyingProjectId) {
|
|
725
|
-
throw new ProjectWorkspaceCheckoutPathCollisionError({
|
|
726
|
-
projectId: project.projectId,
|
|
727
|
-
occupyingProjectId,
|
|
728
|
-
checkoutPathSegments: project.checkoutPathSegments
|
|
729
|
-
});
|
|
730
|
-
}
|
|
731
|
-
}
|
|
732
|
-
const authoritativeProjectIds = new Set(authoritativeDesiredProjects.map(({ projectId }) => projectId));
|
|
733
|
-
const preserveOnlyBranches = normalizePreserveOnlyBranches(input.preserveOnlyBranches ?? []);
|
|
734
|
-
for (const preserved of preserveOnlyBranches) {
|
|
735
|
-
const project = authoritativeDesiredProjects.find(({ projectId }) => projectId === preserved.projectId);
|
|
736
|
-
if (!project) {
|
|
737
|
-
throw new Error(`Preserve-only branch ${preserved.projectId}/${preserved.branchName} has no desired project`);
|
|
738
|
-
}
|
|
739
|
-
if (project.branches.some(({ branchName }) => branchName === preserved.branchName)) {
|
|
740
|
-
throw new Error(`Preserve-only branch ${preserved.projectId}/${preserved.branchName} is already active`);
|
|
741
|
-
}
|
|
742
|
-
}
|
|
743
|
-
const locallyPendingCreatedBranches = mergePendingCreatedBranches(
|
|
744
|
-
previous.locallyPendingCreatedBranches,
|
|
745
|
-
input.locallyPendingCreatedBranches ?? []
|
|
746
|
-
).filter(
|
|
747
|
-
(pending) => authoritativeProjectIds.has(pending.projectId) && !desiredProjectHasBranch(authoritativeDesiredProjects, pending)
|
|
748
|
-
);
|
|
749
|
-
for (const pending of locallyPendingCreatedBranches) {
|
|
750
|
-
const currentProject = previous.desiredProjects.find(({ projectId }) => projectId === pending.projectId);
|
|
751
|
-
const incomingProject = authoritativeDesiredProjects.find(({ projectId }) => projectId === pending.projectId);
|
|
752
|
-
if (currentProject && incomingProject && !samePathSegments(currentProject.checkoutPathSegments, incomingProject.checkoutPathSegments)) {
|
|
753
|
-
throw new ProjectWorkspacePendingBranchPathChangeError({
|
|
754
|
-
projectId: pending.projectId,
|
|
755
|
-
branchName: pending.branchName,
|
|
756
|
-
currentCheckoutPathSegments: currentProject.checkoutPathSegments,
|
|
757
|
-
incomingCheckoutPathSegments: incomingProject.checkoutPathSegments
|
|
758
|
-
});
|
|
759
|
-
}
|
|
760
|
-
}
|
|
761
|
-
const desiredProjects = cloneDesiredProjects(authoritativeDesiredProjects);
|
|
762
|
-
applyPendingCreatedBranches(desiredProjects, preserveOnlyBranches);
|
|
763
|
-
applyPendingCreatedBranches(desiredProjects, locallyPendingCreatedBranches);
|
|
764
|
-
const desiredById = new Map(desiredProjects.map((project) => [project.projectId, project]));
|
|
765
|
-
const tombstones = cloneTombstones(previous.tombstones);
|
|
766
|
-
for (const previousProject of previous.desiredProjects) {
|
|
767
|
-
const desiredProject = desiredById.get(previousProject.projectId);
|
|
768
|
-
if (!desiredProject || !samePathSegments(previousProject.checkoutPathSegments, desiredProject.checkoutPathSegments)) {
|
|
769
|
-
addProjectTombstone(tombstones, previousProject);
|
|
770
|
-
continue;
|
|
771
|
-
}
|
|
772
|
-
const desiredBranchNames = new Set(desiredProject.branches.map(({ branchName }) => branchName));
|
|
773
|
-
for (const { branchName } of previousProject.branches) {
|
|
774
|
-
if (!desiredBranchNames.has(branchName)) addBranchTombstone(tombstones, previousProject, branchName);
|
|
775
|
-
}
|
|
776
|
-
}
|
|
777
|
-
tombstones.splice(
|
|
778
|
-
0,
|
|
779
|
-
tombstones.length,
|
|
780
|
-
...tombstones.filter(
|
|
781
|
-
(tombstone) => !tombstoneTargetIsCurrentlyDesired(desiredProjects, tombstone) && (!tombstone.treePublishedHead || tombstone.kind === "branch" && !branchIsCurrentlyDesired(desiredProjects, tombstone.projectId, tombstone.branchName))
|
|
782
|
-
)
|
|
783
|
-
);
|
|
784
|
-
const nextState = normalizeState({
|
|
785
|
-
version: PROJECT_WORKSPACE_STATE_VERSION,
|
|
786
|
-
desiredProjects,
|
|
787
|
-
locallyPendingCreatedBranches,
|
|
788
|
-
enabledRepositoryTransitions: previous.enabledRepositoryTransitions.filter(({ projectId }) => authoritativeProjectIds.has(projectId)),
|
|
789
|
-
tombstones
|
|
790
|
-
});
|
|
791
|
-
await writeStateAtomically(this.stateRoot, this.statePath, nextState);
|
|
792
|
-
return stateView(this.projectsRoot, nextState);
|
|
793
|
-
}
|
|
794
|
-
/** Record a successful enabled reseed without letting disabled config consume it. */
|
|
795
|
-
async recordEnabledRepositoryTransition(input) {
|
|
796
|
-
const [transition] = normalizeEnabledRepositoryTransitions([input]);
|
|
797
|
-
const state = readState(this.statePath);
|
|
798
|
-
if (!state.desiredProjects.some(({ projectId }) => projectId === transition.projectId)) {
|
|
799
|
-
throw new Error(`Cannot record an enabled repository transition for missing project ${transition.projectId}`);
|
|
800
|
-
}
|
|
801
|
-
state.enabledRepositoryTransitions = state.enabledRepositoryTransitions.filter(({ projectId }) => projectId !== transition.projectId);
|
|
802
|
-
state.enabledRepositoryTransitions.push(transition);
|
|
803
|
-
state.enabledRepositoryTransitions.sort((left, right) => left.projectId.localeCompare(right.projectId));
|
|
804
|
-
await writeStateAtomically(this.stateRoot, this.statePath, state);
|
|
805
|
-
return stateView(this.projectsRoot, state);
|
|
806
|
-
}
|
|
807
|
-
/** Persist the create intent before invoking Git so a crash cannot orphan an untracked worktree. */
|
|
808
|
-
async recordPendingCreatedBranch(input) {
|
|
809
|
-
const [pending] = normalizePendingCreatedBranches([input]);
|
|
810
|
-
const state = readState(this.statePath);
|
|
811
|
-
const existingPending = state.locallyPendingCreatedBranches.find(
|
|
812
|
-
(candidate) => pendingCreatedBranchKey(candidate) === pendingCreatedBranchKey(pending)
|
|
813
|
-
);
|
|
814
|
-
if (existingPending) {
|
|
815
|
-
if (existingPending.branchId !== pending.branchId) {
|
|
816
|
-
throw new Error(`Pending project branch ${pending.branchName} belongs to another durable branch incarnation`);
|
|
817
|
-
}
|
|
818
|
-
return stateView(this.projectsRoot, state);
|
|
819
|
-
}
|
|
820
|
-
const project = state.desiredProjects.find(({ projectId }) => projectId === pending.projectId);
|
|
821
|
-
if (!project) throw new Error(`Cannot record pending branch ${pending.projectId}/${pending.branchName} without a desired project`);
|
|
822
|
-
if (project.branches.some(({ branchName }) => branchName === pending.branchName)) {
|
|
823
|
-
throw new Error(`Cannot record pending branch ${pending.projectId}/${pending.branchName}: branch is already desired`);
|
|
824
|
-
}
|
|
825
|
-
project.branches.push({ branchName: pending.branchName });
|
|
826
|
-
project.branches = normalizeBranchNames(project.branches, `Project ${project.projectId}`).map((branchName) => ({ branchName }));
|
|
827
|
-
state.locallyPendingCreatedBranches.push(pending);
|
|
828
|
-
state.locallyPendingCreatedBranches.sort(
|
|
829
|
-
(left, right) => left.projectId.localeCompare(right.projectId) || left.branchName.localeCompare(right.branchName)
|
|
830
|
-
);
|
|
831
|
-
await writeStateAtomically(this.stateRoot, this.statePath, state);
|
|
832
|
-
return stateView(this.projectsRoot, state);
|
|
833
|
-
}
|
|
834
|
-
/** Roll back an intent only when Git failed before creating its branch. */
|
|
835
|
-
async rollbackPendingCreatedBranch(input) {
|
|
836
|
-
const [pending] = normalizePendingCreatedBranches([input]);
|
|
837
|
-
const state = readState(this.statePath);
|
|
838
|
-
const key = pendingCreatedBranchKey(pending);
|
|
839
|
-
const existing = state.locallyPendingCreatedBranches.find((candidate) => pendingCreatedBranchKey(candidate) === key);
|
|
840
|
-
if (!existing || pending.branchId && existing.branchId !== pending.branchId) {
|
|
841
|
-
return stateView(this.projectsRoot, state);
|
|
842
|
-
}
|
|
843
|
-
state.locallyPendingCreatedBranches = state.locallyPendingCreatedBranches.filter(
|
|
844
|
-
(candidate) => pendingCreatedBranchKey(candidate) !== key
|
|
845
|
-
);
|
|
846
|
-
const project = state.desiredProjects.find(({ projectId }) => projectId === pending.projectId);
|
|
847
|
-
if (project) project.branches = project.branches.filter(({ branchName }) => branchName !== pending.branchName);
|
|
848
|
-
await writeStateAtomically(this.stateRoot, this.statePath, state);
|
|
849
|
-
return stateView(this.projectsRoot, state);
|
|
850
|
-
}
|
|
851
|
-
/** Clear a create intent after an authoritative desired config contains it. */
|
|
852
|
-
async clearPendingCreatedBranch(input) {
|
|
853
|
-
const [pending] = normalizePendingCreatedBranches([input]);
|
|
854
|
-
const state = readState(this.statePath);
|
|
855
|
-
const key = pendingCreatedBranchKey(pending);
|
|
856
|
-
if (!state.locallyPendingCreatedBranches.some((candidate) => pendingCreatedBranchKey(candidate) === key)) {
|
|
857
|
-
return stateView(this.projectsRoot, state);
|
|
858
|
-
}
|
|
859
|
-
state.locallyPendingCreatedBranches = state.locallyPendingCreatedBranches.filter(
|
|
860
|
-
(candidate) => pendingCreatedBranchKey(candidate) !== key
|
|
861
|
-
);
|
|
862
|
-
await writeStateAtomically(this.stateRoot, this.statePath, state);
|
|
863
|
-
return stateView(this.projectsRoot, state);
|
|
864
|
-
}
|
|
865
|
-
/** Persist a branch tombstone before mutating its linked worktree or local ref. */
|
|
866
|
-
async beginBranchDeletion(input) {
|
|
867
|
-
const [target] = normalizePendingCreatedBranches([input]);
|
|
868
|
-
const state = readState(this.statePath);
|
|
869
|
-
const project = state.desiredProjects.find(({ projectId }) => projectId === target.projectId);
|
|
870
|
-
if (!project)
|
|
871
|
-
throw new Error(`Cannot begin branch deletion for ${target.projectId}/${target.branchName}: desired project is missing`);
|
|
872
|
-
const existingTombstone = state.tombstones.find(
|
|
873
|
-
(tombstone) => tombstone.kind === "branch" && tombstone.projectId === target.projectId && samePathSegments(tombstone.checkoutPathSegments, project.checkoutPathSegments) && tombstone.branchName === target.branchName
|
|
874
|
-
);
|
|
875
|
-
if (existingTombstone) return stateView(this.projectsRoot, state);
|
|
876
|
-
addBranchTombstone(state.tombstones, project, target.branchName);
|
|
877
|
-
project.branches = project.branches.filter(({ branchName }) => branchName !== target.branchName);
|
|
878
|
-
const key = pendingCreatedBranchKey(target);
|
|
879
|
-
state.locallyPendingCreatedBranches = state.locallyPendingCreatedBranches.filter(
|
|
880
|
-
(candidate) => pendingCreatedBranchKey(candidate) !== key
|
|
881
|
-
);
|
|
882
|
-
await writeStateAtomically(this.stateRoot, this.statePath, state);
|
|
883
|
-
return stateView(this.projectsRoot, state);
|
|
884
|
-
}
|
|
885
|
-
async recordTreePublication(input) {
|
|
886
|
-
if (!GIT_OBJECT_ID_PATTERN.test(input.publishedHead)) throw new Error("Invalid published workspace head");
|
|
887
|
-
const state = readState(this.statePath);
|
|
888
|
-
const ids = new Set(input.tombstoneIds);
|
|
889
|
-
let changed = false;
|
|
890
|
-
for (const tombstone of state.tombstones) {
|
|
891
|
-
if (!ids.has(tombstone.id) || tombstone.treePublishedHead) continue;
|
|
892
|
-
tombstone.treePublishedHead = input.publishedHead;
|
|
893
|
-
changed = true;
|
|
894
|
-
}
|
|
895
|
-
state.tombstones = state.tombstones.filter(
|
|
896
|
-
(tombstone) => !tombstone.treePublishedHead || tombstone.kind === "branch" && !branchIsCurrentlyDesired(state.desiredProjects, tombstone.projectId, tombstone.branchName)
|
|
897
|
-
);
|
|
898
|
-
if (changed) await writeStateAtomically(this.stateRoot, this.statePath, state);
|
|
899
|
-
return stateView(this.projectsRoot, state);
|
|
900
|
-
}
|
|
901
|
-
async recordMirrorRefDeletion(input) {
|
|
902
|
-
validateManagedBranchName(input.branchName);
|
|
903
|
-
const state = readState(this.statePath);
|
|
904
|
-
const tombstone = state.tombstones.find(({ id }) => id === input.tombstoneId);
|
|
905
|
-
if (!tombstone) return stateView(this.projectsRoot, state);
|
|
906
|
-
const branchNames = tombstoneBranchNames(tombstone);
|
|
907
|
-
if (!branchNames.includes(input.branchName)) {
|
|
908
|
-
throw new Error(`Branch ${input.branchName} does not belong to deletion ${input.tombstoneId}`);
|
|
909
|
-
}
|
|
910
|
-
if (!tombstone.treePublishedHead) {
|
|
911
|
-
throw new Error(`Cannot record mirror deletion before workspace tree publication for ${input.tombstoneId}`);
|
|
912
|
-
}
|
|
913
|
-
if (!tombstone.mirrorRefsDeleted.includes(input.branchName)) {
|
|
914
|
-
tombstone.mirrorRefsDeleted.push(input.branchName);
|
|
915
|
-
tombstone.mirrorRefsDeleted.sort();
|
|
916
|
-
}
|
|
917
|
-
if (branchNames.every((branchName) => tombstone.mirrorRefsDeleted.includes(branchName))) {
|
|
918
|
-
state.tombstones = state.tombstones.filter(({ id }) => id !== tombstone.id);
|
|
919
|
-
}
|
|
920
|
-
await writeStateAtomically(this.stateRoot, this.statePath, state);
|
|
921
|
-
return stateView(this.projectsRoot, state);
|
|
922
|
-
}
|
|
923
|
-
}
|
|
924
|
-
const projectWorkspaceStateTestHarness = {
|
|
925
|
-
writeEmptyStateAtDurableRootBoundary(stateRoot, afterStateRootFsync) {
|
|
926
|
-
return writeStateAtomically(stateRoot, path.join(stateRoot, PROJECT_WORKSPACE_STATE_FILE), EMPTY_STATE, afterStateRootFsync);
|
|
927
|
-
}
|
|
928
|
-
};
|
|
929
|
-
export {
|
|
930
|
-
PROJECT_WORKSPACE_STATE_FILE,
|
|
931
|
-
ProjectWorkspaceCheckoutPathCollisionError,
|
|
932
|
-
ProjectWorkspacePendingBranchPathChangeError,
|
|
933
|
-
ProjectWorkspaceStateStore,
|
|
934
|
-
discoverStaleOuterProjectWorkspaceEntries,
|
|
935
|
-
projectWorkspaceStateTestHarness,
|
|
936
|
-
pruneAuthoritativelyDesiredBranchDeletions
|
|
937
|
-
};
|