@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
|
@@ -28,6 +28,8 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
28
28
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
29
|
var project_worktrees_exports = {};
|
|
30
30
|
__export(project_worktrees_exports, {
|
|
31
|
+
PROJECT_WORKTREE_SNAPSHOT_PREFIX: () => PROJECT_WORKTREE_SNAPSHOT_PREFIX,
|
|
32
|
+
cleanupStaleProjectWorktreeSnapshots: () => cleanupStaleProjectWorktreeSnapshots,
|
|
31
33
|
createLinkedProjectBranch: () => createLinkedProjectBranch,
|
|
32
34
|
deleteLinkedProjectBranch: () => deleteLinkedProjectBranch,
|
|
33
35
|
deleteProjectMirrorBranch: () => deleteProjectMirrorBranch,
|
|
@@ -45,8 +47,13 @@ var import_node_crypto = require("node:crypto");
|
|
|
45
47
|
var import_node_fs = __toESM(require("node:fs"), 1);
|
|
46
48
|
var import_node_os = __toESM(require("node:os"), 1);
|
|
47
49
|
var import_node_path = __toESM(require("node:path"), 1);
|
|
50
|
+
var import_git_process_environment = require("./git-process-environment.cjs");
|
|
48
51
|
var import_managed_paths = require("./managed-paths.cjs");
|
|
49
52
|
var import_working_tree_mirror = require("./working-tree-mirror.cjs");
|
|
53
|
+
const PROJECT_WORKTREE_SNAPSHOT_PREFIX = "r5d-project-worktrees-";
|
|
54
|
+
const PROJECT_WORKTREE_SNAPSHOT_NAME = new RegExp(
|
|
55
|
+
`^${PROJECT_WORKTREE_SNAPSHOT_PREFIX.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}([1-9]\\d*)-([A-Za-z0-9]{6})$`
|
|
56
|
+
);
|
|
50
57
|
function projectWorktreeConfigurationFingerprint(input) {
|
|
51
58
|
return (0, import_node_crypto.createHash)("sha256").update(
|
|
52
59
|
JSON.stringify({
|
|
@@ -72,25 +79,15 @@ const NON_RECURSIVE_GIT_CONFIG = [
|
|
|
72
79
|
"-c",
|
|
73
80
|
"push.recurseSubmodules=false"
|
|
74
81
|
];
|
|
75
|
-
function
|
|
76
|
-
|
|
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}`];
|
|
82
|
+
function gitCommandArgs(args) {
|
|
83
|
+
return ["git", ...NON_RECURSIVE_GIT_CONFIG, ...args];
|
|
87
84
|
}
|
|
88
|
-
function gitResult(cwd, args
|
|
89
|
-
const result = Bun.spawnSync(
|
|
85
|
+
function gitResult(cwd, args) {
|
|
86
|
+
const result = Bun.spawnSync(gitCommandArgs(args), {
|
|
90
87
|
cwd,
|
|
91
88
|
stdout: "pipe",
|
|
92
89
|
stderr: "pipe",
|
|
93
|
-
env:
|
|
90
|
+
env: (0, import_git_process_environment.workerGitProcessEnvironment)()
|
|
94
91
|
});
|
|
95
92
|
return {
|
|
96
93
|
exitCode: result.exitCode,
|
|
@@ -98,13 +95,13 @@ function gitResult(cwd, args, auth) {
|
|
|
98
95
|
stderr: result.stderr.toString().trim()
|
|
99
96
|
};
|
|
100
97
|
}
|
|
101
|
-
function git(cwd, args, action
|
|
102
|
-
const result = gitResult(cwd, args
|
|
98
|
+
function git(cwd, args, action) {
|
|
99
|
+
const result = gitResult(cwd, args);
|
|
103
100
|
if (result.exitCode !== 0) throw new Error(`${action}: ${result.stderr || result.stdout || `git exited ${result.exitCode}`}`);
|
|
104
101
|
return result.stdout;
|
|
105
102
|
}
|
|
106
|
-
function tryGit(cwd, args
|
|
107
|
-
return gitResult(cwd, args
|
|
103
|
+
function tryGit(cwd, args) {
|
|
104
|
+
return gitResult(cwd, args).exitCode === 0;
|
|
108
105
|
}
|
|
109
106
|
function branchPath(projectRoot, branchName) {
|
|
110
107
|
(0, import_managed_paths.validateManagedBranchName)(branchName);
|
|
@@ -167,17 +164,63 @@ function hasCompatibleLinkedProjectWorktreeLayout(input) {
|
|
|
167
164
|
return kind === null || kind === "file" && commonGitDirectory(checkoutPath) === primaryCommonDir;
|
|
168
165
|
});
|
|
169
166
|
}
|
|
167
|
+
function createProjectWorktreeSnapshotRoot() {
|
|
168
|
+
return import_node_fs.default.mkdtempSync(import_node_path.default.join(import_node_os.default.tmpdir(), `${PROJECT_WORKTREE_SNAPSHOT_PREFIX}${process.pid}-`));
|
|
169
|
+
}
|
|
170
170
|
function snapshotBranchTrees(projectRoot, branches) {
|
|
171
|
-
const root =
|
|
171
|
+
const root = createProjectWorktreeSnapshotRoot();
|
|
172
172
|
const paths = /* @__PURE__ */ new Map();
|
|
173
|
-
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
173
|
+
try {
|
|
174
|
+
for (const { branchName } of branches) {
|
|
175
|
+
const checkoutPath = branchPath(projectRoot, branchName);
|
|
176
|
+
if (!import_node_fs.default.existsSync(checkoutPath)) continue;
|
|
177
|
+
const snapshotPath = import_node_path.default.join(root, ...branchName.split("/"));
|
|
178
|
+
(0, import_working_tree_mirror.mirrorWorkingTree)({ sourceRoot: checkoutPath, targetRoot: snapshotPath, sourceMode: "all", deletionMode: "all" });
|
|
179
|
+
paths.set(branchName, snapshotPath);
|
|
180
|
+
}
|
|
181
|
+
return { root, paths };
|
|
182
|
+
} catch (error) {
|
|
183
|
+
import_node_fs.default.rmSync(root, { recursive: true, force: true });
|
|
184
|
+
throw error;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
function isProcessAlive(processId) {
|
|
188
|
+
try {
|
|
189
|
+
process.kill(processId, 0);
|
|
190
|
+
return true;
|
|
191
|
+
} catch (error) {
|
|
192
|
+
return error.code !== "ESRCH";
|
|
179
193
|
}
|
|
180
|
-
|
|
194
|
+
}
|
|
195
|
+
function cleanupStaleProjectWorktreeSnapshots(input = {}) {
|
|
196
|
+
const temporaryRoot = import_node_path.default.resolve(input.temporaryRoot ?? import_node_os.default.tmpdir());
|
|
197
|
+
const processAlive = input.processAlive ?? isProcessAlive;
|
|
198
|
+
const removed = [];
|
|
199
|
+
const failed = [];
|
|
200
|
+
let entries;
|
|
201
|
+
try {
|
|
202
|
+
entries = import_node_fs.default.readdirSync(temporaryRoot, { withFileTypes: true });
|
|
203
|
+
} catch (error) {
|
|
204
|
+
if (error.code === "ENOENT") return { removed, failed };
|
|
205
|
+
return { removed, failed: [{ path: temporaryRoot, error: error instanceof Error ? error.message : String(error) }] };
|
|
206
|
+
}
|
|
207
|
+
for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
|
|
208
|
+
const match = PROJECT_WORKTREE_SNAPSHOT_NAME.exec(entry.name);
|
|
209
|
+
if (!match) continue;
|
|
210
|
+
const ownerProcessId = Number(match[1]);
|
|
211
|
+
if (!Number.isSafeInteger(ownerProcessId) || processAlive(ownerProcessId)) continue;
|
|
212
|
+
const candidate = import_node_path.default.join(temporaryRoot, entry.name);
|
|
213
|
+
try {
|
|
214
|
+
const stat = import_node_fs.default.lstatSync(candidate);
|
|
215
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) continue;
|
|
216
|
+
import_node_fs.default.rmSync(candidate, { recursive: true, force: true });
|
|
217
|
+
removed.push(candidate);
|
|
218
|
+
} catch (error) {
|
|
219
|
+
if (error.code === "ENOENT") continue;
|
|
220
|
+
failed.push({ path: candidate, error: error instanceof Error ? error.message : String(error) });
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return { removed, failed };
|
|
181
224
|
}
|
|
182
225
|
function configureRepository(input) {
|
|
183
226
|
if (tryGit(input.primaryPath, ["remote", "get-url", "origin"])) {
|
|
@@ -185,7 +228,16 @@ function configureRepository(input) {
|
|
|
185
228
|
} else {
|
|
186
229
|
git(input.primaryPath, ["remote", "add", "origin", input.originUrl], "configure project origin");
|
|
187
230
|
}
|
|
231
|
+
tryGit(input.primaryPath, ["config", "--local", "--unset-all", "remote.origin.pushurl"]);
|
|
188
232
|
git(input.primaryPath, ["config", "--local", "--replace-all", "credential.helper", ""], "reset project credential helpers");
|
|
233
|
+
git(input.primaryPath, ["config", "--local", "credential.useHttpPath", "false"], "configure project credential path matching");
|
|
234
|
+
if (input.originCredentialUsername) {
|
|
235
|
+
git(
|
|
236
|
+
input.primaryPath,
|
|
237
|
+
["config", "--local", "--replace-all", (0, import_git_process_environment.gitCredentialUsernameConfigKey)(input.originUrl), input.originCredentialUsername],
|
|
238
|
+
"configure project credential username"
|
|
239
|
+
);
|
|
240
|
+
}
|
|
189
241
|
if (input.credentialHelper) {
|
|
190
242
|
git(
|
|
191
243
|
input.primaryPath,
|
|
@@ -202,19 +254,25 @@ function configureRepository(input) {
|
|
|
202
254
|
}
|
|
203
255
|
}
|
|
204
256
|
function fetchProjectHeads(input) {
|
|
205
|
-
const originFetch = gitResult(
|
|
206
|
-
input.
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
257
|
+
const originFetch = gitResult(input.primaryPath, [
|
|
258
|
+
...(0, import_git_process_environment.gitTransportSecurityArgs)(input.originUrl, input.credentialHelper, input.originCredentialUsername),
|
|
259
|
+
"fetch",
|
|
260
|
+
"--no-recurse-submodules",
|
|
261
|
+
"--prune",
|
|
262
|
+
"origin",
|
|
263
|
+
"+refs/heads/*:refs/remotes/origin/*"
|
|
264
|
+
]);
|
|
210
265
|
if (originFetch.exitCode !== 0) {
|
|
211
266
|
throw new Error(`Fetch project origin: ${originFetch.stderr || originFetch.stdout || `git exited ${originFetch.exitCode}`}`);
|
|
212
267
|
}
|
|
213
|
-
const mirrorFetch = gitResult(
|
|
214
|
-
input.
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
268
|
+
const mirrorFetch = gitResult(input.primaryPath, [
|
|
269
|
+
...(0, import_git_process_environment.gitTransportSecurityArgs)(input.mirrorUrl, input.credentialHelper, input.mirrorCredentialUsername),
|
|
270
|
+
"fetch",
|
|
271
|
+
"--no-recurse-submodules",
|
|
272
|
+
"--prune",
|
|
273
|
+
input.mirrorUrl,
|
|
274
|
+
"+refs/heads/*:refs/r5d/mirror/*"
|
|
275
|
+
]);
|
|
218
276
|
if (mirrorFetch.exitCode !== 0 && !/(couldn't find remote ref|does not have any commits|remote repository is empty|no such ref)/i.test(
|
|
219
277
|
`${mirrorFetch.stderr}
|
|
220
278
|
${mirrorFetch.stdout}`
|
|
@@ -224,11 +282,19 @@ ${mirrorFetch.stdout}`
|
|
|
224
282
|
}
|
|
225
283
|
function ensureCommitAvailable(input) {
|
|
226
284
|
if (tryGit(input.primaryPath, ["cat-file", "-e", `${input.commitHash}^{commit}`])) return;
|
|
227
|
-
for (const [url,
|
|
228
|
-
[input.mirrorUrl, input.
|
|
229
|
-
[input.originUrl, input.
|
|
285
|
+
for (const [url, username] of [
|
|
286
|
+
[input.mirrorUrl, input.mirrorCredentialUsername],
|
|
287
|
+
[input.originUrl, input.originCredentialUsername]
|
|
230
288
|
]) {
|
|
231
|
-
if (tryGit(input.primaryPath, [
|
|
289
|
+
if (tryGit(input.primaryPath, [
|
|
290
|
+
...(0, import_git_process_environment.gitTransportSecurityArgs)(url, input.credentialHelper, username),
|
|
291
|
+
"fetch",
|
|
292
|
+
"--no-recurse-submodules",
|
|
293
|
+
"--no-tags",
|
|
294
|
+
url,
|
|
295
|
+
input.commitHash
|
|
296
|
+
]))
|
|
297
|
+
return;
|
|
232
298
|
}
|
|
233
299
|
throw new Error(`Project base commit ${input.commitHash} is unavailable from origin and the canonical mirror`);
|
|
234
300
|
}
|
|
@@ -373,7 +439,7 @@ function createLinkedProjectBranch(input) {
|
|
|
373
439
|
});
|
|
374
440
|
if (overlappingWorktree) throw new Error(`Project branch folder overlaps linked worktree ${overlappingWorktree}`);
|
|
375
441
|
const sourceHead = git(sourcePath, ["rev-parse", "HEAD"], "resolve source branch head");
|
|
376
|
-
const snapshotRoot =
|
|
442
|
+
const snapshotRoot = createProjectWorktreeSnapshotRoot();
|
|
377
443
|
try {
|
|
378
444
|
(0, import_working_tree_mirror.mirrorWorkingTree)({ sourceRoot: sourcePath, targetRoot: snapshotRoot, sourceMode: "git", deletionMode: "all" });
|
|
379
445
|
git(sourcePath, ["branch", input.branchName, sourceHead], `create project branch ${input.branchName}`);
|
|
@@ -432,11 +498,13 @@ function removeProjectWorktrees(input) {
|
|
|
432
498
|
}
|
|
433
499
|
function deleteProjectMirrorBranch(input) {
|
|
434
500
|
(0, import_managed_paths.validateManagedBranchName)(input.branchName);
|
|
435
|
-
const deleted = gitResult(
|
|
436
|
-
input.
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
501
|
+
const deleted = gitResult(input.gitDirectory, [
|
|
502
|
+
...(0, import_git_process_environment.gitTransportSecurityArgs)(input.mirrorUrl, input.credentialHelper, input.credentialUsername),
|
|
503
|
+
"push",
|
|
504
|
+
"--no-recurse-submodules",
|
|
505
|
+
input.mirrorUrl,
|
|
506
|
+
`:refs/heads/${input.branchName}`
|
|
507
|
+
]);
|
|
440
508
|
if (deleted.exitCode !== 0 && !/(remote ref does not exist|unable to delete|no such ref)/i.test(`${deleted.stderr}
|
|
441
509
|
${deleted.stdout}`)) {
|
|
442
510
|
throw new Error(
|
|
@@ -456,9 +524,14 @@ function pushProjectMirrorHeads(input) {
|
|
|
456
524
|
}
|
|
457
525
|
git(
|
|
458
526
|
checkoutPath,
|
|
459
|
-
[
|
|
460
|
-
|
|
461
|
-
|
|
527
|
+
[
|
|
528
|
+
...(0, import_git_process_environment.gitTransportSecurityArgs)(input.mirrorUrl, input.credentialHelper, input.credentialUsername),
|
|
529
|
+
"push",
|
|
530
|
+
"--no-recurse-submodules",
|
|
531
|
+
input.mirrorUrl,
|
|
532
|
+
`+HEAD:refs/heads/${branchName}`
|
|
533
|
+
],
|
|
534
|
+
`push hidden project mirror for ${branchName}`
|
|
462
535
|
);
|
|
463
536
|
results.push({ branchName, head, pushed: true });
|
|
464
537
|
}
|
|
@@ -466,11 +539,14 @@ function pushProjectMirrorHeads(input) {
|
|
|
466
539
|
}
|
|
467
540
|
function fastForwardProjectHeadsFromMirror(input) {
|
|
468
541
|
const primaryPath = branchPath(input.projectRoot, input.primaryBranchName);
|
|
469
|
-
const fetch = gitResult(
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
542
|
+
const fetch = gitResult(primaryPath, [
|
|
543
|
+
...(0, import_git_process_environment.gitTransportSecurityArgs)(input.mirrorUrl, input.credentialHelper, input.credentialUsername),
|
|
544
|
+
"fetch",
|
|
545
|
+
"--no-recurse-submodules",
|
|
546
|
+
"--prune",
|
|
547
|
+
input.mirrorUrl,
|
|
548
|
+
"+refs/heads/*:refs/r5d/mirror/*"
|
|
549
|
+
]);
|
|
474
550
|
if (fetch.exitCode !== 0)
|
|
475
551
|
throw new Error(`Refresh project head mirror: ${fetch.stderr || fetch.stdout || `git exited ${fetch.exitCode}`}`);
|
|
476
552
|
return [...input.branchNames].sort().map((branchName) => {
|
|
@@ -487,10 +563,13 @@ function fastForwardProjectHeadsFromMirror(input) {
|
|
|
487
563
|
});
|
|
488
564
|
}
|
|
489
565
|
const projectWorktreesTestHarness = {
|
|
490
|
-
commandArgs:
|
|
566
|
+
commandArgs: gitCommandArgs,
|
|
567
|
+
configureRepository
|
|
491
568
|
};
|
|
492
569
|
// Annotate the CommonJS export names for ESM import in node:
|
|
493
570
|
0 && (module.exports = {
|
|
571
|
+
PROJECT_WORKTREE_SNAPSHOT_PREFIX,
|
|
572
|
+
cleanupStaleProjectWorktreeSnapshots,
|
|
494
573
|
createLinkedProjectBranch,
|
|
495
574
|
deleteLinkedProjectBranch,
|
|
496
575
|
deleteProjectMirrorBranch,
|
|
@@ -34,6 +34,7 @@ __export(working_tree_mirror_exports, {
|
|
|
34
34
|
module.exports = __toCommonJS(working_tree_mirror_exports);
|
|
35
35
|
var import_node_fs = __toESM(require("node:fs"), 1);
|
|
36
36
|
var import_node_path = __toESM(require("node:path"), 1);
|
|
37
|
+
var import_git_process_environment = require("./git-process-environment.cjs");
|
|
37
38
|
function isGitMetadataPath(relativePath) {
|
|
38
39
|
return relativePath.split("/").includes(".git");
|
|
39
40
|
}
|
|
@@ -70,7 +71,7 @@ function gitEligibleRoots(root) {
|
|
|
70
71
|
cwd: root,
|
|
71
72
|
stdout: "pipe",
|
|
72
73
|
stderr: "pipe",
|
|
73
|
-
env:
|
|
74
|
+
env: (0, import_git_process_environment.workerGitProcessEnvironment)()
|
|
74
75
|
}
|
|
75
76
|
);
|
|
76
77
|
if (result.exitCode !== 0) {
|
|
@@ -40,6 +40,7 @@ __export(workspace_git_sync_exports, {
|
|
|
40
40
|
module.exports = __toCommonJS(workspace_git_sync_exports);
|
|
41
41
|
var import_node_fs = __toESM(require("node:fs"), 1);
|
|
42
42
|
var import_node_path = __toESM(require("node:path"), 1);
|
|
43
|
+
var import_git_process_environment = require("./git-process-environment.cjs");
|
|
43
44
|
var import_working_tree_mirror = require("./working-tree-mirror.cjs");
|
|
44
45
|
const WORKSPACE_GIT_BRANCH = "main";
|
|
45
46
|
const MAX_WORKSPACE_GIT_DIFF_BYTES = 5 * 1024 * 1024;
|
|
@@ -53,25 +54,18 @@ const NON_RECURSIVE_GIT_CONFIG = [
|
|
|
53
54
|
"-c",
|
|
54
55
|
"push.recurseSubmodules=false"
|
|
55
56
|
];
|
|
56
|
-
function
|
|
57
|
-
|
|
58
|
-
const url = new URL(value);
|
|
59
|
-
return `${url.protocol}//${url.host}`;
|
|
60
|
-
} catch {
|
|
61
|
-
return value.replace(/\/+$/, "");
|
|
62
|
-
}
|
|
57
|
+
function gitCommandArgs(args) {
|
|
58
|
+
return ["git", ...NON_RECURSIVE_GIT_CONFIG, ...args];
|
|
63
59
|
}
|
|
64
|
-
function
|
|
65
|
-
|
|
66
|
-
const key = `http.${normalizedHttpOrigin(auth.extraHeaderUrl)}/.extraHeader`;
|
|
67
|
-
return ["-c", "http.extraHeader=", "-c", `${key}=`, "-c", `${key}=${auth.header}`];
|
|
60
|
+
function workspaceCloneCommandArgs(args, remoteUrl, credentialHelper, credentialUsername) {
|
|
61
|
+
return gitCommandArgs([...(0, import_git_process_environment.gitTransportSecurityArgs)(remoteUrl, credentialHelper, credentialUsername), ...args]);
|
|
68
62
|
}
|
|
69
|
-
function gitResult(cwd, args
|
|
70
|
-
const result = Bun.spawnSync(
|
|
63
|
+
function gitResult(cwd, args) {
|
|
64
|
+
const result = Bun.spawnSync(gitCommandArgs(args), {
|
|
71
65
|
cwd,
|
|
72
66
|
stdout: "pipe",
|
|
73
67
|
stderr: "pipe",
|
|
74
|
-
env:
|
|
68
|
+
env: (0, import_git_process_environment.workerGitProcessEnvironment)()
|
|
75
69
|
});
|
|
76
70
|
return {
|
|
77
71
|
exitCode: result.exitCode,
|
|
@@ -79,13 +73,13 @@ function gitResult(cwd, args, auth) {
|
|
|
79
73
|
stderr: result.stderr.toString().trim()
|
|
80
74
|
};
|
|
81
75
|
}
|
|
82
|
-
function git(cwd, args, action
|
|
83
|
-
const result = gitResult(cwd, args
|
|
76
|
+
function git(cwd, args, action) {
|
|
77
|
+
const result = gitResult(cwd, args);
|
|
84
78
|
if (result.exitCode !== 0) throw new Error(`${action}: ${result.stderr || result.stdout || `git exited ${result.exitCode}`}`);
|
|
85
79
|
return result.stdout;
|
|
86
80
|
}
|
|
87
|
-
function tryGit(cwd, args
|
|
88
|
-
return gitResult(cwd, args
|
|
81
|
+
function tryGit(cwd, args) {
|
|
82
|
+
return gitResult(cwd, args).exitCode === 0;
|
|
89
83
|
}
|
|
90
84
|
function revParse(workspacePath, revision) {
|
|
91
85
|
const result = gitResult(workspacePath, ["rev-parse", "--verify", revision]);
|
|
@@ -131,9 +125,22 @@ function configureWorkspaceRepository(input) {
|
|
|
131
125
|
} else {
|
|
132
126
|
git(input.workspacePath, ["remote", "add", "origin", input.remoteUrl], "configure workspace origin");
|
|
133
127
|
}
|
|
128
|
+
tryGit(input.workspacePath, ["config", "--local", "--unset-all", "remote.origin.pushurl"]);
|
|
134
129
|
git(input.workspacePath, ["config", "--local", "--replace-all", "credential.helper", ""], "reset workspace credential helpers");
|
|
130
|
+
git(input.workspacePath, ["config", "--local", "credential.useHttpPath", "false"], "configure workspace credential path matching");
|
|
131
|
+
if (input.credentialUsername) {
|
|
132
|
+
git(
|
|
133
|
+
input.workspacePath,
|
|
134
|
+
["config", "--local", "--replace-all", (0, import_git_process_environment.gitCredentialUsernameConfigKey)(input.remoteUrl), input.credentialUsername],
|
|
135
|
+
"configure workspace credential username"
|
|
136
|
+
);
|
|
137
|
+
}
|
|
135
138
|
if (input.credentialHelper) {
|
|
136
|
-
git(
|
|
139
|
+
git(
|
|
140
|
+
input.workspacePath,
|
|
141
|
+
["config", "--local", "--add", "credential.helper", input.credentialHelper],
|
|
142
|
+
"configure workspace credential helper"
|
|
143
|
+
);
|
|
137
144
|
}
|
|
138
145
|
const name = input.gitIdentity.name.trim();
|
|
139
146
|
const email = input.gitIdentity.email.trim();
|
|
@@ -141,19 +148,16 @@ function configureWorkspaceRepository(input) {
|
|
|
141
148
|
git(input.workspacePath, ["config", "--local", "user.name", name], "configure workspace Git user name");
|
|
142
149
|
git(input.workspacePath, ["config", "--local", "user.email", email], "configure workspace Git user email");
|
|
143
150
|
}
|
|
144
|
-
function fetchWorkspaceHead(workspacePath,
|
|
145
|
-
const result = gitResult(
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
],
|
|
155
|
-
remoteAuth
|
|
156
|
-
);
|
|
151
|
+
function fetchWorkspaceHead(workspacePath, remoteUrl, credentialHelper, credentialUsername) {
|
|
152
|
+
const result = gitResult(workspacePath, [
|
|
153
|
+
...(0, import_git_process_environment.gitTransportSecurityArgs)(remoteUrl, credentialHelper, credentialUsername),
|
|
154
|
+
"fetch",
|
|
155
|
+
"--no-recurse-submodules",
|
|
156
|
+
"--prune",
|
|
157
|
+
"--update-shallow",
|
|
158
|
+
"origin",
|
|
159
|
+
`+refs/heads/${WORKSPACE_GIT_BRANCH}:refs/remotes/origin/${WORKSPACE_GIT_BRANCH}`
|
|
160
|
+
]);
|
|
157
161
|
if (result.exitCode !== 0) {
|
|
158
162
|
const detail = `${result.stderr}
|
|
159
163
|
${result.stdout}`;
|
|
@@ -170,11 +174,17 @@ function ensureWorkspaceGitClone(input) {
|
|
|
170
174
|
if (!import_node_fs.default.existsSync(gitPath)) {
|
|
171
175
|
import_node_fs.default.rmSync(workspacePath, { recursive: true, force: true });
|
|
172
176
|
import_node_fs.default.mkdirSync(import_node_path.default.dirname(workspacePath), { recursive: true });
|
|
173
|
-
const
|
|
174
|
-
void 0,
|
|
177
|
+
const cloneCommand = workspaceCloneCommandArgs(
|
|
175
178
|
["clone", "--no-recurse-submodules", "--branch", WORKSPACE_GIT_BRANCH, input.remoteUrl, workspacePath],
|
|
176
|
-
input.
|
|
179
|
+
input.remoteUrl,
|
|
180
|
+
input.credentialHelper,
|
|
181
|
+
input.credentialUsername
|
|
177
182
|
);
|
|
183
|
+
const clone = Bun.spawnSync(cloneCommand, {
|
|
184
|
+
stdout: "pipe",
|
|
185
|
+
stderr: "pipe",
|
|
186
|
+
env: (0, import_git_process_environment.workerGitProcessEnvironment)()
|
|
187
|
+
});
|
|
178
188
|
if (clone.exitCode !== 0) {
|
|
179
189
|
import_node_fs.default.rmSync(workspacePath, { recursive: true, force: true });
|
|
180
190
|
import_node_fs.default.mkdirSync(workspacePath, { recursive: true });
|
|
@@ -187,7 +197,7 @@ function ensureWorkspaceGitClone(input) {
|
|
|
187
197
|
if (!revParse(workspacePath, WORKSPACE_GIT_INTEGRATED_REF) && previousRemoteHead && localHeadBeforeFetch && tryGit(workspacePath, ["merge-base", "--is-ancestor", previousRemoteHead, localHeadBeforeFetch])) {
|
|
188
198
|
updateIntegratedWorkspaceHead(workspacePath, previousRemoteHead);
|
|
189
199
|
}
|
|
190
|
-
const remoteHead = fetchWorkspaceHead(workspacePath, input.
|
|
200
|
+
const remoteHead = fetchWorkspaceHead(workspacePath, input.remoteUrl, input.credentialHelper, input.credentialUsername);
|
|
191
201
|
let localHead = revParse(workspacePath, "HEAD");
|
|
192
202
|
if (!localHead && remoteHead) {
|
|
193
203
|
git(
|
|
@@ -273,7 +283,7 @@ function resetWorkspaceGit(input) {
|
|
|
273
283
|
const initial = ensureWorkspaceGitClone({ ...input, workspacePath });
|
|
274
284
|
const status = gitResult(workspacePath, ["status", "--porcelain=v1", "-z"]);
|
|
275
285
|
const discardedPaths = status.exitCode === 0 ? status.stdout.split("\0").filter(Boolean).map((entry) => entry.slice(3)).sort() : [];
|
|
276
|
-
const remoteHead = fetchWorkspaceHead(workspacePath, input.
|
|
286
|
+
const remoteHead = fetchWorkspaceHead(workspacePath, input.remoteUrl, input.credentialHelper, input.credentialUsername);
|
|
277
287
|
if (remoteHead) {
|
|
278
288
|
git(workspacePath, ["checkout", "--no-recurse-submodules", "-B", WORKSPACE_GIT_BRANCH, remoteHead], "reset workspace main");
|
|
279
289
|
git(workspacePath, ["clean", "-fd", "--", "."], "remove untracked workspace changes");
|
|
@@ -299,9 +309,10 @@ function emptyTreeHash(workspacePath) {
|
|
|
299
309
|
stdin: Buffer.alloc(0),
|
|
300
310
|
stdout: "pipe",
|
|
301
311
|
stderr: "pipe",
|
|
302
|
-
env:
|
|
312
|
+
env: (0, import_git_process_environment.workerGitProcessEnvironment)()
|
|
303
313
|
});
|
|
304
|
-
if (result.exitCode !== 0)
|
|
314
|
+
if (result.exitCode !== 0)
|
|
315
|
+
throw new Error(`Create empty workspace tree: ${result.stderr.toString().trim() || `git exited ${result.exitCode}`}`);
|
|
305
316
|
return result.stdout.toString().trim();
|
|
306
317
|
}
|
|
307
318
|
function changedPaths(workspacePath, baseRevision, headRevision) {
|
|
@@ -310,15 +321,12 @@ function changedPaths(workspacePath, baseRevision, headRevision) {
|
|
|
310
321
|
}
|
|
311
322
|
async function diffSizeBytes(workspacePath, baseRevision, headRevision, limit) {
|
|
312
323
|
const base = baseRevision ?? emptyTreeHash(workspacePath);
|
|
313
|
-
const subprocess = Bun.spawn(
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
env: { ...process.env, GIT_TERMINAL_PROMPT: "0" }
|
|
320
|
-
}
|
|
321
|
-
);
|
|
324
|
+
const subprocess = Bun.spawn(["git", ...NON_RECURSIVE_GIT_CONFIG, "diff", "--binary", "--no-ext-diff", base, headRevision], {
|
|
325
|
+
cwd: workspacePath,
|
|
326
|
+
stdout: "pipe",
|
|
327
|
+
stderr: "pipe",
|
|
328
|
+
env: (0, import_git_process_environment.workerGitProcessEnvironment)()
|
|
329
|
+
});
|
|
322
330
|
const stderrPromise = new Response(subprocess.stderr).text();
|
|
323
331
|
const reader = subprocess.stdout.getReader();
|
|
324
332
|
let total = 0;
|
|
@@ -439,7 +447,7 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
439
447
|
"commit workspace working trees"
|
|
440
448
|
);
|
|
441
449
|
}
|
|
442
|
-
let remoteHead = fetchWorkspaceHead(workspacePath, input.
|
|
450
|
+
let remoteHead = fetchWorkspaceHead(workspacePath, input.remoteUrl, input.credentialHelper, input.credentialUsername);
|
|
443
451
|
let rebaseCount = 0;
|
|
444
452
|
let updated = false;
|
|
445
453
|
for (let pushAttempt = 0; pushAttempt < maxPushAttempts; pushAttempt += 1) {
|
|
@@ -515,10 +523,14 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
515
523
|
skippedMountIds: selected.skipped.map(({ id }) => id).sort()
|
|
516
524
|
};
|
|
517
525
|
}
|
|
518
|
-
const pushArgs = [
|
|
526
|
+
const pushArgs = [
|
|
527
|
+
...(0, import_git_process_environment.gitTransportSecurityArgs)(input.remoteUrl, input.credentialHelper, input.credentialUsername),
|
|
528
|
+
"push",
|
|
529
|
+
"--no-recurse-submodules"
|
|
530
|
+
];
|
|
519
531
|
if (input.allowLargeDiff) pushArgs.push(`--push-option=${WORKSPACE_GIT_CONFIRMED_LARGE_DIFF_PUSH_OPTION}`);
|
|
520
532
|
pushArgs.push("origin", `HEAD:refs/heads/${WORKSPACE_GIT_BRANCH}`);
|
|
521
|
-
const push = gitResult(workspacePath, pushArgs
|
|
533
|
+
const push = gitResult(workspacePath, pushArgs);
|
|
522
534
|
if (push.exitCode === 0) {
|
|
523
535
|
updateIntegratedWorkspaceHead(workspacePath, localHead);
|
|
524
536
|
await input.afterWorkspacePublished?.({
|
|
@@ -543,12 +555,14 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
543
555
|
${push.stdout}`)) {
|
|
544
556
|
throw new Error(`Push workspace main: ${push.stderr || push.stdout || `git exited ${push.exitCode}`}`);
|
|
545
557
|
}
|
|
546
|
-
remoteHead = fetchWorkspaceHead(workspacePath, input.
|
|
558
|
+
remoteHead = fetchWorkspaceHead(workspacePath, input.remoteUrl, input.credentialHelper, input.credentialUsername);
|
|
547
559
|
}
|
|
548
560
|
throw new Error(`Workspace push did not converge after ${maxPushAttempts} attempts`);
|
|
549
561
|
}
|
|
550
562
|
const workspaceGitSyncTestHarness = {
|
|
551
|
-
commandArgs:
|
|
563
|
+
commandArgs: gitCommandArgs,
|
|
564
|
+
workspaceCloneCommandArgs,
|
|
565
|
+
configureWorkspaceRepository,
|
|
552
566
|
mirrorMountsToWorkspace,
|
|
553
567
|
hydrateMountsFromWorkspace: hydrateWorkspaceGitMounts
|
|
554
568
|
};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { devNull } from "node:os";
|
|
2
|
+
const ALLOWED_GIT_ENVIRONMENT_KEYS = [
|
|
3
|
+
"PATH",
|
|
4
|
+
"USER",
|
|
5
|
+
"LOGNAME",
|
|
6
|
+
"SHELL",
|
|
7
|
+
"TMPDIR",
|
|
8
|
+
"TMP",
|
|
9
|
+
"TEMP",
|
|
10
|
+
"TZ",
|
|
11
|
+
"LANG",
|
|
12
|
+
"LANGUAGE",
|
|
13
|
+
"LC_ALL",
|
|
14
|
+
"LC_CTYPE",
|
|
15
|
+
"LC_COLLATE",
|
|
16
|
+
"LC_MESSAGES",
|
|
17
|
+
"LC_MONETARY",
|
|
18
|
+
"LC_NUMERIC",
|
|
19
|
+
"LC_TIME",
|
|
20
|
+
"LC_ADDRESS",
|
|
21
|
+
"LC_IDENTIFICATION",
|
|
22
|
+
"LC_MEASUREMENT",
|
|
23
|
+
"LC_NAME",
|
|
24
|
+
"LC_PAPER",
|
|
25
|
+
"LC_TELEPHONE",
|
|
26
|
+
"SystemRoot",
|
|
27
|
+
"SYSTEMROOT",
|
|
28
|
+
"WINDIR",
|
|
29
|
+
"COMSPEC",
|
|
30
|
+
"PATHEXT",
|
|
31
|
+
"SSL_CERT_FILE",
|
|
32
|
+
"SSL_CERT_DIR",
|
|
33
|
+
"CURL_CA_BUNDLE",
|
|
34
|
+
"GIT_SSL_CAINFO",
|
|
35
|
+
"GIT_SSL_CAPATH",
|
|
36
|
+
"GIT_EXEC_PATH",
|
|
37
|
+
"GIT_OPTIONAL_LOCKS"
|
|
38
|
+
];
|
|
39
|
+
function workerGitProcessEnvironment(source = process.env) {
|
|
40
|
+
const environment = {
|
|
41
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
42
|
+
GIT_CONFIG_GLOBAL: devNull,
|
|
43
|
+
GIT_CONFIG_NOSYSTEM: "1",
|
|
44
|
+
GIT_ATTR_NOSYSTEM: "1"
|
|
45
|
+
};
|
|
46
|
+
for (const key of ALLOWED_GIT_ENVIRONMENT_KEYS) {
|
|
47
|
+
if (source[key] !== void 0) environment[key] = source[key];
|
|
48
|
+
}
|
|
49
|
+
return environment;
|
|
50
|
+
}
|
|
51
|
+
function gitHttpAuthorizationClearArgs(remoteUrl) {
|
|
52
|
+
let remote;
|
|
53
|
+
try {
|
|
54
|
+
remote = new URL(remoteUrl);
|
|
55
|
+
} catch {
|
|
56
|
+
return ["-c", "http.extraHeader="];
|
|
57
|
+
}
|
|
58
|
+
if ((remote.protocol === "https:" || remote.protocol === "http:") && (remote.username || remote.password)) {
|
|
59
|
+
throw new Error("Git credentials must not be embedded in a remote URL");
|
|
60
|
+
}
|
|
61
|
+
if (remote.protocol !== "https:" && remote.protocol !== "http:") return ["-c", "http.extraHeader="];
|
|
62
|
+
remote.hash = "";
|
|
63
|
+
remote.search = "";
|
|
64
|
+
return ["-c", "http.extraHeader=", "-c", `http.${remote.origin}/.extraHeader=`, "-c", `http.${remote.href}.extraHeader=`];
|
|
65
|
+
}
|
|
66
|
+
function gitCredentialHelperConfigArgs(credentialHelper) {
|
|
67
|
+
return [
|
|
68
|
+
"-c",
|
|
69
|
+
"credential.helper=",
|
|
70
|
+
...credentialHelper ? ["-c", `credential.helper=${credentialHelper}`] : [],
|
|
71
|
+
"-c",
|
|
72
|
+
"credential.useHttpPath=false"
|
|
73
|
+
];
|
|
74
|
+
}
|
|
75
|
+
function gitCredentialUsernameConfigKey(remoteUrl) {
|
|
76
|
+
const remote = new URL(remoteUrl);
|
|
77
|
+
if ((remote.protocol === "https:" || remote.protocol === "http:") && (remote.username || remote.password)) {
|
|
78
|
+
throw new Error("Git credentials must not be embedded in a remote URL");
|
|
79
|
+
}
|
|
80
|
+
if (remote.protocol !== "https:" && remote.protocol !== "http:") {
|
|
81
|
+
throw new Error("Git credential usernames require an HTTP remote URL");
|
|
82
|
+
}
|
|
83
|
+
remote.hash = "";
|
|
84
|
+
remote.search = "";
|
|
85
|
+
return `credential.${remote.href}.username`;
|
|
86
|
+
}
|
|
87
|
+
function gitCredentialUsernameConfigArgs(remoteUrl, credentialUsername) {
|
|
88
|
+
if (!credentialUsername) return [];
|
|
89
|
+
if (/[\0\r\n]/.test(credentialUsername)) throw new Error("Invalid Git credential username");
|
|
90
|
+
return ["-c", `${gitCredentialUsernameConfigKey(remoteUrl)}=${credentialUsername}`];
|
|
91
|
+
}
|
|
92
|
+
function gitTransportSecurityArgs(remoteUrl, credentialHelper, credentialUsername) {
|
|
93
|
+
return [
|
|
94
|
+
...gitHttpAuthorizationClearArgs(remoteUrl),
|
|
95
|
+
...gitCredentialHelperConfigArgs(credentialHelper),
|
|
96
|
+
...gitCredentialUsernameConfigArgs(remoteUrl, credentialUsername)
|
|
97
|
+
];
|
|
98
|
+
}
|
|
99
|
+
export {
|
|
100
|
+
gitCredentialHelperConfigArgs,
|
|
101
|
+
gitCredentialUsernameConfigKey,
|
|
102
|
+
gitHttpAuthorizationClearArgs,
|
|
103
|
+
gitTransportSecurityArgs,
|
|
104
|
+
workerGitProcessEnvironment
|
|
105
|
+
};
|