@ricsam/r5d-worker 0.0.74 → 0.0.76
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 +1031 -976
- package/dist/cjs/package.json +1 -1
- package/dist/cjs/project-workspace-state.cjs +777 -0
- package/dist/cjs/project-worktrees.cjs +559 -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 +1048 -987
- package/dist/mjs/package.json +1 -1
- package/dist/mjs/project-workspace-state.mjs +745 -0
- package/dist/mjs/project-worktrees.mjs +513 -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 +112 -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,524 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { mirrorWorkingTree } from "./working-tree-mirror.mjs";
|
|
4
|
+
const WORKSPACE_GIT_BRANCH = "main";
|
|
5
|
+
const MAX_WORKSPACE_GIT_DIFF_BYTES = 5 * 1024 * 1024;
|
|
6
|
+
const WORKSPACE_GIT_CONFIRMED_LARGE_DIFF_PUSH_OPTION = "r5d-confirm-large-diff-v1";
|
|
7
|
+
const WORKSPACE_GIT_INTEGRATED_REF = "refs/r5d/workspace-local/integrated";
|
|
8
|
+
const NON_RECURSIVE_GIT_CONFIG = [
|
|
9
|
+
"-c",
|
|
10
|
+
"submodule.recurse=false",
|
|
11
|
+
"-c",
|
|
12
|
+
"fetch.recurseSubmodules=false",
|
|
13
|
+
"-c",
|
|
14
|
+
"push.recurseSubmodules=false"
|
|
15
|
+
];
|
|
16
|
+
function normalizedHttpOrigin(value) {
|
|
17
|
+
try {
|
|
18
|
+
const url = new URL(value);
|
|
19
|
+
return `${url.protocol}//${url.host}`;
|
|
20
|
+
} catch {
|
|
21
|
+
return value.replace(/\/+$/, "");
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function authArgs(auth) {
|
|
25
|
+
if (!auth) return [];
|
|
26
|
+
const key = `http.${normalizedHttpOrigin(auth.extraHeaderUrl)}/.extraHeader`;
|
|
27
|
+
return ["-c", "http.extraHeader=", "-c", `${key}=`, "-c", `${key}=${auth.header}`];
|
|
28
|
+
}
|
|
29
|
+
function gitResult(cwd, args, auth) {
|
|
30
|
+
const result = Bun.spawnSync(["git", ...NON_RECURSIVE_GIT_CONFIG, ...authArgs(auth), ...args], {
|
|
31
|
+
cwd,
|
|
32
|
+
stdout: "pipe",
|
|
33
|
+
stderr: "pipe",
|
|
34
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: "0" }
|
|
35
|
+
});
|
|
36
|
+
return {
|
|
37
|
+
exitCode: result.exitCode,
|
|
38
|
+
stdout: result.stdout.toString().trim(),
|
|
39
|
+
stderr: result.stderr.toString().trim()
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function git(cwd, args, action, auth) {
|
|
43
|
+
const result = gitResult(cwd, args, auth);
|
|
44
|
+
if (result.exitCode !== 0) throw new Error(`${action}: ${result.stderr || result.stdout || `git exited ${result.exitCode}`}`);
|
|
45
|
+
return result.stdout;
|
|
46
|
+
}
|
|
47
|
+
function tryGit(cwd, args, auth) {
|
|
48
|
+
return gitResult(cwd, args, auth).exitCode === 0;
|
|
49
|
+
}
|
|
50
|
+
function revParse(workspacePath, revision) {
|
|
51
|
+
const result = gitResult(workspacePath, ["rev-parse", "--verify", revision]);
|
|
52
|
+
return result.exitCode === 0 ? result.stdout : null;
|
|
53
|
+
}
|
|
54
|
+
function updateIntegratedWorkspaceHead(workspacePath, head) {
|
|
55
|
+
git(workspacePath, ["update-ref", WORKSPACE_GIT_INTEGRATED_REF, head], "record integrated workspace head");
|
|
56
|
+
}
|
|
57
|
+
function workspaceIsShallow(workspacePath) {
|
|
58
|
+
return gitResult(workspacePath, ["rev-parse", "--is-shallow-repository"]).stdout === "true";
|
|
59
|
+
}
|
|
60
|
+
function workspaceIsClean(workspacePath) {
|
|
61
|
+
const status = gitResult(workspacePath, ["status", "--porcelain=v1", "-z"]);
|
|
62
|
+
return status.exitCode === 0 && status.stdout === "";
|
|
63
|
+
}
|
|
64
|
+
function validateMounts(workspacePath, mounts) {
|
|
65
|
+
const ids = /* @__PURE__ */ new Set();
|
|
66
|
+
const paths = [];
|
|
67
|
+
for (const mount of mounts) {
|
|
68
|
+
if (!mount.id || ids.has(mount.id)) throw new Error(`Workspace mount id must be unique: ${mount.id}`);
|
|
69
|
+
ids.add(mount.id);
|
|
70
|
+
const normalized = mount.workspaceRelativePath.replace(/\\/g, "/").replace(/^\/+|\/+$/g, "");
|
|
71
|
+
if (!normalized || normalized === "." || normalized.split("/").some((segment) => segment === ".." || segment === ".git")) {
|
|
72
|
+
throw new Error(`Invalid workspace mount path: ${mount.workspaceRelativePath}`);
|
|
73
|
+
}
|
|
74
|
+
const targetPath = path.resolve(workspacePath, ...normalized.split("/"));
|
|
75
|
+
const relative = path.relative(path.resolve(workspacePath), targetPath);
|
|
76
|
+
if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
77
|
+
throw new Error(`Workspace mount escapes the workspace clone: ${mount.workspaceRelativePath}`);
|
|
78
|
+
}
|
|
79
|
+
paths.push(normalized);
|
|
80
|
+
}
|
|
81
|
+
paths.sort();
|
|
82
|
+
for (let index = 0; index < paths.length; index += 1) {
|
|
83
|
+
const current = paths[index];
|
|
84
|
+
const overlap = paths.slice(index + 1).find((candidate) => candidate.startsWith(`${current}/`));
|
|
85
|
+
if (overlap) throw new Error(`Workspace mounts overlap: ${current} and ${overlap}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function configureWorkspaceRepository(input) {
|
|
89
|
+
if (tryGit(input.workspacePath, ["remote", "get-url", "origin"])) {
|
|
90
|
+
git(input.workspacePath, ["remote", "set-url", "origin", input.remoteUrl], "configure workspace origin");
|
|
91
|
+
} else {
|
|
92
|
+
git(input.workspacePath, ["remote", "add", "origin", input.remoteUrl], "configure workspace origin");
|
|
93
|
+
}
|
|
94
|
+
git(input.workspacePath, ["config", "--local", "--replace-all", "credential.helper", ""], "reset workspace credential helpers");
|
|
95
|
+
if (input.credentialHelper) {
|
|
96
|
+
git(input.workspacePath, ["config", "--local", "--add", "credential.helper", input.credentialHelper], "configure workspace credential helper");
|
|
97
|
+
}
|
|
98
|
+
const name = input.gitIdentity.name.trim();
|
|
99
|
+
const email = input.gitIdentity.email.trim();
|
|
100
|
+
if (!name || !email) throw new Error("Workspace Git identity must include name and email");
|
|
101
|
+
git(input.workspacePath, ["config", "--local", "user.name", name], "configure workspace Git user name");
|
|
102
|
+
git(input.workspacePath, ["config", "--local", "user.email", email], "configure workspace Git user email");
|
|
103
|
+
}
|
|
104
|
+
function fetchWorkspaceHead(workspacePath, remoteAuth) {
|
|
105
|
+
const result = gitResult(
|
|
106
|
+
workspacePath,
|
|
107
|
+
[
|
|
108
|
+
"fetch",
|
|
109
|
+
"--no-recurse-submodules",
|
|
110
|
+
"--prune",
|
|
111
|
+
"--update-shallow",
|
|
112
|
+
"origin",
|
|
113
|
+
`+refs/heads/${WORKSPACE_GIT_BRANCH}:refs/remotes/origin/${WORKSPACE_GIT_BRANCH}`
|
|
114
|
+
],
|
|
115
|
+
remoteAuth
|
|
116
|
+
);
|
|
117
|
+
if (result.exitCode !== 0) {
|
|
118
|
+
const detail = `${result.stderr}
|
|
119
|
+
${result.stdout}`;
|
|
120
|
+
if (!/(couldn't find remote ref|does not have any commits|remote repository is empty|no such ref)/i.test(detail)) {
|
|
121
|
+
throw new Error(`Fetch workspace main: ${result.stderr || result.stdout || `git exited ${result.exitCode}`}`);
|
|
122
|
+
}
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
return revParse(workspacePath, `refs/remotes/origin/${WORKSPACE_GIT_BRANCH}`);
|
|
126
|
+
}
|
|
127
|
+
function ensureWorkspaceGitClone(input) {
|
|
128
|
+
const workspacePath = path.resolve(input.workspacePath);
|
|
129
|
+
const gitPath = path.join(workspacePath, ".git");
|
|
130
|
+
if (!fs.existsSync(gitPath)) {
|
|
131
|
+
fs.rmSync(workspacePath, { recursive: true, force: true });
|
|
132
|
+
fs.mkdirSync(path.dirname(workspacePath), { recursive: true });
|
|
133
|
+
const clone = gitResult(
|
|
134
|
+
void 0,
|
|
135
|
+
["clone", "--no-recurse-submodules", "--branch", WORKSPACE_GIT_BRANCH, input.remoteUrl, workspacePath],
|
|
136
|
+
input.remoteAuth
|
|
137
|
+
);
|
|
138
|
+
if (clone.exitCode !== 0) {
|
|
139
|
+
fs.rmSync(workspacePath, { recursive: true, force: true });
|
|
140
|
+
fs.mkdirSync(workspacePath, { recursive: true });
|
|
141
|
+
git(workspacePath, ["init", `--initial-branch=${WORKSPACE_GIT_BRANCH}`], "initialize workspace clone");
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
configureWorkspaceRepository({ ...input, workspacePath });
|
|
145
|
+
const previousRemoteHead = revParse(workspacePath, `refs/remotes/origin/${WORKSPACE_GIT_BRANCH}`);
|
|
146
|
+
const localHeadBeforeFetch = revParse(workspacePath, "HEAD");
|
|
147
|
+
if (!revParse(workspacePath, WORKSPACE_GIT_INTEGRATED_REF) && previousRemoteHead && localHeadBeforeFetch && tryGit(workspacePath, ["merge-base", "--is-ancestor", previousRemoteHead, localHeadBeforeFetch])) {
|
|
148
|
+
updateIntegratedWorkspaceHead(workspacePath, previousRemoteHead);
|
|
149
|
+
}
|
|
150
|
+
const remoteHead = fetchWorkspaceHead(workspacePath, input.remoteAuth);
|
|
151
|
+
let localHead = revParse(workspacePath, "HEAD");
|
|
152
|
+
if (!localHead && remoteHead) {
|
|
153
|
+
git(
|
|
154
|
+
workspacePath,
|
|
155
|
+
["checkout", "--no-recurse-submodules", "-B", WORKSPACE_GIT_BRANCH, `refs/remotes/origin/${WORKSPACE_GIT_BRANCH}`],
|
|
156
|
+
"check out workspace main"
|
|
157
|
+
);
|
|
158
|
+
localHead = remoteHead;
|
|
159
|
+
}
|
|
160
|
+
if (localHead && remoteHead && localHead === remoteHead) {
|
|
161
|
+
updateIntegratedWorkspaceHead(workspacePath, remoteHead);
|
|
162
|
+
} else if (localHead && remoteHead && workspaceIsShallow(workspacePath) && !tryGit(workspacePath, ["merge-base", localHead, remoteHead]) && revParse(workspacePath, WORKSPACE_GIT_INTEGRATED_REF) === localHead && workspaceIsClean(workspacePath)) {
|
|
163
|
+
git(workspacePath, ["reset", "--hard", remoteHead], "advance workspace across retained history boundary");
|
|
164
|
+
updateIntegratedWorkspaceHead(workspacePath, remoteHead);
|
|
165
|
+
localHead = remoteHead;
|
|
166
|
+
}
|
|
167
|
+
return { localHead, remoteHead };
|
|
168
|
+
}
|
|
169
|
+
function activeMounts(mounts) {
|
|
170
|
+
const active = [];
|
|
171
|
+
const tombstones = [];
|
|
172
|
+
const skipped = [];
|
|
173
|
+
for (const mount of mounts) {
|
|
174
|
+
if (mount.busy?.()) skipped.push(mount);
|
|
175
|
+
else if (fs.existsSync(mount.sourcePath)) active.push(mount);
|
|
176
|
+
else if (mount.deleteWhenSourceMissing) tombstones.push(mount);
|
|
177
|
+
else skipped.push(mount);
|
|
178
|
+
}
|
|
179
|
+
return { active, tombstones, skipped };
|
|
180
|
+
}
|
|
181
|
+
function mirrorMountsToWorkspace(workspacePath, mounts) {
|
|
182
|
+
for (const mount of mounts) {
|
|
183
|
+
mirrorWorkingTree({
|
|
184
|
+
sourceRoot: mount.sourcePath,
|
|
185
|
+
targetRoot: path.join(workspacePath, ...mount.workspaceRelativePath.replace(/\\/g, "/").split("/")),
|
|
186
|
+
sourceMode: mount.sourceMode,
|
|
187
|
+
deletionMode: "all"
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
function removeWorkspaceMounts(workspacePath, mounts) {
|
|
192
|
+
for (const mount of mounts) {
|
|
193
|
+
const targetPath = path.join(workspacePath, ...mount.workspaceRelativePath.replace(/\\/g, "/").split("/"));
|
|
194
|
+
const relative = path.relative(path.resolve(workspacePath), path.resolve(targetPath));
|
|
195
|
+
if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
196
|
+
throw new Error(`Workspace mount escapes the workspace clone: ${mount.workspaceRelativePath}`);
|
|
197
|
+
}
|
|
198
|
+
fs.rmSync(targetPath, { recursive: true, force: true });
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
function hydrateWorkspaceGitMounts(workspacePath, mounts) {
|
|
202
|
+
for (const mount of mounts) {
|
|
203
|
+
const sourceRoot = path.join(workspacePath, ...mount.workspaceRelativePath.replace(/\\/g, "/").split("/"));
|
|
204
|
+
if (!fs.existsSync(sourceRoot)) {
|
|
205
|
+
if (mount.hydrateDeletionMode === "all") {
|
|
206
|
+
fs.rmSync(mount.sourcePath, { recursive: true, force: true });
|
|
207
|
+
} else if (fs.existsSync(mount.sourcePath)) {
|
|
208
|
+
const emptyRoot = fs.mkdtempSync(path.join(path.dirname(workspacePath), ".r5d-empty-mount-"));
|
|
209
|
+
try {
|
|
210
|
+
mirrorWorkingTree({
|
|
211
|
+
sourceRoot: emptyRoot,
|
|
212
|
+
targetRoot: mount.sourcePath,
|
|
213
|
+
sourceMode: "all",
|
|
214
|
+
deletionMode: "git"
|
|
215
|
+
});
|
|
216
|
+
} finally {
|
|
217
|
+
fs.rmSync(emptyRoot, { recursive: true, force: true });
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
mirrorWorkingTree({
|
|
223
|
+
sourceRoot,
|
|
224
|
+
targetRoot: mount.sourcePath,
|
|
225
|
+
sourceMode: "all",
|
|
226
|
+
deletionMode: mount.hydrateDeletionMode
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
function resetWorkspaceGit(input) {
|
|
231
|
+
const workspacePath = path.resolve(input.workspacePath);
|
|
232
|
+
validateMounts(workspacePath, input.mounts);
|
|
233
|
+
const initial = ensureWorkspaceGitClone({ ...input, workspacePath });
|
|
234
|
+
const status = gitResult(workspacePath, ["status", "--porcelain=v1", "-z"]);
|
|
235
|
+
const discardedPaths = status.exitCode === 0 ? status.stdout.split("\0").filter(Boolean).map((entry) => entry.slice(3)).sort() : [];
|
|
236
|
+
const remoteHead = fetchWorkspaceHead(workspacePath, input.remoteAuth);
|
|
237
|
+
if (remoteHead) {
|
|
238
|
+
git(workspacePath, ["checkout", "--no-recurse-submodules", "-B", WORKSPACE_GIT_BRANCH, remoteHead], "reset workspace main");
|
|
239
|
+
git(workspacePath, ["clean", "-fd", "--", "."], "remove untracked workspace changes");
|
|
240
|
+
} else if (revParse(workspacePath, "HEAD")) {
|
|
241
|
+
git(workspacePath, ["rm", "-rf", "--ignore-unmatch", "--", "."], "clear unborn workspace tree");
|
|
242
|
+
git(workspacePath, ["commit", "--allow-empty", "-m", JSON.stringify({ type: "workspace_reset" })], "record empty workspace reset");
|
|
243
|
+
}
|
|
244
|
+
const selected = activeMounts(input.mounts);
|
|
245
|
+
hydrateWorkspaceGitMounts(workspacePath, selected.active);
|
|
246
|
+
return {
|
|
247
|
+
startingHead: initial.localHead,
|
|
248
|
+
localHead: revParse(workspacePath, "HEAD"),
|
|
249
|
+
remoteHead,
|
|
250
|
+
discardedPaths
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
function stagedPaths(workspacePath) {
|
|
254
|
+
return git(workspacePath, ["diff", "--cached", "--name-only", "--no-renames", "-z"], "inspect staged workspace paths").split("\0").filter(Boolean).sort();
|
|
255
|
+
}
|
|
256
|
+
function emptyTreeHash(workspacePath) {
|
|
257
|
+
const result = Bun.spawnSync(["git", ...NON_RECURSIVE_GIT_CONFIG, "hash-object", "-t", "tree", "--stdin"], {
|
|
258
|
+
cwd: workspacePath,
|
|
259
|
+
stdin: Buffer.alloc(0),
|
|
260
|
+
stdout: "pipe",
|
|
261
|
+
stderr: "pipe",
|
|
262
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: "0" }
|
|
263
|
+
});
|
|
264
|
+
if (result.exitCode !== 0) throw new Error(`Create empty workspace tree: ${result.stderr.toString().trim() || `git exited ${result.exitCode}`}`);
|
|
265
|
+
return result.stdout.toString().trim();
|
|
266
|
+
}
|
|
267
|
+
function changedPaths(workspacePath, baseRevision, headRevision) {
|
|
268
|
+
const base = baseRevision ?? emptyTreeHash(workspacePath);
|
|
269
|
+
return git(workspacePath, ["diff", "--name-only", "--no-renames", "-z", base, headRevision], "inspect workspace commit paths").split("\0").filter(Boolean).sort();
|
|
270
|
+
}
|
|
271
|
+
async function diffSizeBytes(workspacePath, baseRevision, headRevision, limit) {
|
|
272
|
+
const base = baseRevision ?? emptyTreeHash(workspacePath);
|
|
273
|
+
const subprocess = Bun.spawn(
|
|
274
|
+
["git", ...NON_RECURSIVE_GIT_CONFIG, "diff", "--binary", "--no-ext-diff", base, headRevision],
|
|
275
|
+
{
|
|
276
|
+
cwd: workspacePath,
|
|
277
|
+
stdout: "pipe",
|
|
278
|
+
stderr: "pipe",
|
|
279
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: "0" }
|
|
280
|
+
}
|
|
281
|
+
);
|
|
282
|
+
const stderrPromise = new Response(subprocess.stderr).text();
|
|
283
|
+
const reader = subprocess.stdout.getReader();
|
|
284
|
+
let total = 0;
|
|
285
|
+
while (true) {
|
|
286
|
+
const chunk = await reader.read();
|
|
287
|
+
if (chunk.done) break;
|
|
288
|
+
total += chunk.value.byteLength;
|
|
289
|
+
if (total > limit) {
|
|
290
|
+
subprocess.kill();
|
|
291
|
+
await subprocess.exited;
|
|
292
|
+
await stderrPromise;
|
|
293
|
+
return limit + 1;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
const exitCode = await subprocess.exited;
|
|
297
|
+
const stderr = (await stderrPromise).trim();
|
|
298
|
+
if (exitCode !== 0) throw new Error(`Measure workspace diff: ${stderr || `git exited ${exitCode}`}`);
|
|
299
|
+
return total;
|
|
300
|
+
}
|
|
301
|
+
function conflictPaths(workspacePath) {
|
|
302
|
+
const result = gitResult(workspacePath, ["diff", "--name-only", "--diff-filter=U", "-z"]);
|
|
303
|
+
return result.exitCode === 0 ? result.stdout.split("\0").filter(Boolean).sort() : [];
|
|
304
|
+
}
|
|
305
|
+
function snapshotConflict(input) {
|
|
306
|
+
const suffix = input.attemptId.replace(/[^a-zA-Z0-9._-]/g, "-");
|
|
307
|
+
const refs = {
|
|
308
|
+
local: `refs/r5d/workspace-conflicts/${suffix}/local`,
|
|
309
|
+
remote: `refs/r5d/workspace-conflicts/${suffix}/remote`
|
|
310
|
+
};
|
|
311
|
+
git(input.workspacePath, ["update-ref", refs.local, input.localHead], "snapshot local workspace conflict head");
|
|
312
|
+
git(input.workspacePath, ["update-ref", refs.remote, input.remoteHead], "snapshot remote workspace conflict head");
|
|
313
|
+
return refs;
|
|
314
|
+
}
|
|
315
|
+
function rebaseWorkspace(input) {
|
|
316
|
+
const remoteRevision = `refs/remotes/origin/${WORKSPACE_GIT_BRANCH}`;
|
|
317
|
+
const rebase = gitResult(
|
|
318
|
+
input.workspacePath,
|
|
319
|
+
input.truncatedHistoryBase ? ["rebase", "--onto", remoteRevision, input.truncatedHistoryBase] : ["rebase", remoteRevision]
|
|
320
|
+
);
|
|
321
|
+
if (rebase.exitCode === 0) return { ok: true };
|
|
322
|
+
const conflicts = conflictPaths(input.workspacePath);
|
|
323
|
+
const refs = snapshotConflict(input);
|
|
324
|
+
const abort = gitResult(input.workspacePath, ["rebase", "--abort"]);
|
|
325
|
+
if (abort.exitCode !== 0) {
|
|
326
|
+
throw new Error(`Abort workspace rebase: ${abort.stderr || abort.stdout || `git exited ${abort.exitCode}`}`);
|
|
327
|
+
}
|
|
328
|
+
return {
|
|
329
|
+
ok: false,
|
|
330
|
+
conflictPaths: conflicts,
|
|
331
|
+
refs,
|
|
332
|
+
error: rebase.stderr || rebase.stdout || `Workspace rebase onto ${input.remoteHead} conflicted`
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
function synchronizeWithFetchedHead(input) {
|
|
336
|
+
const localHead = revParse(input.workspacePath, "HEAD");
|
|
337
|
+
if (!input.remoteHead) return { kind: "ready", updated: false, rebased: false };
|
|
338
|
+
if (!localHead) {
|
|
339
|
+
git(
|
|
340
|
+
input.workspacePath,
|
|
341
|
+
["checkout", "--no-recurse-submodules", "-B", WORKSPACE_GIT_BRANCH, `refs/remotes/origin/${WORKSPACE_GIT_BRANCH}`],
|
|
342
|
+
"check out fetched workspace main"
|
|
343
|
+
);
|
|
344
|
+
return { kind: "ready", updated: true, rebased: false };
|
|
345
|
+
}
|
|
346
|
+
if (localHead === input.remoteHead) {
|
|
347
|
+
updateIntegratedWorkspaceHead(input.workspacePath, input.remoteHead);
|
|
348
|
+
return { kind: "ready", updated: false, rebased: false };
|
|
349
|
+
}
|
|
350
|
+
if (tryGit(input.workspacePath, ["merge-base", "--is-ancestor", localHead, input.remoteHead])) {
|
|
351
|
+
git(input.workspacePath, ["reset", "--hard", input.remoteHead], "fast-forward workspace main");
|
|
352
|
+
updateIntegratedWorkspaceHead(input.workspacePath, input.remoteHead);
|
|
353
|
+
return { kind: "ready", updated: true, rebased: false };
|
|
354
|
+
}
|
|
355
|
+
if (tryGit(input.workspacePath, ["merge-base", "--is-ancestor", input.remoteHead, localHead])) {
|
|
356
|
+
updateIntegratedWorkspaceHead(input.workspacePath, input.remoteHead);
|
|
357
|
+
return { kind: "ready", updated: false, rebased: false };
|
|
358
|
+
}
|
|
359
|
+
const integratedHead = revParse(input.workspacePath, WORKSPACE_GIT_INTEGRATED_REF);
|
|
360
|
+
const truncatedHistoryBase = workspaceIsShallow(input.workspacePath) && integratedHead && tryGit(input.workspacePath, ["merge-base", "--is-ancestor", integratedHead, localHead]) ? integratedHead : void 0;
|
|
361
|
+
if (workspaceIsShallow(input.workspacePath) && !truncatedHistoryBase) {
|
|
362
|
+
throw new Error(
|
|
363
|
+
"Workspace history was truncated before this clone's integrated base could be identified; reset the workspace clone before retrying"
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
const rebased = rebaseWorkspace({
|
|
367
|
+
workspacePath: input.workspacePath,
|
|
368
|
+
localHead,
|
|
369
|
+
remoteHead: input.remoteHead,
|
|
370
|
+
attemptId: input.attemptId,
|
|
371
|
+
truncatedHistoryBase
|
|
372
|
+
});
|
|
373
|
+
if (rebased.ok) updateIntegratedWorkspaceHead(input.workspacePath, input.remoteHead);
|
|
374
|
+
return rebased.ok ? { kind: "ready", updated: false, rebased: true } : { kind: "conflict", conflictPaths: rebased.conflictPaths, refs: rebased.refs, error: rebased.error };
|
|
375
|
+
}
|
|
376
|
+
async function synchronizeWorkspaceGit(input) {
|
|
377
|
+
const workspacePath = path.resolve(input.workspacePath);
|
|
378
|
+
const attemptId = input.attemptId ?? crypto.randomUUID();
|
|
379
|
+
const maxDiffBytes = input.maxDiffBytes ?? MAX_WORKSPACE_GIT_DIFF_BYTES;
|
|
380
|
+
const maxPushAttempts = Math.max(1, input.maxPushAttempts ?? 4);
|
|
381
|
+
validateMounts(workspacePath, input.mounts);
|
|
382
|
+
const initial = ensureWorkspaceGitClone({ ...input, workspacePath });
|
|
383
|
+
const startingHead = initial.localHead;
|
|
384
|
+
const selected = activeMounts(input.mounts);
|
|
385
|
+
if (!input.skipMountMirror) {
|
|
386
|
+
mirrorMountsToWorkspace(workspacePath, selected.active);
|
|
387
|
+
removeWorkspaceMounts(workspacePath, selected.tombstones);
|
|
388
|
+
}
|
|
389
|
+
git(workspacePath, ["add", "-A", "--", "."], "stage workspace working trees");
|
|
390
|
+
const newlyStagedPaths = stagedPaths(workspacePath);
|
|
391
|
+
if (newlyStagedPaths.length > 0) {
|
|
392
|
+
git(
|
|
393
|
+
workspacePath,
|
|
394
|
+
[
|
|
395
|
+
"commit",
|
|
396
|
+
"-m",
|
|
397
|
+
JSON.stringify({ type: "workspace_sync", worker: input.workerLabel, attemptId, detail: input.commitDetail ?? null })
|
|
398
|
+
],
|
|
399
|
+
"commit workspace working trees"
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
let remoteHead = fetchWorkspaceHead(workspacePath, input.remoteAuth);
|
|
403
|
+
let rebaseCount = 0;
|
|
404
|
+
let updated = false;
|
|
405
|
+
for (let pushAttempt = 0; pushAttempt < maxPushAttempts; pushAttempt += 1) {
|
|
406
|
+
const reconciled = synchronizeWithFetchedHead({ workspacePath, remoteHead, attemptId });
|
|
407
|
+
if (reconciled.kind === "conflict") {
|
|
408
|
+
const localHead2 = revParse(workspacePath, "HEAD");
|
|
409
|
+
return {
|
|
410
|
+
outcome: "conflict_blocked",
|
|
411
|
+
startingHead,
|
|
412
|
+
localHead: localHead2,
|
|
413
|
+
remoteHead,
|
|
414
|
+
publishedHead: remoteHead,
|
|
415
|
+
rebaseCount: rebaseCount + 1,
|
|
416
|
+
diffSizeBytes: 0,
|
|
417
|
+
affectedPaths: [.../* @__PURE__ */ new Set([...newlyStagedPaths, ...reconciled.conflictPaths])].sort(),
|
|
418
|
+
activeMountIds: [...selected.active, ...selected.tombstones].map(({ id }) => id).sort(),
|
|
419
|
+
skippedMountIds: selected.skipped.map(({ id }) => id).sort(),
|
|
420
|
+
conflictPaths: reconciled.conflictPaths,
|
|
421
|
+
conflictSnapshotRefs: reconciled.refs,
|
|
422
|
+
error: reconciled.error
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
updated ||= reconciled.updated;
|
|
426
|
+
if (reconciled.rebased) rebaseCount += 1;
|
|
427
|
+
const localHead = revParse(workspacePath, "HEAD");
|
|
428
|
+
if (!localHead) {
|
|
429
|
+
return {
|
|
430
|
+
outcome: "no_change",
|
|
431
|
+
startingHead,
|
|
432
|
+
localHead: null,
|
|
433
|
+
remoteHead,
|
|
434
|
+
publishedHead: remoteHead,
|
|
435
|
+
rebaseCount,
|
|
436
|
+
diffSizeBytes: 0,
|
|
437
|
+
affectedPaths: [],
|
|
438
|
+
activeMountIds: [...selected.active, ...selected.tombstones].map(({ id }) => id).sort(),
|
|
439
|
+
skippedMountIds: selected.skipped.map(({ id }) => id).sort()
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
const paths = changedPaths(workspacePath, remoteHead, localHead);
|
|
443
|
+
const size = paths.length > 0 ? await diffSizeBytes(workspacePath, remoteHead, localHead, maxDiffBytes) : 0;
|
|
444
|
+
if (paths.length > 0 && size > maxDiffBytes && !input.allowLargeDiff) {
|
|
445
|
+
return {
|
|
446
|
+
outcome: "large_diff_blocked",
|
|
447
|
+
startingHead,
|
|
448
|
+
localHead,
|
|
449
|
+
remoteHead,
|
|
450
|
+
publishedHead: remoteHead,
|
|
451
|
+
rebaseCount,
|
|
452
|
+
diffSizeBytes: size,
|
|
453
|
+
affectedPaths: paths,
|
|
454
|
+
activeMountIds: [...selected.active, ...selected.tombstones].map(({ id }) => id).sort(),
|
|
455
|
+
skippedMountIds: selected.skipped.map(({ id }) => id).sort(),
|
|
456
|
+
error: `Workspace diff exceeds the ${maxDiffBytes}-byte automatic publication limit`
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
if (localHead === remoteHead) {
|
|
460
|
+
await input.afterWorkspacePublished?.({
|
|
461
|
+
publishedHead: localHead,
|
|
462
|
+
activeMountIds: [...selected.active, ...selected.tombstones].map(({ id }) => id).sort()
|
|
463
|
+
});
|
|
464
|
+
if (!input.skipMountHydration) hydrateWorkspaceGitMounts(workspacePath, selected.active);
|
|
465
|
+
return {
|
|
466
|
+
outcome: updated ? "updated" : "no_change",
|
|
467
|
+
startingHead,
|
|
468
|
+
localHead,
|
|
469
|
+
remoteHead,
|
|
470
|
+
publishedHead: localHead,
|
|
471
|
+
rebaseCount,
|
|
472
|
+
diffSizeBytes: size,
|
|
473
|
+
affectedPaths: paths,
|
|
474
|
+
activeMountIds: [...selected.active, ...selected.tombstones].map(({ id }) => id).sort(),
|
|
475
|
+
skippedMountIds: selected.skipped.map(({ id }) => id).sort()
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
const pushArgs = ["push", "--no-recurse-submodules"];
|
|
479
|
+
if (input.allowLargeDiff) pushArgs.push(`--push-option=${WORKSPACE_GIT_CONFIRMED_LARGE_DIFF_PUSH_OPTION}`);
|
|
480
|
+
pushArgs.push("origin", `HEAD:refs/heads/${WORKSPACE_GIT_BRANCH}`);
|
|
481
|
+
const push = gitResult(workspacePath, pushArgs, input.remoteAuth);
|
|
482
|
+
if (push.exitCode === 0) {
|
|
483
|
+
updateIntegratedWorkspaceHead(workspacePath, localHead);
|
|
484
|
+
await input.afterWorkspacePublished?.({
|
|
485
|
+
publishedHead: localHead,
|
|
486
|
+
activeMountIds: [...selected.active, ...selected.tombstones].map(({ id }) => id).sort()
|
|
487
|
+
});
|
|
488
|
+
if (!input.skipMountHydration) hydrateWorkspaceGitMounts(workspacePath, selected.active);
|
|
489
|
+
return {
|
|
490
|
+
outcome: "pushed",
|
|
491
|
+
startingHead,
|
|
492
|
+
localHead,
|
|
493
|
+
remoteHead,
|
|
494
|
+
publishedHead: localHead,
|
|
495
|
+
rebaseCount,
|
|
496
|
+
diffSizeBytes: size,
|
|
497
|
+
affectedPaths: paths,
|
|
498
|
+
activeMountIds: [...selected.active, ...selected.tombstones].map(({ id }) => id).sort(),
|
|
499
|
+
skippedMountIds: selected.skipped.map(({ id }) => id).sort()
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
if (!/(non-fast-forward|fetch first|rejected|failed to push some refs)/i.test(`${push.stderr}
|
|
503
|
+
${push.stdout}`)) {
|
|
504
|
+
throw new Error(`Push workspace main: ${push.stderr || push.stdout || `git exited ${push.exitCode}`}`);
|
|
505
|
+
}
|
|
506
|
+
remoteHead = fetchWorkspaceHead(workspacePath, input.remoteAuth);
|
|
507
|
+
}
|
|
508
|
+
throw new Error(`Workspace push did not converge after ${maxPushAttempts} attempts`);
|
|
509
|
+
}
|
|
510
|
+
const workspaceGitSyncTestHarness = {
|
|
511
|
+
commandArgs: (args, auth) => ["git", ...NON_RECURSIVE_GIT_CONFIG, ...authArgs(auth), ...args],
|
|
512
|
+
mirrorMountsToWorkspace,
|
|
513
|
+
hydrateMountsFromWorkspace: hydrateWorkspaceGitMounts
|
|
514
|
+
};
|
|
515
|
+
export {
|
|
516
|
+
MAX_WORKSPACE_GIT_DIFF_BYTES,
|
|
517
|
+
WORKSPACE_GIT_BRANCH,
|
|
518
|
+
WORKSPACE_GIT_CONFIRMED_LARGE_DIFF_PUSH_OPTION,
|
|
519
|
+
ensureWorkspaceGitClone,
|
|
520
|
+
hydrateWorkspaceGitMounts,
|
|
521
|
+
resetWorkspaceGit,
|
|
522
|
+
synchronizeWorkspaceGit,
|
|
523
|
+
workspaceGitSyncTestHarness
|
|
524
|
+
};
|
|
@@ -1,42 +1,8 @@
|
|
|
1
|
-
const DEFAULT_TERMINAL_INCIDENT_HISTORY_LIMIT = 32;
|
|
2
|
-
function isTerminalWorkspaceIncidentUpdate(update) {
|
|
3
|
-
return update.incidentId !== null && (update.status === "resolved" || update.status === "confirmed" || update.status === "reset");
|
|
4
|
-
}
|
|
5
|
-
class WorkspaceIncidentOrderingFence {
|
|
6
|
-
constructor(historyLimit = DEFAULT_TERMINAL_INCIDENT_HISTORY_LIMIT) {
|
|
7
|
-
this.historyLimit = historyLimit;
|
|
8
|
-
if (!Number.isSafeInteger(historyLimit) || historyLimit < 1) {
|
|
9
|
-
throw new Error("Workspace incident history limit must be a positive integer");
|
|
10
|
-
}
|
|
11
|
-
}
|
|
12
|
-
historyLimit;
|
|
13
|
-
terminalIncidentIds = /* @__PURE__ */ new Set();
|
|
14
|
-
terminalIncidentOrder = [];
|
|
15
|
-
observe(update) {
|
|
16
|
-
if (!isTerminalWorkspaceIncidentUpdate(update) || this.terminalIncidentIds.has(update.incidentId)) return;
|
|
17
|
-
this.terminalIncidentIds.add(update.incidentId);
|
|
18
|
-
this.terminalIncidentOrder.push(update.incidentId);
|
|
19
|
-
while (this.terminalIncidentOrder.length > this.historyLimit) {
|
|
20
|
-
const expired = this.terminalIncidentOrder.shift();
|
|
21
|
-
if (expired) this.terminalIncidentIds.delete(expired);
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
permitsBlockedResponse(incidentId) {
|
|
25
|
-
if (!incidentId) return false;
|
|
26
|
-
return !this.terminalIncidentIds.has(incidentId);
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
1
|
function applyWorkspaceIncidentUpdate(currentIncidentId, update) {
|
|
30
2
|
if (update.status === "remediating" || update.status === "waiting_for_worker") return update.incidentId;
|
|
31
3
|
if (currentIncidentId && update.incidentId && update.incidentId !== currentIncidentId) return currentIncidentId;
|
|
32
4
|
return null;
|
|
33
5
|
}
|
|
34
|
-
function releasePendingWorkspaceHead(previousIncidentId, currentIncidentId, terminalIncidentId) {
|
|
35
|
-
if (!previousIncidentId || currentIncidentId) return { fetchAuthoritativeHead: false };
|
|
36
|
-
return { fetchAuthoritativeHead: terminalIncidentId === previousIncidentId };
|
|
37
|
-
}
|
|
38
6
|
export {
|
|
39
|
-
|
|
40
|
-
applyWorkspaceIncidentUpdate,
|
|
41
|
-
releasePendingWorkspaceHead
|
|
7
|
+
applyWorkspaceIncidentUpdate
|
|
42
8
|
};
|