@ricsam/r5d-worker 0.0.136 → 0.0.138
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 -7537
- 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 +328 -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 +17 -0
- package/dist/types/runtime/daemon.d.ts +8 -0
- package/dist/types/runtime/executor.d.ts +35 -0
- package/dist/types/runtime/index.d.ts +5 -0
- package/dist/types/runtime/protocol.d.ts +544 -0
- package/dist/types/runtime/pty.d.ts +30 -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/storage.d.ts +21 -0
- package/dist/types/runtime/test-harness.d.ts +53 -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 -7511
- 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 -3450
- package/dist/cjs/workspace-hydration-ledger.cjs +0 -66
- package/dist/cjs/workspace-hydration-merge.cjs +0 -433
- 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 -467
- 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 -3407
- package/dist/mjs/workspace-hydration-ledger.mjs +0 -42
- package/dist/mjs/workspace-hydration-merge.mjs +0 -399
- 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 -433
- 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-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 -288
- package/dist/types/workspace-filesystem-jobs.d.ts +0 -7
- package/dist/types/workspace-git-sync.d.ts +0 -495
- package/dist/types/workspace-hydration-ledger.d.ts +0 -43
- package/dist/types/workspace-hydration-merge.d.ts +0 -95
- 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 -60
- 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/adapter.d.ts} +0 -0
- /package/dist/types/{recovery-journal-stall-fixture.d.ts → runtime/releases/main.d.ts} +0 -0
|
@@ -1,1713 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __create = Object.create;
|
|
3
|
-
var __defProp = Object.defineProperty;
|
|
4
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
-
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
-
var __export = (target, all) => {
|
|
9
|
-
for (var name in all)
|
|
10
|
-
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
-
};
|
|
12
|
-
var __copyProps = (to, from, except, desc) => {
|
|
13
|
-
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
-
for (let key of __getOwnPropNames(from))
|
|
15
|
-
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
-
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
-
}
|
|
18
|
-
return to;
|
|
19
|
-
};
|
|
20
|
-
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
-
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
-
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
-
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
-
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
-
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
-
mod
|
|
27
|
-
));
|
|
28
|
-
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
-
var project_worktrees_exports = {};
|
|
30
|
-
__export(project_worktrees_exports, {
|
|
31
|
-
PROJECT_REMOVED_CHECKOUTS_DIRECTORY: () => PROJECT_REMOVED_CHECKOUTS_DIRECTORY,
|
|
32
|
-
PROJECT_WORKTREE_SNAPSHOT_OWNER_SESSION_ID: () => PROJECT_WORKTREE_SNAPSHOT_OWNER_SESSION_ID,
|
|
33
|
-
PROJECT_WORKTREE_SNAPSHOT_PREFIX: () => PROJECT_WORKTREE_SNAPSHOT_PREFIX,
|
|
34
|
-
ProjectBranchCreationRollbackIncompleteError: () => ProjectBranchCreationRollbackIncompleteError,
|
|
35
|
-
applyObservedProjectMirrorHeads: () => applyObservedProjectMirrorHeads,
|
|
36
|
-
assertProjectMirrorHeadsPushed: () => assertProjectMirrorHeadsPushed,
|
|
37
|
-
classifyProjectCheckout: () => classifyProjectCheckout,
|
|
38
|
-
computeLocalProjectMirrorRefsTokens: () => computeLocalProjectMirrorRefsTokens,
|
|
39
|
-
createLinkedProjectBranch: () => createLinkedProjectBranch,
|
|
40
|
-
createOrRetryLinkedProjectBranch: () => createOrRetryLinkedProjectBranch,
|
|
41
|
-
deleteLinkedProjectBranch: () => deleteLinkedProjectBranch,
|
|
42
|
-
deleteProjectMirrorBranch: () => deleteProjectMirrorBranch,
|
|
43
|
-
ensureProjectWorktrees: () => ensureProjectWorktrees,
|
|
44
|
-
fastForwardProjectHeadsFromMirror: () => fastForwardProjectHeadsFromMirror,
|
|
45
|
-
hasLinkedProjectWorktreeLayout: () => hasLinkedProjectWorktreeLayout,
|
|
46
|
-
inventoryProjectCheckouts: () => inventoryProjectCheckouts,
|
|
47
|
-
observeProjectMirrorHeads: () => observeProjectMirrorHeads,
|
|
48
|
-
projectBranchMayExistAfterCreateFailure: () => projectBranchMayExistAfterCreateFailure,
|
|
49
|
-
projectOriginBranchName: () => projectOriginBranchName,
|
|
50
|
-
projectWorktreeConfigurationFingerprint: () => projectWorktreeConfigurationFingerprint,
|
|
51
|
-
projectWorktreeOperationInProgress: () => projectWorktreeOperationInProgress,
|
|
52
|
-
projectWorktreesTestHarness: () => projectWorktreesTestHarness,
|
|
53
|
-
pushProjectMirrorHeads: () => pushProjectMirrorHeads,
|
|
54
|
-
recoverStaleProjectWorktreeSnapshots: () => recoverStaleProjectWorktreeSnapshots,
|
|
55
|
-
removeProjectWorktrees: () => removeProjectWorktrees,
|
|
56
|
-
reseedProjectHeadsFromMirror: () => reseedProjectHeadsFromMirror,
|
|
57
|
-
runProjectSnapshotApplyJob: () => runProjectSnapshotApplyJob,
|
|
58
|
-
runProjectSnapshotCreateJob: () => runProjectSnapshotCreateJob,
|
|
59
|
-
runProjectSnapshotPersistJob: () => runProjectSnapshotPersistJob,
|
|
60
|
-
runProjectSnapshotRollbackJob: () => runProjectSnapshotRollbackJob
|
|
61
|
-
});
|
|
62
|
-
module.exports = __toCommonJS(project_worktrees_exports);
|
|
63
|
-
var import_node_crypto = require("node:crypto");
|
|
64
|
-
var import_node_fs = __toESM(require("node:fs"), 1);
|
|
65
|
-
var import_node_os = __toESM(require("node:os"), 1);
|
|
66
|
-
var import_node_path = __toESM(require("node:path"), 1);
|
|
67
|
-
var import_git_process_environment = require("./git-process-environment.cjs");
|
|
68
|
-
var import_managed_paths = require("./managed-paths.cjs");
|
|
69
|
-
var import_project_checkout_garbage = require("./project-checkout-garbage.cjs");
|
|
70
|
-
var import_project_mirror_refs_token = require("./project-mirror-refs-token.cjs");
|
|
71
|
-
var import_working_tree_mirror = require("./working-tree-mirror.cjs");
|
|
72
|
-
var import_workspace_filesystem_executor = require("./workspace-filesystem-executor.cjs");
|
|
73
|
-
const PROJECT_WORKTREE_SNAPSHOT_PREFIX = "r5d-project-worktrees-";
|
|
74
|
-
const PROJECT_WORKTREE_SNAPSHOT_MANIFEST = "transaction.json";
|
|
75
|
-
const PROJECT_WORKTREE_SNAPSHOT_MANIFEST_VERSION = 1;
|
|
76
|
-
const MAX_PROJECT_WORKTREE_SNAPSHOT_MANIFEST_BYTES = 64 * 1024;
|
|
77
|
-
const PROJECT_WORKTREE_SNAPSHOT_OWNER_SESSION_ID = (0, import_node_crypto.randomUUID)();
|
|
78
|
-
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;
|
|
79
|
-
const PROJECT_GIT_TRANSPORT_TIMEOUT_MS = 3e4;
|
|
80
|
-
const PROJECT_WORKTREE_SNAPSHOT_NAME = new RegExp(
|
|
81
|
-
`^${PROJECT_WORKTREE_SNAPSHOT_PREFIX.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}([1-9]\\d*)-([A-Za-z0-9]{6})$`
|
|
82
|
-
);
|
|
83
|
-
class ProjectBranchCreationRollbackIncompleteError extends Error {
|
|
84
|
-
branchMayExist = true;
|
|
85
|
-
cleanupFailures;
|
|
86
|
-
constructor(branchName, cleanupFailures, cause) {
|
|
87
|
-
super(`Project branch ${branchName} may still exist after incomplete rollback: ${cleanupFailures.join("; ")}`, { cause });
|
|
88
|
-
this.name = "ProjectBranchCreationRollbackIncompleteError";
|
|
89
|
-
this.cleanupFailures = [...cleanupFailures];
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
function projectBranchMayExistAfterCreateFailure(error) {
|
|
93
|
-
return error instanceof ProjectBranchCreationRollbackIncompleteError || typeof error === "object" && error !== null && "branchMayExist" in error && error.branchMayExist === true;
|
|
94
|
-
}
|
|
95
|
-
function projectWorktreeConfigurationFingerprint(input) {
|
|
96
|
-
return (0, import_node_crypto.createHash)("sha256").update(
|
|
97
|
-
JSON.stringify({
|
|
98
|
-
checkoutPathSegments: input.checkoutPathSegments,
|
|
99
|
-
projectPath: input.projectPath,
|
|
100
|
-
repoHttpUrl: input.repoHttpUrl,
|
|
101
|
-
repoAuthHeader: input.repoAuthHeader,
|
|
102
|
-
defaultBranch: input.defaultBranch,
|
|
103
|
-
repositoryTransitionId: input.repositoryTransitionId ?? null,
|
|
104
|
-
executionDisabled: input.executionDisabled ?? false,
|
|
105
|
-
mirrorWritesDisabled: input.mirrorWritesDisabled ?? false,
|
|
106
|
-
preserveOnlyBranches: [...input.preserveOnlyBranches ?? []].sort((left, right) => left.branchName.localeCompare(right.branchName)).map(({ branchId, branchName }) => ({ branchId: branchId ?? null, branchName })),
|
|
107
|
-
branches: [...input.branches].sort((left, right) => left.branchName.localeCompare(right.branchName)).map(({ branchId, branchName, sourceBranchName, baseCommitHash }) => ({
|
|
108
|
-
branchId: branchId ?? null,
|
|
109
|
-
branchName,
|
|
110
|
-
sourceBranchName: sourceBranchName ?? null,
|
|
111
|
-
baseCommitHash
|
|
112
|
-
})),
|
|
113
|
-
gitIdentity: input.gitIdentity
|
|
114
|
-
})
|
|
115
|
-
).digest("hex");
|
|
116
|
-
}
|
|
117
|
-
const NON_RECURSIVE_GIT_CONFIG = [
|
|
118
|
-
"-c",
|
|
119
|
-
"submodule.recurse=false",
|
|
120
|
-
"-c",
|
|
121
|
-
"fetch.recurseSubmodules=false",
|
|
122
|
-
"-c",
|
|
123
|
-
"push.recurseSubmodules=false"
|
|
124
|
-
];
|
|
125
|
-
function gitCommandArgs(args) {
|
|
126
|
-
return ["git", ...NON_RECURSIVE_GIT_CONFIG, ...args];
|
|
127
|
-
}
|
|
128
|
-
function gitResult(cwd, args) {
|
|
129
|
-
const result = Bun.spawnSync(gitCommandArgs(args), {
|
|
130
|
-
cwd,
|
|
131
|
-
stdout: "pipe",
|
|
132
|
-
stderr: "pipe",
|
|
133
|
-
env: (0, import_git_process_environment.workerGitProcessEnvironment)()
|
|
134
|
-
});
|
|
135
|
-
return {
|
|
136
|
-
exitCode: result.exitCode,
|
|
137
|
-
stdout: result.stdout.toString().trim(),
|
|
138
|
-
stderr: result.stderr.toString().trim()
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
|
-
function git(cwd, args, action) {
|
|
142
|
-
const result = gitResult(cwd, args);
|
|
143
|
-
if (result.exitCode !== 0) throw new Error(`${action}: ${result.stderr || result.stdout || `git exited ${result.exitCode}`}`);
|
|
144
|
-
return result.stdout;
|
|
145
|
-
}
|
|
146
|
-
function tryGit(cwd, args) {
|
|
147
|
-
return gitResult(cwd, args).exitCode === 0;
|
|
148
|
-
}
|
|
149
|
-
async function gitResultAsync(cwd, args, timeoutMs) {
|
|
150
|
-
const subprocess = Bun.spawn(gitCommandArgs(args), {
|
|
151
|
-
cwd,
|
|
152
|
-
stdin: "ignore",
|
|
153
|
-
stdout: "pipe",
|
|
154
|
-
stderr: "pipe",
|
|
155
|
-
env: (0, import_git_process_environment.workerGitProcessEnvironment)()
|
|
156
|
-
});
|
|
157
|
-
let timedOut = false;
|
|
158
|
-
const timeout = timeoutMs ? setTimeout(() => {
|
|
159
|
-
timedOut = true;
|
|
160
|
-
subprocess.kill("SIGKILL");
|
|
161
|
-
}, timeoutMs) : void 0;
|
|
162
|
-
try {
|
|
163
|
-
const [stdout, stderr, exitCode] = await Promise.all([
|
|
164
|
-
new Response(subprocess.stdout).text(),
|
|
165
|
-
new Response(subprocess.stderr).text(),
|
|
166
|
-
subprocess.exited
|
|
167
|
-
]);
|
|
168
|
-
if (timedOut) throw new Error(`Git transport timed out after ${timeoutMs}ms`);
|
|
169
|
-
return { exitCode, stdout: stdout.trim(), stderr: stderr.trim() };
|
|
170
|
-
} finally {
|
|
171
|
-
if (timeout) clearTimeout(timeout);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
async function gitAsync(cwd, args, action) {
|
|
175
|
-
const result = await gitResultAsync(cwd, args);
|
|
176
|
-
if (result.exitCode !== 0) throw new Error(`${action}: ${result.stderr || result.stdout || `git exited ${result.exitCode}`}`);
|
|
177
|
-
return result.stdout;
|
|
178
|
-
}
|
|
179
|
-
async function tryGitAsync(cwd, args) {
|
|
180
|
-
return (await gitResultAsync(cwd, args)).exitCode === 0;
|
|
181
|
-
}
|
|
182
|
-
function branchPath(projectRoot, branchName) {
|
|
183
|
-
(0, import_managed_paths.validateManagedBranchName)(branchName);
|
|
184
|
-
const result = import_node_path.default.join(projectRoot, ...branchName.split("/"));
|
|
185
|
-
const relative = import_node_path.default.relative(import_node_path.default.resolve(projectRoot), import_node_path.default.resolve(result));
|
|
186
|
-
if (relative === ".." || relative.startsWith(`..${import_node_path.default.sep}`) || import_node_path.default.isAbsolute(relative)) {
|
|
187
|
-
throw new Error(`Branch path escapes its project root: ${branchName}`);
|
|
188
|
-
}
|
|
189
|
-
return result;
|
|
190
|
-
}
|
|
191
|
-
function assertNoSymlinkBranchPathComponents(projectRoot, branchPath2) {
|
|
192
|
-
const resolvedRoot = import_node_path.default.resolve(projectRoot);
|
|
193
|
-
const resolvedBranchPath = import_node_path.default.resolve(branchPath2);
|
|
194
|
-
const relative = import_node_path.default.relative(resolvedRoot, resolvedBranchPath);
|
|
195
|
-
if (relative === ".." || relative.startsWith(`..${import_node_path.default.sep}`) || import_node_path.default.isAbsolute(relative)) {
|
|
196
|
-
throw new Error(`Branch path escapes its project root: ${branchPath2}`);
|
|
197
|
-
}
|
|
198
|
-
let current = resolvedRoot;
|
|
199
|
-
for (const component of ["", ...relative.split(import_node_path.default.sep).filter(Boolean)]) {
|
|
200
|
-
if (component) current = import_node_path.default.join(current, component);
|
|
201
|
-
try {
|
|
202
|
-
if (import_node_fs.default.lstatSync(current).isSymbolicLink()) {
|
|
203
|
-
throw new Error(`Project branch path contains a symbolic link: ${current}`);
|
|
204
|
-
}
|
|
205
|
-
} catch (error) {
|
|
206
|
-
const code = error.code;
|
|
207
|
-
if (code === "ENOENT" || code === "ENOTDIR") return;
|
|
208
|
-
throw error;
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
function assertNonOverlappingBranches(branches) {
|
|
213
|
-
const names = branches.map(({ branchName }) => branchName).sort();
|
|
214
|
-
if (new Set(names).size !== names.length) throw new Error("Project worktree branch names must be unique");
|
|
215
|
-
for (let index = 0; index < names.length; index += 1) {
|
|
216
|
-
const name = names[index];
|
|
217
|
-
(0, import_managed_paths.validateManagedBranchName)(name);
|
|
218
|
-
const baseCommitHash = branches.find(({ branchName }) => branchName === name)?.baseCommitHash;
|
|
219
|
-
if (!baseCommitHash || !/^[0-9a-f]{40}(?:[0-9a-f]{24})?$/.test(baseCommitHash)) {
|
|
220
|
-
throw new Error(`Invalid base commit hash for project branch ${name}`);
|
|
221
|
-
}
|
|
222
|
-
const overlapping = names.slice(index + 1).find((candidate) => candidate.startsWith(`${name}/`));
|
|
223
|
-
if (overlapping) throw new Error(`Branch folders overlap: ${name} and ${overlapping}`);
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
function gitDirectoryKind(checkoutPath) {
|
|
227
|
-
const gitPath = import_node_path.default.join(checkoutPath, ".git");
|
|
228
|
-
try {
|
|
229
|
-
const stat = import_node_fs.default.lstatSync(gitPath);
|
|
230
|
-
if (stat.isDirectory()) return "directory";
|
|
231
|
-
if (stat.isFile()) return "file";
|
|
232
|
-
return null;
|
|
233
|
-
} catch {
|
|
234
|
-
return null;
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
function commonGitDirectory(checkoutPath) {
|
|
238
|
-
const result = gitResult(checkoutPath, ["rev-parse", "--path-format=absolute", "--git-common-dir"]);
|
|
239
|
-
return result.exitCode === 0 ? import_node_path.default.resolve(result.stdout) : null;
|
|
240
|
-
}
|
|
241
|
-
async function commonGitDirectoryAsync(checkoutPath) {
|
|
242
|
-
const result = await gitResultAsync(checkoutPath, ["rev-parse", "--path-format=absolute", "--git-common-dir"]);
|
|
243
|
-
return result.exitCode === 0 ? import_node_path.default.resolve(result.stdout) : null;
|
|
244
|
-
}
|
|
245
|
-
function hasLinkedProjectWorktreeLayout(input) {
|
|
246
|
-
const primaryPath = branchPath(input.projectRoot, input.primaryBranchName);
|
|
247
|
-
if (gitDirectoryKind(primaryPath) !== "directory") return false;
|
|
248
|
-
const primaryCommonDir = commonGitDirectory(primaryPath);
|
|
249
|
-
if (!primaryCommonDir) return false;
|
|
250
|
-
return input.branches.every(({ branchName }) => {
|
|
251
|
-
const checkoutPath = branchPath(input.projectRoot, branchName);
|
|
252
|
-
if (branchName === input.primaryBranchName) return commonGitDirectory(checkoutPath) === primaryCommonDir;
|
|
253
|
-
return gitDirectoryKind(checkoutPath) === "file" && commonGitDirectory(checkoutPath) === primaryCommonDir;
|
|
254
|
-
});
|
|
255
|
-
}
|
|
256
|
-
async function hasCompatibleLinkedProjectWorktreeLayout(input) {
|
|
257
|
-
const primaryPath = branchPath(input.projectRoot, input.primaryBranchName);
|
|
258
|
-
if (gitDirectoryKind(primaryPath) !== "directory") return false;
|
|
259
|
-
const primaryCommonDir = await commonGitDirectoryAsync(primaryPath);
|
|
260
|
-
if (!primaryCommonDir) return false;
|
|
261
|
-
for (const { branchName } of input.branches) {
|
|
262
|
-
if (branchName === input.primaryBranchName) continue;
|
|
263
|
-
const checkoutPath = branchPath(input.projectRoot, branchName);
|
|
264
|
-
const kind = gitDirectoryKind(checkoutPath);
|
|
265
|
-
if (kind === null) continue;
|
|
266
|
-
if (kind !== "file" || await commonGitDirectoryAsync(checkoutPath) !== primaryCommonDir) return false;
|
|
267
|
-
}
|
|
268
|
-
return true;
|
|
269
|
-
}
|
|
270
|
-
function fsyncDirectory(directory) {
|
|
271
|
-
const descriptor = import_node_fs.default.openSync(directory, "r");
|
|
272
|
-
try {
|
|
273
|
-
import_node_fs.default.fsyncSync(descriptor);
|
|
274
|
-
} finally {
|
|
275
|
-
import_node_fs.default.closeSync(descriptor);
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
function fsyncTree(targetPath) {
|
|
279
|
-
const stat = import_node_fs.default.lstatSync(targetPath);
|
|
280
|
-
if (stat.isSymbolicLink()) return;
|
|
281
|
-
if (stat.isDirectory()) {
|
|
282
|
-
for (const entry of import_node_fs.default.readdirSync(targetPath).sort()) fsyncTree(import_node_path.default.join(targetPath, entry));
|
|
283
|
-
fsyncDirectory(targetPath);
|
|
284
|
-
return;
|
|
285
|
-
}
|
|
286
|
-
if (!stat.isFile()) return;
|
|
287
|
-
const descriptor = import_node_fs.default.openSync(targetPath, "r");
|
|
288
|
-
try {
|
|
289
|
-
import_node_fs.default.fsyncSync(descriptor);
|
|
290
|
-
} finally {
|
|
291
|
-
import_node_fs.default.closeSync(descriptor);
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
function fsyncDirectoryChain(directory, root) {
|
|
295
|
-
const resolvedDirectory = import_node_path.default.resolve(directory);
|
|
296
|
-
const resolvedRoot = import_node_path.default.resolve(root);
|
|
297
|
-
const relative = import_node_path.default.relative(resolvedRoot, resolvedDirectory);
|
|
298
|
-
if (relative === ".." || relative.startsWith(`..${import_node_path.default.sep}`) || import_node_path.default.isAbsolute(relative)) {
|
|
299
|
-
throw new Error(`Directory is outside its durability root: ${resolvedDirectory}`);
|
|
300
|
-
}
|
|
301
|
-
let current = resolvedDirectory;
|
|
302
|
-
while (true) {
|
|
303
|
-
assertRealDirectory(current, "Durability path");
|
|
304
|
-
fsyncDirectory(current);
|
|
305
|
-
if (current === resolvedRoot) break;
|
|
306
|
-
current = import_node_path.default.dirname(current);
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
function removeSnapshotStorage(snapshotRoot) {
|
|
310
|
-
const storageRoot = import_node_path.default.dirname(import_node_path.default.resolve(snapshotRoot));
|
|
311
|
-
import_node_fs.default.rmSync(snapshotRoot, { recursive: true, force: true });
|
|
312
|
-
fsyncDirectory(storageRoot);
|
|
313
|
-
}
|
|
314
|
-
function fsyncManagedProjectRootEntry(projectRoot) {
|
|
315
|
-
fsyncDirectoryChain(projectRoot, import_node_path.default.dirname(import_node_path.default.dirname(projectRoot)));
|
|
316
|
-
}
|
|
317
|
-
function createProjectWorktreeSnapshotRoot(temporaryRoot = import_node_os.default.tmpdir()) {
|
|
318
|
-
const resolvedTemporaryRoot = import_node_path.default.resolve(temporaryRoot);
|
|
319
|
-
const storageRootExisted = import_node_fs.default.existsSync(resolvedTemporaryRoot);
|
|
320
|
-
import_node_fs.default.mkdirSync(resolvedTemporaryRoot, { recursive: true, mode: 448 });
|
|
321
|
-
assertRealDirectory(resolvedTemporaryRoot, "Project worktree snapshot storage root");
|
|
322
|
-
if (!storageRootExisted) fsyncDirectory(import_node_path.default.dirname(resolvedTemporaryRoot));
|
|
323
|
-
fsyncDirectory(resolvedTemporaryRoot);
|
|
324
|
-
const snapshotRoot = import_node_fs.default.mkdtempSync(import_node_path.default.join(resolvedTemporaryRoot, `${PROJECT_WORKTREE_SNAPSHOT_PREFIX}${process.pid}-`));
|
|
325
|
-
fsyncDirectory(snapshotRoot);
|
|
326
|
-
fsyncDirectory(resolvedTemporaryRoot);
|
|
327
|
-
return snapshotRoot;
|
|
328
|
-
}
|
|
329
|
-
function snapshotManifestPath(snapshotRoot) {
|
|
330
|
-
return import_node_path.default.join(snapshotRoot, PROJECT_WORKTREE_SNAPSHOT_MANIFEST);
|
|
331
|
-
}
|
|
332
|
-
function assertRealDirectory(directory, label) {
|
|
333
|
-
const stat = import_node_fs.default.lstatSync(directory);
|
|
334
|
-
if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error(`${label} must be a real directory`);
|
|
335
|
-
}
|
|
336
|
-
function normalizeSnapshotManifest(value) {
|
|
337
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
338
|
-
throw new Error("Project worktree snapshot manifest must be an object");
|
|
339
|
-
}
|
|
340
|
-
const record = value;
|
|
341
|
-
if (record.version !== PROJECT_WORKTREE_SNAPSHOT_MANIFEST_VERSION) {
|
|
342
|
-
throw new Error("Unsupported project worktree snapshot manifest version");
|
|
343
|
-
}
|
|
344
|
-
if (record.state !== "building" && record.state !== "prepared" && record.state !== "restoring" && record.state !== "consumed") {
|
|
345
|
-
throw new Error("Project worktree snapshot manifest has an invalid state");
|
|
346
|
-
}
|
|
347
|
-
const scope = record.scope === void 0 ? "project" : record.scope;
|
|
348
|
-
if (scope !== "project" && scope !== "branches") throw new Error("Project worktree snapshot manifest has an invalid scope");
|
|
349
|
-
if (!Number.isSafeInteger(record.ownerProcessId) || record.ownerProcessId <= 0) {
|
|
350
|
-
throw new Error("Project worktree snapshot manifest has an invalid owner process");
|
|
351
|
-
}
|
|
352
|
-
if (typeof record.ownerSessionId !== "string" || !UUID_PATTERN.test(record.ownerSessionId)) {
|
|
353
|
-
throw new Error("Project worktree snapshot manifest has an invalid owner session");
|
|
354
|
-
}
|
|
355
|
-
if (typeof record.projectRoot !== "string" || !import_node_path.default.isAbsolute(record.projectRoot) || import_node_path.default.resolve(record.projectRoot) !== record.projectRoot) {
|
|
356
|
-
throw new Error("Project worktree snapshot manifest has an invalid project root");
|
|
357
|
-
}
|
|
358
|
-
if (typeof record.createdAt !== "string" || !Number.isFinite(Date.parse(record.createdAt))) {
|
|
359
|
-
throw new Error("Project worktree snapshot manifest has an invalid creation time");
|
|
360
|
-
}
|
|
361
|
-
if (!Array.isArray(record.branches)) throw new Error("Project worktree snapshot manifest has invalid branches");
|
|
362
|
-
const branches = record.branches.map((value2) => {
|
|
363
|
-
if (typeof value2 !== "object" || value2 === null || Array.isArray(value2)) {
|
|
364
|
-
throw new Error("Project worktree snapshot manifest has an invalid branch");
|
|
365
|
-
}
|
|
366
|
-
const branch = value2;
|
|
367
|
-
if (typeof branch.branchName !== "string" || typeof branch.snapshotPresent !== "boolean") {
|
|
368
|
-
throw new Error("Project worktree snapshot manifest has an invalid branch");
|
|
369
|
-
}
|
|
370
|
-
(0, import_managed_paths.validateManagedBranchName)(branch.branchName);
|
|
371
|
-
return { branchName: branch.branchName, snapshotPresent: branch.snapshotPresent };
|
|
372
|
-
});
|
|
373
|
-
const names = branches.map(({ branchName }) => branchName).sort();
|
|
374
|
-
if (new Set(names).size !== names.length) throw new Error("Project worktree snapshot branch names must be unique");
|
|
375
|
-
for (let index = 0; index < names.length; index += 1) {
|
|
376
|
-
const name = names[index];
|
|
377
|
-
if (names.slice(index + 1).some((candidate) => candidate.startsWith(`${name}/`))) {
|
|
378
|
-
throw new Error("Project worktree snapshot branch paths overlap");
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
return {
|
|
382
|
-
version: PROJECT_WORKTREE_SNAPSHOT_MANIFEST_VERSION,
|
|
383
|
-
state: record.state,
|
|
384
|
-
scope,
|
|
385
|
-
ownerProcessId: record.ownerProcessId,
|
|
386
|
-
ownerSessionId: record.ownerSessionId,
|
|
387
|
-
projectRoot: record.projectRoot,
|
|
388
|
-
branches,
|
|
389
|
-
createdAt: record.createdAt
|
|
390
|
-
};
|
|
391
|
-
}
|
|
392
|
-
function readSnapshotManifest(snapshotRoot) {
|
|
393
|
-
assertRealDirectory(snapshotRoot, "Project worktree snapshot root");
|
|
394
|
-
const manifestPath = snapshotManifestPath(snapshotRoot);
|
|
395
|
-
const stat = import_node_fs.default.lstatSync(manifestPath);
|
|
396
|
-
if (!stat.isFile() || stat.isSymbolicLink()) throw new Error("Project worktree snapshot manifest must be a regular file");
|
|
397
|
-
if (stat.size > MAX_PROJECT_WORKTREE_SNAPSHOT_MANIFEST_BYTES) throw new Error("Project worktree snapshot manifest is too large");
|
|
398
|
-
return normalizeSnapshotManifest(JSON.parse(import_node_fs.default.readFileSync(manifestPath, "utf8")));
|
|
399
|
-
}
|
|
400
|
-
function writeSnapshotManifest(snapshotRoot, manifest) {
|
|
401
|
-
assertRealDirectory(snapshotRoot, "Project worktree snapshot root");
|
|
402
|
-
const normalized = normalizeSnapshotManifest(manifest);
|
|
403
|
-
const temporaryPath = import_node_path.default.join(snapshotRoot, `.${PROJECT_WORKTREE_SNAPSHOT_MANIFEST}.${process.pid}.${(0, import_node_crypto.randomUUID)()}.tmp`);
|
|
404
|
-
let descriptor;
|
|
405
|
-
try {
|
|
406
|
-
descriptor = import_node_fs.default.openSync(temporaryPath, "wx", 384);
|
|
407
|
-
import_node_fs.default.writeFileSync(descriptor, `${JSON.stringify(normalized, null, 2)}
|
|
408
|
-
`, "utf8");
|
|
409
|
-
import_node_fs.default.fsyncSync(descriptor);
|
|
410
|
-
import_node_fs.default.closeSync(descriptor);
|
|
411
|
-
descriptor = void 0;
|
|
412
|
-
import_node_fs.default.renameSync(temporaryPath, snapshotManifestPath(snapshotRoot));
|
|
413
|
-
fsyncDirectory(snapshotRoot);
|
|
414
|
-
} finally {
|
|
415
|
-
if (descriptor !== void 0) import_node_fs.default.closeSync(descriptor);
|
|
416
|
-
import_node_fs.default.rmSync(temporaryPath, { force: true });
|
|
417
|
-
}
|
|
418
|
-
return readSnapshotManifest(snapshotRoot);
|
|
419
|
-
}
|
|
420
|
-
function assertSameSnapshotIdentity(expected, actual) {
|
|
421
|
-
if (expected.scope !== actual.scope || expected.ownerProcessId !== actual.ownerProcessId || expected.ownerSessionId !== actual.ownerSessionId || expected.projectRoot !== actual.projectRoot || expected.createdAt !== actual.createdAt || JSON.stringify(expected.branches) !== JSON.stringify(actual.branches)) {
|
|
422
|
-
throw new Error("Project worktree snapshot identity changed");
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
function advanceSnapshotState(snapshotRoot, expected, state) {
|
|
426
|
-
const current = readSnapshotManifest(snapshotRoot);
|
|
427
|
-
assertSameSnapshotIdentity(expected, current);
|
|
428
|
-
const allowed = current.state === "building" && state === "prepared" || current.state === "prepared" && state === "restoring" || current.state === "prepared" && state === "consumed" || current.state === "restoring" && state === "consumed" || current.state === state;
|
|
429
|
-
if (!allowed) throw new Error(`Project worktree snapshot cannot advance from ${current.state} to ${state}`);
|
|
430
|
-
return current.state === state ? current : writeSnapshotManifest(snapshotRoot, { ...current, state });
|
|
431
|
-
}
|
|
432
|
-
function fullSnapshotPath(snapshotRoot, branchName) {
|
|
433
|
-
(0, import_managed_paths.validateManagedBranchName)(branchName);
|
|
434
|
-
return import_node_path.default.join(snapshotRoot, "trees", ...branchName.split("/"));
|
|
435
|
-
}
|
|
436
|
-
function copyCheckoutTree(sourceRoot, targetRoot) {
|
|
437
|
-
const sourceStat = import_node_fs.default.lstatSync(sourceRoot);
|
|
438
|
-
if (!sourceStat.isDirectory() || sourceStat.isSymbolicLink()) {
|
|
439
|
-
throw new Error(`Project checkout snapshot source must be a real directory: ${sourceRoot}`);
|
|
440
|
-
}
|
|
441
|
-
import_node_fs.default.mkdirSync(import_node_path.default.dirname(targetRoot), { recursive: true });
|
|
442
|
-
import_node_fs.default.cpSync(sourceRoot, targetRoot, {
|
|
443
|
-
recursive: true,
|
|
444
|
-
dereference: false,
|
|
445
|
-
errorOnExist: true,
|
|
446
|
-
force: false,
|
|
447
|
-
mode: import_node_fs.default.constants.COPYFILE_FICLONE,
|
|
448
|
-
preserveTimestamps: true,
|
|
449
|
-
verbatimSymlinks: true
|
|
450
|
-
});
|
|
451
|
-
}
|
|
452
|
-
function measureCheckoutTree(root) {
|
|
453
|
-
const stat = import_node_fs.default.lstatSync(root);
|
|
454
|
-
if (stat.isSymbolicLink()) return 0;
|
|
455
|
-
if (stat.isFile()) return stat.size;
|
|
456
|
-
if (!stat.isDirectory()) return 0;
|
|
457
|
-
let bytes = 0;
|
|
458
|
-
for (const entry of import_node_fs.default.readdirSync(root).sort()) bytes += measureCheckoutTree(import_node_path.default.join(root, entry));
|
|
459
|
-
return bytes;
|
|
460
|
-
}
|
|
461
|
-
function snapshotBranchMeasurements(branches, sourcePath) {
|
|
462
|
-
return new Map(
|
|
463
|
-
branches.map(({ branchName, snapshotPresent }) => [branchName, snapshotPresent ? measureCheckoutTree(sourcePath(branchName)) : 0])
|
|
464
|
-
);
|
|
465
|
-
}
|
|
466
|
-
function assertNoOutstandingSnapshotForProject(temporaryRoot, projectRoot) {
|
|
467
|
-
let entries;
|
|
468
|
-
try {
|
|
469
|
-
assertRealDirectory(temporaryRoot, "Project worktree snapshot storage root");
|
|
470
|
-
entries = import_node_fs.default.readdirSync(temporaryRoot, { withFileTypes: true });
|
|
471
|
-
} catch (error) {
|
|
472
|
-
if (error.code === "ENOENT") return;
|
|
473
|
-
throw error;
|
|
474
|
-
}
|
|
475
|
-
for (const entry of entries) {
|
|
476
|
-
if (!entry.isDirectory() || entry.isSymbolicLink() || !PROJECT_WORKTREE_SNAPSHOT_NAME.test(entry.name)) continue;
|
|
477
|
-
let manifest;
|
|
478
|
-
try {
|
|
479
|
-
manifest = readSnapshotManifest(import_node_path.default.join(temporaryRoot, entry.name));
|
|
480
|
-
} catch (error) {
|
|
481
|
-
throw new Error(`Project checkout snapshot state is unreadable at ${import_node_path.default.join(temporaryRoot, entry.name)}`, { cause: error });
|
|
482
|
-
}
|
|
483
|
-
if (manifest.projectRoot === projectRoot && manifest.state !== "consumed") {
|
|
484
|
-
throw new Error(`Project checkout has an unfinished recovery snapshot at ${import_node_path.default.join(temporaryRoot, entry.name)}`);
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
function snapshotBranchTrees(projectRoot, branches, temporaryRoot, onProgress, scope, ownerSessionId) {
|
|
489
|
-
const resolvedProjectRoot = import_node_path.default.resolve(projectRoot);
|
|
490
|
-
const resolvedTemporaryRoot = import_node_path.default.resolve(temporaryRoot ?? import_node_os.default.tmpdir());
|
|
491
|
-
assertNoOutstandingSnapshotForProject(resolvedTemporaryRoot, resolvedProjectRoot);
|
|
492
|
-
const root = createProjectWorktreeSnapshotRoot(resolvedTemporaryRoot);
|
|
493
|
-
const paths = /* @__PURE__ */ new Map();
|
|
494
|
-
try {
|
|
495
|
-
const branchSnapshots = branches.map(({ branchName }) => {
|
|
496
|
-
const checkoutPath = branchPath(resolvedProjectRoot, branchName);
|
|
497
|
-
let snapshotPresent = false;
|
|
498
|
-
try {
|
|
499
|
-
import_node_fs.default.lstatSync(checkoutPath);
|
|
500
|
-
snapshotPresent = true;
|
|
501
|
-
} catch (error) {
|
|
502
|
-
if (error.code !== "ENOENT") throw error;
|
|
503
|
-
}
|
|
504
|
-
return { branchName, snapshotPresent };
|
|
505
|
-
});
|
|
506
|
-
let manifest = writeSnapshotManifest(root, {
|
|
507
|
-
version: PROJECT_WORKTREE_SNAPSHOT_MANIFEST_VERSION,
|
|
508
|
-
state: "building",
|
|
509
|
-
scope,
|
|
510
|
-
ownerProcessId: process.pid,
|
|
511
|
-
ownerSessionId,
|
|
512
|
-
projectRoot: resolvedProjectRoot,
|
|
513
|
-
branches: branchSnapshots,
|
|
514
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
515
|
-
});
|
|
516
|
-
onProgress?.({
|
|
517
|
-
operation: "snapshot",
|
|
518
|
-
phase: "scanning",
|
|
519
|
-
projectRoot: resolvedProjectRoot,
|
|
520
|
-
completedBranches: 0,
|
|
521
|
-
totalBranches: branchSnapshots.length,
|
|
522
|
-
completedBytes: 0
|
|
523
|
-
});
|
|
524
|
-
const branchBytes = snapshotBranchMeasurements(branchSnapshots, (branchName) => branchPath(resolvedProjectRoot, branchName));
|
|
525
|
-
const totalBytes = [...branchBytes.values()].reduce((total, bytes) => total + bytes, 0);
|
|
526
|
-
let completedBytes = 0;
|
|
527
|
-
let completedBranches = 0;
|
|
528
|
-
for (const { branchName, snapshotPresent } of branchSnapshots) {
|
|
529
|
-
onProgress?.({
|
|
530
|
-
operation: "snapshot",
|
|
531
|
-
phase: "copying",
|
|
532
|
-
projectRoot: resolvedProjectRoot,
|
|
533
|
-
branchName,
|
|
534
|
-
completedBranches,
|
|
535
|
-
totalBranches: branchSnapshots.length,
|
|
536
|
-
completedBytes,
|
|
537
|
-
totalBytes
|
|
538
|
-
});
|
|
539
|
-
if (snapshotPresent) {
|
|
540
|
-
const snapshotPath = fullSnapshotPath(root, branchName);
|
|
541
|
-
copyCheckoutTree(branchPath(resolvedProjectRoot, branchName), snapshotPath);
|
|
542
|
-
paths.set(branchName, snapshotPath);
|
|
543
|
-
}
|
|
544
|
-
completedBytes += branchBytes.get(branchName) ?? 0;
|
|
545
|
-
completedBranches += 1;
|
|
546
|
-
onProgress?.({
|
|
547
|
-
operation: "snapshot",
|
|
548
|
-
phase: "copying",
|
|
549
|
-
projectRoot: resolvedProjectRoot,
|
|
550
|
-
branchName,
|
|
551
|
-
completedBranches,
|
|
552
|
-
totalBranches: branchSnapshots.length,
|
|
553
|
-
completedBytes,
|
|
554
|
-
totalBytes
|
|
555
|
-
});
|
|
556
|
-
}
|
|
557
|
-
const treesRoot = import_node_path.default.join(root, "trees");
|
|
558
|
-
onProgress?.({
|
|
559
|
-
operation: "snapshot",
|
|
560
|
-
phase: "syncing",
|
|
561
|
-
projectRoot: resolvedProjectRoot,
|
|
562
|
-
completedBranches,
|
|
563
|
-
totalBranches: branchSnapshots.length,
|
|
564
|
-
completedBytes,
|
|
565
|
-
totalBytes
|
|
566
|
-
});
|
|
567
|
-
if (import_node_fs.default.existsSync(treesRoot)) fsyncTree(treesRoot);
|
|
568
|
-
manifest = advanceSnapshotState(root, manifest, "prepared");
|
|
569
|
-
return { root, manifest, paths };
|
|
570
|
-
} catch (error) {
|
|
571
|
-
removeSnapshotStorage(root);
|
|
572
|
-
throw error;
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
function restoreProjectWorktreeSnapshot(snapshotRoot, manifest, onProgress) {
|
|
576
|
-
let current = readSnapshotManifest(snapshotRoot);
|
|
577
|
-
assertSameSnapshotIdentity(manifest, current);
|
|
578
|
-
if (current.state !== "prepared" && current.state !== "restoring") {
|
|
579
|
-
throw new Error(`Project worktree snapshot is ${current.state}, not prepared or restoring`);
|
|
580
|
-
}
|
|
581
|
-
const treesRoot = import_node_path.default.join(snapshotRoot, "trees");
|
|
582
|
-
const treesRootPresent = import_node_fs.default.existsSync(treesRoot);
|
|
583
|
-
const treesRootStat = treesRootPresent ? import_node_fs.default.lstatSync(treesRoot) : null;
|
|
584
|
-
if (treesRootStat && (!treesRootStat.isDirectory() || treesRootStat.isSymbolicLink())) {
|
|
585
|
-
throw new Error("Project worktree snapshot trees root must be a real directory");
|
|
586
|
-
}
|
|
587
|
-
const branchSourceRoot = current.state === "restoring" && !treesRootPresent ? current.projectRoot : treesRoot;
|
|
588
|
-
for (const { branchName, snapshotPresent } of current.branches) {
|
|
589
|
-
const targetPath = branchPath(current.projectRoot, branchName);
|
|
590
|
-
assertNoSymlinkBranchPathComponents(current.projectRoot, targetPath);
|
|
591
|
-
const sourcePath = import_node_path.default.join(branchSourceRoot, ...branchName.split("/"));
|
|
592
|
-
if (!snapshotPresent) {
|
|
593
|
-
if (current.state === "restoring" && !treesRootPresent && import_node_fs.default.existsSync(sourcePath)) {
|
|
594
|
-
throw new Error(`Restored project branch unexpectedly exists: ${branchName}`);
|
|
595
|
-
}
|
|
596
|
-
continue;
|
|
597
|
-
}
|
|
598
|
-
const stat = import_node_fs.default.lstatSync(sourcePath);
|
|
599
|
-
if (!stat.isDirectory() || stat.isSymbolicLink()) {
|
|
600
|
-
throw new Error(`Project worktree snapshot branch must be a real directory: ${branchName}`);
|
|
601
|
-
}
|
|
602
|
-
}
|
|
603
|
-
onProgress?.({
|
|
604
|
-
operation: "restore",
|
|
605
|
-
phase: "scanning",
|
|
606
|
-
projectRoot: current.projectRoot,
|
|
607
|
-
completedBranches: 0,
|
|
608
|
-
totalBranches: current.branches.length,
|
|
609
|
-
completedBytes: 0
|
|
610
|
-
});
|
|
611
|
-
const branchBytes = snapshotBranchMeasurements(current.branches, (branchName) => import_node_path.default.join(branchSourceRoot, ...branchName.split("/")));
|
|
612
|
-
const totalBytes = [...branchBytes.values()].reduce((total, bytes) => total + bytes, 0);
|
|
613
|
-
const presentBranches = current.branches.filter(({ snapshotPresent }) => snapshotPresent);
|
|
614
|
-
const destinationParent = import_node_path.default.dirname(current.projectRoot);
|
|
615
|
-
import_node_fs.default.mkdirSync(destinationParent, { recursive: true });
|
|
616
|
-
const canMoveWholeSnapshot = current.scope === "project" && presentBranches.length > 0 && (current.state === "restoring" || treesRootStat !== null && treesRootStat.dev === import_node_fs.default.lstatSync(destinationParent).dev);
|
|
617
|
-
if (canMoveWholeSnapshot) {
|
|
618
|
-
if (current.state === "prepared") current = advanceSnapshotState(snapshotRoot, current, "restoring");
|
|
619
|
-
onProgress?.({
|
|
620
|
-
operation: "restore",
|
|
621
|
-
phase: "moving",
|
|
622
|
-
projectRoot: current.projectRoot,
|
|
623
|
-
completedBranches: treesRootPresent ? 0 : current.branches.length,
|
|
624
|
-
totalBranches: current.branches.length,
|
|
625
|
-
completedBytes: treesRootPresent ? 0 : totalBytes,
|
|
626
|
-
totalBytes
|
|
627
|
-
});
|
|
628
|
-
if (treesRootPresent) {
|
|
629
|
-
import_node_fs.default.rmSync(current.projectRoot, { recursive: true, force: true });
|
|
630
|
-
fsyncDirectoryChain(destinationParent, import_node_path.default.dirname(destinationParent));
|
|
631
|
-
import_node_fs.default.renameSync(treesRoot, current.projectRoot);
|
|
632
|
-
fsyncDirectory(snapshotRoot);
|
|
633
|
-
}
|
|
634
|
-
fsyncManagedProjectRootEntry(current.projectRoot);
|
|
635
|
-
onProgress?.({
|
|
636
|
-
operation: "restore",
|
|
637
|
-
phase: "moving",
|
|
638
|
-
projectRoot: current.projectRoot,
|
|
639
|
-
completedBranches: current.branches.length,
|
|
640
|
-
totalBranches: current.branches.length,
|
|
641
|
-
completedBytes: totalBytes,
|
|
642
|
-
totalBytes
|
|
643
|
-
});
|
|
644
|
-
return;
|
|
645
|
-
}
|
|
646
|
-
if (current.state === "restoring") {
|
|
647
|
-
throw new Error("Project worktree snapshot entered atomic restore state without a movable snapshot tree");
|
|
648
|
-
}
|
|
649
|
-
for (const { branchName } of [...current.branches].sort((left, right) => right.branchName.length - left.branchName.length)) {
|
|
650
|
-
const targetPath = branchPath(current.projectRoot, branchName);
|
|
651
|
-
const targetParent = import_node_path.default.dirname(targetPath);
|
|
652
|
-
import_node_fs.default.rmSync(targetPath, { recursive: true, force: true });
|
|
653
|
-
if (import_node_fs.default.existsSync(targetParent)) fsyncDirectoryChain(targetParent, current.projectRoot);
|
|
654
|
-
}
|
|
655
|
-
let completedBytes = 0;
|
|
656
|
-
let completedBranches = 0;
|
|
657
|
-
for (const { branchName, snapshotPresent } of current.branches) {
|
|
658
|
-
onProgress?.({
|
|
659
|
-
operation: "restore",
|
|
660
|
-
phase: "copying",
|
|
661
|
-
projectRoot: current.projectRoot,
|
|
662
|
-
branchName,
|
|
663
|
-
completedBranches,
|
|
664
|
-
totalBranches: current.branches.length,
|
|
665
|
-
completedBytes,
|
|
666
|
-
totalBytes
|
|
667
|
-
});
|
|
668
|
-
if (snapshotPresent) {
|
|
669
|
-
const targetPath = branchPath(current.projectRoot, branchName);
|
|
670
|
-
copyCheckoutTree(fullSnapshotPath(snapshotRoot, branchName), targetPath);
|
|
671
|
-
fsyncTree(targetPath);
|
|
672
|
-
fsyncDirectoryChain(import_node_path.default.dirname(targetPath), current.projectRoot);
|
|
673
|
-
}
|
|
674
|
-
completedBytes += branchBytes.get(branchName) ?? 0;
|
|
675
|
-
completedBranches += 1;
|
|
676
|
-
onProgress?.({
|
|
677
|
-
operation: "restore",
|
|
678
|
-
phase: "copying",
|
|
679
|
-
projectRoot: current.projectRoot,
|
|
680
|
-
branchName,
|
|
681
|
-
completedBranches,
|
|
682
|
-
totalBranches: current.branches.length,
|
|
683
|
-
completedBytes,
|
|
684
|
-
totalBytes
|
|
685
|
-
});
|
|
686
|
-
}
|
|
687
|
-
onProgress?.({
|
|
688
|
-
operation: "restore",
|
|
689
|
-
phase: "syncing",
|
|
690
|
-
projectRoot: current.projectRoot,
|
|
691
|
-
completedBranches,
|
|
692
|
-
totalBranches: current.branches.length,
|
|
693
|
-
completedBytes,
|
|
694
|
-
totalBytes
|
|
695
|
-
});
|
|
696
|
-
import_node_fs.default.mkdirSync(current.projectRoot, { recursive: true });
|
|
697
|
-
fsyncManagedProjectRootEntry(current.projectRoot);
|
|
698
|
-
}
|
|
699
|
-
function consumeAndRemoveProjectWorktreeSnapshot(snapshotRoot, manifest) {
|
|
700
|
-
const consumed = advanceSnapshotState(snapshotRoot, manifest, "consumed");
|
|
701
|
-
try {
|
|
702
|
-
assertSameSnapshotIdentity(manifest, consumed);
|
|
703
|
-
removeSnapshotStorage(snapshotRoot);
|
|
704
|
-
} catch {
|
|
705
|
-
}
|
|
706
|
-
}
|
|
707
|
-
function persistReconciledProjectAndConsumeSnapshot(snapshot, afterProjectTreeFsync, durabilityPaths) {
|
|
708
|
-
if (durabilityPaths) {
|
|
709
|
-
for (const durabilityPath of durabilityPaths) if (import_node_fs.default.existsSync(durabilityPath)) fsyncTree(durabilityPath);
|
|
710
|
-
} else {
|
|
711
|
-
fsyncTree(snapshot.manifest.projectRoot);
|
|
712
|
-
}
|
|
713
|
-
fsyncManagedProjectRootEntry(snapshot.manifest.projectRoot);
|
|
714
|
-
afterProjectTreeFsync?.();
|
|
715
|
-
consumeAndRemoveProjectWorktreeSnapshot(snapshot.root, snapshot.manifest);
|
|
716
|
-
}
|
|
717
|
-
function rollbackProjectWorktreeSnapshot(snapshot) {
|
|
718
|
-
const current = readSnapshotManifest(snapshot.root);
|
|
719
|
-
assertSameSnapshotIdentity(snapshot.manifest, current);
|
|
720
|
-
if (current.state === "prepared" || current.state === "restoring") {
|
|
721
|
-
restoreProjectWorktreeSnapshot(snapshot.root, current);
|
|
722
|
-
consumeAndRemoveProjectWorktreeSnapshot(snapshot.root, current);
|
|
723
|
-
return;
|
|
724
|
-
}
|
|
725
|
-
if (current.state === "consumed") {
|
|
726
|
-
try {
|
|
727
|
-
removeSnapshotStorage(snapshot.root);
|
|
728
|
-
} catch {
|
|
729
|
-
}
|
|
730
|
-
return;
|
|
731
|
-
}
|
|
732
|
-
throw new Error(`Project worktree snapshot is unexpectedly ${current.state}`);
|
|
733
|
-
}
|
|
734
|
-
function isProcessAlive(processId) {
|
|
735
|
-
try {
|
|
736
|
-
process.kill(processId, 0);
|
|
737
|
-
return true;
|
|
738
|
-
} catch (error) {
|
|
739
|
-
return error.code !== "ESRCH";
|
|
740
|
-
}
|
|
741
|
-
}
|
|
742
|
-
function isPathInside(root, candidate) {
|
|
743
|
-
const relative = import_node_path.default.relative(import_node_path.default.resolve(root), import_node_path.default.resolve(candidate));
|
|
744
|
-
return relative !== "" && relative !== ".." && !relative.startsWith(`..${import_node_path.default.sep}`) && !import_node_path.default.isAbsolute(relative);
|
|
745
|
-
}
|
|
746
|
-
function assertNoSymlinkPathComponents(root, candidate) {
|
|
747
|
-
const resolvedRoot = import_node_path.default.resolve(root);
|
|
748
|
-
const resolvedCandidate = import_node_path.default.resolve(candidate);
|
|
749
|
-
if (!isPathInside(resolvedRoot, resolvedCandidate)) throw new Error("Project worktree snapshot target escapes the projects root");
|
|
750
|
-
let current = resolvedRoot;
|
|
751
|
-
for (const component of ["", ...import_node_path.default.relative(resolvedRoot, resolvedCandidate).split(import_node_path.default.sep).filter(Boolean)]) {
|
|
752
|
-
if (component) current = import_node_path.default.join(current, component);
|
|
753
|
-
try {
|
|
754
|
-
if (import_node_fs.default.lstatSync(current).isSymbolicLink()) {
|
|
755
|
-
throw new Error(`Project worktree snapshot target contains a symbolic link: ${current}`);
|
|
756
|
-
}
|
|
757
|
-
} catch (error) {
|
|
758
|
-
const code = error.code;
|
|
759
|
-
if (code === "ENOENT" || code === "ENOTDIR") return;
|
|
760
|
-
throw error;
|
|
761
|
-
}
|
|
762
|
-
}
|
|
763
|
-
}
|
|
764
|
-
function recoverStaleProjectWorktreeSnapshots(input) {
|
|
765
|
-
const temporaryRoot = import_node_path.default.resolve(input.temporaryRoot ?? import_node_os.default.tmpdir());
|
|
766
|
-
const projectsRoot = import_node_path.default.resolve(input.projectsRoot);
|
|
767
|
-
const processAlive = input.processAlive ?? isProcessAlive;
|
|
768
|
-
const currentProcessId = input.currentProcessId ?? process.pid;
|
|
769
|
-
const currentOwnerSessionId = input.currentOwnerSessionId ?? PROJECT_WORKTREE_SNAPSHOT_OWNER_SESSION_ID;
|
|
770
|
-
if (!Number.isSafeInteger(currentProcessId) || currentProcessId <= 0) throw new Error("Current snapshot recovery process ID is invalid");
|
|
771
|
-
if (!UUID_PATTERN.test(currentOwnerSessionId)) throw new Error("Current snapshot recovery owner session is invalid");
|
|
772
|
-
const restored = [];
|
|
773
|
-
const removed = [];
|
|
774
|
-
const failed = [];
|
|
775
|
-
let entries;
|
|
776
|
-
try {
|
|
777
|
-
assertRealDirectory(temporaryRoot, "Project worktree snapshot storage root");
|
|
778
|
-
entries = import_node_fs.default.readdirSync(temporaryRoot, { withFileTypes: true });
|
|
779
|
-
} catch (error) {
|
|
780
|
-
if (error.code === "ENOENT") return { restored, removed, failed };
|
|
781
|
-
return { restored, removed, failed: [{ path: temporaryRoot, error: error instanceof Error ? error.message : String(error) }] };
|
|
782
|
-
}
|
|
783
|
-
for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
|
|
784
|
-
const match = PROJECT_WORKTREE_SNAPSHOT_NAME.exec(entry.name);
|
|
785
|
-
if (!match) continue;
|
|
786
|
-
const ownerProcessId = Number(match[1]);
|
|
787
|
-
if (!Number.isSafeInteger(ownerProcessId)) continue;
|
|
788
|
-
const candidate = import_node_path.default.join(temporaryRoot, entry.name);
|
|
789
|
-
try {
|
|
790
|
-
const stat = import_node_fs.default.lstatSync(candidate);
|
|
791
|
-
if (!stat.isDirectory() || stat.isSymbolicLink()) continue;
|
|
792
|
-
const manifest = readSnapshotManifest(candidate);
|
|
793
|
-
if (manifest.ownerProcessId !== ownerProcessId) {
|
|
794
|
-
throw new Error("Project worktree snapshot owner does not match its directory name");
|
|
795
|
-
}
|
|
796
|
-
if (!isPathInside(projectsRoot, manifest.projectRoot)) {
|
|
797
|
-
throw new Error("Project worktree snapshot target is outside the configured projects root");
|
|
798
|
-
}
|
|
799
|
-
assertNoSymlinkPathComponents(projectsRoot, manifest.projectRoot);
|
|
800
|
-
const ownedByThisProcessSession = manifest.ownerProcessId === currentProcessId && manifest.ownerSessionId === currentOwnerSessionId;
|
|
801
|
-
if (ownedByThisProcessSession || ownerProcessId !== currentProcessId && processAlive(ownerProcessId)) continue;
|
|
802
|
-
if (manifest.state === "prepared" || manifest.state === "restoring") {
|
|
803
|
-
restoreProjectWorktreeSnapshot(candidate, manifest, input.onProgress);
|
|
804
|
-
const consumed = advanceSnapshotState(candidate, manifest, "consumed");
|
|
805
|
-
removeSnapshotStorage(candidate);
|
|
806
|
-
assertSameSnapshotIdentity(manifest, consumed);
|
|
807
|
-
restored.push(candidate);
|
|
808
|
-
} else {
|
|
809
|
-
removeSnapshotStorage(candidate);
|
|
810
|
-
removed.push(candidate);
|
|
811
|
-
}
|
|
812
|
-
} catch (error) {
|
|
813
|
-
if (ownerProcessId !== currentProcessId && processAlive(ownerProcessId)) continue;
|
|
814
|
-
if (error.code === "ENOENT") {
|
|
815
|
-
try {
|
|
816
|
-
import_node_fs.default.lstatSync(candidate);
|
|
817
|
-
} catch (candidateError) {
|
|
818
|
-
if (candidateError.code === "ENOENT") continue;
|
|
819
|
-
}
|
|
820
|
-
}
|
|
821
|
-
failed.push({ path: candidate, error: error instanceof Error ? error.message : String(error) });
|
|
822
|
-
}
|
|
823
|
-
}
|
|
824
|
-
return { restored, removed, failed };
|
|
825
|
-
}
|
|
826
|
-
function projectOriginBranchName(input) {
|
|
827
|
-
(0, import_managed_paths.validateManagedBranchName)(input.branchName);
|
|
828
|
-
(0, import_managed_paths.validateManagedBranchName)(input.primaryBranchName);
|
|
829
|
-
(0, import_managed_paths.validateManagedBranchName)(input.defaultBranch);
|
|
830
|
-
return input.branchName === input.primaryBranchName ? input.defaultBranch : input.branchName;
|
|
831
|
-
}
|
|
832
|
-
function configureProjectBranchTracking(checkoutPath, branchName, originBranchName) {
|
|
833
|
-
(0, import_managed_paths.validateManagedBranchName)(branchName);
|
|
834
|
-
(0, import_managed_paths.validateManagedBranchName)(originBranchName);
|
|
835
|
-
git(checkoutPath, ["config", "--local", `branch.${branchName}.remote`, "origin"], `configure origin for ${branchName}`);
|
|
836
|
-
git(
|
|
837
|
-
checkoutPath,
|
|
838
|
-
["config", "--local", `branch.${branchName}.merge`, `refs/heads/${originBranchName}`],
|
|
839
|
-
`configure upstream branch for ${branchName}`
|
|
840
|
-
);
|
|
841
|
-
}
|
|
842
|
-
async function configureProjectBranchTrackingAsync(checkoutPath, branchName, originBranchName) {
|
|
843
|
-
(0, import_managed_paths.validateManagedBranchName)(branchName);
|
|
844
|
-
(0, import_managed_paths.validateManagedBranchName)(originBranchName);
|
|
845
|
-
await gitAsync(checkoutPath, ["config", "--local", `branch.${branchName}.remote`, "origin"], `configure origin for ${branchName}`);
|
|
846
|
-
await gitAsync(
|
|
847
|
-
checkoutPath,
|
|
848
|
-
["config", "--local", `branch.${branchName}.merge`, `refs/heads/${originBranchName}`],
|
|
849
|
-
`configure upstream branch for ${branchName}`
|
|
850
|
-
);
|
|
851
|
-
}
|
|
852
|
-
async function configureRepository(input) {
|
|
853
|
-
if (await tryGitAsync(input.primaryPath, ["remote", "get-url", "origin"])) {
|
|
854
|
-
await gitAsync(input.primaryPath, ["remote", "set-url", "origin", input.originUrl], "configure project origin");
|
|
855
|
-
} else {
|
|
856
|
-
await gitAsync(input.primaryPath, ["remote", "add", "origin", input.originUrl], "configure project origin");
|
|
857
|
-
}
|
|
858
|
-
await tryGitAsync(input.primaryPath, ["config", "--local", "--unset-all", "remote.origin.pushurl"]);
|
|
859
|
-
await gitAsync(input.primaryPath, ["config", "--local", "--replace-all", "credential.helper", ""], "reset project credential helpers");
|
|
860
|
-
await gitAsync(input.primaryPath, ["config", "--local", "credential.useHttpPath", "false"], "configure project credential path matching");
|
|
861
|
-
if (input.originCredentialUsername) {
|
|
862
|
-
await gitAsync(
|
|
863
|
-
input.primaryPath,
|
|
864
|
-
["config", "--local", "--replace-all", (0, import_git_process_environment.gitCredentialUsernameConfigKey)(input.originUrl), input.originCredentialUsername],
|
|
865
|
-
"configure project credential username"
|
|
866
|
-
);
|
|
867
|
-
}
|
|
868
|
-
if (input.credentialHelper) {
|
|
869
|
-
await gitAsync(
|
|
870
|
-
input.primaryPath,
|
|
871
|
-
["config", "--local", "--add", "credential.helper", input.credentialHelper],
|
|
872
|
-
"configure project credential helper"
|
|
873
|
-
);
|
|
874
|
-
}
|
|
875
|
-
if (input.gitIdentity) {
|
|
876
|
-
const name = input.gitIdentity.name.trim();
|
|
877
|
-
const email = input.gitIdentity.email.trim();
|
|
878
|
-
if (!name || !email) throw new Error("Git identity must include name and email");
|
|
879
|
-
await gitAsync(input.primaryPath, ["config", "--local", "user.name", name], "configure project Git user name");
|
|
880
|
-
await gitAsync(input.primaryPath, ["config", "--local", "user.email", email], "configure project Git user email");
|
|
881
|
-
}
|
|
882
|
-
if (input.primaryBranchName && input.defaultBranch && input.branches) {
|
|
883
|
-
await gitAsync(input.primaryPath, ["config", "--local", "push.default", "upstream"], "configure project push mapping");
|
|
884
|
-
for (const { branchName } of input.branches) {
|
|
885
|
-
const originBranchName = projectOriginBranchName({
|
|
886
|
-
branchName,
|
|
887
|
-
primaryBranchName: input.primaryBranchName,
|
|
888
|
-
defaultBranch: input.defaultBranch
|
|
889
|
-
});
|
|
890
|
-
await configureProjectBranchTrackingAsync(input.primaryPath, branchName, originBranchName);
|
|
891
|
-
}
|
|
892
|
-
}
|
|
893
|
-
}
|
|
894
|
-
async function fetchProjectHeads(input) {
|
|
895
|
-
const originFetch = await gitResultAsync(
|
|
896
|
-
input.primaryPath,
|
|
897
|
-
[
|
|
898
|
-
...(0, import_git_process_environment.gitTransportSecurityArgs)(input.originUrl, input.credentialHelper, input.originCredentialUsername),
|
|
899
|
-
"fetch",
|
|
900
|
-
"--no-recurse-submodules",
|
|
901
|
-
"--prune",
|
|
902
|
-
"origin",
|
|
903
|
-
"+refs/heads/*:refs/remotes/origin/*"
|
|
904
|
-
],
|
|
905
|
-
PROJECT_GIT_TRANSPORT_TIMEOUT_MS
|
|
906
|
-
);
|
|
907
|
-
if (originFetch.exitCode !== 0) {
|
|
908
|
-
throw new Error(`Fetch project origin: ${originFetch.stderr || originFetch.stdout || `git exited ${originFetch.exitCode}`}`);
|
|
909
|
-
}
|
|
910
|
-
const mirrorFetch = await gitResultAsync(
|
|
911
|
-
input.primaryPath,
|
|
912
|
-
[
|
|
913
|
-
...(0, import_git_process_environment.gitTransportSecurityArgs)(input.mirrorUrl, input.credentialHelper, input.mirrorCredentialUsername),
|
|
914
|
-
"fetch",
|
|
915
|
-
"--no-recurse-submodules",
|
|
916
|
-
"--prune",
|
|
917
|
-
input.mirrorUrl,
|
|
918
|
-
"+refs/heads/*:refs/r5d/mirror/*"
|
|
919
|
-
],
|
|
920
|
-
PROJECT_GIT_TRANSPORT_TIMEOUT_MS
|
|
921
|
-
);
|
|
922
|
-
if (mirrorFetch.exitCode !== 0 && !/(couldn't find remote ref|does not have any commits|remote repository is empty|no such ref)/i.test(
|
|
923
|
-
`${mirrorFetch.stderr}
|
|
924
|
-
${mirrorFetch.stdout}`
|
|
925
|
-
)) {
|
|
926
|
-
throw new Error(`Fetch project head mirror: ${mirrorFetch.stderr || mirrorFetch.stdout || `git exited ${mirrorFetch.exitCode}`}`);
|
|
927
|
-
}
|
|
928
|
-
}
|
|
929
|
-
async function ensureCommitAvailable(input) {
|
|
930
|
-
if (await tryGitAsync(input.primaryPath, ["cat-file", "-e", `${input.commitHash}^{commit}`])) return;
|
|
931
|
-
for (const [url, username] of [
|
|
932
|
-
[input.mirrorUrl, input.mirrorCredentialUsername],
|
|
933
|
-
[input.originUrl, input.originCredentialUsername]
|
|
934
|
-
]) {
|
|
935
|
-
if (await tryGitAsync(input.primaryPath, [
|
|
936
|
-
...(0, import_git_process_environment.gitTransportSecurityArgs)(url, input.credentialHelper, username),
|
|
937
|
-
"fetch",
|
|
938
|
-
"--no-recurse-submodules",
|
|
939
|
-
"--no-tags",
|
|
940
|
-
url,
|
|
941
|
-
input.commitHash
|
|
942
|
-
]))
|
|
943
|
-
return;
|
|
944
|
-
}
|
|
945
|
-
throw new Error(`Project base commit ${input.commitHash} is unavailable from origin and the canonical mirror`);
|
|
946
|
-
}
|
|
947
|
-
async function seedForBranch(input) {
|
|
948
|
-
const mirrorRef = `refs/r5d/mirror/${input.branch.branchName}`;
|
|
949
|
-
const mirrorHead = await tryGitAsync(input.primaryPath, ["show-ref", "--verify", "--quiet", mirrorRef]) ? await gitAsync(input.primaryPath, ["rev-parse", mirrorRef], "resolve project mirror head") : null;
|
|
950
|
-
if (input.requireMirrorHead && !mirrorHead) {
|
|
951
|
-
throw new Error(`Exact hidden project mirror head is missing for ${input.branch.branchName}`);
|
|
952
|
-
}
|
|
953
|
-
const seed = mirrorHead ?? input.branch.baseCommitHash;
|
|
954
|
-
await ensureCommitAvailable({ ...input, commitHash: seed });
|
|
955
|
-
return { seed, mirrorHead };
|
|
956
|
-
}
|
|
957
|
-
async function preserveHead(checkoutPath) {
|
|
958
|
-
if (!await tryGitAsync(checkoutPath, ["rev-parse", "--verify", "HEAD"])) return;
|
|
959
|
-
const head = await gitAsync(checkoutPath, ["rev-parse", "HEAD"], "read pre-reconciliation project head");
|
|
960
|
-
await gitAsync(checkoutPath, ["update-ref", `refs/r5d/pre-connect/${head}`, head], "preserve pre-reconciliation project head");
|
|
961
|
-
}
|
|
962
|
-
async function initializeLinkedLayout(input) {
|
|
963
|
-
for (const { branchName } of [...input.branches].sort((left, right) => right.branchName.length - left.branchName.length)) {
|
|
964
|
-
import_node_fs.default.rmSync(branchPath(input.projectRoot, branchName), { recursive: true, force: true });
|
|
965
|
-
}
|
|
966
|
-
const primaryPath = branchPath(input.projectRoot, input.primaryBranchName);
|
|
967
|
-
import_node_fs.default.mkdirSync(import_node_path.default.dirname(primaryPath), { recursive: true });
|
|
968
|
-
await gitAsync(void 0, ["init", `--initial-branch=${input.primaryBranchName}`, primaryPath], "initialize primary project clone");
|
|
969
|
-
await configureRepository({ ...input, primaryPath });
|
|
970
|
-
await fetchProjectHeads({ ...input, primaryPath });
|
|
971
|
-
const seeds = /* @__PURE__ */ new Map();
|
|
972
|
-
const mirrorHeads = /* @__PURE__ */ new Map();
|
|
973
|
-
for (const branch of input.branches) {
|
|
974
|
-
const resolved = await seedForBranch({
|
|
975
|
-
...input,
|
|
976
|
-
primaryPath,
|
|
977
|
-
branch,
|
|
978
|
-
requireMirrorHead: input.exactMirrorBranches?.has(branch.branchName)
|
|
979
|
-
});
|
|
980
|
-
seeds.set(branch.branchName, resolved.seed);
|
|
981
|
-
mirrorHeads.set(branch.branchName, resolved.mirrorHead);
|
|
982
|
-
}
|
|
983
|
-
await gitAsync(
|
|
984
|
-
primaryPath,
|
|
985
|
-
["checkout", "--no-recurse-submodules", "-B", input.primaryBranchName, seeds.get(input.primaryBranchName)],
|
|
986
|
-
"anchor primary project worktree"
|
|
987
|
-
);
|
|
988
|
-
for (const branch of input.branches) {
|
|
989
|
-
if (branch.branchName === input.primaryBranchName) continue;
|
|
990
|
-
await gitAsync(
|
|
991
|
-
primaryPath,
|
|
992
|
-
["branch", "-f", branch.branchName, seeds.get(branch.branchName)],
|
|
993
|
-
`anchor project branch ${branch.branchName}`
|
|
994
|
-
);
|
|
995
|
-
const checkoutPath = branchPath(input.projectRoot, branch.branchName);
|
|
996
|
-
import_node_fs.default.mkdirSync(import_node_path.default.dirname(checkoutPath), { recursive: true });
|
|
997
|
-
await gitAsync(
|
|
998
|
-
primaryPath,
|
|
999
|
-
["worktree", "add", "--force", checkoutPath, branch.branchName],
|
|
1000
|
-
`create linked worktree ${branch.branchName}`
|
|
1001
|
-
);
|
|
1002
|
-
}
|
|
1003
|
-
return mirrorHeads;
|
|
1004
|
-
}
|
|
1005
|
-
async function reconcileLinkedLayout(input) {
|
|
1006
|
-
const primaryPath = branchPath(input.projectRoot, input.primaryBranchName);
|
|
1007
|
-
await configureRepository({ ...input, primaryPath });
|
|
1008
|
-
await tryGitAsync(primaryPath, ["worktree", "prune"]);
|
|
1009
|
-
await fetchProjectHeads({ ...input, primaryPath });
|
|
1010
|
-
const primaryCommonDir = await commonGitDirectoryAsync(primaryPath);
|
|
1011
|
-
if (!primaryCommonDir) throw new Error("Primary project checkout is unavailable");
|
|
1012
|
-
const mirrorHeads = /* @__PURE__ */ new Map();
|
|
1013
|
-
const plan = [];
|
|
1014
|
-
for (const branch of input.branches) {
|
|
1015
|
-
const checkoutPath = branchPath(input.projectRoot, branch.branchName);
|
|
1016
|
-
const exactMirror = input.exactMirrorBranches?.has(branch.branchName) === true;
|
|
1017
|
-
const resolved = await seedForBranch({
|
|
1018
|
-
...input,
|
|
1019
|
-
primaryPath,
|
|
1020
|
-
branch,
|
|
1021
|
-
requireMirrorHead: exactMirror
|
|
1022
|
-
});
|
|
1023
|
-
mirrorHeads.set(branch.branchName, resolved.mirrorHead);
|
|
1024
|
-
const linked = branch.branchName === input.primaryBranchName || gitDirectoryKind(checkoutPath) !== null && await commonGitDirectoryAsync(checkoutPath) === primaryCommonDir;
|
|
1025
|
-
if (!linked) {
|
|
1026
|
-
plan.push({ branchName: branch.branchName, checkoutPath, seed: resolved.seed, action: "recreate" });
|
|
1027
|
-
continue;
|
|
1028
|
-
}
|
|
1029
|
-
const currentHead = await gitAsync(checkoutPath, ["rev-parse", "HEAD"], `resolve linked worktree head ${branch.branchName}`);
|
|
1030
|
-
const keep = currentHead === resolved.seed || !exactMirror && await tryGitAsync(checkoutPath, ["merge-base", "--is-ancestor", resolved.seed, currentHead]);
|
|
1031
|
-
plan.push({ branchName: branch.branchName, checkoutPath, seed: resolved.seed, action: keep ? "keep" : "reset" });
|
|
1032
|
-
}
|
|
1033
|
-
const mutatedBranches = new Set(plan.filter(({ action }) => action !== "keep").map(({ branchName }) => branchName));
|
|
1034
|
-
if (mutatedBranches.size > 0) await input.beforeDestructiveMutation(mutatedBranches);
|
|
1035
|
-
for (const entry of plan) {
|
|
1036
|
-
if (entry.action === "keep") continue;
|
|
1037
|
-
if (entry.action === "reset") {
|
|
1038
|
-
await preserveHead(entry.checkoutPath);
|
|
1039
|
-
await gitAsync(entry.checkoutPath, ["reset", "--hard", entry.seed], `anchor linked worktree ${entry.branchName}`);
|
|
1040
|
-
continue;
|
|
1041
|
-
}
|
|
1042
|
-
import_node_fs.default.rmSync(entry.checkoutPath, { recursive: true, force: true });
|
|
1043
|
-
await gitAsync(primaryPath, ["branch", "-f", entry.branchName, entry.seed], `anchor project branch ${entry.branchName}`);
|
|
1044
|
-
import_node_fs.default.mkdirSync(import_node_path.default.dirname(entry.checkoutPath), { recursive: true });
|
|
1045
|
-
await gitAsync(
|
|
1046
|
-
primaryPath,
|
|
1047
|
-
["worktree", "add", "--force", entry.checkoutPath, entry.branchName],
|
|
1048
|
-
`create linked worktree ${entry.branchName}`
|
|
1049
|
-
);
|
|
1050
|
-
}
|
|
1051
|
-
return { mirrorHeads, mutatedBranches };
|
|
1052
|
-
}
|
|
1053
|
-
async function aheadBehind(checkoutPath, branchName, primaryBranchName, defaultBranch) {
|
|
1054
|
-
const originBranchName = projectOriginBranchName({ branchName, primaryBranchName, defaultBranch });
|
|
1055
|
-
const originRef = `refs/remotes/origin/${originBranchName}`;
|
|
1056
|
-
if (!await tryGitAsync(checkoutPath, ["show-ref", "--verify", "--quiet", originRef])) return { ahead: null, behind: null };
|
|
1057
|
-
const output = await gitAsync(
|
|
1058
|
-
checkoutPath,
|
|
1059
|
-
["rev-list", "--left-right", "--count", `${originRef}...HEAD`],
|
|
1060
|
-
"measure project origin divergence"
|
|
1061
|
-
);
|
|
1062
|
-
const [behind, ahead] = output.split(/\s+/).map(Number);
|
|
1063
|
-
return Number.isFinite(ahead) && Number.isFinite(behind) ? { ahead, behind } : { ahead: null, behind: null };
|
|
1064
|
-
}
|
|
1065
|
-
async function ensureProjectWorktrees(input) {
|
|
1066
|
-
const projectRoot = import_node_path.default.resolve(input.projectRoot);
|
|
1067
|
-
const defaultBranch = input.defaultBranch ?? input.primaryBranchName;
|
|
1068
|
-
assertNonOverlappingBranches(input.branches);
|
|
1069
|
-
if (!input.branches.some(({ branchName }) => branchName === input.primaryBranchName)) {
|
|
1070
|
-
throw new Error(`Primary project branch ${input.primaryBranchName} is missing from the workspace configuration`);
|
|
1071
|
-
}
|
|
1072
|
-
const snapshotStorageRoot = import_node_path.default.resolve(input.snapshotRoot ?? import_node_os.default.tmpdir());
|
|
1073
|
-
assertNoOutstandingSnapshotForProject(snapshotStorageRoot, projectRoot);
|
|
1074
|
-
let snapshot;
|
|
1075
|
-
const ensureSnapshot = async (branchNames) => {
|
|
1076
|
-
snapshot ??= await (0, import_workspace_filesystem_executor.workspaceFilesystemExecutor)().run(
|
|
1077
|
-
"project_snapshot_create",
|
|
1078
|
-
{
|
|
1079
|
-
projectRoot,
|
|
1080
|
-
branches: (branchNames ? input.branches.filter(({ branchName }) => branchNames.has(branchName)) : input.branches).map((branch) => ({
|
|
1081
|
-
...branch
|
|
1082
|
-
})),
|
|
1083
|
-
temporaryRoot: snapshotStorageRoot,
|
|
1084
|
-
scope: branchNames ? "branches" : "project",
|
|
1085
|
-
ownerSessionId: PROJECT_WORKTREE_SNAPSHOT_OWNER_SESSION_ID
|
|
1086
|
-
},
|
|
1087
|
-
{ onProgress: input.onSnapshotProgress }
|
|
1088
|
-
);
|
|
1089
|
-
return snapshot;
|
|
1090
|
-
};
|
|
1091
|
-
try {
|
|
1092
|
-
import_node_fs.default.mkdirSync(projectRoot, { recursive: true });
|
|
1093
|
-
const linked = await hasCompatibleLinkedProjectWorktreeLayout({
|
|
1094
|
-
projectRoot,
|
|
1095
|
-
primaryBranchName: input.primaryBranchName,
|
|
1096
|
-
branches: input.branches
|
|
1097
|
-
});
|
|
1098
|
-
let mirrorHeads;
|
|
1099
|
-
let mutatedBranches;
|
|
1100
|
-
if (linked) {
|
|
1101
|
-
({ mirrorHeads, mutatedBranches } = await reconcileLinkedLayout({
|
|
1102
|
-
...input,
|
|
1103
|
-
projectRoot,
|
|
1104
|
-
defaultBranch,
|
|
1105
|
-
beforeDestructiveMutation: async (branchNames) => {
|
|
1106
|
-
await ensureSnapshot(branchNames);
|
|
1107
|
-
}
|
|
1108
|
-
}));
|
|
1109
|
-
} else {
|
|
1110
|
-
await ensureSnapshot();
|
|
1111
|
-
mirrorHeads = await initializeLinkedLayout({ ...input, projectRoot, defaultBranch });
|
|
1112
|
-
mutatedBranches = new Set(input.branches.map(({ branchName }) => branchName));
|
|
1113
|
-
}
|
|
1114
|
-
const preparedSnapshot = snapshot;
|
|
1115
|
-
if (mutatedBranches.size > 0 && !preparedSnapshot) {
|
|
1116
|
-
throw new Error("Project worktree mutation started without a prepared recovery snapshot");
|
|
1117
|
-
}
|
|
1118
|
-
const snapshotMirrors = input.branches.flatMap((branch) => {
|
|
1119
|
-
if (!mutatedBranches.has(branch.branchName)) return [];
|
|
1120
|
-
const snapshotPath = preparedSnapshot?.paths.get(branch.branchName);
|
|
1121
|
-
return snapshotPath ? [{ snapshotPath, targetPath: branchPath(projectRoot, branch.branchName) }] : [];
|
|
1122
|
-
});
|
|
1123
|
-
if (snapshotMirrors.length > 0) await (0, import_workspace_filesystem_executor.workspaceFilesystemExecutor)().run("project_snapshot_apply", { mirrors: snapshotMirrors });
|
|
1124
|
-
const states = [];
|
|
1125
|
-
for (const branch of input.branches) {
|
|
1126
|
-
const checkoutPath = branchPath(projectRoot, branch.branchName);
|
|
1127
|
-
const divergence = await aheadBehind(checkoutPath, branch.branchName, input.primaryBranchName, defaultBranch);
|
|
1128
|
-
states.push({
|
|
1129
|
-
branchName: branch.branchName,
|
|
1130
|
-
branchPath: checkoutPath,
|
|
1131
|
-
head: await gitAsync(checkoutPath, ["rev-parse", "HEAD"], `resolve project head ${branch.branchName}`),
|
|
1132
|
-
mirrorHead: mirrorHeads.get(branch.branchName) ?? null,
|
|
1133
|
-
...divergence
|
|
1134
|
-
});
|
|
1135
|
-
}
|
|
1136
|
-
states.sort((left, right) => left.branchName.localeCompare(right.branchName));
|
|
1137
|
-
if (snapshot) {
|
|
1138
|
-
const primaryGitDirectory = import_node_path.default.join(branchPath(projectRoot, input.primaryBranchName), ".git");
|
|
1139
|
-
await (0, import_workspace_filesystem_executor.workspaceFilesystemExecutor)().run("project_snapshot_persist", {
|
|
1140
|
-
snapshot,
|
|
1141
|
-
durabilityPaths: snapshot.manifest.scope === "branches" ? [
|
|
1142
|
-
...snapshot.manifest.branches.map(({ branchName }) => branchPath(projectRoot, branchName)),
|
|
1143
|
-
// Branch refs and worktree registrations moved in the shared
|
|
1144
|
-
// administrative directory; objects are fsynced by git itself.
|
|
1145
|
-
import_node_path.default.join(primaryGitDirectory, "refs"),
|
|
1146
|
-
import_node_path.default.join(primaryGitDirectory, "packed-refs"),
|
|
1147
|
-
import_node_path.default.join(primaryGitDirectory, "worktrees")
|
|
1148
|
-
] : null
|
|
1149
|
-
});
|
|
1150
|
-
}
|
|
1151
|
-
return states;
|
|
1152
|
-
} catch (error) {
|
|
1153
|
-
if (!snapshot) throw error;
|
|
1154
|
-
try {
|
|
1155
|
-
await (0, import_workspace_filesystem_executor.workspaceFilesystemExecutor)().run("project_snapshot_rollback", { snapshot });
|
|
1156
|
-
} catch (recoveryError) {
|
|
1157
|
-
throw new Error(
|
|
1158
|
-
`Project worktree reconciliation failed and its snapshot could not be fully restored; recovery state is retained at ${snapshot.root}`,
|
|
1159
|
-
{ cause: new AggregateError([error, recoveryError]) }
|
|
1160
|
-
);
|
|
1161
|
-
}
|
|
1162
|
-
throw error;
|
|
1163
|
-
}
|
|
1164
|
-
}
|
|
1165
|
-
const WORKTREE_OPERATION_STATE_PATHS = ["MERGE_HEAD", "CHERRY_PICK_HEAD", "REVERT_HEAD", "rebase-merge", "rebase-apply", "sequencer"];
|
|
1166
|
-
function perWorktreeGitDirectory(checkoutPath) {
|
|
1167
|
-
const gitPath = import_node_path.default.join(checkoutPath, ".git");
|
|
1168
|
-
let stat;
|
|
1169
|
-
try {
|
|
1170
|
-
stat = import_node_fs.default.lstatSync(gitPath);
|
|
1171
|
-
} catch {
|
|
1172
|
-
return null;
|
|
1173
|
-
}
|
|
1174
|
-
if (stat.isDirectory()) return gitPath;
|
|
1175
|
-
if (!stat.isFile()) return null;
|
|
1176
|
-
let pointer;
|
|
1177
|
-
try {
|
|
1178
|
-
pointer = import_node_fs.default.readFileSync(gitPath, "utf8");
|
|
1179
|
-
} catch {
|
|
1180
|
-
return null;
|
|
1181
|
-
}
|
|
1182
|
-
const match = /^gitdir:[ \t]*(.+?)[ \t\r\n]*$/m.exec(pointer);
|
|
1183
|
-
if (!match?.[1]) return null;
|
|
1184
|
-
return import_node_path.default.resolve(checkoutPath, match[1]);
|
|
1185
|
-
}
|
|
1186
|
-
function projectWorktreeOperationInProgress(checkoutPath) {
|
|
1187
|
-
const gitDirectory = perWorktreeGitDirectory(checkoutPath);
|
|
1188
|
-
if (!gitDirectory) return false;
|
|
1189
|
-
return WORKTREE_OPERATION_STATE_PATHS.some((statePath) => import_node_fs.default.existsSync(import_node_path.default.join(gitDirectory, statePath)));
|
|
1190
|
-
}
|
|
1191
|
-
function createLinkedProjectBranch(input) {
|
|
1192
|
-
(0, import_managed_paths.validateManagedBranchName)(input.sourceBranchName);
|
|
1193
|
-
(0, import_managed_paths.validateManagedBranchName)(input.branchName);
|
|
1194
|
-
if (input.sourceBranchName === input.branchName) throw new Error("Source and target branch names must differ");
|
|
1195
|
-
const sourcePath = branchPath(input.projectRoot, input.sourceBranchName);
|
|
1196
|
-
const targetPath = branchPath(input.projectRoot, input.branchName);
|
|
1197
|
-
assertNoSymlinkBranchPathComponents(input.projectRoot, sourcePath);
|
|
1198
|
-
assertNoSymlinkBranchPathComponents(input.projectRoot, targetPath);
|
|
1199
|
-
if (!commonGitDirectory(sourcePath)) throw new Error(`Source worktree ${input.sourceBranchName} is unavailable`);
|
|
1200
|
-
if (projectWorktreeOperationInProgress(sourcePath)) {
|
|
1201
|
-
throw new Error(`Source worktree ${input.sourceBranchName} has an in-progress Git operation`);
|
|
1202
|
-
}
|
|
1203
|
-
if (import_node_fs.default.existsSync(targetPath) || tryGit(sourcePath, ["show-ref", "--verify", "--quiet", `refs/heads/${input.branchName}`])) {
|
|
1204
|
-
throw new Error(`Project branch ${input.branchName} already exists`);
|
|
1205
|
-
}
|
|
1206
|
-
const registeredWorktrees = git(sourcePath, ["worktree", "list", "--porcelain"], "inspect linked project worktrees").split(/\r?\n/).flatMap((line) => line.startsWith("worktree ") ? [import_node_path.default.resolve(line.slice("worktree ".length))] : []);
|
|
1207
|
-
const overlappingWorktree = registeredWorktrees.find((worktreePath) => {
|
|
1208
|
-
const targetRelative = import_node_path.default.relative(worktreePath, targetPath);
|
|
1209
|
-
const worktreeRelative = import_node_path.default.relative(targetPath, worktreePath);
|
|
1210
|
-
return targetRelative === "" || targetRelative !== ".." && !targetRelative.startsWith(`..${import_node_path.default.sep}`) && !import_node_path.default.isAbsolute(targetRelative) || worktreeRelative !== ".." && !worktreeRelative.startsWith(`..${import_node_path.default.sep}`) && !import_node_path.default.isAbsolute(worktreeRelative);
|
|
1211
|
-
});
|
|
1212
|
-
if (overlappingWorktree) throw new Error(`Project branch folder overlaps linked worktree ${overlappingWorktree}`);
|
|
1213
|
-
const sourceHead = git(sourcePath, ["rev-parse", "HEAD"], "resolve source branch head");
|
|
1214
|
-
let branchCreated = false;
|
|
1215
|
-
try {
|
|
1216
|
-
git(sourcePath, ["branch", input.branchName, sourceHead], `create project branch ${input.branchName}`);
|
|
1217
|
-
branchCreated = true;
|
|
1218
|
-
import_node_fs.default.mkdirSync(import_node_path.default.dirname(targetPath), { recursive: true });
|
|
1219
|
-
assertNoSymlinkBranchPathComponents(input.projectRoot, targetPath);
|
|
1220
|
-
git(
|
|
1221
|
-
sourcePath,
|
|
1222
|
-
["worktree", "add", "--force", "--no-checkout", targetPath, input.branchName],
|
|
1223
|
-
`create linked worktree ${input.branchName}`
|
|
1224
|
-
);
|
|
1225
|
-
configureProjectBranchTracking(targetPath, input.branchName, input.branchName);
|
|
1226
|
-
assertNoSymlinkBranchPathComponents(input.projectRoot, targetPath);
|
|
1227
|
-
if (input.workingTree === "carry") {
|
|
1228
|
-
git(targetPath, ["read-tree", sourceHead], `initialize linked worktree index ${input.branchName}`);
|
|
1229
|
-
(0, import_working_tree_mirror.mirrorWorkingTree)({ sourceRoot: sourcePath, targetRoot: targetPath, sourceMode: "git", deletionMode: "git" });
|
|
1230
|
-
} else {
|
|
1231
|
-
git(targetPath, ["reset", "--hard", sourceHead], `check out linked worktree ${input.branchName}`);
|
|
1232
|
-
}
|
|
1233
|
-
return { branchPath: targetPath, baseCommitHash: sourceHead };
|
|
1234
|
-
} catch (error) {
|
|
1235
|
-
if (branchCreated) {
|
|
1236
|
-
let removeError;
|
|
1237
|
-
try {
|
|
1238
|
-
import_node_fs.default.rmSync(targetPath, { recursive: true, force: true });
|
|
1239
|
-
} catch (cleanupError) {
|
|
1240
|
-
removeError = cleanupError;
|
|
1241
|
-
}
|
|
1242
|
-
const prune = gitResult(sourcePath, ["worktree", "prune"]);
|
|
1243
|
-
const deleteBranch = gitResult(sourcePath, ["branch", "-D", input.branchName]);
|
|
1244
|
-
const cleanupFailures = [];
|
|
1245
|
-
try {
|
|
1246
|
-
import_node_fs.default.lstatSync(targetPath);
|
|
1247
|
-
cleanupFailures.push(`target path remains at ${targetPath}`);
|
|
1248
|
-
} catch (verificationError) {
|
|
1249
|
-
const code = verificationError.code;
|
|
1250
|
-
if (code !== "ENOENT" && code !== "ENOTDIR") {
|
|
1251
|
-
cleanupFailures.push(
|
|
1252
|
-
`could not verify target removal${removeError ? ` after ${removeError instanceof Error ? removeError.message : String(removeError)}` : ""}: ${verificationError instanceof Error ? verificationError.message : String(verificationError)}`
|
|
1253
|
-
);
|
|
1254
|
-
}
|
|
1255
|
-
}
|
|
1256
|
-
const worktrees = gitResult(sourcePath, ["worktree", "list", "--porcelain"]);
|
|
1257
|
-
if (worktrees.exitCode !== 0) {
|
|
1258
|
-
cleanupFailures.push(
|
|
1259
|
-
`could not verify linked-worktree removal: ${worktrees.stderr || worktrees.stdout || `git exited ${worktrees.exitCode}`}`
|
|
1260
|
-
);
|
|
1261
|
-
} else {
|
|
1262
|
-
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));
|
|
1263
|
-
if (targetRegistered) {
|
|
1264
|
-
cleanupFailures.push(
|
|
1265
|
-
`linked worktree remains registered at ${targetPath}${prune.exitCode === 0 ? "" : ` (${prune.stderr || prune.stdout || `git exited ${prune.exitCode}`})`}`
|
|
1266
|
-
);
|
|
1267
|
-
}
|
|
1268
|
-
}
|
|
1269
|
-
const branchRef = gitResult(sourcePath, ["show-ref", "--verify", "--quiet", `refs/heads/${input.branchName}`]);
|
|
1270
|
-
if (branchRef.exitCode === 0) {
|
|
1271
|
-
cleanupFailures.push(
|
|
1272
|
-
`branch ref remains${deleteBranch.exitCode === 0 ? "" : ` (${deleteBranch.stderr || deleteBranch.stdout || `git exited ${deleteBranch.exitCode}`})`}`
|
|
1273
|
-
);
|
|
1274
|
-
} else if (branchRef.exitCode !== 1) {
|
|
1275
|
-
cleanupFailures.push(
|
|
1276
|
-
`could not verify branch ref removal: ${branchRef.stderr || branchRef.stdout || `git exited ${branchRef.exitCode}`}`
|
|
1277
|
-
);
|
|
1278
|
-
}
|
|
1279
|
-
if (cleanupFailures.length > 0) {
|
|
1280
|
-
throw new ProjectBranchCreationRollbackIncompleteError(input.branchName, cleanupFailures, error);
|
|
1281
|
-
}
|
|
1282
|
-
}
|
|
1283
|
-
throw error;
|
|
1284
|
-
}
|
|
1285
|
-
}
|
|
1286
|
-
async function createOrRetryLinkedProjectBranch(input) {
|
|
1287
|
-
if (input.pendingRetry) {
|
|
1288
|
-
deleteLinkedProjectBranch({
|
|
1289
|
-
projectRoot: input.projectRoot,
|
|
1290
|
-
primaryBranchName: input.primaryBranchName,
|
|
1291
|
-
branchName: input.branchName
|
|
1292
|
-
});
|
|
1293
|
-
}
|
|
1294
|
-
return await (0, import_workspace_filesystem_executor.workspaceFilesystemExecutor)().run("project_branch_create", {
|
|
1295
|
-
projectRoot: input.projectRoot,
|
|
1296
|
-
sourceBranchName: input.sourceBranchName,
|
|
1297
|
-
branchName: input.branchName,
|
|
1298
|
-
workingTree: input.workingTree
|
|
1299
|
-
});
|
|
1300
|
-
}
|
|
1301
|
-
function classifyProjectCheckout(input) {
|
|
1302
|
-
const checkoutPath = branchPath(input.projectRoot, input.branchName);
|
|
1303
|
-
let stat;
|
|
1304
|
-
try {
|
|
1305
|
-
stat = import_node_fs.default.lstatSync(checkoutPath);
|
|
1306
|
-
} catch {
|
|
1307
|
-
return "missing";
|
|
1308
|
-
}
|
|
1309
|
-
if (!stat.isDirectory()) return "not_a_worktree";
|
|
1310
|
-
if (input.branchName === input.primaryBranchName) return gitDirectoryKind(checkoutPath) === "directory" ? "linked" : "not_a_worktree";
|
|
1311
|
-
if (gitDirectoryKind(checkoutPath) !== "file") return "not_a_worktree";
|
|
1312
|
-
const gitDirectory = perWorktreeGitDirectory(checkoutPath);
|
|
1313
|
-
if (!gitDirectory) return "not_a_worktree";
|
|
1314
|
-
let registry;
|
|
1315
|
-
let registered;
|
|
1316
|
-
try {
|
|
1317
|
-
registry = import_node_fs.default.realpathSync(import_node_path.default.join(branchPath(input.projectRoot, input.primaryBranchName), ".git", "worktrees"));
|
|
1318
|
-
registered = import_node_fs.default.realpathSync(gitDirectory);
|
|
1319
|
-
} catch {
|
|
1320
|
-
return "not_a_worktree";
|
|
1321
|
-
}
|
|
1322
|
-
const relative = import_node_path.default.relative(registry, registered);
|
|
1323
|
-
if (!relative || relative === ".." || relative.startsWith(`..${import_node_path.default.sep}`) || import_node_path.default.isAbsolute(relative)) return "not_a_worktree";
|
|
1324
|
-
return "linked";
|
|
1325
|
-
}
|
|
1326
|
-
function inventoryProjectCheckouts(input) {
|
|
1327
|
-
const states = input.branchNames.map((branchName) => ({
|
|
1328
|
-
branchName,
|
|
1329
|
-
checkoutPath: branchPath(input.projectRoot, branchName),
|
|
1330
|
-
state: classifyProjectCheckout({ ...input, branchName })
|
|
1331
|
-
}));
|
|
1332
|
-
const primaryLinked = states.some(({ branchName, state }) => branchName === input.primaryBranchName && state === "linked");
|
|
1333
|
-
const inventory = { linkedBranchNames: [], missingCheckouts: [] };
|
|
1334
|
-
for (const { branchName, checkoutPath, state } of states) {
|
|
1335
|
-
if (primaryLinked && state === "linked") inventory.linkedBranchNames.push(branchName);
|
|
1336
|
-
else inventory.missingCheckouts.push({ branchName, checkoutPath, reason: state === "linked" ? "not_a_worktree" : state });
|
|
1337
|
-
}
|
|
1338
|
-
return inventory;
|
|
1339
|
-
}
|
|
1340
|
-
const PROJECT_REMOVED_CHECKOUTS_DIRECTORY = ".r5d-removed";
|
|
1341
|
-
function moveProjectCheckoutAside(projectRoot, branchName, checkoutPath) {
|
|
1342
|
-
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
1343
|
-
const target = import_node_path.default.join(projectRoot, PROJECT_REMOVED_CHECKOUTS_DIRECTORY, ...`${branchName}-${timestamp}`.split("/"));
|
|
1344
|
-
import_node_fs.default.mkdirSync(import_node_path.default.dirname(target), { recursive: true });
|
|
1345
|
-
import_node_fs.default.renameSync(checkoutPath, target);
|
|
1346
|
-
return target;
|
|
1347
|
-
}
|
|
1348
|
-
function deleteLinkedProjectBranch(input) {
|
|
1349
|
-
(0, import_managed_paths.validateManagedBranchName)(input.primaryBranchName);
|
|
1350
|
-
(0, import_managed_paths.validateManagedBranchName)(input.branchName);
|
|
1351
|
-
if (input.branchName === input.primaryBranchName) throw new Error(`Cannot delete the primary project branch ${input.primaryBranchName}`);
|
|
1352
|
-
const primaryPath = branchPath(input.projectRoot, input.primaryBranchName);
|
|
1353
|
-
const checkoutPath = branchPath(input.projectRoot, input.branchName);
|
|
1354
|
-
const primaryCommonDir = commonGitDirectory(primaryPath);
|
|
1355
|
-
if (!primaryCommonDir) throw new Error("Primary project checkout is unavailable");
|
|
1356
|
-
let movedAsidePath;
|
|
1357
|
-
let stagedForCollectionPath;
|
|
1358
|
-
if (import_node_fs.default.existsSync(checkoutPath)) {
|
|
1359
|
-
const checkoutCommonDir = import_node_fs.default.lstatSync(checkoutPath).isDirectory() ? commonGitDirectory(checkoutPath) : null;
|
|
1360
|
-
if (checkoutCommonDir !== primaryCommonDir) {
|
|
1361
|
-
movedAsidePath = moveProjectCheckoutAside(input.projectRoot, input.branchName, checkoutPath);
|
|
1362
|
-
tryGit(primaryPath, ["worktree", "prune"]);
|
|
1363
|
-
} else {
|
|
1364
|
-
if (projectWorktreeOperationInProgress(checkoutPath)) {
|
|
1365
|
-
throw new Error(`Project branch ${input.branchName} has an in-progress Git operation`);
|
|
1366
|
-
}
|
|
1367
|
-
stagedForCollectionPath = (0, import_project_checkout_garbage.stageProjectCheckoutForDeletion)(input.projectRoot, input.branchName, checkoutPath);
|
|
1368
|
-
git(primaryPath, ["worktree", "prune"], `prune linked worktree ${input.branchName}`);
|
|
1369
|
-
}
|
|
1370
|
-
} else {
|
|
1371
|
-
tryGit(primaryPath, ["worktree", "prune"]);
|
|
1372
|
-
}
|
|
1373
|
-
const branchRef = `refs/heads/${input.branchName}`;
|
|
1374
|
-
if (tryGit(primaryPath, ["show-ref", "--verify", "--quiet", branchRef])) {
|
|
1375
|
-
git(primaryPath, ["branch", "-D", input.branchName], `delete project branch ${input.branchName}`);
|
|
1376
|
-
}
|
|
1377
|
-
return {
|
|
1378
|
-
branchName: input.branchName,
|
|
1379
|
-
...movedAsidePath ? { movedAsidePath } : {},
|
|
1380
|
-
...stagedForCollectionPath ? { stagedForCollectionPath } : {}
|
|
1381
|
-
};
|
|
1382
|
-
}
|
|
1383
|
-
function removeProjectWorktrees(input) {
|
|
1384
|
-
const projectRoot = import_node_path.default.resolve(input.projectRoot);
|
|
1385
|
-
if (input.branchNames.length === 0) throw new Error("Cannot remove a project with no configured branches");
|
|
1386
|
-
for (const branchName of input.branchNames) {
|
|
1387
|
-
(0, import_managed_paths.validateManagedBranchName)(branchName);
|
|
1388
|
-
const checkoutPath = branchPath(projectRoot, branchName);
|
|
1389
|
-
if (import_node_fs.default.existsSync(checkoutPath) && projectWorktreeOperationInProgress(checkoutPath)) {
|
|
1390
|
-
throw new Error(`Project branch ${branchName} has an in-progress Git operation`);
|
|
1391
|
-
}
|
|
1392
|
-
}
|
|
1393
|
-
if (!import_node_fs.default.existsSync(projectRoot)) return {};
|
|
1394
|
-
return { stagedForCollectionPath: (0, import_project_checkout_garbage.stageProjectForDeletion)(input.projectsRoot, projectRoot) };
|
|
1395
|
-
}
|
|
1396
|
-
async function deleteProjectMirrorBranch(input) {
|
|
1397
|
-
(0, import_managed_paths.validateManagedBranchName)(input.branchName);
|
|
1398
|
-
const deleted = await gitResultAsync(
|
|
1399
|
-
input.gitDirectory,
|
|
1400
|
-
[
|
|
1401
|
-
...(0, import_git_process_environment.gitTransportSecurityArgs)(input.mirrorUrl, input.credentialHelper, input.credentialUsername),
|
|
1402
|
-
"push",
|
|
1403
|
-
"--no-recurse-submodules",
|
|
1404
|
-
input.mirrorUrl,
|
|
1405
|
-
`:refs/heads/${input.branchName}`
|
|
1406
|
-
],
|
|
1407
|
-
PROJECT_GIT_TRANSPORT_TIMEOUT_MS
|
|
1408
|
-
);
|
|
1409
|
-
if (deleted.exitCode !== 0 && !/(remote ref does not exist|unable to delete|no such ref)/i.test(`${deleted.stderr}
|
|
1410
|
-
${deleted.stdout}`)) {
|
|
1411
|
-
throw new Error(
|
|
1412
|
-
`Delete hidden project mirror for ${input.branchName}: ${deleted.stderr || deleted.stdout || `git exited ${deleted.exitCode}`}`
|
|
1413
|
-
);
|
|
1414
|
-
}
|
|
1415
|
-
await gitResultAsync(input.gitDirectory, ["update-ref", "-d", `refs/r5d/mirror/${input.branchName}`]);
|
|
1416
|
-
}
|
|
1417
|
-
async function pushProjectMirrorHeads(input) {
|
|
1418
|
-
const results = [];
|
|
1419
|
-
for (const branchName of [...input.branchNames].sort()) {
|
|
1420
|
-
if (input.onlyBranches && !input.onlyBranches.has(branchName)) continue;
|
|
1421
|
-
const checkoutPath = branchPath(input.projectRoot, branchName);
|
|
1422
|
-
const head = input.publicationHeads ? input.publicationHeads.get(branchName) : await gitAsync(checkoutPath, ["rev-parse", "HEAD"], `resolve mirror head for ${branchName}`);
|
|
1423
|
-
if (!head || !/^[0-9a-f]{40}(?:[0-9a-f]{24})?$/.test(head)) {
|
|
1424
|
-
throw new Error(`Missing exact publication head for project branch ${branchName}`);
|
|
1425
|
-
}
|
|
1426
|
-
if (!await tryGitAsync(checkoutPath, ["cat-file", "-e", `${head}^{commit}`])) {
|
|
1427
|
-
throw new Error(`Exact publication head ${head} is unavailable for project branch ${branchName}`);
|
|
1428
|
-
}
|
|
1429
|
-
if (!input.expectedMirrorHeads.has(branchName)) {
|
|
1430
|
-
throw new Error(`Missing hidden-mirror lease for project branch ${branchName}`);
|
|
1431
|
-
}
|
|
1432
|
-
const expectedMirrorHead = input.expectedMirrorHeads.get(branchName) ?? null;
|
|
1433
|
-
if (expectedMirrorHead !== null && !/^[0-9a-f]{40}(?:[0-9a-f]{24})?$/.test(expectedMirrorHead)) {
|
|
1434
|
-
throw new Error(`Invalid hidden-mirror lease for project branch ${branchName}`);
|
|
1435
|
-
}
|
|
1436
|
-
const branchId = input.branchIds.get(branchName);
|
|
1437
|
-
if (!branchId || !/^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/.test(branchId)) {
|
|
1438
|
-
throw new Error(`Missing valid branch incarnation for project branch ${branchName}`);
|
|
1439
|
-
}
|
|
1440
|
-
if (expectedMirrorHead) {
|
|
1441
|
-
const handoffPrefix = `refs/r5d/server-synced/${branchId}/`;
|
|
1442
|
-
const markedCommits = (await gitAsync(
|
|
1443
|
-
checkoutPath,
|
|
1444
|
-
["for-each-ref", "--format=%(objectname)", handoffPrefix],
|
|
1445
|
-
`inspect synchronized server commits before publishing ${branchName}`
|
|
1446
|
-
)).split(/\r?\n/).filter((value) => /^[0-9a-f]{40}(?:[0-9a-f]{24})?$/.test(value));
|
|
1447
|
-
for (const markedCommit of markedCommits) {
|
|
1448
|
-
const markerIsInLease = await tryGitAsync(checkoutPath, ["merge-base", "--is-ancestor", markedCommit, expectedMirrorHead]);
|
|
1449
|
-
if (markerIsInLease && !await tryGitAsync(checkoutPath, ["merge-base", "--is-ancestor", markedCommit, head])) {
|
|
1450
|
-
throw new Error(`Refusing to overwrite synchronized server commit history for project branch ${branchName}`);
|
|
1451
|
-
}
|
|
1452
|
-
}
|
|
1453
|
-
}
|
|
1454
|
-
if (projectWorktreeOperationInProgress(checkoutPath)) {
|
|
1455
|
-
results.push({ branchName, head, pushed: false, reason: "git_operation_in_progress" });
|
|
1456
|
-
continue;
|
|
1457
|
-
}
|
|
1458
|
-
const pushed = await gitResultAsync(
|
|
1459
|
-
checkoutPath,
|
|
1460
|
-
[
|
|
1461
|
-
...(0, import_git_process_environment.gitTransportSecurityArgs)(input.mirrorUrl, input.credentialHelper, input.credentialUsername),
|
|
1462
|
-
"push",
|
|
1463
|
-
"--no-recurse-submodules",
|
|
1464
|
-
`--force-with-lease=refs/heads/${branchName}:${expectedMirrorHead ?? ""}`,
|
|
1465
|
-
input.mirrorUrl,
|
|
1466
|
-
`${head}:refs/heads/${branchName}`
|
|
1467
|
-
],
|
|
1468
|
-
PROJECT_GIT_TRANSPORT_TIMEOUT_MS
|
|
1469
|
-
);
|
|
1470
|
-
if (pushed.exitCode !== 0) {
|
|
1471
|
-
throw new Error(`Push hidden project mirror for ${branchName}: ${pushed.stderr || pushed.stdout || `git exited ${pushed.exitCode}`}`);
|
|
1472
|
-
}
|
|
1473
|
-
await gitAsync(checkoutPath, ["update-ref", `refs/r5d/mirror/${branchName}`, head], `record pushed mirror tip ${branchName}`);
|
|
1474
|
-
results.push({ branchName, head, pushed: true });
|
|
1475
|
-
}
|
|
1476
|
-
return results;
|
|
1477
|
-
}
|
|
1478
|
-
function assertProjectMirrorHeadsPushed(results) {
|
|
1479
|
-
const skipped = results.find((result) => !result.pushed);
|
|
1480
|
-
if (skipped) {
|
|
1481
|
-
throw new Error(`Hidden project mirror publication skipped ${skipped.branchName}${skipped.reason ? ` (${skipped.reason})` : ""}`);
|
|
1482
|
-
}
|
|
1483
|
-
}
|
|
1484
|
-
const PROJECT_MIRROR_FETCH_REFSPECS = ["+refs/heads/*:refs/r5d/mirror/*", "+refs/r5d/server-synced/*:refs/r5d/server-synced/*"];
|
|
1485
|
-
function projectMirrorMountFetchRefspecs(branchName, branchId) {
|
|
1486
|
-
(0, import_managed_paths.validateManagedBranchName)(branchName);
|
|
1487
|
-
if (branchId !== void 0 && !/^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/.test(branchId)) throw new Error(`Invalid branch id for ${branchName}`);
|
|
1488
|
-
return [
|
|
1489
|
-
`+refs/heads/${branchName}:refs/r5d/mirror/${branchName}`,
|
|
1490
|
-
branchId === void 0 ? "+refs/r5d/server-synced/*:refs/r5d/server-synced/*" : `+refs/r5d/server-synced/${branchId}/*:refs/r5d/server-synced/${branchId}/*`
|
|
1491
|
-
];
|
|
1492
|
-
}
|
|
1493
|
-
async function fetchProjectMirrorRefs(input) {
|
|
1494
|
-
const fetch = (refspecs) => gitResultAsync(
|
|
1495
|
-
input.primaryPath,
|
|
1496
|
-
[
|
|
1497
|
-
...(0, import_git_process_environment.gitTransportSecurityArgs)(input.mirrorUrl, input.credentialHelper, input.credentialUsername),
|
|
1498
|
-
"fetch",
|
|
1499
|
-
"--no-recurse-submodules",
|
|
1500
|
-
"--prune",
|
|
1501
|
-
input.mirrorUrl,
|
|
1502
|
-
...refspecs
|
|
1503
|
-
],
|
|
1504
|
-
PROJECT_GIT_TRANSPORT_TIMEOUT_MS
|
|
1505
|
-
);
|
|
1506
|
-
if (input.fetchOnlyBranches && input.fetchOnlyBranches.size > 0) {
|
|
1507
|
-
const refspecs = [...input.fetchOnlyBranches].sort().flatMap((branchName) => projectMirrorMountFetchRefspecs(branchName, input.branchIds?.get(branchName)));
|
|
1508
|
-
const scoped = await fetch(refspecs);
|
|
1509
|
-
if (scoped.exitCode === 0) return;
|
|
1510
|
-
if (!/(couldn't find remote ref|no such ref)/i.test(`${scoped.stderr}
|
|
1511
|
-
${scoped.stdout}`)) {
|
|
1512
|
-
throw new Error(`Refresh project head mirror: ${scoped.stderr || scoped.stdout || `git exited ${scoped.exitCode}`}`);
|
|
1513
|
-
}
|
|
1514
|
-
}
|
|
1515
|
-
const whole = await fetch(PROJECT_MIRROR_FETCH_REFSPECS);
|
|
1516
|
-
if (whole.exitCode !== 0) {
|
|
1517
|
-
throw new Error(`Refresh project head mirror: ${whole.stderr || whole.stdout || `git exited ${whole.exitCode}`}`);
|
|
1518
|
-
}
|
|
1519
|
-
}
|
|
1520
|
-
async function observeProjectMirrorHeads(input) {
|
|
1521
|
-
const primaryPath = branchPath(input.projectRoot, input.primaryBranchName);
|
|
1522
|
-
if (input.refreshRemote !== false && !(input.fetchOnlyBranches && input.fetchOnlyBranches.size === 0)) {
|
|
1523
|
-
await fetchProjectMirrorRefs({ ...input, primaryPath });
|
|
1524
|
-
}
|
|
1525
|
-
const observations = [];
|
|
1526
|
-
for (const branchName of [...input.branchNames].sort()) {
|
|
1527
|
-
const checkoutPath = branchPath(input.projectRoot, branchName);
|
|
1528
|
-
const previousHead = await gitAsync(checkoutPath, ["rev-parse", "HEAD"], `resolve local project head ${branchName}`);
|
|
1529
|
-
const mirrorRef = `refs/r5d/mirror/${branchName}`;
|
|
1530
|
-
const mirrorHead = await tryGitAsync(primaryPath, ["show-ref", "--verify", "--quiet", mirrorRef]) ? await gitAsync(primaryPath, ["rev-parse", mirrorRef], `resolve refreshed mirror head ${branchName}`) : null;
|
|
1531
|
-
const branchId = input.branchIds?.get(branchName);
|
|
1532
|
-
if (branchId && !/^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/.test(branchId)) throw new Error(`Invalid branch id for ${branchName}`);
|
|
1533
|
-
const handoffPrefix = branchId ? `refs/r5d/server-synced/${branchId}/` : null;
|
|
1534
|
-
const handoffRows = handoffPrefix ? await gitAsync(
|
|
1535
|
-
primaryPath,
|
|
1536
|
-
["for-each-ref", "--format=%(objectname) %(refname)", handoffPrefix],
|
|
1537
|
-
`inspect synchronized server commits for ${branchName}`
|
|
1538
|
-
) : "";
|
|
1539
|
-
const serverSyncedWorkspaceHeads = (await Promise.all(
|
|
1540
|
-
handoffRows.split(/\r?\n/).map(async (row) => {
|
|
1541
|
-
const separator = row.indexOf(" ");
|
|
1542
|
-
if (separator < 0 || !mirrorHead) return null;
|
|
1543
|
-
const markedCommit = row.slice(0, separator);
|
|
1544
|
-
const ref = row.slice(separator + 1);
|
|
1545
|
-
if (!/^[0-9a-f]{40}(?:[0-9a-f]{24})?$/.test(markedCommit) || !handoffPrefix || !ref.startsWith(handoffPrefix)) {
|
|
1546
|
-
return null;
|
|
1547
|
-
}
|
|
1548
|
-
const markerIsIntegrated = markedCommit === mirrorHead || await tryGitAsync(primaryPath, ["merge-base", "--is-ancestor", markedCommit, mirrorHead]);
|
|
1549
|
-
if (!markerIsIntegrated) return null;
|
|
1550
|
-
const workspaceHead = ref.slice(handoffPrefix.length);
|
|
1551
|
-
return /^[0-9a-f]{40}(?:[0-9a-f]{24})?$/.test(workspaceHead) ? workspaceHead : null;
|
|
1552
|
-
})
|
|
1553
|
-
)).filter((workspaceHead) => workspaceHead !== null).sort();
|
|
1554
|
-
const fastForward = Boolean(
|
|
1555
|
-
mirrorHead && mirrorHead !== previousHead && await tryGitAsync(checkoutPath, ["merge-base", "--is-ancestor", previousHead, mirrorHead])
|
|
1556
|
-
);
|
|
1557
|
-
const serverSynced = Boolean(mirrorHead && mirrorHead !== previousHead && serverSyncedWorkspaceHeads.length > 0);
|
|
1558
|
-
const shouldMove = Boolean(mirrorHead && mirrorHead !== previousHead && (input.allowNonFastForward || fastForward || serverSynced));
|
|
1559
|
-
observations.push({
|
|
1560
|
-
branchName,
|
|
1561
|
-
previousHead,
|
|
1562
|
-
mirrorHead,
|
|
1563
|
-
mode: input.allowNonFastForward ? "reseed" : serverSynced && !fastForward ? "server_synced" : "fast_forward",
|
|
1564
|
-
serverSyncedWorkspaceHeads,
|
|
1565
|
-
shouldMove
|
|
1566
|
-
});
|
|
1567
|
-
}
|
|
1568
|
-
return observations;
|
|
1569
|
-
}
|
|
1570
|
-
async function computeLocalProjectMirrorRefsTokens(input) {
|
|
1571
|
-
const primaryPath = branchPath(input.projectRoot, input.primaryBranchName);
|
|
1572
|
-
const rows = await gitAsync(
|
|
1573
|
-
primaryPath,
|
|
1574
|
-
["for-each-ref", import_project_mirror_refs_token.PROJECT_MIRROR_REF_LISTING_FORMAT, import_project_mirror_refs_token.PROJECT_MIRROR_REF_PREFIX, import_project_mirror_refs_token.PROJECT_SERVER_SYNCED_REF_PREFIX],
|
|
1575
|
-
"list cached project mirror refs"
|
|
1576
|
-
);
|
|
1577
|
-
return (0, import_project_mirror_refs_token.projectMirrorRefsTokens)((0, import_project_mirror_refs_token.canonicalizeLocalProjectMirrorRefs)((0, import_project_mirror_refs_token.parseProjectMirrorRefListing)(rows)), input.mounts);
|
|
1578
|
-
}
|
|
1579
|
-
async function applyObservedProjectMirrorHeads(input) {
|
|
1580
|
-
const results = [];
|
|
1581
|
-
for (const observation of [...input.observations].sort((left, right) => left.branchName.localeCompare(right.branchName))) {
|
|
1582
|
-
const { branchName, mirrorHead } = observation;
|
|
1583
|
-
const checkoutPath = branchPath(input.projectRoot, branchName);
|
|
1584
|
-
const currentHead = await gitAsync(checkoutPath, ["rev-parse", "HEAD"], `resolve local project head ${branchName}`);
|
|
1585
|
-
const serverSyncedCovered = observation.mode !== "server_synced" || (input.serverSyncedWorkspaceCovered ? await input.serverSyncedWorkspaceCovered(observation) : false);
|
|
1586
|
-
if (!observation.shouldMove || !serverSyncedCovered || !mirrorHead || currentHead === mirrorHead || currentHead !== observation.previousHead) {
|
|
1587
|
-
results.push({ branchName, previousHead: currentHead, mirrorHead, moved: false });
|
|
1588
|
-
continue;
|
|
1589
|
-
}
|
|
1590
|
-
if (!await tryGitAsync(checkoutPath, ["cat-file", "-e", `${mirrorHead}^{commit}`])) {
|
|
1591
|
-
throw new Error(`Observed hidden mirror head is no longer available for ${branchName}: ${mirrorHead}`);
|
|
1592
|
-
}
|
|
1593
|
-
const mutationAllowed = () => !projectWorktreeOperationInProgress(checkoutPath) && input.branchMutationAllowed?.(branchName, checkoutPath) !== false;
|
|
1594
|
-
if (!mutationAllowed()) {
|
|
1595
|
-
results.push({ branchName, previousHead: currentHead, mirrorHead, moved: false });
|
|
1596
|
-
continue;
|
|
1597
|
-
}
|
|
1598
|
-
if (observation.mode === "reseed" || observation.mode === "server_synced") {
|
|
1599
|
-
await preserveHead(checkoutPath);
|
|
1600
|
-
if (!mutationAllowed()) {
|
|
1601
|
-
results.push({ branchName, previousHead: currentHead, mirrorHead, moved: false });
|
|
1602
|
-
continue;
|
|
1603
|
-
}
|
|
1604
|
-
}
|
|
1605
|
-
const action = observation.mode === "reseed" ? "reseed" : observation.mode === "server_synced" ? "adopt server commit" : "fast-forward";
|
|
1606
|
-
await gitAsync(checkoutPath, ["read-tree", "--reset", mirrorHead], `${action} project index ${branchName}`);
|
|
1607
|
-
await gitAsync(
|
|
1608
|
-
checkoutPath,
|
|
1609
|
-
["update-ref", `refs/heads/${branchName}`, mirrorHead, currentHead],
|
|
1610
|
-
`${action} project branch ${branchName}`
|
|
1611
|
-
);
|
|
1612
|
-
results.push({ branchName, previousHead: currentHead, mirrorHead, moved: true });
|
|
1613
|
-
}
|
|
1614
|
-
return results;
|
|
1615
|
-
}
|
|
1616
|
-
async function updateProjectHeadsFromMirror(input) {
|
|
1617
|
-
const observations = await observeProjectMirrorHeads(input);
|
|
1618
|
-
return applyObservedProjectMirrorHeads({
|
|
1619
|
-
projectRoot: input.projectRoot,
|
|
1620
|
-
primaryBranchName: input.primaryBranchName,
|
|
1621
|
-
observations,
|
|
1622
|
-
branchMutationAllowed: input.branchMutationAllowed
|
|
1623
|
-
});
|
|
1624
|
-
}
|
|
1625
|
-
async function fastForwardProjectHeadsFromMirror(input) {
|
|
1626
|
-
return (await updateProjectHeadsFromMirror({ ...input, allowNonFastForward: false })).map(({ moved, ...state }) => ({
|
|
1627
|
-
...state,
|
|
1628
|
-
fastForwarded: moved
|
|
1629
|
-
}));
|
|
1630
|
-
}
|
|
1631
|
-
async function reseedProjectHeadsFromMirror(input) {
|
|
1632
|
-
return (await updateProjectHeadsFromMirror({ ...input, allowNonFastForward: true })).map(({ moved, ...state }) => ({
|
|
1633
|
-
...state,
|
|
1634
|
-
reseeded: moved
|
|
1635
|
-
}));
|
|
1636
|
-
}
|
|
1637
|
-
function runProjectSnapshotCreateJob(input, context) {
|
|
1638
|
-
return snapshotBranchTrees(input.projectRoot, input.branches, input.temporaryRoot, context.progress, input.scope, input.ownerSessionId);
|
|
1639
|
-
}
|
|
1640
|
-
function runProjectSnapshotApplyJob(input) {
|
|
1641
|
-
for (const { snapshotPath, targetPath } of input.mirrors) {
|
|
1642
|
-
(0, import_working_tree_mirror.mirrorWorkingTree)({ sourceRoot: snapshotPath, targetRoot: targetPath, sourceMode: "all", deletionMode: "git" });
|
|
1643
|
-
}
|
|
1644
|
-
return null;
|
|
1645
|
-
}
|
|
1646
|
-
function runProjectSnapshotPersistJob(input) {
|
|
1647
|
-
persistReconciledProjectAndConsumeSnapshot(input.snapshot, void 0, input.durabilityPaths ?? void 0);
|
|
1648
|
-
return null;
|
|
1649
|
-
}
|
|
1650
|
-
function runProjectSnapshotRollbackJob(input) {
|
|
1651
|
-
rollbackProjectWorktreeSnapshot(input.snapshot);
|
|
1652
|
-
return null;
|
|
1653
|
-
}
|
|
1654
|
-
const projectWorktreesTestHarness = {
|
|
1655
|
-
commandArgs: gitCommandArgs,
|
|
1656
|
-
configureRepository,
|
|
1657
|
-
createPreparedSnapshot(input) {
|
|
1658
|
-
return {
|
|
1659
|
-
root: snapshotBranchTrees(
|
|
1660
|
-
input.projectRoot,
|
|
1661
|
-
input.branches,
|
|
1662
|
-
input.temporaryRoot,
|
|
1663
|
-
void 0,
|
|
1664
|
-
input.scope ?? "project",
|
|
1665
|
-
PROJECT_WORKTREE_SNAPSHOT_OWNER_SESSION_ID
|
|
1666
|
-
).root
|
|
1667
|
-
};
|
|
1668
|
-
},
|
|
1669
|
-
consumeSnapshot(snapshotRoot) {
|
|
1670
|
-
const manifest = readSnapshotManifest(snapshotRoot);
|
|
1671
|
-
advanceSnapshotState(snapshotRoot, manifest, "consumed");
|
|
1672
|
-
},
|
|
1673
|
-
persistProjectAndConsumeSnapshot(snapshotRoot, afterProjectTreeFsync) {
|
|
1674
|
-
const manifest = readSnapshotManifest(snapshotRoot);
|
|
1675
|
-
persistReconciledProjectAndConsumeSnapshot({ root: snapshotRoot, manifest, paths: /* @__PURE__ */ new Map() }, afterProjectTreeFsync);
|
|
1676
|
-
},
|
|
1677
|
-
snapshotState(snapshotRoot) {
|
|
1678
|
-
return readSnapshotManifest(snapshotRoot).state;
|
|
1679
|
-
}
|
|
1680
|
-
};
|
|
1681
|
-
// Annotate the CommonJS export names for ESM import in node:
|
|
1682
|
-
0 && (module.exports = {
|
|
1683
|
-
PROJECT_REMOVED_CHECKOUTS_DIRECTORY,
|
|
1684
|
-
PROJECT_WORKTREE_SNAPSHOT_OWNER_SESSION_ID,
|
|
1685
|
-
PROJECT_WORKTREE_SNAPSHOT_PREFIX,
|
|
1686
|
-
ProjectBranchCreationRollbackIncompleteError,
|
|
1687
|
-
applyObservedProjectMirrorHeads,
|
|
1688
|
-
assertProjectMirrorHeadsPushed,
|
|
1689
|
-
classifyProjectCheckout,
|
|
1690
|
-
computeLocalProjectMirrorRefsTokens,
|
|
1691
|
-
createLinkedProjectBranch,
|
|
1692
|
-
createOrRetryLinkedProjectBranch,
|
|
1693
|
-
deleteLinkedProjectBranch,
|
|
1694
|
-
deleteProjectMirrorBranch,
|
|
1695
|
-
ensureProjectWorktrees,
|
|
1696
|
-
fastForwardProjectHeadsFromMirror,
|
|
1697
|
-
hasLinkedProjectWorktreeLayout,
|
|
1698
|
-
inventoryProjectCheckouts,
|
|
1699
|
-
observeProjectMirrorHeads,
|
|
1700
|
-
projectBranchMayExistAfterCreateFailure,
|
|
1701
|
-
projectOriginBranchName,
|
|
1702
|
-
projectWorktreeConfigurationFingerprint,
|
|
1703
|
-
projectWorktreeOperationInProgress,
|
|
1704
|
-
projectWorktreesTestHarness,
|
|
1705
|
-
pushProjectMirrorHeads,
|
|
1706
|
-
recoverStaleProjectWorktreeSnapshots,
|
|
1707
|
-
removeProjectWorktrees,
|
|
1708
|
-
reseedProjectHeadsFromMirror,
|
|
1709
|
-
runProjectSnapshotApplyJob,
|
|
1710
|
-
runProjectSnapshotCreateJob,
|
|
1711
|
-
runProjectSnapshotPersistJob,
|
|
1712
|
-
runProjectSnapshotRollbackJob
|
|
1713
|
-
});
|