@ricsam/r5d-worker 0.0.75 → 0.0.77
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/dist/cjs/git-process-environment.cjs +133 -0
- package/dist/cjs/main.cjs +276 -100
- package/dist/cjs/package.json +1 -1
- package/dist/cjs/project-worktrees.cjs +135 -56
- package/dist/cjs/working-tree-mirror.cjs +2 -1
- package/dist/cjs/workspace-git-sync.cjs +67 -53
- package/dist/mjs/git-process-environment.mjs +105 -0
- package/dist/mjs/main.mjs +276 -100
- package/dist/mjs/package.json +1 -1
- package/dist/mjs/project-worktrees.mjs +133 -56
- package/dist/mjs/working-tree-mirror.mjs +2 -1
- package/dist/mjs/workspace-git-sync.mjs +67 -53
- package/dist/types/git-process-environment.d.ts +9 -0
- package/dist/types/main.d.ts +24 -0
- package/dist/types/project-worktrees.d.ts +33 -10
- package/dist/types/workspace-git-sync.d.ts +18 -5
- package/package.json +1 -1
|
@@ -2,8 +2,13 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
|
+
import { gitCredentialUsernameConfigKey, gitTransportSecurityArgs, workerGitProcessEnvironment } from "./git-process-environment.mjs";
|
|
5
6
|
import { validateManagedBranchName } from "./managed-paths.mjs";
|
|
6
7
|
import { mirrorWorkingTree } from "./working-tree-mirror.mjs";
|
|
8
|
+
const PROJECT_WORKTREE_SNAPSHOT_PREFIX = "r5d-project-worktrees-";
|
|
9
|
+
const PROJECT_WORKTREE_SNAPSHOT_NAME = new RegExp(
|
|
10
|
+
`^${PROJECT_WORKTREE_SNAPSHOT_PREFIX.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}([1-9]\\d*)-([A-Za-z0-9]{6})$`
|
|
11
|
+
);
|
|
7
12
|
function projectWorktreeConfigurationFingerprint(input) {
|
|
8
13
|
return createHash("sha256").update(
|
|
9
14
|
JSON.stringify({
|
|
@@ -29,25 +34,15 @@ const NON_RECURSIVE_GIT_CONFIG = [
|
|
|
29
34
|
"-c",
|
|
30
35
|
"push.recurseSubmodules=false"
|
|
31
36
|
];
|
|
32
|
-
function
|
|
33
|
-
|
|
34
|
-
const url = new URL(value);
|
|
35
|
-
return `${url.protocol}//${url.host}`;
|
|
36
|
-
} catch {
|
|
37
|
-
return value.replace(/\/+$/, "");
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
function authArgs(auth) {
|
|
41
|
-
if (!auth) return [];
|
|
42
|
-
const key = `http.${normalizedHttpOrigin(auth.extraHeaderUrl)}/.extraHeader`;
|
|
43
|
-
return ["-c", "http.extraHeader=", "-c", `${key}=`, "-c", `${key}=${auth.header}`];
|
|
37
|
+
function gitCommandArgs(args) {
|
|
38
|
+
return ["git", ...NON_RECURSIVE_GIT_CONFIG, ...args];
|
|
44
39
|
}
|
|
45
|
-
function gitResult(cwd, args
|
|
46
|
-
const result = Bun.spawnSync(
|
|
40
|
+
function gitResult(cwd, args) {
|
|
41
|
+
const result = Bun.spawnSync(gitCommandArgs(args), {
|
|
47
42
|
cwd,
|
|
48
43
|
stdout: "pipe",
|
|
49
44
|
stderr: "pipe",
|
|
50
|
-
env:
|
|
45
|
+
env: workerGitProcessEnvironment()
|
|
51
46
|
});
|
|
52
47
|
return {
|
|
53
48
|
exitCode: result.exitCode,
|
|
@@ -55,13 +50,13 @@ function gitResult(cwd, args, auth) {
|
|
|
55
50
|
stderr: result.stderr.toString().trim()
|
|
56
51
|
};
|
|
57
52
|
}
|
|
58
|
-
function git(cwd, args, action
|
|
59
|
-
const result = gitResult(cwd, args
|
|
53
|
+
function git(cwd, args, action) {
|
|
54
|
+
const result = gitResult(cwd, args);
|
|
60
55
|
if (result.exitCode !== 0) throw new Error(`${action}: ${result.stderr || result.stdout || `git exited ${result.exitCode}`}`);
|
|
61
56
|
return result.stdout;
|
|
62
57
|
}
|
|
63
|
-
function tryGit(cwd, args
|
|
64
|
-
return gitResult(cwd, args
|
|
58
|
+
function tryGit(cwd, args) {
|
|
59
|
+
return gitResult(cwd, args).exitCode === 0;
|
|
65
60
|
}
|
|
66
61
|
function branchPath(projectRoot, branchName) {
|
|
67
62
|
validateManagedBranchName(branchName);
|
|
@@ -124,17 +119,63 @@ function hasCompatibleLinkedProjectWorktreeLayout(input) {
|
|
|
124
119
|
return kind === null || kind === "file" && commonGitDirectory(checkoutPath) === primaryCommonDir;
|
|
125
120
|
});
|
|
126
121
|
}
|
|
122
|
+
function createProjectWorktreeSnapshotRoot() {
|
|
123
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), `${PROJECT_WORKTREE_SNAPSHOT_PREFIX}${process.pid}-`));
|
|
124
|
+
}
|
|
127
125
|
function snapshotBranchTrees(projectRoot, branches) {
|
|
128
|
-
const root =
|
|
126
|
+
const root = createProjectWorktreeSnapshotRoot();
|
|
129
127
|
const paths = /* @__PURE__ */ new Map();
|
|
130
|
-
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
128
|
+
try {
|
|
129
|
+
for (const { branchName } of branches) {
|
|
130
|
+
const checkoutPath = branchPath(projectRoot, branchName);
|
|
131
|
+
if (!fs.existsSync(checkoutPath)) continue;
|
|
132
|
+
const snapshotPath = path.join(root, ...branchName.split("/"));
|
|
133
|
+
mirrorWorkingTree({ sourceRoot: checkoutPath, targetRoot: snapshotPath, sourceMode: "all", deletionMode: "all" });
|
|
134
|
+
paths.set(branchName, snapshotPath);
|
|
135
|
+
}
|
|
136
|
+
return { root, paths };
|
|
137
|
+
} catch (error) {
|
|
138
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
139
|
+
throw error;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
function isProcessAlive(processId) {
|
|
143
|
+
try {
|
|
144
|
+
process.kill(processId, 0);
|
|
145
|
+
return true;
|
|
146
|
+
} catch (error) {
|
|
147
|
+
return error.code !== "ESRCH";
|
|
136
148
|
}
|
|
137
|
-
|
|
149
|
+
}
|
|
150
|
+
function cleanupStaleProjectWorktreeSnapshots(input = {}) {
|
|
151
|
+
const temporaryRoot = path.resolve(input.temporaryRoot ?? os.tmpdir());
|
|
152
|
+
const processAlive = input.processAlive ?? isProcessAlive;
|
|
153
|
+
const removed = [];
|
|
154
|
+
const failed = [];
|
|
155
|
+
let entries;
|
|
156
|
+
try {
|
|
157
|
+
entries = fs.readdirSync(temporaryRoot, { withFileTypes: true });
|
|
158
|
+
} catch (error) {
|
|
159
|
+
if (error.code === "ENOENT") return { removed, failed };
|
|
160
|
+
return { removed, failed: [{ path: temporaryRoot, error: error instanceof Error ? error.message : String(error) }] };
|
|
161
|
+
}
|
|
162
|
+
for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
|
|
163
|
+
const match = PROJECT_WORKTREE_SNAPSHOT_NAME.exec(entry.name);
|
|
164
|
+
if (!match) continue;
|
|
165
|
+
const ownerProcessId = Number(match[1]);
|
|
166
|
+
if (!Number.isSafeInteger(ownerProcessId) || processAlive(ownerProcessId)) continue;
|
|
167
|
+
const candidate = path.join(temporaryRoot, entry.name);
|
|
168
|
+
try {
|
|
169
|
+
const stat = fs.lstatSync(candidate);
|
|
170
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) continue;
|
|
171
|
+
fs.rmSync(candidate, { recursive: true, force: true });
|
|
172
|
+
removed.push(candidate);
|
|
173
|
+
} catch (error) {
|
|
174
|
+
if (error.code === "ENOENT") continue;
|
|
175
|
+
failed.push({ path: candidate, error: error instanceof Error ? error.message : String(error) });
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return { removed, failed };
|
|
138
179
|
}
|
|
139
180
|
function configureRepository(input) {
|
|
140
181
|
if (tryGit(input.primaryPath, ["remote", "get-url", "origin"])) {
|
|
@@ -142,7 +183,16 @@ function configureRepository(input) {
|
|
|
142
183
|
} else {
|
|
143
184
|
git(input.primaryPath, ["remote", "add", "origin", input.originUrl], "configure project origin");
|
|
144
185
|
}
|
|
186
|
+
tryGit(input.primaryPath, ["config", "--local", "--unset-all", "remote.origin.pushurl"]);
|
|
145
187
|
git(input.primaryPath, ["config", "--local", "--replace-all", "credential.helper", ""], "reset project credential helpers");
|
|
188
|
+
git(input.primaryPath, ["config", "--local", "credential.useHttpPath", "false"], "configure project credential path matching");
|
|
189
|
+
if (input.originCredentialUsername) {
|
|
190
|
+
git(
|
|
191
|
+
input.primaryPath,
|
|
192
|
+
["config", "--local", "--replace-all", gitCredentialUsernameConfigKey(input.originUrl), input.originCredentialUsername],
|
|
193
|
+
"configure project credential username"
|
|
194
|
+
);
|
|
195
|
+
}
|
|
146
196
|
if (input.credentialHelper) {
|
|
147
197
|
git(
|
|
148
198
|
input.primaryPath,
|
|
@@ -159,19 +209,25 @@ function configureRepository(input) {
|
|
|
159
209
|
}
|
|
160
210
|
}
|
|
161
211
|
function fetchProjectHeads(input) {
|
|
162
|
-
const originFetch = gitResult(
|
|
163
|
-
input.
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
212
|
+
const originFetch = gitResult(input.primaryPath, [
|
|
213
|
+
...gitTransportSecurityArgs(input.originUrl, input.credentialHelper, input.originCredentialUsername),
|
|
214
|
+
"fetch",
|
|
215
|
+
"--no-recurse-submodules",
|
|
216
|
+
"--prune",
|
|
217
|
+
"origin",
|
|
218
|
+
"+refs/heads/*:refs/remotes/origin/*"
|
|
219
|
+
]);
|
|
167
220
|
if (originFetch.exitCode !== 0) {
|
|
168
221
|
throw new Error(`Fetch project origin: ${originFetch.stderr || originFetch.stdout || `git exited ${originFetch.exitCode}`}`);
|
|
169
222
|
}
|
|
170
|
-
const mirrorFetch = gitResult(
|
|
171
|
-
input.
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
223
|
+
const mirrorFetch = gitResult(input.primaryPath, [
|
|
224
|
+
...gitTransportSecurityArgs(input.mirrorUrl, input.credentialHelper, input.mirrorCredentialUsername),
|
|
225
|
+
"fetch",
|
|
226
|
+
"--no-recurse-submodules",
|
|
227
|
+
"--prune",
|
|
228
|
+
input.mirrorUrl,
|
|
229
|
+
"+refs/heads/*:refs/r5d/mirror/*"
|
|
230
|
+
]);
|
|
175
231
|
if (mirrorFetch.exitCode !== 0 && !/(couldn't find remote ref|does not have any commits|remote repository is empty|no such ref)/i.test(
|
|
176
232
|
`${mirrorFetch.stderr}
|
|
177
233
|
${mirrorFetch.stdout}`
|
|
@@ -181,11 +237,19 @@ ${mirrorFetch.stdout}`
|
|
|
181
237
|
}
|
|
182
238
|
function ensureCommitAvailable(input) {
|
|
183
239
|
if (tryGit(input.primaryPath, ["cat-file", "-e", `${input.commitHash}^{commit}`])) return;
|
|
184
|
-
for (const [url,
|
|
185
|
-
[input.mirrorUrl, input.
|
|
186
|
-
[input.originUrl, input.
|
|
240
|
+
for (const [url, username] of [
|
|
241
|
+
[input.mirrorUrl, input.mirrorCredentialUsername],
|
|
242
|
+
[input.originUrl, input.originCredentialUsername]
|
|
187
243
|
]) {
|
|
188
|
-
if (tryGit(input.primaryPath, [
|
|
244
|
+
if (tryGit(input.primaryPath, [
|
|
245
|
+
...gitTransportSecurityArgs(url, input.credentialHelper, username),
|
|
246
|
+
"fetch",
|
|
247
|
+
"--no-recurse-submodules",
|
|
248
|
+
"--no-tags",
|
|
249
|
+
url,
|
|
250
|
+
input.commitHash
|
|
251
|
+
]))
|
|
252
|
+
return;
|
|
189
253
|
}
|
|
190
254
|
throw new Error(`Project base commit ${input.commitHash} is unavailable from origin and the canonical mirror`);
|
|
191
255
|
}
|
|
@@ -330,7 +394,7 @@ function createLinkedProjectBranch(input) {
|
|
|
330
394
|
});
|
|
331
395
|
if (overlappingWorktree) throw new Error(`Project branch folder overlaps linked worktree ${overlappingWorktree}`);
|
|
332
396
|
const sourceHead = git(sourcePath, ["rev-parse", "HEAD"], "resolve source branch head");
|
|
333
|
-
const snapshotRoot =
|
|
397
|
+
const snapshotRoot = createProjectWorktreeSnapshotRoot();
|
|
334
398
|
try {
|
|
335
399
|
mirrorWorkingTree({ sourceRoot: sourcePath, targetRoot: snapshotRoot, sourceMode: "git", deletionMode: "all" });
|
|
336
400
|
git(sourcePath, ["branch", input.branchName, sourceHead], `create project branch ${input.branchName}`);
|
|
@@ -389,11 +453,13 @@ function removeProjectWorktrees(input) {
|
|
|
389
453
|
}
|
|
390
454
|
function deleteProjectMirrorBranch(input) {
|
|
391
455
|
validateManagedBranchName(input.branchName);
|
|
392
|
-
const deleted = gitResult(
|
|
393
|
-
input.
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
456
|
+
const deleted = gitResult(input.gitDirectory, [
|
|
457
|
+
...gitTransportSecurityArgs(input.mirrorUrl, input.credentialHelper, input.credentialUsername),
|
|
458
|
+
"push",
|
|
459
|
+
"--no-recurse-submodules",
|
|
460
|
+
input.mirrorUrl,
|
|
461
|
+
`:refs/heads/${input.branchName}`
|
|
462
|
+
]);
|
|
397
463
|
if (deleted.exitCode !== 0 && !/(remote ref does not exist|unable to delete|no such ref)/i.test(`${deleted.stderr}
|
|
398
464
|
${deleted.stdout}`)) {
|
|
399
465
|
throw new Error(
|
|
@@ -413,9 +479,14 @@ function pushProjectMirrorHeads(input) {
|
|
|
413
479
|
}
|
|
414
480
|
git(
|
|
415
481
|
checkoutPath,
|
|
416
|
-
[
|
|
417
|
-
|
|
418
|
-
|
|
482
|
+
[
|
|
483
|
+
...gitTransportSecurityArgs(input.mirrorUrl, input.credentialHelper, input.credentialUsername),
|
|
484
|
+
"push",
|
|
485
|
+
"--no-recurse-submodules",
|
|
486
|
+
input.mirrorUrl,
|
|
487
|
+
`+HEAD:refs/heads/${branchName}`
|
|
488
|
+
],
|
|
489
|
+
`push hidden project mirror for ${branchName}`
|
|
419
490
|
);
|
|
420
491
|
results.push({ branchName, head, pushed: true });
|
|
421
492
|
}
|
|
@@ -423,11 +494,14 @@ function pushProjectMirrorHeads(input) {
|
|
|
423
494
|
}
|
|
424
495
|
function fastForwardProjectHeadsFromMirror(input) {
|
|
425
496
|
const primaryPath = branchPath(input.projectRoot, input.primaryBranchName);
|
|
426
|
-
const fetch = gitResult(
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
497
|
+
const fetch = gitResult(primaryPath, [
|
|
498
|
+
...gitTransportSecurityArgs(input.mirrorUrl, input.credentialHelper, input.credentialUsername),
|
|
499
|
+
"fetch",
|
|
500
|
+
"--no-recurse-submodules",
|
|
501
|
+
"--prune",
|
|
502
|
+
input.mirrorUrl,
|
|
503
|
+
"+refs/heads/*:refs/r5d/mirror/*"
|
|
504
|
+
]);
|
|
431
505
|
if (fetch.exitCode !== 0)
|
|
432
506
|
throw new Error(`Refresh project head mirror: ${fetch.stderr || fetch.stdout || `git exited ${fetch.exitCode}`}`);
|
|
433
507
|
return [...input.branchNames].sort().map((branchName) => {
|
|
@@ -444,9 +518,12 @@ function fastForwardProjectHeadsFromMirror(input) {
|
|
|
444
518
|
});
|
|
445
519
|
}
|
|
446
520
|
const projectWorktreesTestHarness = {
|
|
447
|
-
commandArgs:
|
|
521
|
+
commandArgs: gitCommandArgs,
|
|
522
|
+
configureRepository
|
|
448
523
|
};
|
|
449
524
|
export {
|
|
525
|
+
PROJECT_WORKTREE_SNAPSHOT_PREFIX,
|
|
526
|
+
cleanupStaleProjectWorktreeSnapshots,
|
|
450
527
|
createLinkedProjectBranch,
|
|
451
528
|
deleteLinkedProjectBranch,
|
|
452
529
|
deleteProjectMirrorBranch,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { workerGitProcessEnvironment } from "./git-process-environment.mjs";
|
|
3
4
|
function isGitMetadataPath(relativePath) {
|
|
4
5
|
return relativePath.split("/").includes(".git");
|
|
5
6
|
}
|
|
@@ -36,7 +37,7 @@ function gitEligibleRoots(root) {
|
|
|
36
37
|
cwd: root,
|
|
37
38
|
stdout: "pipe",
|
|
38
39
|
stderr: "pipe",
|
|
39
|
-
env:
|
|
40
|
+
env: workerGitProcessEnvironment()
|
|
40
41
|
}
|
|
41
42
|
);
|
|
42
43
|
if (result.exitCode !== 0) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { gitCredentialUsernameConfigKey, gitTransportSecurityArgs, workerGitProcessEnvironment } from "./git-process-environment.mjs";
|
|
3
4
|
import { mirrorWorkingTree } from "./working-tree-mirror.mjs";
|
|
4
5
|
const WORKSPACE_GIT_BRANCH = "main";
|
|
5
6
|
const MAX_WORKSPACE_GIT_DIFF_BYTES = 5 * 1024 * 1024;
|
|
@@ -13,25 +14,18 @@ const NON_RECURSIVE_GIT_CONFIG = [
|
|
|
13
14
|
"-c",
|
|
14
15
|
"push.recurseSubmodules=false"
|
|
15
16
|
];
|
|
16
|
-
function
|
|
17
|
-
|
|
18
|
-
const url = new URL(value);
|
|
19
|
-
return `${url.protocol}//${url.host}`;
|
|
20
|
-
} catch {
|
|
21
|
-
return value.replace(/\/+$/, "");
|
|
22
|
-
}
|
|
17
|
+
function gitCommandArgs(args) {
|
|
18
|
+
return ["git", ...NON_RECURSIVE_GIT_CONFIG, ...args];
|
|
23
19
|
}
|
|
24
|
-
function
|
|
25
|
-
|
|
26
|
-
const key = `http.${normalizedHttpOrigin(auth.extraHeaderUrl)}/.extraHeader`;
|
|
27
|
-
return ["-c", "http.extraHeader=", "-c", `${key}=`, "-c", `${key}=${auth.header}`];
|
|
20
|
+
function workspaceCloneCommandArgs(args, remoteUrl, credentialHelper, credentialUsername) {
|
|
21
|
+
return gitCommandArgs([...gitTransportSecurityArgs(remoteUrl, credentialHelper, credentialUsername), ...args]);
|
|
28
22
|
}
|
|
29
|
-
function gitResult(cwd, args
|
|
30
|
-
const result = Bun.spawnSync(
|
|
23
|
+
function gitResult(cwd, args) {
|
|
24
|
+
const result = Bun.spawnSync(gitCommandArgs(args), {
|
|
31
25
|
cwd,
|
|
32
26
|
stdout: "pipe",
|
|
33
27
|
stderr: "pipe",
|
|
34
|
-
env:
|
|
28
|
+
env: workerGitProcessEnvironment()
|
|
35
29
|
});
|
|
36
30
|
return {
|
|
37
31
|
exitCode: result.exitCode,
|
|
@@ -39,13 +33,13 @@ function gitResult(cwd, args, auth) {
|
|
|
39
33
|
stderr: result.stderr.toString().trim()
|
|
40
34
|
};
|
|
41
35
|
}
|
|
42
|
-
function git(cwd, args, action
|
|
43
|
-
const result = gitResult(cwd, args
|
|
36
|
+
function git(cwd, args, action) {
|
|
37
|
+
const result = gitResult(cwd, args);
|
|
44
38
|
if (result.exitCode !== 0) throw new Error(`${action}: ${result.stderr || result.stdout || `git exited ${result.exitCode}`}`);
|
|
45
39
|
return result.stdout;
|
|
46
40
|
}
|
|
47
|
-
function tryGit(cwd, args
|
|
48
|
-
return gitResult(cwd, args
|
|
41
|
+
function tryGit(cwd, args) {
|
|
42
|
+
return gitResult(cwd, args).exitCode === 0;
|
|
49
43
|
}
|
|
50
44
|
function revParse(workspacePath, revision) {
|
|
51
45
|
const result = gitResult(workspacePath, ["rev-parse", "--verify", revision]);
|
|
@@ -91,9 +85,22 @@ function configureWorkspaceRepository(input) {
|
|
|
91
85
|
} else {
|
|
92
86
|
git(input.workspacePath, ["remote", "add", "origin", input.remoteUrl], "configure workspace origin");
|
|
93
87
|
}
|
|
88
|
+
tryGit(input.workspacePath, ["config", "--local", "--unset-all", "remote.origin.pushurl"]);
|
|
94
89
|
git(input.workspacePath, ["config", "--local", "--replace-all", "credential.helper", ""], "reset workspace credential helpers");
|
|
90
|
+
git(input.workspacePath, ["config", "--local", "credential.useHttpPath", "false"], "configure workspace credential path matching");
|
|
91
|
+
if (input.credentialUsername) {
|
|
92
|
+
git(
|
|
93
|
+
input.workspacePath,
|
|
94
|
+
["config", "--local", "--replace-all", gitCredentialUsernameConfigKey(input.remoteUrl), input.credentialUsername],
|
|
95
|
+
"configure workspace credential username"
|
|
96
|
+
);
|
|
97
|
+
}
|
|
95
98
|
if (input.credentialHelper) {
|
|
96
|
-
git(
|
|
99
|
+
git(
|
|
100
|
+
input.workspacePath,
|
|
101
|
+
["config", "--local", "--add", "credential.helper", input.credentialHelper],
|
|
102
|
+
"configure workspace credential helper"
|
|
103
|
+
);
|
|
97
104
|
}
|
|
98
105
|
const name = input.gitIdentity.name.trim();
|
|
99
106
|
const email = input.gitIdentity.email.trim();
|
|
@@ -101,19 +108,16 @@ function configureWorkspaceRepository(input) {
|
|
|
101
108
|
git(input.workspacePath, ["config", "--local", "user.name", name], "configure workspace Git user name");
|
|
102
109
|
git(input.workspacePath, ["config", "--local", "user.email", email], "configure workspace Git user email");
|
|
103
110
|
}
|
|
104
|
-
function fetchWorkspaceHead(workspacePath,
|
|
105
|
-
const result = gitResult(
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
],
|
|
115
|
-
remoteAuth
|
|
116
|
-
);
|
|
111
|
+
function fetchWorkspaceHead(workspacePath, remoteUrl, credentialHelper, credentialUsername) {
|
|
112
|
+
const result = gitResult(workspacePath, [
|
|
113
|
+
...gitTransportSecurityArgs(remoteUrl, credentialHelper, credentialUsername),
|
|
114
|
+
"fetch",
|
|
115
|
+
"--no-recurse-submodules",
|
|
116
|
+
"--prune",
|
|
117
|
+
"--update-shallow",
|
|
118
|
+
"origin",
|
|
119
|
+
`+refs/heads/${WORKSPACE_GIT_BRANCH}:refs/remotes/origin/${WORKSPACE_GIT_BRANCH}`
|
|
120
|
+
]);
|
|
117
121
|
if (result.exitCode !== 0) {
|
|
118
122
|
const detail = `${result.stderr}
|
|
119
123
|
${result.stdout}`;
|
|
@@ -130,11 +134,17 @@ function ensureWorkspaceGitClone(input) {
|
|
|
130
134
|
if (!fs.existsSync(gitPath)) {
|
|
131
135
|
fs.rmSync(workspacePath, { recursive: true, force: true });
|
|
132
136
|
fs.mkdirSync(path.dirname(workspacePath), { recursive: true });
|
|
133
|
-
const
|
|
134
|
-
void 0,
|
|
137
|
+
const cloneCommand = workspaceCloneCommandArgs(
|
|
135
138
|
["clone", "--no-recurse-submodules", "--branch", WORKSPACE_GIT_BRANCH, input.remoteUrl, workspacePath],
|
|
136
|
-
input.
|
|
139
|
+
input.remoteUrl,
|
|
140
|
+
input.credentialHelper,
|
|
141
|
+
input.credentialUsername
|
|
137
142
|
);
|
|
143
|
+
const clone = Bun.spawnSync(cloneCommand, {
|
|
144
|
+
stdout: "pipe",
|
|
145
|
+
stderr: "pipe",
|
|
146
|
+
env: workerGitProcessEnvironment()
|
|
147
|
+
});
|
|
138
148
|
if (clone.exitCode !== 0) {
|
|
139
149
|
fs.rmSync(workspacePath, { recursive: true, force: true });
|
|
140
150
|
fs.mkdirSync(workspacePath, { recursive: true });
|
|
@@ -147,7 +157,7 @@ function ensureWorkspaceGitClone(input) {
|
|
|
147
157
|
if (!revParse(workspacePath, WORKSPACE_GIT_INTEGRATED_REF) && previousRemoteHead && localHeadBeforeFetch && tryGit(workspacePath, ["merge-base", "--is-ancestor", previousRemoteHead, localHeadBeforeFetch])) {
|
|
148
158
|
updateIntegratedWorkspaceHead(workspacePath, previousRemoteHead);
|
|
149
159
|
}
|
|
150
|
-
const remoteHead = fetchWorkspaceHead(workspacePath, input.
|
|
160
|
+
const remoteHead = fetchWorkspaceHead(workspacePath, input.remoteUrl, input.credentialHelper, input.credentialUsername);
|
|
151
161
|
let localHead = revParse(workspacePath, "HEAD");
|
|
152
162
|
if (!localHead && remoteHead) {
|
|
153
163
|
git(
|
|
@@ -233,7 +243,7 @@ function resetWorkspaceGit(input) {
|
|
|
233
243
|
const initial = ensureWorkspaceGitClone({ ...input, workspacePath });
|
|
234
244
|
const status = gitResult(workspacePath, ["status", "--porcelain=v1", "-z"]);
|
|
235
245
|
const discardedPaths = status.exitCode === 0 ? status.stdout.split("\0").filter(Boolean).map((entry) => entry.slice(3)).sort() : [];
|
|
236
|
-
const remoteHead = fetchWorkspaceHead(workspacePath, input.
|
|
246
|
+
const remoteHead = fetchWorkspaceHead(workspacePath, input.remoteUrl, input.credentialHelper, input.credentialUsername);
|
|
237
247
|
if (remoteHead) {
|
|
238
248
|
git(workspacePath, ["checkout", "--no-recurse-submodules", "-B", WORKSPACE_GIT_BRANCH, remoteHead], "reset workspace main");
|
|
239
249
|
git(workspacePath, ["clean", "-fd", "--", "."], "remove untracked workspace changes");
|
|
@@ -259,9 +269,10 @@ function emptyTreeHash(workspacePath) {
|
|
|
259
269
|
stdin: Buffer.alloc(0),
|
|
260
270
|
stdout: "pipe",
|
|
261
271
|
stderr: "pipe",
|
|
262
|
-
env:
|
|
272
|
+
env: workerGitProcessEnvironment()
|
|
263
273
|
});
|
|
264
|
-
if (result.exitCode !== 0)
|
|
274
|
+
if (result.exitCode !== 0)
|
|
275
|
+
throw new Error(`Create empty workspace tree: ${result.stderr.toString().trim() || `git exited ${result.exitCode}`}`);
|
|
265
276
|
return result.stdout.toString().trim();
|
|
266
277
|
}
|
|
267
278
|
function changedPaths(workspacePath, baseRevision, headRevision) {
|
|
@@ -270,15 +281,12 @@ function changedPaths(workspacePath, baseRevision, headRevision) {
|
|
|
270
281
|
}
|
|
271
282
|
async function diffSizeBytes(workspacePath, baseRevision, headRevision, limit) {
|
|
272
283
|
const base = baseRevision ?? emptyTreeHash(workspacePath);
|
|
273
|
-
const subprocess = Bun.spawn(
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
env: { ...process.env, GIT_TERMINAL_PROMPT: "0" }
|
|
280
|
-
}
|
|
281
|
-
);
|
|
284
|
+
const subprocess = Bun.spawn(["git", ...NON_RECURSIVE_GIT_CONFIG, "diff", "--binary", "--no-ext-diff", base, headRevision], {
|
|
285
|
+
cwd: workspacePath,
|
|
286
|
+
stdout: "pipe",
|
|
287
|
+
stderr: "pipe",
|
|
288
|
+
env: workerGitProcessEnvironment()
|
|
289
|
+
});
|
|
282
290
|
const stderrPromise = new Response(subprocess.stderr).text();
|
|
283
291
|
const reader = subprocess.stdout.getReader();
|
|
284
292
|
let total = 0;
|
|
@@ -399,7 +407,7 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
399
407
|
"commit workspace working trees"
|
|
400
408
|
);
|
|
401
409
|
}
|
|
402
|
-
let remoteHead = fetchWorkspaceHead(workspacePath, input.
|
|
410
|
+
let remoteHead = fetchWorkspaceHead(workspacePath, input.remoteUrl, input.credentialHelper, input.credentialUsername);
|
|
403
411
|
let rebaseCount = 0;
|
|
404
412
|
let updated = false;
|
|
405
413
|
for (let pushAttempt = 0; pushAttempt < maxPushAttempts; pushAttempt += 1) {
|
|
@@ -475,10 +483,14 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
475
483
|
skippedMountIds: selected.skipped.map(({ id }) => id).sort()
|
|
476
484
|
};
|
|
477
485
|
}
|
|
478
|
-
const pushArgs = [
|
|
486
|
+
const pushArgs = [
|
|
487
|
+
...gitTransportSecurityArgs(input.remoteUrl, input.credentialHelper, input.credentialUsername),
|
|
488
|
+
"push",
|
|
489
|
+
"--no-recurse-submodules"
|
|
490
|
+
];
|
|
479
491
|
if (input.allowLargeDiff) pushArgs.push(`--push-option=${WORKSPACE_GIT_CONFIRMED_LARGE_DIFF_PUSH_OPTION}`);
|
|
480
492
|
pushArgs.push("origin", `HEAD:refs/heads/${WORKSPACE_GIT_BRANCH}`);
|
|
481
|
-
const push = gitResult(workspacePath, pushArgs
|
|
493
|
+
const push = gitResult(workspacePath, pushArgs);
|
|
482
494
|
if (push.exitCode === 0) {
|
|
483
495
|
updateIntegratedWorkspaceHead(workspacePath, localHead);
|
|
484
496
|
await input.afterWorkspacePublished?.({
|
|
@@ -503,12 +515,14 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
503
515
|
${push.stdout}`)) {
|
|
504
516
|
throw new Error(`Push workspace main: ${push.stderr || push.stdout || `git exited ${push.exitCode}`}`);
|
|
505
517
|
}
|
|
506
|
-
remoteHead = fetchWorkspaceHead(workspacePath, input.
|
|
518
|
+
remoteHead = fetchWorkspaceHead(workspacePath, input.remoteUrl, input.credentialHelper, input.credentialUsername);
|
|
507
519
|
}
|
|
508
520
|
throw new Error(`Workspace push did not converge after ${maxPushAttempts} attempts`);
|
|
509
521
|
}
|
|
510
522
|
const workspaceGitSyncTestHarness = {
|
|
511
|
-
commandArgs:
|
|
523
|
+
commandArgs: gitCommandArgs,
|
|
524
|
+
workspaceCloneCommandArgs,
|
|
525
|
+
configureWorkspaceRepository,
|
|
512
526
|
mirrorMountsToWorkspace,
|
|
513
527
|
hydrateMountsFromWorkspace: hydrateWorkspaceGitMounts
|
|
514
528
|
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Build the complete environment for a worker-owned Git child process. */
|
|
2
|
+
export declare function workerGitProcessEnvironment(source?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
|
|
3
|
+
/** Clear inherited and repository-local HTTP authorization headers for one transport URL. */
|
|
4
|
+
export declare function gitHttpAuthorizationClearArgs(remoteUrl: string): string[];
|
|
5
|
+
/** Select only the app-owned credential store, regardless of repository-local helpers. */
|
|
6
|
+
export declare function gitCredentialHelperConfigArgs(credentialHelper?: string | null): string[];
|
|
7
|
+
export declare function gitCredentialUsernameConfigKey(remoteUrl: string): string;
|
|
8
|
+
/** Non-secret command configuration required for an authenticated HTTP transport. */
|
|
9
|
+
export declare function gitTransportSecurityArgs(remoteUrl: string, credentialHelper?: string | null, credentialUsername?: string | null): string[];
|
package/dist/types/main.d.ts
CHANGED
|
@@ -130,6 +130,30 @@ type ResolvedWorkerFilePath = {
|
|
|
130
130
|
virtualRootPath: string;
|
|
131
131
|
};
|
|
132
132
|
export declare function resolveWorkerFilePath(branchPath: string, inputPath: string, builtInPaths?: WorkerBuiltInToolPaths): ResolvedWorkerFilePath;
|
|
133
|
+
declare function readOnlyCredentialStoreHelper(storePath: string): string;
|
|
134
|
+
declare function legacySharedCredentialStorePath(): string;
|
|
135
|
+
declare function credentialStorePathForProject(projectId: string): string;
|
|
136
|
+
declare function credentialStorePathForWorkspace(remoteUrl: string): string;
|
|
137
|
+
declare function removeLegacySharedCredentialStore(storePath?: string): void;
|
|
138
|
+
declare function resetCredentialStoreDirectory(directoryPath?: string): void;
|
|
139
|
+
declare function pruneCredentialStoreFiles(requiredStorePaths: ReadonlySet<string>, directoryPath?: string): string[];
|
|
140
|
+
declare function replaceCredentialStoreFile(storePath: string, content: string): void;
|
|
141
|
+
declare function credentialHelperForRemotes(remotes: readonly {
|
|
142
|
+
remoteUrl: string;
|
|
143
|
+
authHeader: string | null | undefined;
|
|
144
|
+
}[], storePath: string): string | null;
|
|
145
|
+
export declare const workerGitSecurityTestHarness: {
|
|
146
|
+
commandArgs: (args: string[]) => string[];
|
|
147
|
+
credentialHelperForRemotes: typeof credentialHelperForRemotes;
|
|
148
|
+
credentialStorePathForProject: typeof credentialStorePathForProject;
|
|
149
|
+
credentialStorePathForWorkspace: typeof credentialStorePathForWorkspace;
|
|
150
|
+
legacySharedCredentialStorePath: typeof legacySharedCredentialStorePath;
|
|
151
|
+
removeLegacySharedCredentialStore: typeof removeLegacySharedCredentialStore;
|
|
152
|
+
pruneCredentialStoreFiles: typeof pruneCredentialStoreFiles;
|
|
153
|
+
readOnlyCredentialStoreHelper: typeof readOnlyCredentialStoreHelper;
|
|
154
|
+
resetCredentialStoreDirectory: typeof resetCredentialStoreDirectory;
|
|
155
|
+
replaceCredentialStoreFile: typeof replaceCredentialStoreFile;
|
|
156
|
+
};
|
|
133
157
|
export declare function githubCliEnv(token: string | null | undefined): Record<string, string>;
|
|
134
158
|
type ResolvedWorkerSessionTarget = {
|
|
135
159
|
target: WorkerSessionTarget;
|
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
export
|
|
2
|
-
extraHeaderUrl: string;
|
|
3
|
-
header: string;
|
|
4
|
-
};
|
|
1
|
+
export declare const PROJECT_WORKTREE_SNAPSHOT_PREFIX = "r5d-project-worktrees-";
|
|
5
2
|
export type ProjectWorktreeBranch = {
|
|
6
3
|
branchName: string;
|
|
7
4
|
baseCommitHash: string;
|
|
@@ -27,20 +24,41 @@ export declare function projectWorktreeConfigurationFingerprint(input: {
|
|
|
27
24
|
email: string;
|
|
28
25
|
} | null;
|
|
29
26
|
}): string;
|
|
27
|
+
declare function gitCommandArgs(args: string[]): string[];
|
|
30
28
|
export declare function hasLinkedProjectWorktreeLayout(input: {
|
|
31
29
|
projectRoot: string;
|
|
32
30
|
primaryBranchName: string;
|
|
33
31
|
branches: readonly Pick<ProjectWorktreeBranch, "branchName">[];
|
|
34
32
|
}): boolean;
|
|
33
|
+
export declare function cleanupStaleProjectWorktreeSnapshots(input?: {
|
|
34
|
+
temporaryRoot?: string;
|
|
35
|
+
processAlive?: (processId: number) => boolean;
|
|
36
|
+
}): {
|
|
37
|
+
removed: string[];
|
|
38
|
+
failed: Array<{
|
|
39
|
+
path: string;
|
|
40
|
+
error: string;
|
|
41
|
+
}>;
|
|
42
|
+
};
|
|
43
|
+
declare function configureRepository(input: {
|
|
44
|
+
primaryPath: string;
|
|
45
|
+
originUrl: string;
|
|
46
|
+
credentialHelper?: string | null;
|
|
47
|
+
originCredentialUsername?: string | null;
|
|
48
|
+
gitIdentity?: {
|
|
49
|
+
name: string;
|
|
50
|
+
email: string;
|
|
51
|
+
} | null;
|
|
52
|
+
}): void;
|
|
35
53
|
export declare function ensureProjectWorktrees(input: {
|
|
36
54
|
projectRoot: string;
|
|
37
55
|
primaryBranchName: string;
|
|
38
56
|
branches: readonly ProjectWorktreeBranch[];
|
|
39
57
|
originUrl: string;
|
|
40
|
-
originAuth?: GitHttpAuth;
|
|
41
58
|
mirrorUrl: string;
|
|
42
|
-
mirrorAuth?: GitHttpAuth;
|
|
43
59
|
credentialHelper?: string | null;
|
|
60
|
+
originCredentialUsername?: string | null;
|
|
61
|
+
mirrorCredentialUsername?: string | null;
|
|
44
62
|
gitIdentity?: {
|
|
45
63
|
name: string;
|
|
46
64
|
email: string;
|
|
@@ -70,13 +88,15 @@ export declare function deleteProjectMirrorBranch(input: {
|
|
|
70
88
|
gitDirectory: string;
|
|
71
89
|
branchName: string;
|
|
72
90
|
mirrorUrl: string;
|
|
73
|
-
|
|
91
|
+
credentialHelper?: string | null;
|
|
92
|
+
credentialUsername?: string | null;
|
|
74
93
|
}): void;
|
|
75
94
|
export declare function pushProjectMirrorHeads(input: {
|
|
76
95
|
projectRoot: string;
|
|
77
96
|
branchNames: readonly string[];
|
|
78
97
|
mirrorUrl: string;
|
|
79
|
-
|
|
98
|
+
credentialHelper?: string | null;
|
|
99
|
+
credentialUsername?: string | null;
|
|
80
100
|
onlyBranches?: ReadonlySet<string>;
|
|
81
101
|
}): Array<{
|
|
82
102
|
branchName: string;
|
|
@@ -89,7 +109,8 @@ export declare function fastForwardProjectHeadsFromMirror(input: {
|
|
|
89
109
|
primaryBranchName: string;
|
|
90
110
|
branchNames: readonly string[];
|
|
91
111
|
mirrorUrl: string;
|
|
92
|
-
|
|
112
|
+
credentialHelper?: string | null;
|
|
113
|
+
credentialUsername?: string | null;
|
|
93
114
|
}): Array<{
|
|
94
115
|
branchName: string;
|
|
95
116
|
previousHead: string;
|
|
@@ -97,5 +118,7 @@ export declare function fastForwardProjectHeadsFromMirror(input: {
|
|
|
97
118
|
fastForwarded: boolean;
|
|
98
119
|
}>;
|
|
99
120
|
export declare const projectWorktreesTestHarness: {
|
|
100
|
-
commandArgs:
|
|
121
|
+
commandArgs: typeof gitCommandArgs;
|
|
122
|
+
configureRepository: typeof configureRepository;
|
|
101
123
|
};
|
|
124
|
+
export {};
|