@ricsam/r5d-worker 0.0.74 → 0.0.75
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -4
- package/dist/cjs/main.cjs +1022 -976
- package/dist/cjs/package.json +1 -1
- package/dist/cjs/project-workspace-state.cjs +777 -0
- package/dist/cjs/project-worktrees.cjs +505 -0
- package/dist/cjs/working-tree-mirror.cjs +225 -0
- package/dist/cjs/workspace-git-sync.cjs +565 -0
- package/dist/cjs/workspace-incident-state.cjs +2 -38
- package/dist/mjs/main.mjs +1038 -987
- package/dist/mjs/package.json +1 -1
- package/dist/mjs/project-workspace-state.mjs +745 -0
- package/dist/mjs/project-worktrees.mjs +461 -0
- package/dist/mjs/working-tree-mirror.mjs +190 -0
- package/dist/mjs/workspace-git-sync.mjs +524 -0
- package/dist/mjs/workspace-incident-state.mjs +1 -35
- package/dist/types/main.d.ts +35 -55
- package/dist/types/project-workspace-state.d.ts +144 -0
- package/dist/types/project-worktrees.d.ts +101 -0
- package/dist/types/working-tree-mirror.d.ts +24 -0
- package/dist/types/workspace-git-sync.d.ts +97 -0
- package/dist/types/workspace-incident-state.d.ts +0 -17
- package/dist/types/workspace-mutation-gate.d.ts +3 -3
- package/package.json +1 -1
- package/dist/cjs/workspace-convergence.cjs +0 -280
- package/dist/cjs/workspace-manifest-admission.cjs +0 -60
- package/dist/cjs/workspace-sync.cjs +0 -2469
- package/dist/mjs/workspace-convergence.mjs +0 -236
- package/dist/mjs/workspace-manifest-admission.mjs +0 -26
- package/dist/mjs/workspace-sync.mjs +0 -2417
- package/dist/types/workspace-convergence.d.ts +0 -93
- package/dist/types/workspace-manifest-admission.d.ts +0 -22
- package/dist/types/workspace-sync.d.ts +0 -167
|
@@ -0,0 +1,505 @@
|
|
|
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
|
+
createLinkedProjectBranch: () => createLinkedProjectBranch,
|
|
32
|
+
deleteLinkedProjectBranch: () => deleteLinkedProjectBranch,
|
|
33
|
+
deleteProjectMirrorBranch: () => deleteProjectMirrorBranch,
|
|
34
|
+
ensureProjectWorktrees: () => ensureProjectWorktrees,
|
|
35
|
+
fastForwardProjectHeadsFromMirror: () => fastForwardProjectHeadsFromMirror,
|
|
36
|
+
hasLinkedProjectWorktreeLayout: () => hasLinkedProjectWorktreeLayout,
|
|
37
|
+
projectWorktreeConfigurationFingerprint: () => projectWorktreeConfigurationFingerprint,
|
|
38
|
+
projectWorktreeOperationInProgress: () => projectWorktreeOperationInProgress,
|
|
39
|
+
projectWorktreesTestHarness: () => projectWorktreesTestHarness,
|
|
40
|
+
pushProjectMirrorHeads: () => pushProjectMirrorHeads,
|
|
41
|
+
removeProjectWorktrees: () => removeProjectWorktrees
|
|
42
|
+
});
|
|
43
|
+
module.exports = __toCommonJS(project_worktrees_exports);
|
|
44
|
+
var import_node_crypto = require("node:crypto");
|
|
45
|
+
var import_node_fs = __toESM(require("node:fs"), 1);
|
|
46
|
+
var import_node_os = __toESM(require("node:os"), 1);
|
|
47
|
+
var import_node_path = __toESM(require("node:path"), 1);
|
|
48
|
+
var import_managed_paths = require("./managed-paths.cjs");
|
|
49
|
+
var import_working_tree_mirror = require("./working-tree-mirror.cjs");
|
|
50
|
+
function projectWorktreeConfigurationFingerprint(input) {
|
|
51
|
+
return (0, import_node_crypto.createHash)("sha256").update(
|
|
52
|
+
JSON.stringify({
|
|
53
|
+
checkoutPathSegments: input.checkoutPathSegments,
|
|
54
|
+
projectPath: input.projectPath,
|
|
55
|
+
repoHttpUrl: input.repoHttpUrl,
|
|
56
|
+
repoAuthHeader: input.repoAuthHeader,
|
|
57
|
+
defaultBranch: input.defaultBranch,
|
|
58
|
+
branches: [...input.branches].sort((left, right) => left.branchName.localeCompare(right.branchName)).map(({ branchName, sourceBranchName, baseCommitHash }) => ({
|
|
59
|
+
branchName,
|
|
60
|
+
sourceBranchName: sourceBranchName ?? null,
|
|
61
|
+
baseCommitHash
|
|
62
|
+
})),
|
|
63
|
+
gitIdentity: input.gitIdentity
|
|
64
|
+
})
|
|
65
|
+
).digest("hex");
|
|
66
|
+
}
|
|
67
|
+
const NON_RECURSIVE_GIT_CONFIG = [
|
|
68
|
+
"-c",
|
|
69
|
+
"submodule.recurse=false",
|
|
70
|
+
"-c",
|
|
71
|
+
"fetch.recurseSubmodules=false",
|
|
72
|
+
"-c",
|
|
73
|
+
"push.recurseSubmodules=false"
|
|
74
|
+
];
|
|
75
|
+
function normalizedHttpOrigin(value) {
|
|
76
|
+
try {
|
|
77
|
+
const url = new URL(value);
|
|
78
|
+
return `${url.protocol}//${url.host}`;
|
|
79
|
+
} catch {
|
|
80
|
+
return value.replace(/\/+$/, "");
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function authArgs(auth) {
|
|
84
|
+
if (!auth) return [];
|
|
85
|
+
const key = `http.${normalizedHttpOrigin(auth.extraHeaderUrl)}/.extraHeader`;
|
|
86
|
+
return ["-c", "http.extraHeader=", "-c", `${key}=`, "-c", `${key}=${auth.header}`];
|
|
87
|
+
}
|
|
88
|
+
function gitResult(cwd, args, auth) {
|
|
89
|
+
const result = Bun.spawnSync(["git", ...NON_RECURSIVE_GIT_CONFIG, ...authArgs(auth), ...args], {
|
|
90
|
+
cwd,
|
|
91
|
+
stdout: "pipe",
|
|
92
|
+
stderr: "pipe",
|
|
93
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: "0" }
|
|
94
|
+
});
|
|
95
|
+
return {
|
|
96
|
+
exitCode: result.exitCode,
|
|
97
|
+
stdout: result.stdout.toString().trim(),
|
|
98
|
+
stderr: result.stderr.toString().trim()
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
function git(cwd, args, action, auth) {
|
|
102
|
+
const result = gitResult(cwd, args, auth);
|
|
103
|
+
if (result.exitCode !== 0) throw new Error(`${action}: ${result.stderr || result.stdout || `git exited ${result.exitCode}`}`);
|
|
104
|
+
return result.stdout;
|
|
105
|
+
}
|
|
106
|
+
function tryGit(cwd, args, auth) {
|
|
107
|
+
return gitResult(cwd, args, auth).exitCode === 0;
|
|
108
|
+
}
|
|
109
|
+
function branchPath(projectRoot, branchName) {
|
|
110
|
+
(0, import_managed_paths.validateManagedBranchName)(branchName);
|
|
111
|
+
const result = import_node_path.default.join(projectRoot, ...branchName.split("/"));
|
|
112
|
+
const relative = import_node_path.default.relative(import_node_path.default.resolve(projectRoot), import_node_path.default.resolve(result));
|
|
113
|
+
if (relative === ".." || relative.startsWith(`..${import_node_path.default.sep}`) || import_node_path.default.isAbsolute(relative)) {
|
|
114
|
+
throw new Error(`Branch path escapes its project root: ${branchName}`);
|
|
115
|
+
}
|
|
116
|
+
return result;
|
|
117
|
+
}
|
|
118
|
+
function assertNonOverlappingBranches(branches) {
|
|
119
|
+
const names = branches.map(({ branchName }) => branchName).sort();
|
|
120
|
+
if (new Set(names).size !== names.length) throw new Error("Project worktree branch names must be unique");
|
|
121
|
+
for (let index = 0; index < names.length; index += 1) {
|
|
122
|
+
const name = names[index];
|
|
123
|
+
(0, import_managed_paths.validateManagedBranchName)(name);
|
|
124
|
+
const baseCommitHash = branches.find(({ branchName }) => branchName === name)?.baseCommitHash;
|
|
125
|
+
if (!baseCommitHash || !/^[0-9a-f]{40}(?:[0-9a-f]{24})?$/.test(baseCommitHash)) {
|
|
126
|
+
throw new Error(`Invalid base commit hash for project branch ${name}`);
|
|
127
|
+
}
|
|
128
|
+
const overlapping = names.slice(index + 1).find((candidate) => candidate.startsWith(`${name}/`));
|
|
129
|
+
if (overlapping) throw new Error(`Branch folders overlap: ${name} and ${overlapping}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
function gitDirectoryKind(checkoutPath) {
|
|
133
|
+
const gitPath = import_node_path.default.join(checkoutPath, ".git");
|
|
134
|
+
try {
|
|
135
|
+
const stat = import_node_fs.default.lstatSync(gitPath);
|
|
136
|
+
if (stat.isDirectory()) return "directory";
|
|
137
|
+
if (stat.isFile()) return "file";
|
|
138
|
+
return null;
|
|
139
|
+
} catch {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
function commonGitDirectory(checkoutPath) {
|
|
144
|
+
const result = gitResult(checkoutPath, ["rev-parse", "--path-format=absolute", "--git-common-dir"]);
|
|
145
|
+
return result.exitCode === 0 ? import_node_path.default.resolve(result.stdout) : null;
|
|
146
|
+
}
|
|
147
|
+
function hasLinkedProjectWorktreeLayout(input) {
|
|
148
|
+
const primaryPath = branchPath(input.projectRoot, input.primaryBranchName);
|
|
149
|
+
if (gitDirectoryKind(primaryPath) !== "directory") return false;
|
|
150
|
+
const primaryCommonDir = commonGitDirectory(primaryPath);
|
|
151
|
+
if (!primaryCommonDir) return false;
|
|
152
|
+
return input.branches.every(({ branchName }) => {
|
|
153
|
+
const checkoutPath = branchPath(input.projectRoot, branchName);
|
|
154
|
+
if (branchName === input.primaryBranchName) return commonGitDirectory(checkoutPath) === primaryCommonDir;
|
|
155
|
+
return gitDirectoryKind(checkoutPath) === "file" && commonGitDirectory(checkoutPath) === primaryCommonDir;
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
function hasCompatibleLinkedProjectWorktreeLayout(input) {
|
|
159
|
+
const primaryPath = branchPath(input.projectRoot, input.primaryBranchName);
|
|
160
|
+
if (gitDirectoryKind(primaryPath) !== "directory") return false;
|
|
161
|
+
const primaryCommonDir = commonGitDirectory(primaryPath);
|
|
162
|
+
if (!primaryCommonDir) return false;
|
|
163
|
+
return input.branches.every(({ branchName }) => {
|
|
164
|
+
if (branchName === input.primaryBranchName) return true;
|
|
165
|
+
const checkoutPath = branchPath(input.projectRoot, branchName);
|
|
166
|
+
const kind = gitDirectoryKind(checkoutPath);
|
|
167
|
+
return kind === null || kind === "file" && commonGitDirectory(checkoutPath) === primaryCommonDir;
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
function snapshotBranchTrees(projectRoot, branches) {
|
|
171
|
+
const root = import_node_fs.default.mkdtempSync(import_node_path.default.join(import_node_os.default.tmpdir(), "r5d-project-worktrees-"));
|
|
172
|
+
const paths = /* @__PURE__ */ new Map();
|
|
173
|
+
for (const { branchName } of branches) {
|
|
174
|
+
const checkoutPath = branchPath(projectRoot, branchName);
|
|
175
|
+
if (!import_node_fs.default.existsSync(checkoutPath)) continue;
|
|
176
|
+
const snapshotPath = import_node_path.default.join(root, ...branchName.split("/"));
|
|
177
|
+
(0, import_working_tree_mirror.mirrorWorkingTree)({ sourceRoot: checkoutPath, targetRoot: snapshotPath, sourceMode: "all", deletionMode: "all" });
|
|
178
|
+
paths.set(branchName, snapshotPath);
|
|
179
|
+
}
|
|
180
|
+
return { root, paths };
|
|
181
|
+
}
|
|
182
|
+
function configureRepository(input) {
|
|
183
|
+
if (tryGit(input.primaryPath, ["remote", "get-url", "origin"])) {
|
|
184
|
+
git(input.primaryPath, ["remote", "set-url", "origin", input.originUrl], "configure project origin");
|
|
185
|
+
} else {
|
|
186
|
+
git(input.primaryPath, ["remote", "add", "origin", input.originUrl], "configure project origin");
|
|
187
|
+
}
|
|
188
|
+
git(input.primaryPath, ["config", "--local", "--replace-all", "credential.helper", ""], "reset project credential helpers");
|
|
189
|
+
if (input.credentialHelper) {
|
|
190
|
+
git(
|
|
191
|
+
input.primaryPath,
|
|
192
|
+
["config", "--local", "--add", "credential.helper", input.credentialHelper],
|
|
193
|
+
"configure project credential helper"
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
if (input.gitIdentity) {
|
|
197
|
+
const name = input.gitIdentity.name.trim();
|
|
198
|
+
const email = input.gitIdentity.email.trim();
|
|
199
|
+
if (!name || !email) throw new Error("Git identity must include name and email");
|
|
200
|
+
git(input.primaryPath, ["config", "--local", "user.name", name], "configure project Git user name");
|
|
201
|
+
git(input.primaryPath, ["config", "--local", "user.email", email], "configure project Git user email");
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
function fetchProjectHeads(input) {
|
|
205
|
+
const originFetch = gitResult(
|
|
206
|
+
input.primaryPath,
|
|
207
|
+
["fetch", "--no-recurse-submodules", "--prune", "origin", "+refs/heads/*:refs/remotes/origin/*"],
|
|
208
|
+
input.originAuth
|
|
209
|
+
);
|
|
210
|
+
if (originFetch.exitCode !== 0) {
|
|
211
|
+
throw new Error(`Fetch project origin: ${originFetch.stderr || originFetch.stdout || `git exited ${originFetch.exitCode}`}`);
|
|
212
|
+
}
|
|
213
|
+
const mirrorFetch = gitResult(
|
|
214
|
+
input.primaryPath,
|
|
215
|
+
["fetch", "--no-recurse-submodules", "--prune", input.mirrorUrl, "+refs/heads/*:refs/r5d/mirror/*"],
|
|
216
|
+
input.mirrorAuth
|
|
217
|
+
);
|
|
218
|
+
if (mirrorFetch.exitCode !== 0 && !/(couldn't find remote ref|does not have any commits|remote repository is empty|no such ref)/i.test(
|
|
219
|
+
`${mirrorFetch.stderr}
|
|
220
|
+
${mirrorFetch.stdout}`
|
|
221
|
+
)) {
|
|
222
|
+
throw new Error(`Fetch project head mirror: ${mirrorFetch.stderr || mirrorFetch.stdout || `git exited ${mirrorFetch.exitCode}`}`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
function ensureCommitAvailable(input) {
|
|
226
|
+
if (tryGit(input.primaryPath, ["cat-file", "-e", `${input.commitHash}^{commit}`])) return;
|
|
227
|
+
for (const [url, auth] of [
|
|
228
|
+
[input.mirrorUrl, input.mirrorAuth],
|
|
229
|
+
[input.originUrl, input.originAuth]
|
|
230
|
+
]) {
|
|
231
|
+
if (tryGit(input.primaryPath, ["fetch", "--no-recurse-submodules", "--no-tags", url, input.commitHash], auth)) return;
|
|
232
|
+
}
|
|
233
|
+
throw new Error(`Project base commit ${input.commitHash} is unavailable from origin and the canonical mirror`);
|
|
234
|
+
}
|
|
235
|
+
function seedForBranch(input) {
|
|
236
|
+
const mirrorRef = `refs/r5d/mirror/${input.branch.branchName}`;
|
|
237
|
+
const mirrorHead = tryGit(input.primaryPath, ["show-ref", "--verify", "--quiet", mirrorRef]) ? git(input.primaryPath, ["rev-parse", mirrorRef], "resolve project mirror head") : null;
|
|
238
|
+
const seed = mirrorHead ?? input.branch.baseCommitHash;
|
|
239
|
+
ensureCommitAvailable({ ...input, commitHash: seed });
|
|
240
|
+
return { seed, mirrorHead };
|
|
241
|
+
}
|
|
242
|
+
function preserveHead(checkoutPath) {
|
|
243
|
+
if (!tryGit(checkoutPath, ["rev-parse", "--verify", "HEAD"])) return;
|
|
244
|
+
const head = git(checkoutPath, ["rev-parse", "HEAD"], "read pre-reconciliation project head");
|
|
245
|
+
git(checkoutPath, ["update-ref", `refs/r5d/pre-connect/${head}`, head], "preserve pre-reconciliation project head");
|
|
246
|
+
}
|
|
247
|
+
function initializeLinkedLayout(input) {
|
|
248
|
+
for (const { branchName } of [...input.branches].sort((left, right) => right.branchName.length - left.branchName.length)) {
|
|
249
|
+
import_node_fs.default.rmSync(branchPath(input.projectRoot, branchName), { recursive: true, force: true });
|
|
250
|
+
}
|
|
251
|
+
const primaryPath = branchPath(input.projectRoot, input.primaryBranchName);
|
|
252
|
+
import_node_fs.default.mkdirSync(import_node_path.default.dirname(primaryPath), { recursive: true });
|
|
253
|
+
git(void 0, ["init", `--initial-branch=${input.primaryBranchName}`, primaryPath], "initialize primary project clone");
|
|
254
|
+
configureRepository({ ...input, primaryPath });
|
|
255
|
+
fetchProjectHeads({ ...input, primaryPath });
|
|
256
|
+
const seeds = /* @__PURE__ */ new Map();
|
|
257
|
+
const mirrorHeads = /* @__PURE__ */ new Map();
|
|
258
|
+
for (const branch of input.branches) {
|
|
259
|
+
const resolved = seedForBranch({ ...input, primaryPath, branch });
|
|
260
|
+
seeds.set(branch.branchName, resolved.seed);
|
|
261
|
+
mirrorHeads.set(branch.branchName, resolved.mirrorHead);
|
|
262
|
+
}
|
|
263
|
+
git(
|
|
264
|
+
primaryPath,
|
|
265
|
+
["checkout", "--no-recurse-submodules", "-B", input.primaryBranchName, seeds.get(input.primaryBranchName)],
|
|
266
|
+
"anchor primary project worktree"
|
|
267
|
+
);
|
|
268
|
+
for (const branch of input.branches) {
|
|
269
|
+
if (branch.branchName === input.primaryBranchName) continue;
|
|
270
|
+
git(primaryPath, ["branch", "-f", branch.branchName, seeds.get(branch.branchName)], `anchor project branch ${branch.branchName}`);
|
|
271
|
+
const checkoutPath = branchPath(input.projectRoot, branch.branchName);
|
|
272
|
+
import_node_fs.default.mkdirSync(import_node_path.default.dirname(checkoutPath), { recursive: true });
|
|
273
|
+
git(primaryPath, ["worktree", "add", "--force", checkoutPath, branch.branchName], `create linked worktree ${branch.branchName}`);
|
|
274
|
+
}
|
|
275
|
+
return mirrorHeads;
|
|
276
|
+
}
|
|
277
|
+
function reconcileLinkedLayout(input) {
|
|
278
|
+
const primaryPath = branchPath(input.projectRoot, input.primaryBranchName);
|
|
279
|
+
configureRepository({ ...input, primaryPath });
|
|
280
|
+
tryGit(primaryPath, ["worktree", "prune"]);
|
|
281
|
+
fetchProjectHeads({ ...input, primaryPath });
|
|
282
|
+
const mirrorHeads = /* @__PURE__ */ new Map();
|
|
283
|
+
for (const branch of input.branches) {
|
|
284
|
+
const checkoutPath = branchPath(input.projectRoot, branch.branchName);
|
|
285
|
+
const resolved = seedForBranch({ ...input, primaryPath, branch });
|
|
286
|
+
mirrorHeads.set(branch.branchName, resolved.mirrorHead);
|
|
287
|
+
if (branch.branchName === input.primaryBranchName || commonGitDirectory(checkoutPath) === commonGitDirectory(primaryPath)) {
|
|
288
|
+
preserveHead(checkoutPath);
|
|
289
|
+
git(checkoutPath, ["reset", "--hard", resolved.seed], `anchor linked worktree ${branch.branchName}`);
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
import_node_fs.default.rmSync(checkoutPath, { recursive: true, force: true });
|
|
293
|
+
git(primaryPath, ["branch", "-f", branch.branchName, resolved.seed], `anchor project branch ${branch.branchName}`);
|
|
294
|
+
import_node_fs.default.mkdirSync(import_node_path.default.dirname(checkoutPath), { recursive: true });
|
|
295
|
+
git(primaryPath, ["worktree", "add", "--force", checkoutPath, branch.branchName], `create linked worktree ${branch.branchName}`);
|
|
296
|
+
}
|
|
297
|
+
return mirrorHeads;
|
|
298
|
+
}
|
|
299
|
+
function aheadBehind(checkoutPath, branchName) {
|
|
300
|
+
const originRef = `refs/remotes/origin/${branchName}`;
|
|
301
|
+
if (!tryGit(checkoutPath, ["show-ref", "--verify", "--quiet", originRef])) return { ahead: null, behind: null };
|
|
302
|
+
const output = git(checkoutPath, ["rev-list", "--left-right", "--count", `${originRef}...HEAD`], "measure project origin divergence");
|
|
303
|
+
const [behind, ahead] = output.split(/\s+/).map(Number);
|
|
304
|
+
return Number.isFinite(ahead) && Number.isFinite(behind) ? { ahead, behind } : { ahead: null, behind: null };
|
|
305
|
+
}
|
|
306
|
+
function ensureProjectWorktrees(input) {
|
|
307
|
+
const projectRoot = import_node_path.default.resolve(input.projectRoot);
|
|
308
|
+
assertNonOverlappingBranches(input.branches);
|
|
309
|
+
if (!input.branches.some(({ branchName }) => branchName === input.primaryBranchName)) {
|
|
310
|
+
throw new Error(`Primary project branch ${input.primaryBranchName} is missing from the workspace configuration`);
|
|
311
|
+
}
|
|
312
|
+
const snapshot = snapshotBranchTrees(projectRoot, input.branches);
|
|
313
|
+
try {
|
|
314
|
+
import_node_fs.default.mkdirSync(projectRoot, { recursive: true });
|
|
315
|
+
const linked = hasCompatibleLinkedProjectWorktreeLayout({
|
|
316
|
+
projectRoot,
|
|
317
|
+
primaryBranchName: input.primaryBranchName,
|
|
318
|
+
branches: input.branches
|
|
319
|
+
});
|
|
320
|
+
const mirrorHeads = linked ? reconcileLinkedLayout({ ...input, projectRoot }) : initializeLinkedLayout({ ...input, projectRoot });
|
|
321
|
+
for (const branch of input.branches) {
|
|
322
|
+
const snapshotPath = snapshot.paths.get(branch.branchName);
|
|
323
|
+
if (!snapshotPath) continue;
|
|
324
|
+
(0, import_working_tree_mirror.mirrorWorkingTree)({
|
|
325
|
+
sourceRoot: snapshotPath,
|
|
326
|
+
targetRoot: branchPath(projectRoot, branch.branchName),
|
|
327
|
+
sourceMode: "all",
|
|
328
|
+
deletionMode: "git"
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
return input.branches.map((branch) => {
|
|
332
|
+
const checkoutPath = branchPath(projectRoot, branch.branchName);
|
|
333
|
+
const divergence = aheadBehind(checkoutPath, branch.branchName);
|
|
334
|
+
return {
|
|
335
|
+
branchName: branch.branchName,
|
|
336
|
+
branchPath: checkoutPath,
|
|
337
|
+
head: git(checkoutPath, ["rev-parse", "HEAD"], `resolve project head ${branch.branchName}`),
|
|
338
|
+
mirrorHead: mirrorHeads.get(branch.branchName) ?? null,
|
|
339
|
+
...divergence
|
|
340
|
+
};
|
|
341
|
+
}).sort((left, right) => left.branchName.localeCompare(right.branchName));
|
|
342
|
+
} finally {
|
|
343
|
+
import_node_fs.default.rmSync(snapshot.root, { recursive: true, force: true });
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
function projectWorktreeOperationInProgress(checkoutPath) {
|
|
347
|
+
const gitPaths = ["MERGE_HEAD", "CHERRY_PICK_HEAD", "REVERT_HEAD", "rebase-merge", "rebase-apply", "sequencer"];
|
|
348
|
+
return gitPaths.some((gitPath) => {
|
|
349
|
+
const result = gitResult(checkoutPath, ["rev-parse", "--git-path", gitPath]);
|
|
350
|
+
if (result.exitCode !== 0) return false;
|
|
351
|
+
const resolved = import_node_path.default.isAbsolute(result.stdout) ? result.stdout : import_node_path.default.resolve(checkoutPath, result.stdout);
|
|
352
|
+
return import_node_fs.default.existsSync(resolved);
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
function createLinkedProjectBranch(input) {
|
|
356
|
+
(0, import_managed_paths.validateManagedBranchName)(input.sourceBranchName);
|
|
357
|
+
(0, import_managed_paths.validateManagedBranchName)(input.branchName);
|
|
358
|
+
if (input.sourceBranchName === input.branchName) throw new Error("Source and target branch names must differ");
|
|
359
|
+
const sourcePath = branchPath(input.projectRoot, input.sourceBranchName);
|
|
360
|
+
const targetPath = branchPath(input.projectRoot, input.branchName);
|
|
361
|
+
if (!commonGitDirectory(sourcePath)) throw new Error(`Source worktree ${input.sourceBranchName} is unavailable`);
|
|
362
|
+
if (projectWorktreeOperationInProgress(sourcePath)) {
|
|
363
|
+
throw new Error(`Source worktree ${input.sourceBranchName} has an in-progress Git operation`);
|
|
364
|
+
}
|
|
365
|
+
if (import_node_fs.default.existsSync(targetPath) || tryGit(sourcePath, ["show-ref", "--verify", "--quiet", `refs/heads/${input.branchName}`])) {
|
|
366
|
+
throw new Error(`Project branch ${input.branchName} already exists`);
|
|
367
|
+
}
|
|
368
|
+
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))] : []);
|
|
369
|
+
const overlappingWorktree = registeredWorktrees.find((worktreePath) => {
|
|
370
|
+
const targetRelative = import_node_path.default.relative(worktreePath, targetPath);
|
|
371
|
+
const worktreeRelative = import_node_path.default.relative(targetPath, worktreePath);
|
|
372
|
+
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);
|
|
373
|
+
});
|
|
374
|
+
if (overlappingWorktree) throw new Error(`Project branch folder overlaps linked worktree ${overlappingWorktree}`);
|
|
375
|
+
const sourceHead = git(sourcePath, ["rev-parse", "HEAD"], "resolve source branch head");
|
|
376
|
+
const snapshotRoot = import_node_fs.default.mkdtempSync(import_node_path.default.join(import_node_os.default.tmpdir(), "r5d-project-branch-"));
|
|
377
|
+
try {
|
|
378
|
+
(0, import_working_tree_mirror.mirrorWorkingTree)({ sourceRoot: sourcePath, targetRoot: snapshotRoot, sourceMode: "git", deletionMode: "all" });
|
|
379
|
+
git(sourcePath, ["branch", input.branchName, sourceHead], `create project branch ${input.branchName}`);
|
|
380
|
+
import_node_fs.default.mkdirSync(import_node_path.default.dirname(targetPath), { recursive: true });
|
|
381
|
+
try {
|
|
382
|
+
git(sourcePath, ["worktree", "add", "--force", targetPath, input.branchName], `create linked worktree ${input.branchName}`);
|
|
383
|
+
(0, import_working_tree_mirror.mirrorWorkingTree)({ sourceRoot: snapshotRoot, targetRoot: targetPath, sourceMode: "all", deletionMode: "git" });
|
|
384
|
+
} catch (error) {
|
|
385
|
+
import_node_fs.default.rmSync(targetPath, { recursive: true, force: true });
|
|
386
|
+
tryGit(sourcePath, ["worktree", "prune"]);
|
|
387
|
+
tryGit(sourcePath, ["branch", "-D", input.branchName]);
|
|
388
|
+
throw error;
|
|
389
|
+
}
|
|
390
|
+
return { branchPath: targetPath, baseCommitHash: sourceHead };
|
|
391
|
+
} finally {
|
|
392
|
+
import_node_fs.default.rmSync(snapshotRoot, { recursive: true, force: true });
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
function deleteLinkedProjectBranch(input) {
|
|
396
|
+
(0, import_managed_paths.validateManagedBranchName)(input.primaryBranchName);
|
|
397
|
+
(0, import_managed_paths.validateManagedBranchName)(input.branchName);
|
|
398
|
+
if (input.branchName === input.primaryBranchName) throw new Error(`Cannot delete the primary project branch ${input.primaryBranchName}`);
|
|
399
|
+
const primaryPath = branchPath(input.projectRoot, input.primaryBranchName);
|
|
400
|
+
const checkoutPath = branchPath(input.projectRoot, input.branchName);
|
|
401
|
+
const primaryCommonDir = commonGitDirectory(primaryPath);
|
|
402
|
+
if (!primaryCommonDir) throw new Error("Primary project checkout is unavailable");
|
|
403
|
+
if (import_node_fs.default.existsSync(checkoutPath)) {
|
|
404
|
+
const checkoutCommonDir = commonGitDirectory(checkoutPath);
|
|
405
|
+
if (checkoutCommonDir !== primaryCommonDir) {
|
|
406
|
+
throw new Error(`Project branch path ${input.branchName} is not a linked worktree of the configured project`);
|
|
407
|
+
}
|
|
408
|
+
if (projectWorktreeOperationInProgress(checkoutPath)) {
|
|
409
|
+
throw new Error(`Project branch ${input.branchName} has an in-progress Git operation`);
|
|
410
|
+
}
|
|
411
|
+
git(primaryPath, ["worktree", "remove", "--force", checkoutPath], `remove linked worktree ${input.branchName}`);
|
|
412
|
+
} else {
|
|
413
|
+
tryGit(primaryPath, ["worktree", "prune"]);
|
|
414
|
+
}
|
|
415
|
+
const branchRef = `refs/heads/${input.branchName}`;
|
|
416
|
+
if (tryGit(primaryPath, ["show-ref", "--verify", "--quiet", branchRef])) {
|
|
417
|
+
git(primaryPath, ["branch", "-D", input.branchName], `delete project branch ${input.branchName}`);
|
|
418
|
+
}
|
|
419
|
+
return { branchName: input.branchName };
|
|
420
|
+
}
|
|
421
|
+
function removeProjectWorktrees(input) {
|
|
422
|
+
const projectRoot = import_node_path.default.resolve(input.projectRoot);
|
|
423
|
+
if (input.branchNames.length === 0) throw new Error("Cannot remove a project with no configured branches");
|
|
424
|
+
for (const branchName of input.branchNames) {
|
|
425
|
+
(0, import_managed_paths.validateManagedBranchName)(branchName);
|
|
426
|
+
const checkoutPath = branchPath(projectRoot, branchName);
|
|
427
|
+
if (import_node_fs.default.existsSync(checkoutPath) && projectWorktreeOperationInProgress(checkoutPath)) {
|
|
428
|
+
throw new Error(`Project branch ${branchName} has an in-progress Git operation`);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
import_node_fs.default.rmSync(projectRoot, { recursive: true, force: true });
|
|
432
|
+
}
|
|
433
|
+
function deleteProjectMirrorBranch(input) {
|
|
434
|
+
(0, import_managed_paths.validateManagedBranchName)(input.branchName);
|
|
435
|
+
const deleted = gitResult(
|
|
436
|
+
input.gitDirectory,
|
|
437
|
+
["push", "--no-recurse-submodules", input.mirrorUrl, `:refs/heads/${input.branchName}`],
|
|
438
|
+
input.mirrorAuth
|
|
439
|
+
);
|
|
440
|
+
if (deleted.exitCode !== 0 && !/(remote ref does not exist|unable to delete|no such ref)/i.test(`${deleted.stderr}
|
|
441
|
+
${deleted.stdout}`)) {
|
|
442
|
+
throw new Error(
|
|
443
|
+
`Delete hidden project mirror for ${input.branchName}: ${deleted.stderr || deleted.stdout || `git exited ${deleted.exitCode}`}`
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
function pushProjectMirrorHeads(input) {
|
|
448
|
+
const results = [];
|
|
449
|
+
for (const branchName of [...input.branchNames].sort()) {
|
|
450
|
+
if (input.onlyBranches && !input.onlyBranches.has(branchName)) continue;
|
|
451
|
+
const checkoutPath = branchPath(input.projectRoot, branchName);
|
|
452
|
+
const head = git(checkoutPath, ["rev-parse", "HEAD"], `resolve mirror head for ${branchName}`);
|
|
453
|
+
if (projectWorktreeOperationInProgress(checkoutPath)) {
|
|
454
|
+
results.push({ branchName, head, pushed: false, reason: "git_operation_in_progress" });
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
git(
|
|
458
|
+
checkoutPath,
|
|
459
|
+
["push", "--no-recurse-submodules", input.mirrorUrl, `+HEAD:refs/heads/${branchName}`],
|
|
460
|
+
`push hidden project mirror for ${branchName}`,
|
|
461
|
+
input.mirrorAuth
|
|
462
|
+
);
|
|
463
|
+
results.push({ branchName, head, pushed: true });
|
|
464
|
+
}
|
|
465
|
+
return results;
|
|
466
|
+
}
|
|
467
|
+
function fastForwardProjectHeadsFromMirror(input) {
|
|
468
|
+
const primaryPath = branchPath(input.projectRoot, input.primaryBranchName);
|
|
469
|
+
const fetch = gitResult(
|
|
470
|
+
primaryPath,
|
|
471
|
+
["fetch", "--no-recurse-submodules", "--prune", input.mirrorUrl, "+refs/heads/*:refs/r5d/mirror/*"],
|
|
472
|
+
input.mirrorAuth
|
|
473
|
+
);
|
|
474
|
+
if (fetch.exitCode !== 0)
|
|
475
|
+
throw new Error(`Refresh project head mirror: ${fetch.stderr || fetch.stdout || `git exited ${fetch.exitCode}`}`);
|
|
476
|
+
return [...input.branchNames].sort().map((branchName) => {
|
|
477
|
+
const checkoutPath = branchPath(input.projectRoot, branchName);
|
|
478
|
+
const previousHead = git(checkoutPath, ["rev-parse", "HEAD"], `resolve local project head ${branchName}`);
|
|
479
|
+
const mirrorRef = `refs/r5d/mirror/${branchName}`;
|
|
480
|
+
const mirrorHead = tryGit(primaryPath, ["show-ref", "--verify", "--quiet", mirrorRef]) ? git(primaryPath, ["rev-parse", mirrorRef], `resolve refreshed mirror head ${branchName}`) : null;
|
|
481
|
+
if (!mirrorHead || mirrorHead === previousHead || projectWorktreeOperationInProgress(checkoutPath) || !tryGit(checkoutPath, ["merge-base", "--is-ancestor", previousHead, mirrorHead])) {
|
|
482
|
+
return { branchName, previousHead, mirrorHead, fastForwarded: false };
|
|
483
|
+
}
|
|
484
|
+
git(checkoutPath, ["update-ref", `refs/heads/${branchName}`, mirrorHead, previousHead], `fast-forward project branch ${branchName}`);
|
|
485
|
+
git(checkoutPath, ["reset", "--mixed", mirrorHead], `refresh project index ${branchName}`);
|
|
486
|
+
return { branchName, previousHead, mirrorHead, fastForwarded: true };
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
const projectWorktreesTestHarness = {
|
|
490
|
+
commandArgs: (args, auth) => ["git", ...NON_RECURSIVE_GIT_CONFIG, ...authArgs(auth), ...args]
|
|
491
|
+
};
|
|
492
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
493
|
+
0 && (module.exports = {
|
|
494
|
+
createLinkedProjectBranch,
|
|
495
|
+
deleteLinkedProjectBranch,
|
|
496
|
+
deleteProjectMirrorBranch,
|
|
497
|
+
ensureProjectWorktrees,
|
|
498
|
+
fastForwardProjectHeadsFromMirror,
|
|
499
|
+
hasLinkedProjectWorktreeLayout,
|
|
500
|
+
projectWorktreeConfigurationFingerprint,
|
|
501
|
+
projectWorktreeOperationInProgress,
|
|
502
|
+
projectWorktreesTestHarness,
|
|
503
|
+
pushProjectMirrorHeads,
|
|
504
|
+
removeProjectWorktrees
|
|
505
|
+
});
|