@ricsam/r5d-worker 0.0.78 → 0.0.80
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 +6 -0
- package/dist/cjs/main.cjs +1550 -335
- package/dist/cjs/managed-paths.cjs +101 -1
- package/dist/cjs/package.json +1 -1
- package/dist/cjs/project-workspace-state.cjs +184 -5
- package/dist/cjs/project-worktrees.cjs +676 -58
- package/dist/cjs/registry-auth.cjs +310 -0
- package/dist/cjs/repository-transition-policy.cjs +49 -0
- package/dist/cjs/supervisor.cjs +62 -11
- package/dist/cjs/working-tree-mirror.cjs +196 -36
- package/dist/cjs/workspace-automatic-sync-policy.cjs +69 -0
- package/dist/cjs/workspace-branch-incarnation-policy.cjs +37 -0
- package/dist/cjs/workspace-command-sync-policy.cjs +18 -0
- package/dist/cjs/workspace-git-sync.cjs +1037 -54
- package/dist/cjs/workspace-mount-boundary.cjs +71 -0
- package/dist/cjs/workspace-path-move.cjs +195 -0
- package/dist/cjs/workspace-preserve-only-policy.cjs +36 -0
- package/dist/cjs/workspace-project-config-policy.cjs +46 -0
- package/dist/cjs/workspace-publication-evidence.cjs +46 -0
- package/dist/mjs/main.mjs +1584 -341
- package/dist/mjs/managed-paths.mjs +100 -1
- package/dist/mjs/package.json +1 -1
- package/dist/mjs/project-workspace-state.mjs +181 -5
- package/dist/mjs/project-worktrees.mjs +667 -57
- package/dist/mjs/registry-auth.mjs +269 -0
- package/dist/mjs/repository-transition-policy.mjs +22 -0
- package/dist/mjs/supervisor.mjs +61 -11
- package/dist/mjs/working-tree-mirror.mjs +195 -36
- package/dist/mjs/workspace-automatic-sync-policy.mjs +40 -0
- package/dist/mjs/workspace-branch-incarnation-policy.mjs +13 -0
- package/dist/mjs/workspace-command-sync-policy.mjs +17 -0
- package/dist/mjs/workspace-git-sync.mjs +1032 -54
- package/dist/mjs/workspace-mount-boundary.mjs +37 -0
- package/dist/mjs/workspace-path-move.mjs +160 -0
- package/dist/mjs/workspace-preserve-only-policy.mjs +11 -0
- package/dist/mjs/workspace-project-config-policy.mjs +21 -0
- package/dist/mjs/workspace-publication-evidence.mjs +21 -0
- package/dist/types/credential-authority-lock-fixture.d.ts +1 -0
- package/dist/types/main.d.ts +175 -1
- package/dist/types/managed-paths.d.ts +11 -0
- package/dist/types/project-workspace-state.d.ts +45 -1
- package/dist/types/project-worktrees.d.ts +119 -2
- package/dist/types/registry-auth.d.ts +47 -0
- package/dist/types/repository-transition-policy.d.ts +26 -0
- package/dist/types/supervisor-daemonized-fixture.d.ts +1 -0
- package/dist/types/supervisor-signal-fixture.d.ts +1 -0
- package/dist/types/supervisor.d.ts +6 -4
- package/dist/types/working-tree-mirror.d.ts +14 -0
- package/dist/types/workspace-automatic-sync-policy.d.ts +37 -0
- package/dist/types/workspace-branch-incarnation-policy.d.ts +14 -0
- package/dist/types/workspace-command-sync-policy.d.ts +9 -0
- package/dist/types/workspace-git-sync.d.ts +33 -3
- package/dist/types/workspace-mount-boundary.d.ts +10 -0
- package/dist/types/workspace-path-move.d.ts +21 -0
- package/dist/types/workspace-preserve-only-policy.d.ts +15 -0
- package/dist/types/workspace-project-config-policy.d.ts +34 -0
- package/dist/types/workspace-publication-evidence.d.ts +17 -0
- package/package.json +1 -1
|
@@ -29,18 +29,26 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
29
29
|
var project_worktrees_exports = {};
|
|
30
30
|
__export(project_worktrees_exports, {
|
|
31
31
|
PROJECT_WORKTREE_SNAPSHOT_PREFIX: () => PROJECT_WORKTREE_SNAPSHOT_PREFIX,
|
|
32
|
-
|
|
32
|
+
ProjectBranchCreationRollbackIncompleteError: () => ProjectBranchCreationRollbackIncompleteError,
|
|
33
|
+
applyObservedProjectMirrorHeads: () => applyObservedProjectMirrorHeads,
|
|
34
|
+
assertProjectMirrorHeadsPushed: () => assertProjectMirrorHeadsPushed,
|
|
33
35
|
createLinkedProjectBranch: () => createLinkedProjectBranch,
|
|
36
|
+
createOrRetryLinkedProjectBranch: () => createOrRetryLinkedProjectBranch,
|
|
34
37
|
deleteLinkedProjectBranch: () => deleteLinkedProjectBranch,
|
|
35
38
|
deleteProjectMirrorBranch: () => deleteProjectMirrorBranch,
|
|
36
39
|
ensureProjectWorktrees: () => ensureProjectWorktrees,
|
|
37
40
|
fastForwardProjectHeadsFromMirror: () => fastForwardProjectHeadsFromMirror,
|
|
38
41
|
hasLinkedProjectWorktreeLayout: () => hasLinkedProjectWorktreeLayout,
|
|
42
|
+
observeProjectMirrorHeads: () => observeProjectMirrorHeads,
|
|
43
|
+
projectBranchMayExistAfterCreateFailure: () => projectBranchMayExistAfterCreateFailure,
|
|
44
|
+
projectOriginBranchName: () => projectOriginBranchName,
|
|
39
45
|
projectWorktreeConfigurationFingerprint: () => projectWorktreeConfigurationFingerprint,
|
|
40
46
|
projectWorktreeOperationInProgress: () => projectWorktreeOperationInProgress,
|
|
41
47
|
projectWorktreesTestHarness: () => projectWorktreesTestHarness,
|
|
42
48
|
pushProjectMirrorHeads: () => pushProjectMirrorHeads,
|
|
43
|
-
|
|
49
|
+
recoverStaleProjectWorktreeSnapshots: () => recoverStaleProjectWorktreeSnapshots,
|
|
50
|
+
removeProjectWorktrees: () => removeProjectWorktrees,
|
|
51
|
+
reseedProjectHeadsFromMirror: () => reseedProjectHeadsFromMirror
|
|
44
52
|
});
|
|
45
53
|
module.exports = __toCommonJS(project_worktrees_exports);
|
|
46
54
|
var import_node_crypto = require("node:crypto");
|
|
@@ -51,9 +59,26 @@ var import_git_process_environment = require("./git-process-environment.cjs");
|
|
|
51
59
|
var import_managed_paths = require("./managed-paths.cjs");
|
|
52
60
|
var import_working_tree_mirror = require("./working-tree-mirror.cjs");
|
|
53
61
|
const PROJECT_WORKTREE_SNAPSHOT_PREFIX = "r5d-project-worktrees-";
|
|
62
|
+
const PROJECT_WORKTREE_SNAPSHOT_MANIFEST = "transaction.json";
|
|
63
|
+
const PROJECT_WORKTREE_SNAPSHOT_MANIFEST_VERSION = 1;
|
|
64
|
+
const MAX_PROJECT_WORKTREE_SNAPSHOT_MANIFEST_BYTES = 64 * 1024;
|
|
65
|
+
const PROJECT_WORKTREE_SNAPSHOT_OWNER_SESSION_ID = (0, import_node_crypto.randomUUID)();
|
|
66
|
+
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
54
67
|
const PROJECT_WORKTREE_SNAPSHOT_NAME = new RegExp(
|
|
55
68
|
`^${PROJECT_WORKTREE_SNAPSHOT_PREFIX.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}([1-9]\\d*)-([A-Za-z0-9]{6})$`
|
|
56
69
|
);
|
|
70
|
+
class ProjectBranchCreationRollbackIncompleteError extends Error {
|
|
71
|
+
branchMayExist = true;
|
|
72
|
+
cleanupFailures;
|
|
73
|
+
constructor(branchName, cleanupFailures, cause) {
|
|
74
|
+
super(`Project branch ${branchName} may still exist after incomplete rollback: ${cleanupFailures.join("; ")}`, { cause });
|
|
75
|
+
this.name = "ProjectBranchCreationRollbackIncompleteError";
|
|
76
|
+
this.cleanupFailures = [...cleanupFailures];
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function projectBranchMayExistAfterCreateFailure(error) {
|
|
80
|
+
return error instanceof ProjectBranchCreationRollbackIncompleteError || typeof error === "object" && error !== null && "branchMayExist" in error && error.branchMayExist === true;
|
|
81
|
+
}
|
|
57
82
|
function projectWorktreeConfigurationFingerprint(input) {
|
|
58
83
|
return (0, import_node_crypto.createHash)("sha256").update(
|
|
59
84
|
JSON.stringify({
|
|
@@ -62,7 +87,12 @@ function projectWorktreeConfigurationFingerprint(input) {
|
|
|
62
87
|
repoHttpUrl: input.repoHttpUrl,
|
|
63
88
|
repoAuthHeader: input.repoAuthHeader,
|
|
64
89
|
defaultBranch: input.defaultBranch,
|
|
65
|
-
|
|
90
|
+
repositoryTransitionId: input.repositoryTransitionId ?? null,
|
|
91
|
+
executionDisabled: input.executionDisabled ?? false,
|
|
92
|
+
mirrorWritesDisabled: input.mirrorWritesDisabled ?? false,
|
|
93
|
+
preserveOnlyBranches: [...input.preserveOnlyBranches ?? []].sort((left, right) => left.branchName.localeCompare(right.branchName)).map(({ branchId, branchName }) => ({ branchId: branchId ?? null, branchName })),
|
|
94
|
+
branches: [...input.branches].sort((left, right) => left.branchName.localeCompare(right.branchName)).map(({ branchId, branchName, sourceBranchName, baseCommitHash }) => ({
|
|
95
|
+
branchId: branchId ?? null,
|
|
66
96
|
branchName,
|
|
67
97
|
sourceBranchName: sourceBranchName ?? null,
|
|
68
98
|
baseCommitHash
|
|
@@ -112,6 +142,27 @@ function branchPath(projectRoot, branchName) {
|
|
|
112
142
|
}
|
|
113
143
|
return result;
|
|
114
144
|
}
|
|
145
|
+
function assertNoSymlinkBranchPathComponents(projectRoot, branchPath2) {
|
|
146
|
+
const resolvedRoot = import_node_path.default.resolve(projectRoot);
|
|
147
|
+
const resolvedBranchPath = import_node_path.default.resolve(branchPath2);
|
|
148
|
+
const relative = import_node_path.default.relative(resolvedRoot, resolvedBranchPath);
|
|
149
|
+
if (relative === ".." || relative.startsWith(`..${import_node_path.default.sep}`) || import_node_path.default.isAbsolute(relative)) {
|
|
150
|
+
throw new Error(`Branch path escapes its project root: ${branchPath2}`);
|
|
151
|
+
}
|
|
152
|
+
let current = resolvedRoot;
|
|
153
|
+
for (const component of ["", ...relative.split(import_node_path.default.sep).filter(Boolean)]) {
|
|
154
|
+
if (component) current = import_node_path.default.join(current, component);
|
|
155
|
+
try {
|
|
156
|
+
if (import_node_fs.default.lstatSync(current).isSymbolicLink()) {
|
|
157
|
+
throw new Error(`Project branch path contains a symbolic link: ${current}`);
|
|
158
|
+
}
|
|
159
|
+
} catch (error) {
|
|
160
|
+
const code = error.code;
|
|
161
|
+
if (code === "ENOENT" || code === "ENOTDIR") return;
|
|
162
|
+
throw error;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
115
166
|
function assertNonOverlappingBranches(branches) {
|
|
116
167
|
const names = branches.map(({ branchName }) => branchName).sort();
|
|
117
168
|
if (new Set(names).size !== names.length) throw new Error("Project worktree branch names must be unique");
|
|
@@ -164,26 +215,310 @@ function hasCompatibleLinkedProjectWorktreeLayout(input) {
|
|
|
164
215
|
return kind === null || kind === "file" && commonGitDirectory(checkoutPath) === primaryCommonDir;
|
|
165
216
|
});
|
|
166
217
|
}
|
|
167
|
-
function
|
|
168
|
-
|
|
218
|
+
function fsyncDirectory(directory) {
|
|
219
|
+
const descriptor = import_node_fs.default.openSync(directory, "r");
|
|
220
|
+
try {
|
|
221
|
+
import_node_fs.default.fsyncSync(descriptor);
|
|
222
|
+
} finally {
|
|
223
|
+
import_node_fs.default.closeSync(descriptor);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
function fsyncTree(targetPath) {
|
|
227
|
+
const stat = import_node_fs.default.lstatSync(targetPath);
|
|
228
|
+
if (stat.isSymbolicLink()) return;
|
|
229
|
+
if (stat.isDirectory()) {
|
|
230
|
+
for (const entry of import_node_fs.default.readdirSync(targetPath).sort()) fsyncTree(import_node_path.default.join(targetPath, entry));
|
|
231
|
+
fsyncDirectory(targetPath);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
if (!stat.isFile()) return;
|
|
235
|
+
const descriptor = import_node_fs.default.openSync(targetPath, "r");
|
|
236
|
+
try {
|
|
237
|
+
import_node_fs.default.fsyncSync(descriptor);
|
|
238
|
+
} finally {
|
|
239
|
+
import_node_fs.default.closeSync(descriptor);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
function fsyncDirectoryChain(directory, root) {
|
|
243
|
+
const resolvedDirectory = import_node_path.default.resolve(directory);
|
|
244
|
+
const resolvedRoot = import_node_path.default.resolve(root);
|
|
245
|
+
const relative = import_node_path.default.relative(resolvedRoot, resolvedDirectory);
|
|
246
|
+
if (relative === ".." || relative.startsWith(`..${import_node_path.default.sep}`) || import_node_path.default.isAbsolute(relative)) {
|
|
247
|
+
throw new Error(`Directory is outside its durability root: ${resolvedDirectory}`);
|
|
248
|
+
}
|
|
249
|
+
let current = resolvedDirectory;
|
|
250
|
+
while (true) {
|
|
251
|
+
assertRealDirectory(current, "Durability path");
|
|
252
|
+
fsyncDirectory(current);
|
|
253
|
+
if (current === resolvedRoot) break;
|
|
254
|
+
current = import_node_path.default.dirname(current);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
function removeSnapshotStorage(snapshotRoot) {
|
|
258
|
+
const storageRoot = import_node_path.default.dirname(import_node_path.default.resolve(snapshotRoot));
|
|
259
|
+
import_node_fs.default.rmSync(snapshotRoot, { recursive: true, force: true });
|
|
260
|
+
fsyncDirectory(storageRoot);
|
|
261
|
+
}
|
|
262
|
+
function fsyncManagedProjectRootEntry(projectRoot) {
|
|
263
|
+
fsyncDirectoryChain(projectRoot, import_node_path.default.dirname(import_node_path.default.dirname(projectRoot)));
|
|
264
|
+
}
|
|
265
|
+
function createProjectWorktreeSnapshotRoot(temporaryRoot = import_node_os.default.tmpdir()) {
|
|
266
|
+
const resolvedTemporaryRoot = import_node_path.default.resolve(temporaryRoot);
|
|
267
|
+
const storageRootExisted = import_node_fs.default.existsSync(resolvedTemporaryRoot);
|
|
268
|
+
import_node_fs.default.mkdirSync(resolvedTemporaryRoot, { recursive: true, mode: 448 });
|
|
269
|
+
assertRealDirectory(resolvedTemporaryRoot, "Project worktree snapshot storage root");
|
|
270
|
+
if (!storageRootExisted) fsyncDirectory(import_node_path.default.dirname(resolvedTemporaryRoot));
|
|
271
|
+
fsyncDirectory(resolvedTemporaryRoot);
|
|
272
|
+
const snapshotRoot = import_node_fs.default.mkdtempSync(import_node_path.default.join(resolvedTemporaryRoot, `${PROJECT_WORKTREE_SNAPSHOT_PREFIX}${process.pid}-`));
|
|
273
|
+
fsyncDirectory(snapshotRoot);
|
|
274
|
+
fsyncDirectory(resolvedTemporaryRoot);
|
|
275
|
+
return snapshotRoot;
|
|
276
|
+
}
|
|
277
|
+
function snapshotManifestPath(snapshotRoot) {
|
|
278
|
+
return import_node_path.default.join(snapshotRoot, PROJECT_WORKTREE_SNAPSHOT_MANIFEST);
|
|
279
|
+
}
|
|
280
|
+
function assertRealDirectory(directory, label) {
|
|
281
|
+
const stat = import_node_fs.default.lstatSync(directory);
|
|
282
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error(`${label} must be a real directory`);
|
|
283
|
+
}
|
|
284
|
+
function normalizeSnapshotManifest(value) {
|
|
285
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
286
|
+
throw new Error("Project worktree snapshot manifest must be an object");
|
|
287
|
+
}
|
|
288
|
+
const record = value;
|
|
289
|
+
if (record.version !== PROJECT_WORKTREE_SNAPSHOT_MANIFEST_VERSION) {
|
|
290
|
+
throw new Error("Unsupported project worktree snapshot manifest version");
|
|
291
|
+
}
|
|
292
|
+
if (record.state !== "building" && record.state !== "prepared" && record.state !== "consumed") {
|
|
293
|
+
throw new Error("Project worktree snapshot manifest has an invalid state");
|
|
294
|
+
}
|
|
295
|
+
if (!Number.isSafeInteger(record.ownerProcessId) || record.ownerProcessId <= 0) {
|
|
296
|
+
throw new Error("Project worktree snapshot manifest has an invalid owner process");
|
|
297
|
+
}
|
|
298
|
+
if (typeof record.ownerSessionId !== "string" || !UUID_PATTERN.test(record.ownerSessionId)) {
|
|
299
|
+
throw new Error("Project worktree snapshot manifest has an invalid owner session");
|
|
300
|
+
}
|
|
301
|
+
if (typeof record.projectRoot !== "string" || !import_node_path.default.isAbsolute(record.projectRoot) || import_node_path.default.resolve(record.projectRoot) !== record.projectRoot) {
|
|
302
|
+
throw new Error("Project worktree snapshot manifest has an invalid project root");
|
|
303
|
+
}
|
|
304
|
+
if (typeof record.createdAt !== "string" || !Number.isFinite(Date.parse(record.createdAt))) {
|
|
305
|
+
throw new Error("Project worktree snapshot manifest has an invalid creation time");
|
|
306
|
+
}
|
|
307
|
+
if (!Array.isArray(record.branches)) throw new Error("Project worktree snapshot manifest has invalid branches");
|
|
308
|
+
const branches = record.branches.map((value2) => {
|
|
309
|
+
if (typeof value2 !== "object" || value2 === null || Array.isArray(value2)) {
|
|
310
|
+
throw new Error("Project worktree snapshot manifest has an invalid branch");
|
|
311
|
+
}
|
|
312
|
+
const branch = value2;
|
|
313
|
+
if (typeof branch.branchName !== "string" || typeof branch.snapshotPresent !== "boolean") {
|
|
314
|
+
throw new Error("Project worktree snapshot manifest has an invalid branch");
|
|
315
|
+
}
|
|
316
|
+
(0, import_managed_paths.validateManagedBranchName)(branch.branchName);
|
|
317
|
+
return { branchName: branch.branchName, snapshotPresent: branch.snapshotPresent };
|
|
318
|
+
});
|
|
319
|
+
const names = branches.map(({ branchName }) => branchName).sort();
|
|
320
|
+
if (new Set(names).size !== names.length) throw new Error("Project worktree snapshot branch names must be unique");
|
|
321
|
+
for (let index = 0; index < names.length; index += 1) {
|
|
322
|
+
const name = names[index];
|
|
323
|
+
if (names.slice(index + 1).some((candidate) => candidate.startsWith(`${name}/`))) {
|
|
324
|
+
throw new Error("Project worktree snapshot branch paths overlap");
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return {
|
|
328
|
+
version: PROJECT_WORKTREE_SNAPSHOT_MANIFEST_VERSION,
|
|
329
|
+
state: record.state,
|
|
330
|
+
ownerProcessId: record.ownerProcessId,
|
|
331
|
+
ownerSessionId: record.ownerSessionId,
|
|
332
|
+
projectRoot: record.projectRoot,
|
|
333
|
+
branches,
|
|
334
|
+
createdAt: record.createdAt
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
function readSnapshotManifest(snapshotRoot) {
|
|
338
|
+
assertRealDirectory(snapshotRoot, "Project worktree snapshot root");
|
|
339
|
+
const manifestPath = snapshotManifestPath(snapshotRoot);
|
|
340
|
+
const stat = import_node_fs.default.lstatSync(manifestPath);
|
|
341
|
+
if (!stat.isFile() || stat.isSymbolicLink()) throw new Error("Project worktree snapshot manifest must be a regular file");
|
|
342
|
+
if (stat.size > MAX_PROJECT_WORKTREE_SNAPSHOT_MANIFEST_BYTES) throw new Error("Project worktree snapshot manifest is too large");
|
|
343
|
+
return normalizeSnapshotManifest(JSON.parse(import_node_fs.default.readFileSync(manifestPath, "utf8")));
|
|
344
|
+
}
|
|
345
|
+
function writeSnapshotManifest(snapshotRoot, manifest) {
|
|
346
|
+
assertRealDirectory(snapshotRoot, "Project worktree snapshot root");
|
|
347
|
+
const normalized = normalizeSnapshotManifest(manifest);
|
|
348
|
+
const temporaryPath = import_node_path.default.join(snapshotRoot, `.${PROJECT_WORKTREE_SNAPSHOT_MANIFEST}.${process.pid}.${(0, import_node_crypto.randomUUID)()}.tmp`);
|
|
349
|
+
let descriptor;
|
|
350
|
+
try {
|
|
351
|
+
descriptor = import_node_fs.default.openSync(temporaryPath, "wx", 384);
|
|
352
|
+
import_node_fs.default.writeFileSync(descriptor, `${JSON.stringify(normalized, null, 2)}
|
|
353
|
+
`, "utf8");
|
|
354
|
+
import_node_fs.default.fsyncSync(descriptor);
|
|
355
|
+
import_node_fs.default.closeSync(descriptor);
|
|
356
|
+
descriptor = void 0;
|
|
357
|
+
import_node_fs.default.renameSync(temporaryPath, snapshotManifestPath(snapshotRoot));
|
|
358
|
+
fsyncDirectory(snapshotRoot);
|
|
359
|
+
} finally {
|
|
360
|
+
if (descriptor !== void 0) import_node_fs.default.closeSync(descriptor);
|
|
361
|
+
import_node_fs.default.rmSync(temporaryPath, { force: true });
|
|
362
|
+
}
|
|
363
|
+
return readSnapshotManifest(snapshotRoot);
|
|
364
|
+
}
|
|
365
|
+
function assertSameSnapshotIdentity(expected, actual) {
|
|
366
|
+
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)) {
|
|
367
|
+
throw new Error("Project worktree snapshot identity changed");
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
function advanceSnapshotState(snapshotRoot, expected, state) {
|
|
371
|
+
const current = readSnapshotManifest(snapshotRoot);
|
|
372
|
+
assertSameSnapshotIdentity(expected, current);
|
|
373
|
+
const allowed = current.state === "building" && state === "prepared" || current.state === "prepared" && state === "consumed" || current.state === state;
|
|
374
|
+
if (!allowed) throw new Error(`Project worktree snapshot cannot advance from ${current.state} to ${state}`);
|
|
375
|
+
return current.state === state ? current : writeSnapshotManifest(snapshotRoot, { ...current, state });
|
|
376
|
+
}
|
|
377
|
+
function fullSnapshotPath(snapshotRoot, branchName) {
|
|
378
|
+
(0, import_managed_paths.validateManagedBranchName)(branchName);
|
|
379
|
+
return import_node_path.default.join(snapshotRoot, "trees", ...branchName.split("/"));
|
|
380
|
+
}
|
|
381
|
+
function copyCheckoutTree(sourceRoot, targetRoot) {
|
|
382
|
+
const sourceStat = import_node_fs.default.lstatSync(sourceRoot);
|
|
383
|
+
if (!sourceStat.isDirectory() || sourceStat.isSymbolicLink()) {
|
|
384
|
+
throw new Error(`Project checkout snapshot source must be a real directory: ${sourceRoot}`);
|
|
385
|
+
}
|
|
386
|
+
import_node_fs.default.mkdirSync(import_node_path.default.dirname(targetRoot), { recursive: true });
|
|
387
|
+
import_node_fs.default.cpSync(sourceRoot, targetRoot, {
|
|
388
|
+
recursive: true,
|
|
389
|
+
dereference: false,
|
|
390
|
+
errorOnExist: true,
|
|
391
|
+
force: false,
|
|
392
|
+
mode: import_node_fs.default.constants.COPYFILE_FICLONE,
|
|
393
|
+
preserveTimestamps: true,
|
|
394
|
+
verbatimSymlinks: true
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
function assertNoOutstandingSnapshotForProject(temporaryRoot, projectRoot) {
|
|
398
|
+
let entries;
|
|
399
|
+
try {
|
|
400
|
+
assertRealDirectory(temporaryRoot, "Project worktree snapshot storage root");
|
|
401
|
+
entries = import_node_fs.default.readdirSync(temporaryRoot, { withFileTypes: true });
|
|
402
|
+
} catch (error) {
|
|
403
|
+
if (error.code === "ENOENT") return;
|
|
404
|
+
throw error;
|
|
405
|
+
}
|
|
406
|
+
for (const entry of entries) {
|
|
407
|
+
if (!entry.isDirectory() || entry.isSymbolicLink() || !PROJECT_WORKTREE_SNAPSHOT_NAME.test(entry.name)) continue;
|
|
408
|
+
let manifest;
|
|
409
|
+
try {
|
|
410
|
+
manifest = readSnapshotManifest(import_node_path.default.join(temporaryRoot, entry.name));
|
|
411
|
+
} catch (error) {
|
|
412
|
+
throw new Error(`Project checkout snapshot state is unreadable at ${import_node_path.default.join(temporaryRoot, entry.name)}`, { cause: error });
|
|
413
|
+
}
|
|
414
|
+
if (manifest.projectRoot === projectRoot && manifest.state !== "consumed") {
|
|
415
|
+
throw new Error(`Project checkout has an unfinished recovery snapshot at ${import_node_path.default.join(temporaryRoot, entry.name)}`);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
169
418
|
}
|
|
170
|
-
function snapshotBranchTrees(projectRoot, branches) {
|
|
171
|
-
const
|
|
419
|
+
function snapshotBranchTrees(projectRoot, branches, temporaryRoot) {
|
|
420
|
+
const resolvedProjectRoot = import_node_path.default.resolve(projectRoot);
|
|
421
|
+
const resolvedTemporaryRoot = import_node_path.default.resolve(temporaryRoot ?? import_node_os.default.tmpdir());
|
|
422
|
+
assertNoOutstandingSnapshotForProject(resolvedTemporaryRoot, resolvedProjectRoot);
|
|
423
|
+
const root = createProjectWorktreeSnapshotRoot(resolvedTemporaryRoot);
|
|
172
424
|
const paths = /* @__PURE__ */ new Map();
|
|
173
425
|
try {
|
|
174
|
-
|
|
175
|
-
const checkoutPath = branchPath(
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
426
|
+
const branchSnapshots = branches.map(({ branchName }) => {
|
|
427
|
+
const checkoutPath = branchPath(resolvedProjectRoot, branchName);
|
|
428
|
+
let snapshotPresent = false;
|
|
429
|
+
try {
|
|
430
|
+
import_node_fs.default.lstatSync(checkoutPath);
|
|
431
|
+
snapshotPresent = true;
|
|
432
|
+
} catch (error) {
|
|
433
|
+
if (error.code !== "ENOENT") throw error;
|
|
434
|
+
}
|
|
435
|
+
return { branchName, snapshotPresent };
|
|
436
|
+
});
|
|
437
|
+
let manifest = writeSnapshotManifest(root, {
|
|
438
|
+
version: PROJECT_WORKTREE_SNAPSHOT_MANIFEST_VERSION,
|
|
439
|
+
state: "building",
|
|
440
|
+
ownerProcessId: process.pid,
|
|
441
|
+
ownerSessionId: PROJECT_WORKTREE_SNAPSHOT_OWNER_SESSION_ID,
|
|
442
|
+
projectRoot: resolvedProjectRoot,
|
|
443
|
+
branches: branchSnapshots,
|
|
444
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
445
|
+
});
|
|
446
|
+
for (const { branchName, snapshotPresent } of branchSnapshots) {
|
|
447
|
+
if (!snapshotPresent) continue;
|
|
448
|
+
const snapshotPath = fullSnapshotPath(root, branchName);
|
|
449
|
+
copyCheckoutTree(branchPath(resolvedProjectRoot, branchName), snapshotPath);
|
|
179
450
|
paths.set(branchName, snapshotPath);
|
|
180
451
|
}
|
|
181
|
-
|
|
452
|
+
const treesRoot = import_node_path.default.join(root, "trees");
|
|
453
|
+
if (import_node_fs.default.existsSync(treesRoot)) fsyncTree(treesRoot);
|
|
454
|
+
manifest = advanceSnapshotState(root, manifest, "prepared");
|
|
455
|
+
return { root, manifest, paths };
|
|
182
456
|
} catch (error) {
|
|
183
|
-
|
|
457
|
+
removeSnapshotStorage(root);
|
|
184
458
|
throw error;
|
|
185
459
|
}
|
|
186
460
|
}
|
|
461
|
+
function restoreProjectWorktreeSnapshot(snapshotRoot, manifest) {
|
|
462
|
+
const current = readSnapshotManifest(snapshotRoot);
|
|
463
|
+
assertSameSnapshotIdentity(manifest, current);
|
|
464
|
+
if (current.state !== "prepared") throw new Error(`Project worktree snapshot is ${current.state}, not prepared`);
|
|
465
|
+
for (const { branchName, snapshotPresent } of current.branches) {
|
|
466
|
+
const targetPath = branchPath(current.projectRoot, branchName);
|
|
467
|
+
assertNoSymlinkBranchPathComponents(current.projectRoot, targetPath);
|
|
468
|
+
if (!snapshotPresent) continue;
|
|
469
|
+
const sourcePath = fullSnapshotPath(snapshotRoot, branchName);
|
|
470
|
+
const stat = import_node_fs.default.lstatSync(sourcePath);
|
|
471
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) {
|
|
472
|
+
throw new Error(`Project worktree snapshot branch must be a real directory: ${branchName}`);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
for (const { branchName } of [...current.branches].sort((left, right) => right.branchName.length - left.branchName.length)) {
|
|
476
|
+
const targetPath = branchPath(current.projectRoot, branchName);
|
|
477
|
+
const targetParent = import_node_path.default.dirname(targetPath);
|
|
478
|
+
import_node_fs.default.rmSync(targetPath, { recursive: true, force: true });
|
|
479
|
+
if (import_node_fs.default.existsSync(targetParent)) fsyncDirectoryChain(targetParent, current.projectRoot);
|
|
480
|
+
}
|
|
481
|
+
for (const { branchName, snapshotPresent } of current.branches) {
|
|
482
|
+
if (!snapshotPresent) continue;
|
|
483
|
+
const targetPath = branchPath(current.projectRoot, branchName);
|
|
484
|
+
copyCheckoutTree(fullSnapshotPath(snapshotRoot, branchName), targetPath);
|
|
485
|
+
fsyncTree(targetPath);
|
|
486
|
+
fsyncDirectoryChain(import_node_path.default.dirname(targetPath), current.projectRoot);
|
|
487
|
+
}
|
|
488
|
+
import_node_fs.default.mkdirSync(current.projectRoot, { recursive: true });
|
|
489
|
+
fsyncManagedProjectRootEntry(current.projectRoot);
|
|
490
|
+
}
|
|
491
|
+
function consumeAndRemoveProjectWorktreeSnapshot(snapshotRoot, manifest) {
|
|
492
|
+
const consumed = advanceSnapshotState(snapshotRoot, manifest, "consumed");
|
|
493
|
+
try {
|
|
494
|
+
assertSameSnapshotIdentity(manifest, consumed);
|
|
495
|
+
removeSnapshotStorage(snapshotRoot);
|
|
496
|
+
} catch {
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
function persistReconciledProjectAndConsumeSnapshot(snapshot, afterProjectTreeFsync) {
|
|
500
|
+
fsyncTree(snapshot.manifest.projectRoot);
|
|
501
|
+
fsyncManagedProjectRootEntry(snapshot.manifest.projectRoot);
|
|
502
|
+
afterProjectTreeFsync?.();
|
|
503
|
+
consumeAndRemoveProjectWorktreeSnapshot(snapshot.root, snapshot.manifest);
|
|
504
|
+
}
|
|
505
|
+
function rollbackProjectWorktreeSnapshot(snapshot) {
|
|
506
|
+
const current = readSnapshotManifest(snapshot.root);
|
|
507
|
+
assertSameSnapshotIdentity(snapshot.manifest, current);
|
|
508
|
+
if (current.state === "prepared") {
|
|
509
|
+
restoreProjectWorktreeSnapshot(snapshot.root, current);
|
|
510
|
+
consumeAndRemoveProjectWorktreeSnapshot(snapshot.root, current);
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
if (current.state === "consumed") {
|
|
514
|
+
try {
|
|
515
|
+
removeSnapshotStorage(snapshot.root);
|
|
516
|
+
} catch {
|
|
517
|
+
}
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
throw new Error(`Project worktree snapshot is unexpectedly ${current.state}`);
|
|
521
|
+
}
|
|
187
522
|
function isProcessAlive(processId) {
|
|
188
523
|
try {
|
|
189
524
|
process.kill(processId, 0);
|
|
@@ -192,35 +527,105 @@ function isProcessAlive(processId) {
|
|
|
192
527
|
return error.code !== "ESRCH";
|
|
193
528
|
}
|
|
194
529
|
}
|
|
195
|
-
function
|
|
530
|
+
function isPathInside(root, candidate) {
|
|
531
|
+
const relative = import_node_path.default.relative(import_node_path.default.resolve(root), import_node_path.default.resolve(candidate));
|
|
532
|
+
return relative !== "" && relative !== ".." && !relative.startsWith(`..${import_node_path.default.sep}`) && !import_node_path.default.isAbsolute(relative);
|
|
533
|
+
}
|
|
534
|
+
function assertNoSymlinkPathComponents(root, candidate) {
|
|
535
|
+
const resolvedRoot = import_node_path.default.resolve(root);
|
|
536
|
+
const resolvedCandidate = import_node_path.default.resolve(candidate);
|
|
537
|
+
if (!isPathInside(resolvedRoot, resolvedCandidate)) throw new Error("Project worktree snapshot target escapes the projects root");
|
|
538
|
+
let current = resolvedRoot;
|
|
539
|
+
for (const component of ["", ...import_node_path.default.relative(resolvedRoot, resolvedCandidate).split(import_node_path.default.sep).filter(Boolean)]) {
|
|
540
|
+
if (component) current = import_node_path.default.join(current, component);
|
|
541
|
+
try {
|
|
542
|
+
if (import_node_fs.default.lstatSync(current).isSymbolicLink()) {
|
|
543
|
+
throw new Error(`Project worktree snapshot target contains a symbolic link: ${current}`);
|
|
544
|
+
}
|
|
545
|
+
} catch (error) {
|
|
546
|
+
const code = error.code;
|
|
547
|
+
if (code === "ENOENT" || code === "ENOTDIR") return;
|
|
548
|
+
throw error;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
function recoverStaleProjectWorktreeSnapshots(input) {
|
|
196
553
|
const temporaryRoot = import_node_path.default.resolve(input.temporaryRoot ?? import_node_os.default.tmpdir());
|
|
554
|
+
const projectsRoot = import_node_path.default.resolve(input.projectsRoot);
|
|
197
555
|
const processAlive = input.processAlive ?? isProcessAlive;
|
|
556
|
+
const currentProcessId = input.currentProcessId ?? process.pid;
|
|
557
|
+
const currentOwnerSessionId = input.currentOwnerSessionId ?? PROJECT_WORKTREE_SNAPSHOT_OWNER_SESSION_ID;
|
|
558
|
+
if (!Number.isSafeInteger(currentProcessId) || currentProcessId <= 0) throw new Error("Current snapshot recovery process ID is invalid");
|
|
559
|
+
if (!UUID_PATTERN.test(currentOwnerSessionId)) throw new Error("Current snapshot recovery owner session is invalid");
|
|
560
|
+
const restored = [];
|
|
198
561
|
const removed = [];
|
|
199
562
|
const failed = [];
|
|
200
563
|
let entries;
|
|
201
564
|
try {
|
|
565
|
+
assertRealDirectory(temporaryRoot, "Project worktree snapshot storage root");
|
|
202
566
|
entries = import_node_fs.default.readdirSync(temporaryRoot, { withFileTypes: true });
|
|
203
567
|
} catch (error) {
|
|
204
|
-
if (error.code === "ENOENT") return { removed, failed };
|
|
205
|
-
return { removed, failed: [{ path: temporaryRoot, error: error instanceof Error ? error.message : String(error) }] };
|
|
568
|
+
if (error.code === "ENOENT") return { restored, removed, failed };
|
|
569
|
+
return { restored, removed, failed: [{ path: temporaryRoot, error: error instanceof Error ? error.message : String(error) }] };
|
|
206
570
|
}
|
|
207
571
|
for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
|
|
208
572
|
const match = PROJECT_WORKTREE_SNAPSHOT_NAME.exec(entry.name);
|
|
209
573
|
if (!match) continue;
|
|
210
574
|
const ownerProcessId = Number(match[1]);
|
|
211
|
-
if (!Number.isSafeInteger(ownerProcessId)
|
|
575
|
+
if (!Number.isSafeInteger(ownerProcessId)) continue;
|
|
212
576
|
const candidate = import_node_path.default.join(temporaryRoot, entry.name);
|
|
213
577
|
try {
|
|
214
578
|
const stat = import_node_fs.default.lstatSync(candidate);
|
|
215
579
|
if (!stat.isDirectory() || stat.isSymbolicLink()) continue;
|
|
216
|
-
|
|
217
|
-
|
|
580
|
+
const manifest = readSnapshotManifest(candidate);
|
|
581
|
+
if (manifest.ownerProcessId !== ownerProcessId) {
|
|
582
|
+
throw new Error("Project worktree snapshot owner does not match its directory name");
|
|
583
|
+
}
|
|
584
|
+
if (!isPathInside(projectsRoot, manifest.projectRoot)) {
|
|
585
|
+
throw new Error("Project worktree snapshot target is outside the configured projects root");
|
|
586
|
+
}
|
|
587
|
+
assertNoSymlinkPathComponents(projectsRoot, manifest.projectRoot);
|
|
588
|
+
const ownedByThisProcessSession = manifest.ownerProcessId === currentProcessId && manifest.ownerSessionId === currentOwnerSessionId;
|
|
589
|
+
if (ownedByThisProcessSession || ownerProcessId !== currentProcessId && processAlive(ownerProcessId)) continue;
|
|
590
|
+
if (manifest.state === "prepared") {
|
|
591
|
+
restoreProjectWorktreeSnapshot(candidate, manifest);
|
|
592
|
+
const consumed = advanceSnapshotState(candidate, manifest, "consumed");
|
|
593
|
+
removeSnapshotStorage(candidate);
|
|
594
|
+
assertSameSnapshotIdentity(manifest, consumed);
|
|
595
|
+
restored.push(candidate);
|
|
596
|
+
} else {
|
|
597
|
+
removeSnapshotStorage(candidate);
|
|
598
|
+
removed.push(candidate);
|
|
599
|
+
}
|
|
218
600
|
} catch (error) {
|
|
219
|
-
if (
|
|
601
|
+
if (ownerProcessId !== currentProcessId && processAlive(ownerProcessId)) continue;
|
|
602
|
+
if (error.code === "ENOENT") {
|
|
603
|
+
try {
|
|
604
|
+
import_node_fs.default.lstatSync(candidate);
|
|
605
|
+
} catch (candidateError) {
|
|
606
|
+
if (candidateError.code === "ENOENT") continue;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
220
609
|
failed.push({ path: candidate, error: error instanceof Error ? error.message : String(error) });
|
|
221
610
|
}
|
|
222
611
|
}
|
|
223
|
-
return { removed, failed };
|
|
612
|
+
return { restored, removed, failed };
|
|
613
|
+
}
|
|
614
|
+
function projectOriginBranchName(input) {
|
|
615
|
+
(0, import_managed_paths.validateManagedBranchName)(input.branchName);
|
|
616
|
+
(0, import_managed_paths.validateManagedBranchName)(input.primaryBranchName);
|
|
617
|
+
(0, import_managed_paths.validateManagedBranchName)(input.defaultBranch);
|
|
618
|
+
return input.branchName === input.primaryBranchName ? input.defaultBranch : input.branchName;
|
|
619
|
+
}
|
|
620
|
+
function configureProjectBranchTracking(checkoutPath, branchName, originBranchName) {
|
|
621
|
+
(0, import_managed_paths.validateManagedBranchName)(branchName);
|
|
622
|
+
(0, import_managed_paths.validateManagedBranchName)(originBranchName);
|
|
623
|
+
git(checkoutPath, ["config", "--local", `branch.${branchName}.remote`, "origin"], `configure origin for ${branchName}`);
|
|
624
|
+
git(
|
|
625
|
+
checkoutPath,
|
|
626
|
+
["config", "--local", `branch.${branchName}.merge`, `refs/heads/${originBranchName}`],
|
|
627
|
+
`configure upstream branch for ${branchName}`
|
|
628
|
+
);
|
|
224
629
|
}
|
|
225
630
|
function configureRepository(input) {
|
|
226
631
|
if (tryGit(input.primaryPath, ["remote", "get-url", "origin"])) {
|
|
@@ -252,6 +657,17 @@ function configureRepository(input) {
|
|
|
252
657
|
git(input.primaryPath, ["config", "--local", "user.name", name], "configure project Git user name");
|
|
253
658
|
git(input.primaryPath, ["config", "--local", "user.email", email], "configure project Git user email");
|
|
254
659
|
}
|
|
660
|
+
if (input.primaryBranchName && input.defaultBranch && input.branches) {
|
|
661
|
+
git(input.primaryPath, ["config", "--local", "push.default", "upstream"], "configure project push mapping");
|
|
662
|
+
for (const { branchName } of input.branches) {
|
|
663
|
+
const originBranchName = projectOriginBranchName({
|
|
664
|
+
branchName,
|
|
665
|
+
primaryBranchName: input.primaryBranchName,
|
|
666
|
+
defaultBranch: input.defaultBranch
|
|
667
|
+
});
|
|
668
|
+
configureProjectBranchTracking(input.primaryPath, branchName, originBranchName);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
255
671
|
}
|
|
256
672
|
function fetchProjectHeads(input) {
|
|
257
673
|
const originFetch = gitResult(input.primaryPath, [
|
|
@@ -301,6 +717,9 @@ function ensureCommitAvailable(input) {
|
|
|
301
717
|
function seedForBranch(input) {
|
|
302
718
|
const mirrorRef = `refs/r5d/mirror/${input.branch.branchName}`;
|
|
303
719
|
const mirrorHead = tryGit(input.primaryPath, ["show-ref", "--verify", "--quiet", mirrorRef]) ? git(input.primaryPath, ["rev-parse", mirrorRef], "resolve project mirror head") : null;
|
|
720
|
+
if (input.requireMirrorHead && !mirrorHead) {
|
|
721
|
+
throw new Error(`Exact hidden project mirror head is missing for ${input.branch.branchName}`);
|
|
722
|
+
}
|
|
304
723
|
const seed = mirrorHead ?? input.branch.baseCommitHash;
|
|
305
724
|
ensureCommitAvailable({ ...input, commitHash: seed });
|
|
306
725
|
return { seed, mirrorHead };
|
|
@@ -322,7 +741,12 @@ function initializeLinkedLayout(input) {
|
|
|
322
741
|
const seeds = /* @__PURE__ */ new Map();
|
|
323
742
|
const mirrorHeads = /* @__PURE__ */ new Map();
|
|
324
743
|
for (const branch of input.branches) {
|
|
325
|
-
const resolved = seedForBranch({
|
|
744
|
+
const resolved = seedForBranch({
|
|
745
|
+
...input,
|
|
746
|
+
primaryPath,
|
|
747
|
+
branch,
|
|
748
|
+
requireMirrorHead: input.exactMirrorBranches?.has(branch.branchName)
|
|
749
|
+
});
|
|
326
750
|
seeds.set(branch.branchName, resolved.seed);
|
|
327
751
|
mirrorHeads.set(branch.branchName, resolved.mirrorHead);
|
|
328
752
|
}
|
|
@@ -346,24 +770,37 @@ function reconcileLinkedLayout(input) {
|
|
|
346
770
|
tryGit(primaryPath, ["worktree", "prune"]);
|
|
347
771
|
fetchProjectHeads({ ...input, primaryPath });
|
|
348
772
|
const mirrorHeads = /* @__PURE__ */ new Map();
|
|
773
|
+
const mutatedBranches = /* @__PURE__ */ new Set();
|
|
349
774
|
for (const branch of input.branches) {
|
|
350
775
|
const checkoutPath = branchPath(input.projectRoot, branch.branchName);
|
|
351
|
-
const resolved = seedForBranch({
|
|
776
|
+
const resolved = seedForBranch({
|
|
777
|
+
...input,
|
|
778
|
+
primaryPath,
|
|
779
|
+
branch,
|
|
780
|
+
requireMirrorHead: input.exactMirrorBranches?.has(branch.branchName)
|
|
781
|
+
});
|
|
352
782
|
mirrorHeads.set(branch.branchName, resolved.mirrorHead);
|
|
353
783
|
if (branch.branchName === input.primaryBranchName || commonGitDirectory(checkoutPath) === commonGitDirectory(primaryPath)) {
|
|
784
|
+
const currentHead = git(checkoutPath, ["rev-parse", "HEAD"], `resolve linked worktree head ${branch.branchName}`);
|
|
785
|
+
if (currentHead === resolved.seed) continue;
|
|
786
|
+
input.beforeDestructiveMutation();
|
|
787
|
+
mutatedBranches.add(branch.branchName);
|
|
354
788
|
preserveHead(checkoutPath);
|
|
355
789
|
git(checkoutPath, ["reset", "--hard", resolved.seed], `anchor linked worktree ${branch.branchName}`);
|
|
356
790
|
continue;
|
|
357
791
|
}
|
|
792
|
+
input.beforeDestructiveMutation();
|
|
793
|
+
mutatedBranches.add(branch.branchName);
|
|
358
794
|
import_node_fs.default.rmSync(checkoutPath, { recursive: true, force: true });
|
|
359
795
|
git(primaryPath, ["branch", "-f", branch.branchName, resolved.seed], `anchor project branch ${branch.branchName}`);
|
|
360
796
|
import_node_fs.default.mkdirSync(import_node_path.default.dirname(checkoutPath), { recursive: true });
|
|
361
797
|
git(primaryPath, ["worktree", "add", "--force", checkoutPath, branch.branchName], `create linked worktree ${branch.branchName}`);
|
|
362
798
|
}
|
|
363
|
-
return mirrorHeads;
|
|
799
|
+
return { mirrorHeads, mutatedBranches };
|
|
364
800
|
}
|
|
365
|
-
function aheadBehind(checkoutPath, branchName) {
|
|
366
|
-
const
|
|
801
|
+
function aheadBehind(checkoutPath, branchName, primaryBranchName, defaultBranch) {
|
|
802
|
+
const originBranchName = projectOriginBranchName({ branchName, primaryBranchName, defaultBranch });
|
|
803
|
+
const originRef = `refs/remotes/origin/${originBranchName}`;
|
|
367
804
|
if (!tryGit(checkoutPath, ["show-ref", "--verify", "--quiet", originRef])) return { ahead: null, behind: null };
|
|
368
805
|
const output = git(checkoutPath, ["rev-list", "--left-right", "--count", `${originRef}...HEAD`], "measure project origin divergence");
|
|
369
806
|
const [behind, ahead] = output.split(/\s+/).map(Number);
|
|
@@ -371,11 +808,18 @@ function aheadBehind(checkoutPath, branchName) {
|
|
|
371
808
|
}
|
|
372
809
|
function ensureProjectWorktrees(input) {
|
|
373
810
|
const projectRoot = import_node_path.default.resolve(input.projectRoot);
|
|
811
|
+
const defaultBranch = input.defaultBranch ?? input.primaryBranchName;
|
|
374
812
|
assertNonOverlappingBranches(input.branches);
|
|
375
813
|
if (!input.branches.some(({ branchName }) => branchName === input.primaryBranchName)) {
|
|
376
814
|
throw new Error(`Primary project branch ${input.primaryBranchName} is missing from the workspace configuration`);
|
|
377
815
|
}
|
|
378
|
-
const
|
|
816
|
+
const snapshotStorageRoot = import_node_path.default.resolve(input.snapshotRoot ?? import_node_os.default.tmpdir());
|
|
817
|
+
assertNoOutstandingSnapshotForProject(snapshotStorageRoot, projectRoot);
|
|
818
|
+
let snapshot;
|
|
819
|
+
const ensureSnapshot = () => {
|
|
820
|
+
snapshot ??= snapshotBranchTrees(projectRoot, input.branches, snapshotStorageRoot);
|
|
821
|
+
return snapshot;
|
|
822
|
+
};
|
|
379
823
|
try {
|
|
380
824
|
import_node_fs.default.mkdirSync(projectRoot, { recursive: true });
|
|
381
825
|
const linked = hasCompatibleLinkedProjectWorktreeLayout({
|
|
@@ -383,9 +827,27 @@ function ensureProjectWorktrees(input) {
|
|
|
383
827
|
primaryBranchName: input.primaryBranchName,
|
|
384
828
|
branches: input.branches
|
|
385
829
|
});
|
|
386
|
-
|
|
830
|
+
let mirrorHeads;
|
|
831
|
+
let mutatedBranches;
|
|
832
|
+
if (linked) {
|
|
833
|
+
({ mirrorHeads, mutatedBranches } = reconcileLinkedLayout({
|
|
834
|
+
...input,
|
|
835
|
+
projectRoot,
|
|
836
|
+
defaultBranch,
|
|
837
|
+
beforeDestructiveMutation: ensureSnapshot
|
|
838
|
+
}));
|
|
839
|
+
} else {
|
|
840
|
+
ensureSnapshot();
|
|
841
|
+
mirrorHeads = initializeLinkedLayout({ ...input, projectRoot, defaultBranch });
|
|
842
|
+
mutatedBranches = new Set(input.branches.map(({ branchName }) => branchName));
|
|
843
|
+
}
|
|
844
|
+
const preparedSnapshot = snapshot;
|
|
845
|
+
if (mutatedBranches.size > 0 && !preparedSnapshot) {
|
|
846
|
+
throw new Error("Project worktree mutation started without a prepared recovery snapshot");
|
|
847
|
+
}
|
|
387
848
|
for (const branch of input.branches) {
|
|
388
|
-
|
|
849
|
+
if (!mutatedBranches.has(branch.branchName)) continue;
|
|
850
|
+
const snapshotPath = preparedSnapshot?.paths.get(branch.branchName);
|
|
389
851
|
if (!snapshotPath) continue;
|
|
390
852
|
(0, import_working_tree_mirror.mirrorWorkingTree)({
|
|
391
853
|
sourceRoot: snapshotPath,
|
|
@@ -394,9 +856,9 @@ function ensureProjectWorktrees(input) {
|
|
|
394
856
|
deletionMode: "git"
|
|
395
857
|
});
|
|
396
858
|
}
|
|
397
|
-
|
|
859
|
+
const states = input.branches.map((branch) => {
|
|
398
860
|
const checkoutPath = branchPath(projectRoot, branch.branchName);
|
|
399
|
-
const divergence = aheadBehind(checkoutPath, branch.branchName);
|
|
861
|
+
const divergence = aheadBehind(checkoutPath, branch.branchName, input.primaryBranchName, defaultBranch);
|
|
400
862
|
return {
|
|
401
863
|
branchName: branch.branchName,
|
|
402
864
|
branchPath: checkoutPath,
|
|
@@ -405,8 +867,19 @@ function ensureProjectWorktrees(input) {
|
|
|
405
867
|
...divergence
|
|
406
868
|
};
|
|
407
869
|
}).sort((left, right) => left.branchName.localeCompare(right.branchName));
|
|
408
|
-
|
|
409
|
-
|
|
870
|
+
if (snapshot) persistReconciledProjectAndConsumeSnapshot(snapshot);
|
|
871
|
+
return states;
|
|
872
|
+
} catch (error) {
|
|
873
|
+
if (!snapshot) throw error;
|
|
874
|
+
try {
|
|
875
|
+
rollbackProjectWorktreeSnapshot(snapshot);
|
|
876
|
+
} catch (recoveryError) {
|
|
877
|
+
throw new Error(
|
|
878
|
+
`Project worktree reconciliation failed and its snapshot could not be fully restored; recovery state is retained at ${snapshot.root}`,
|
|
879
|
+
{ cause: new AggregateError([error, recoveryError]) }
|
|
880
|
+
);
|
|
881
|
+
}
|
|
882
|
+
throw error;
|
|
410
883
|
}
|
|
411
884
|
}
|
|
412
885
|
function projectWorktreeOperationInProgress(checkoutPath) {
|
|
@@ -424,6 +897,8 @@ function createLinkedProjectBranch(input) {
|
|
|
424
897
|
if (input.sourceBranchName === input.branchName) throw new Error("Source and target branch names must differ");
|
|
425
898
|
const sourcePath = branchPath(input.projectRoot, input.sourceBranchName);
|
|
426
899
|
const targetPath = branchPath(input.projectRoot, input.branchName);
|
|
900
|
+
assertNoSymlinkBranchPathComponents(input.projectRoot, sourcePath);
|
|
901
|
+
assertNoSymlinkBranchPathComponents(input.projectRoot, targetPath);
|
|
427
902
|
if (!commonGitDirectory(sourcePath)) throw new Error(`Source worktree ${input.sourceBranchName} is unavailable`);
|
|
428
903
|
if (projectWorktreeOperationInProgress(sourcePath)) {
|
|
429
904
|
throw new Error(`Source worktree ${input.sourceBranchName} has an in-progress Git operation`);
|
|
@@ -439,25 +914,84 @@ function createLinkedProjectBranch(input) {
|
|
|
439
914
|
});
|
|
440
915
|
if (overlappingWorktree) throw new Error(`Project branch folder overlaps linked worktree ${overlappingWorktree}`);
|
|
441
916
|
const sourceHead = git(sourcePath, ["rev-parse", "HEAD"], "resolve source branch head");
|
|
442
|
-
|
|
917
|
+
let branchCreated = false;
|
|
443
918
|
try {
|
|
444
|
-
(0, import_working_tree_mirror.mirrorWorkingTree)({ sourceRoot: sourcePath, targetRoot: snapshotRoot, sourceMode: "git", deletionMode: "all" });
|
|
445
919
|
git(sourcePath, ["branch", input.branchName, sourceHead], `create project branch ${input.branchName}`);
|
|
920
|
+
branchCreated = true;
|
|
446
921
|
import_node_fs.default.mkdirSync(import_node_path.default.dirname(targetPath), { recursive: true });
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
}
|
|
922
|
+
assertNoSymlinkBranchPathComponents(input.projectRoot, targetPath);
|
|
923
|
+
git(
|
|
924
|
+
sourcePath,
|
|
925
|
+
["worktree", "add", "--force", "--no-checkout", targetPath, input.branchName],
|
|
926
|
+
`create linked worktree ${input.branchName}`
|
|
927
|
+
);
|
|
928
|
+
configureProjectBranchTracking(targetPath, input.branchName, input.branchName);
|
|
929
|
+
assertNoSymlinkBranchPathComponents(input.projectRoot, targetPath);
|
|
930
|
+
git(targetPath, ["read-tree", sourceHead], `initialize linked worktree index ${input.branchName}`);
|
|
931
|
+
(0, import_working_tree_mirror.mirrorWorkingTree)({ sourceRoot: sourcePath, targetRoot: targetPath, sourceMode: "git", deletionMode: "git" });
|
|
456
932
|
return { branchPath: targetPath, baseCommitHash: sourceHead };
|
|
457
|
-
}
|
|
458
|
-
|
|
933
|
+
} catch (error) {
|
|
934
|
+
if (branchCreated) {
|
|
935
|
+
let removeError;
|
|
936
|
+
try {
|
|
937
|
+
import_node_fs.default.rmSync(targetPath, { recursive: true, force: true });
|
|
938
|
+
} catch (cleanupError) {
|
|
939
|
+
removeError = cleanupError;
|
|
940
|
+
}
|
|
941
|
+
const prune = gitResult(sourcePath, ["worktree", "prune"]);
|
|
942
|
+
const deleteBranch = gitResult(sourcePath, ["branch", "-D", input.branchName]);
|
|
943
|
+
const cleanupFailures = [];
|
|
944
|
+
try {
|
|
945
|
+
import_node_fs.default.lstatSync(targetPath);
|
|
946
|
+
cleanupFailures.push(`target path remains at ${targetPath}`);
|
|
947
|
+
} catch (verificationError) {
|
|
948
|
+
const code = verificationError.code;
|
|
949
|
+
if (code !== "ENOENT" && code !== "ENOTDIR") {
|
|
950
|
+
cleanupFailures.push(
|
|
951
|
+
`could not verify target removal${removeError ? ` after ${removeError instanceof Error ? removeError.message : String(removeError)}` : ""}: ${verificationError instanceof Error ? verificationError.message : String(verificationError)}`
|
|
952
|
+
);
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
const worktrees = gitResult(sourcePath, ["worktree", "list", "--porcelain"]);
|
|
956
|
+
if (worktrees.exitCode !== 0) {
|
|
957
|
+
cleanupFailures.push(
|
|
958
|
+
`could not verify linked-worktree removal: ${worktrees.stderr || worktrees.stdout || `git exited ${worktrees.exitCode}`}`
|
|
959
|
+
);
|
|
960
|
+
} else {
|
|
961
|
+
const targetRegistered = worktrees.stdout.split(/\r?\n/).some((line) => line.startsWith("worktree ") && import_node_path.default.resolve(line.slice("worktree ".length)) === import_node_path.default.resolve(targetPath));
|
|
962
|
+
if (targetRegistered) {
|
|
963
|
+
cleanupFailures.push(
|
|
964
|
+
`linked worktree remains registered at ${targetPath}${prune.exitCode === 0 ? "" : ` (${prune.stderr || prune.stdout || `git exited ${prune.exitCode}`})`}`
|
|
965
|
+
);
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
const branchRef = gitResult(sourcePath, ["show-ref", "--verify", "--quiet", `refs/heads/${input.branchName}`]);
|
|
969
|
+
if (branchRef.exitCode === 0) {
|
|
970
|
+
cleanupFailures.push(
|
|
971
|
+
`branch ref remains${deleteBranch.exitCode === 0 ? "" : ` (${deleteBranch.stderr || deleteBranch.stdout || `git exited ${deleteBranch.exitCode}`})`}`
|
|
972
|
+
);
|
|
973
|
+
} else if (branchRef.exitCode !== 1) {
|
|
974
|
+
cleanupFailures.push(
|
|
975
|
+
`could not verify branch ref removal: ${branchRef.stderr || branchRef.stdout || `git exited ${branchRef.exitCode}`}`
|
|
976
|
+
);
|
|
977
|
+
}
|
|
978
|
+
if (cleanupFailures.length > 0) {
|
|
979
|
+
throw new ProjectBranchCreationRollbackIncompleteError(input.branchName, cleanupFailures, error);
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
throw error;
|
|
459
983
|
}
|
|
460
984
|
}
|
|
985
|
+
function createOrRetryLinkedProjectBranch(input) {
|
|
986
|
+
if (input.pendingRetry) {
|
|
987
|
+
deleteLinkedProjectBranch({
|
|
988
|
+
projectRoot: input.projectRoot,
|
|
989
|
+
primaryBranchName: input.primaryBranchName,
|
|
990
|
+
branchName: input.branchName
|
|
991
|
+
});
|
|
992
|
+
}
|
|
993
|
+
return createLinkedProjectBranch(input);
|
|
994
|
+
}
|
|
461
995
|
function deleteLinkedProjectBranch(input) {
|
|
462
996
|
(0, import_managed_paths.validateManagedBranchName)(input.primaryBranchName);
|
|
463
997
|
(0, import_managed_paths.validateManagedBranchName)(input.branchName);
|
|
@@ -517,7 +1051,13 @@ function pushProjectMirrorHeads(input) {
|
|
|
517
1051
|
for (const branchName of [...input.branchNames].sort()) {
|
|
518
1052
|
if (input.onlyBranches && !input.onlyBranches.has(branchName)) continue;
|
|
519
1053
|
const checkoutPath = branchPath(input.projectRoot, branchName);
|
|
520
|
-
const head = git(checkoutPath, ["rev-parse", "HEAD"], `resolve mirror head for ${branchName}`);
|
|
1054
|
+
const head = input.publicationHeads ? input.publicationHeads.get(branchName) : git(checkoutPath, ["rev-parse", "HEAD"], `resolve mirror head for ${branchName}`);
|
|
1055
|
+
if (!head || !/^[0-9a-f]{40}(?:[0-9a-f]{24})?$/.test(head)) {
|
|
1056
|
+
throw new Error(`Missing exact publication head for project branch ${branchName}`);
|
|
1057
|
+
}
|
|
1058
|
+
if (!tryGit(checkoutPath, ["cat-file", "-e", `${head}^{commit}`])) {
|
|
1059
|
+
throw new Error(`Exact publication head ${head} is unavailable for project branch ${branchName}`);
|
|
1060
|
+
}
|
|
521
1061
|
if (projectWorktreeOperationInProgress(checkoutPath)) {
|
|
522
1062
|
results.push({ branchName, head, pushed: false, reason: "git_operation_in_progress" });
|
|
523
1063
|
continue;
|
|
@@ -529,7 +1069,7 @@ function pushProjectMirrorHeads(input) {
|
|
|
529
1069
|
"push",
|
|
530
1070
|
"--no-recurse-submodules",
|
|
531
1071
|
input.mirrorUrl,
|
|
532
|
-
|
|
1072
|
+
`+${head}:refs/heads/${branchName}`
|
|
533
1073
|
],
|
|
534
1074
|
`push hidden project mirror for ${branchName}`
|
|
535
1075
|
);
|
|
@@ -537,7 +1077,13 @@ function pushProjectMirrorHeads(input) {
|
|
|
537
1077
|
}
|
|
538
1078
|
return results;
|
|
539
1079
|
}
|
|
540
|
-
function
|
|
1080
|
+
function assertProjectMirrorHeadsPushed(results) {
|
|
1081
|
+
const skipped = results.find((result) => !result.pushed);
|
|
1082
|
+
if (skipped) {
|
|
1083
|
+
throw new Error(`Hidden project mirror publication skipped ${skipped.branchName}${skipped.reason ? ` (${skipped.reason})` : ""}`);
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
function observeProjectMirrorHeads(input) {
|
|
541
1087
|
const primaryPath = branchPath(input.projectRoot, input.primaryBranchName);
|
|
542
1088
|
const fetch = gitResult(primaryPath, [
|
|
543
1089
|
...(0, import_git_process_environment.gitTransportSecurityArgs)(input.mirrorUrl, input.credentialHelper, input.credentialUsername),
|
|
@@ -554,31 +1100,103 @@ function fastForwardProjectHeadsFromMirror(input) {
|
|
|
554
1100
|
const previousHead = git(checkoutPath, ["rev-parse", "HEAD"], `resolve local project head ${branchName}`);
|
|
555
1101
|
const mirrorRef = `refs/r5d/mirror/${branchName}`;
|
|
556
1102
|
const mirrorHead = tryGit(primaryPath, ["show-ref", "--verify", "--quiet", mirrorRef]) ? git(primaryPath, ["rev-parse", mirrorRef], `resolve refreshed mirror head ${branchName}`) : null;
|
|
557
|
-
|
|
558
|
-
|
|
1103
|
+
const shouldMove = Boolean(
|
|
1104
|
+
mirrorHead && mirrorHead !== previousHead && (input.allowNonFastForward || tryGit(checkoutPath, ["merge-base", "--is-ancestor", previousHead, mirrorHead]))
|
|
1105
|
+
);
|
|
1106
|
+
return {
|
|
1107
|
+
branchName,
|
|
1108
|
+
previousHead,
|
|
1109
|
+
mirrorHead,
|
|
1110
|
+
mode: input.allowNonFastForward ? "reseed" : "fast_forward",
|
|
1111
|
+
shouldMove
|
|
1112
|
+
};
|
|
1113
|
+
});
|
|
1114
|
+
}
|
|
1115
|
+
function applyObservedProjectMirrorHeads(input) {
|
|
1116
|
+
return [...input.observations].sort((left, right) => left.branchName.localeCompare(right.branchName)).map((observation) => {
|
|
1117
|
+
const { branchName, mirrorHead } = observation;
|
|
1118
|
+
const checkoutPath = branchPath(input.projectRoot, branchName);
|
|
1119
|
+
const currentHead = git(checkoutPath, ["rev-parse", "HEAD"], `resolve local project head ${branchName}`);
|
|
1120
|
+
if (!observation.shouldMove || !mirrorHead || currentHead === mirrorHead || currentHead !== observation.previousHead) {
|
|
1121
|
+
return { branchName, previousHead: currentHead, mirrorHead, moved: false };
|
|
1122
|
+
}
|
|
1123
|
+
if (!tryGit(checkoutPath, ["cat-file", "-e", `${mirrorHead}^{commit}`])) {
|
|
1124
|
+
throw new Error(`Observed hidden mirror head is no longer available for ${branchName}: ${mirrorHead}`);
|
|
559
1125
|
}
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
1126
|
+
const mutationAllowed = () => !projectWorktreeOperationInProgress(checkoutPath) && input.branchMutationAllowed?.(branchName, checkoutPath) !== false;
|
|
1127
|
+
if (!mutationAllowed()) {
|
|
1128
|
+
return { branchName, previousHead: currentHead, mirrorHead, moved: false };
|
|
1129
|
+
}
|
|
1130
|
+
if (observation.mode === "reseed") {
|
|
1131
|
+
preserveHead(checkoutPath);
|
|
1132
|
+
if (!mutationAllowed()) return { branchName, previousHead: currentHead, mirrorHead, moved: false };
|
|
1133
|
+
}
|
|
1134
|
+
const action = observation.mode === "reseed" ? "reseed" : "fast-forward";
|
|
1135
|
+
git(checkoutPath, ["read-tree", "--reset", mirrorHead], `${action} project index ${branchName}`);
|
|
1136
|
+
git(checkoutPath, ["update-ref", `refs/heads/${branchName}`, mirrorHead, currentHead], `${action} project branch ${branchName}`);
|
|
1137
|
+
return { branchName, previousHead: currentHead, mirrorHead, moved: true };
|
|
1138
|
+
});
|
|
1139
|
+
}
|
|
1140
|
+
function updateProjectHeadsFromMirror(input) {
|
|
1141
|
+
const observations = observeProjectMirrorHeads(input);
|
|
1142
|
+
return applyObservedProjectMirrorHeads({
|
|
1143
|
+
projectRoot: input.projectRoot,
|
|
1144
|
+
primaryBranchName: input.primaryBranchName,
|
|
1145
|
+
observations,
|
|
1146
|
+
branchMutationAllowed: input.branchMutationAllowed
|
|
563
1147
|
});
|
|
564
1148
|
}
|
|
1149
|
+
function fastForwardProjectHeadsFromMirror(input) {
|
|
1150
|
+
return updateProjectHeadsFromMirror({ ...input, allowNonFastForward: false }).map(({ moved, ...state }) => ({
|
|
1151
|
+
...state,
|
|
1152
|
+
fastForwarded: moved
|
|
1153
|
+
}));
|
|
1154
|
+
}
|
|
1155
|
+
function reseedProjectHeadsFromMirror(input) {
|
|
1156
|
+
return updateProjectHeadsFromMirror({ ...input, allowNonFastForward: true }).map(({ moved, ...state }) => ({
|
|
1157
|
+
...state,
|
|
1158
|
+
reseeded: moved
|
|
1159
|
+
}));
|
|
1160
|
+
}
|
|
565
1161
|
const projectWorktreesTestHarness = {
|
|
566
1162
|
commandArgs: gitCommandArgs,
|
|
567
|
-
configureRepository
|
|
1163
|
+
configureRepository,
|
|
1164
|
+
createPreparedSnapshot(input) {
|
|
1165
|
+
return { root: snapshotBranchTrees(input.projectRoot, input.branches, input.temporaryRoot).root };
|
|
1166
|
+
},
|
|
1167
|
+
consumeSnapshot(snapshotRoot) {
|
|
1168
|
+
const manifest = readSnapshotManifest(snapshotRoot);
|
|
1169
|
+
advanceSnapshotState(snapshotRoot, manifest, "consumed");
|
|
1170
|
+
},
|
|
1171
|
+
persistProjectAndConsumeSnapshot(snapshotRoot, afterProjectTreeFsync) {
|
|
1172
|
+
const manifest = readSnapshotManifest(snapshotRoot);
|
|
1173
|
+
persistReconciledProjectAndConsumeSnapshot({ root: snapshotRoot, manifest, paths: /* @__PURE__ */ new Map() }, afterProjectTreeFsync);
|
|
1174
|
+
},
|
|
1175
|
+
snapshotState(snapshotRoot) {
|
|
1176
|
+
return readSnapshotManifest(snapshotRoot).state;
|
|
1177
|
+
}
|
|
568
1178
|
};
|
|
569
1179
|
// Annotate the CommonJS export names for ESM import in node:
|
|
570
1180
|
0 && (module.exports = {
|
|
571
1181
|
PROJECT_WORKTREE_SNAPSHOT_PREFIX,
|
|
572
|
-
|
|
1182
|
+
ProjectBranchCreationRollbackIncompleteError,
|
|
1183
|
+
applyObservedProjectMirrorHeads,
|
|
1184
|
+
assertProjectMirrorHeadsPushed,
|
|
573
1185
|
createLinkedProjectBranch,
|
|
1186
|
+
createOrRetryLinkedProjectBranch,
|
|
574
1187
|
deleteLinkedProjectBranch,
|
|
575
1188
|
deleteProjectMirrorBranch,
|
|
576
1189
|
ensureProjectWorktrees,
|
|
577
1190
|
fastForwardProjectHeadsFromMirror,
|
|
578
1191
|
hasLinkedProjectWorktreeLayout,
|
|
1192
|
+
observeProjectMirrorHeads,
|
|
1193
|
+
projectBranchMayExistAfterCreateFailure,
|
|
1194
|
+
projectOriginBranchName,
|
|
579
1195
|
projectWorktreeConfigurationFingerprint,
|
|
580
1196
|
projectWorktreeOperationInProgress,
|
|
581
1197
|
projectWorktreesTestHarness,
|
|
582
1198
|
pushProjectMirrorHeads,
|
|
583
|
-
|
|
1199
|
+
recoverStaleProjectWorktreeSnapshots,
|
|
1200
|
+
removeProjectWorktrees,
|
|
1201
|
+
reseedProjectHeadsFromMirror
|
|
584
1202
|
});
|