@ricsam/r5d-worker 0.0.99 → 0.0.101
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/main.cjs +37 -5
- package/dist/cjs/package.json +1 -1
- package/dist/cjs/working-tree-mirror.cjs +347 -99
- package/dist/cjs/workspace-git-sync.cjs +198 -60
- package/dist/cjs/workspace-merge-projection.cjs +5 -19
- package/dist/mjs/main.mjs +37 -5
- package/dist/mjs/package.json +1 -1
- package/dist/mjs/working-tree-mirror.mjs +347 -99
- package/dist/mjs/workspace-git-sync.mjs +198 -60
- package/dist/mjs/workspace-merge-projection.mjs +5 -19
- package/dist/types/main.d.ts +181 -0
- package/dist/types/working-tree-mirror.d.ts +56 -11
- package/dist/types/workspace-git-sync.d.ts +42 -2
- package/package.json +1 -1
package/dist/cjs/main.cjs
CHANGED
|
@@ -97,9 +97,35 @@ function workerServerHeartbeatAction(input) {
|
|
|
97
97
|
if (input.watchdogSweepDelayed || input.probeElapsedMs === void 0) return "start_probe";
|
|
98
98
|
return input.probeElapsedMs >= input.timeoutMs ? "terminate" : "wait";
|
|
99
99
|
}
|
|
100
|
+
const WORKSPACE_HYDRATION_LEASE_OPERATION_PREFIX = "hydration:";
|
|
101
|
+
function createWorkspaceHydrationLeaseHooks(send, options = {}) {
|
|
102
|
+
const yieldToEventLoop = options.yieldToEventLoop ?? (() => new Promise((resolve) => setImmediate(resolve)));
|
|
103
|
+
const log = options.log ?? ((line) => process.stdout.write(`${line}
|
|
104
|
+
`));
|
|
105
|
+
const now = options.now ?? (() => performance.now());
|
|
106
|
+
const operationId = (context) => `${WORKSPACE_HYDRATION_LEASE_OPERATION_PREFIX}${context.transactionId}`;
|
|
107
|
+
const startedAtMs = /* @__PURE__ */ new Map();
|
|
108
|
+
return {
|
|
109
|
+
async beforeTransaction(context) {
|
|
110
|
+
send({ type: "heartbeat_lease", operationId: operationId(context), kind: "hydration", active: true });
|
|
111
|
+
startedAtMs.set(context.transactionId, now());
|
|
112
|
+
log(`[r5d-worker] hydrating ${context.mountIds.length} workspace mount(s) under hydration lease ${context.transactionId}`);
|
|
113
|
+
await yieldToEventLoop();
|
|
114
|
+
},
|
|
115
|
+
afterTransaction(context) {
|
|
116
|
+
const startedAt = startedAtMs.get(context.transactionId);
|
|
117
|
+
startedAtMs.delete(context.transactionId);
|
|
118
|
+
send({ type: "heartbeat_lease", operationId: operationId(context), kind: "hydration", active: false });
|
|
119
|
+
log(
|
|
120
|
+
`[r5d-worker] released hydration lease ${context.transactionId} after ${startedAt === void 0 ? "?" : Math.round(now() - startedAt)}ms`
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
}
|
|
100
125
|
const workerLifecycleTestHarness = {
|
|
101
126
|
boundedWorkerLifecycleProgress,
|
|
102
|
-
workerServerHeartbeatAction
|
|
127
|
+
workerServerHeartbeatAction,
|
|
128
|
+
createWorkspaceHydrationLeaseHooks
|
|
103
129
|
};
|
|
104
130
|
class ProjectWorkspaceConfigurationDeferredError extends Error {
|
|
105
131
|
}
|
|
@@ -4370,6 +4396,7 @@ async function startWorker(options) {
|
|
|
4370
4396
|
error: error instanceof Error ? error.message : String(error)
|
|
4371
4397
|
};
|
|
4372
4398
|
};
|
|
4399
|
+
const workspaceHydrationTransactionHooks = createWorkspaceHydrationLeaseHooks((message) => sendWorkerMessageFromCurrentSource(ws, message));
|
|
4373
4400
|
const performWorkspaceSync = async (input) => {
|
|
4374
4401
|
if (!workspaceRemoteUrl || !workspaceCredentialHelper || !workspaceCredentialUsername || !workspaceGitIdentity) {
|
|
4375
4402
|
throw new Error("Worker workspace configuration has not been received");
|
|
@@ -4392,7 +4419,8 @@ async function startWorker(options) {
|
|
|
4392
4419
|
credentialHelper: workspaceCredentialHelper,
|
|
4393
4420
|
credentialUsername: workspaceCredentialUsername,
|
|
4394
4421
|
gitIdentity: workspaceGitIdentity,
|
|
4395
|
-
mounts: configuredMounts
|
|
4422
|
+
mounts: configuredMounts,
|
|
4423
|
+
hydrationHooks: workspaceHydrationTransactionHooks
|
|
4396
4424
|
});
|
|
4397
4425
|
await applyObservedProjectHeadsAfterInboundWorkspace(observedProjectHeads2, reset.activeMountIds, true);
|
|
4398
4426
|
return {
|
|
@@ -4446,7 +4474,8 @@ async function startWorker(options) {
|
|
|
4446
4474
|
assertStillAdmitted: input.assertStillAdmitted,
|
|
4447
4475
|
afterWorkspacePublished: outerRemediation ? void 0 : async ({ activeMountIds, publishedHead }) => {
|
|
4448
4476
|
await pushChangedProjectHeads(activeMountIds, publishedHead, publicationHeads, expectedMirrorHeads, inboundMoveMountIds);
|
|
4449
|
-
}
|
|
4477
|
+
},
|
|
4478
|
+
hydrationHooks: workspaceHydrationTransactionHooks
|
|
4450
4479
|
});
|
|
4451
4480
|
input.assertStillAdmitted?.();
|
|
4452
4481
|
if (result.initialPublicationMountIds && result.initialPublicationMountIds.length > 0) {
|
|
@@ -5040,7 +5069,10 @@ async function startWorker(options) {
|
|
|
5040
5069
|
sourcePath: recoverySourceOverrides.get(mount.id) ?? mount.sourcePath,
|
|
5041
5070
|
busy: mount.busyForRecovery ?? mount.busy
|
|
5042
5071
|
}));
|
|
5043
|
-
await (0, import_workspace_git_sync.recoverWorkspaceGitHydration)(workspaceShadowRoot, recoveryMounts, {
|
|
5072
|
+
await (0, import_workspace_git_sync.recoverWorkspaceGitHydration)(workspaceShadowRoot, recoveryMounts, {
|
|
5073
|
+
preserveStaleBases: true,
|
|
5074
|
+
hydrationHooks: workspaceHydrationTransactionHooks
|
|
5075
|
+
});
|
|
5044
5076
|
for (const project of message.projects) {
|
|
5045
5077
|
for (const { branchName } of project.preserveOnlyBranches) {
|
|
5046
5078
|
if (stillPendingCreatedBranches.has((0, import_workspace_automatic_sync_policy.pendingCreatedBranchKey)(project.projectId, branchName))) {
|
|
@@ -5084,7 +5116,7 @@ async function startWorker(options) {
|
|
|
5084
5116
|
// layouts can be marked ready. No process can resolve an unready
|
|
5085
5117
|
// target, and preserving the fetched outer tree here prevents clean
|
|
5086
5118
|
// checkout initialization from erasing synchronized dirt.
|
|
5087
|
-
{ ignoreBusy: true }
|
|
5119
|
+
{ ignoreBusy: true, hydrationHooks: workspaceHydrationTransactionHooks }
|
|
5088
5120
|
);
|
|
5089
5121
|
await ensureConfiguredProjects((progress) => {
|
|
5090
5122
|
sendLifecycleProgress({ phase: "configuring", operationId: configurationOperationId, ...progress });
|
package/dist/cjs/package.json
CHANGED
|
@@ -44,6 +44,16 @@ var import_node_crypto = require("node:crypto");
|
|
|
44
44
|
var import_git_process_environment = require("./git-process-environment.cjs");
|
|
45
45
|
const HFS_IGNORED_CODE_POINTS = /[\u200c-\u200f\u202a-\u202e\u206a-\u206f\ufeff]/gu;
|
|
46
46
|
const MIRROR_TEMPORARY_FILE_PREFIX = ".r5d-working-tree-mirror-";
|
|
47
|
+
const OBJECT_ID_PATTERN = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/u;
|
|
48
|
+
const GITLINK_STALE_HEADS_FILE = "gitlink-heads.json";
|
|
49
|
+
const NON_RECURSIVE_GIT_CONFIG = [
|
|
50
|
+
"-c",
|
|
51
|
+
"submodule.recurse=false",
|
|
52
|
+
"-c",
|
|
53
|
+
"fetch.recurseSubmodules=false",
|
|
54
|
+
"-c",
|
|
55
|
+
"push.recurseSubmodules=false"
|
|
56
|
+
];
|
|
47
57
|
function isPortableGitMetadataSegment(segment) {
|
|
48
58
|
const hfsFolded = segment.replace(HFS_IGNORED_CODE_POINTS, "").toLowerCase();
|
|
49
59
|
if (hfsFolded === ".git") return true;
|
|
@@ -119,42 +129,145 @@ function inspectDirectoryRoot(root, options) {
|
|
|
119
129
|
}
|
|
120
130
|
return true;
|
|
121
131
|
}
|
|
122
|
-
function
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
132
|
+
function lstatIfExists(targetPath) {
|
|
133
|
+
try {
|
|
134
|
+
return import_node_fs.default.lstatSync(targetPath);
|
|
135
|
+
} catch (error) {
|
|
136
|
+
if (error.code === "ENOENT") return null;
|
|
137
|
+
throw error;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
function nulRecords(content) {
|
|
141
|
+
const records = [];
|
|
142
|
+
let start = 0;
|
|
143
|
+
for (let index = 0; index < content.length; index += 1) {
|
|
144
|
+
if (content[index] !== 0) continue;
|
|
145
|
+
if (index > start) records.push(content.subarray(start, index));
|
|
146
|
+
start = index + 1;
|
|
147
|
+
}
|
|
148
|
+
if (start < content.length) records.push(content.subarray(start));
|
|
149
|
+
return records;
|
|
150
|
+
}
|
|
151
|
+
function gitOutput(cwd, args, action, options = {}) {
|
|
152
|
+
const result = Bun.spawnSync(["git", ...NON_RECURSIVE_GIT_CONFIG, ...args], {
|
|
153
|
+
cwd,
|
|
154
|
+
...options.stdin ? { stdin: options.stdin } : {},
|
|
155
|
+
stdout: "pipe",
|
|
156
|
+
stderr: "pipe",
|
|
157
|
+
env: options.environment ?? (0, import_git_process_environment.workerGitProcessEnvironment)()
|
|
158
|
+
});
|
|
147
159
|
if (result.exitCode !== 0) {
|
|
148
|
-
throw new Error(
|
|
160
|
+
throw new Error(`${action}: ${result.stderr.toString().trim() || `git exited ${result.exitCode}`}`);
|
|
149
161
|
}
|
|
150
|
-
return result.stdout
|
|
151
|
-
const normalized = normalizeRelativePath(entry);
|
|
152
|
-
return normalized ? [normalized] : [];
|
|
153
|
-
}).sort();
|
|
162
|
+
return Buffer.from(result.stdout);
|
|
154
163
|
}
|
|
155
|
-
function
|
|
164
|
+
function listGitCheckout(root) {
|
|
165
|
+
const output = gitOutput(
|
|
166
|
+
root,
|
|
167
|
+
["ls-files", "-z", "-t", "--stage", "--cached", "--others", "--exclude-standard", "--", "."],
|
|
168
|
+
"Inspect Git working tree"
|
|
169
|
+
);
|
|
170
|
+
const roots = /* @__PURE__ */ new Map();
|
|
171
|
+
const indexRecords = [];
|
|
172
|
+
for (const record of nulRecords(output)) {
|
|
173
|
+
const text = record.toString();
|
|
174
|
+
const other = /^\? (.*)$/su.exec(text);
|
|
175
|
+
if (other) {
|
|
176
|
+
const normalized2 = normalizeRelativePath(other[1]);
|
|
177
|
+
if (normalized2 && !roots.has(normalized2)) roots.set(normalized2, other[1].endsWith("/"));
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
const staged = /^[A-Z] ([0-7]{6}) ([0-9a-f]{40,64}) ([0-3])\t(.*)$/su.exec(text);
|
|
181
|
+
if (!staged) continue;
|
|
182
|
+
const normalized = normalizeRelativePath(staged[4]);
|
|
183
|
+
if (!normalized) continue;
|
|
184
|
+
indexRecords.push({ mode: staged[1], objectId: staged[2], stage: Number(staged[3]), relativePath: normalized });
|
|
185
|
+
if (!roots.has(normalized)) roots.set(normalized, false);
|
|
186
|
+
}
|
|
187
|
+
return {
|
|
188
|
+
roots: [...roots].map(([relativePath, nestedRepository]) => ({ relativePath, nestedRepository })).sort((left, right) => left.relativePath < right.relativePath ? -1 : left.relativePath > right.relativePath ? 1 : 0),
|
|
189
|
+
indexRecords
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
function populatedCheckoutHead(root, absolutePath) {
|
|
193
|
+
if (!lstatIfExists(import_node_path.default.join(absolutePath, ".git"))) return null;
|
|
194
|
+
const result = Bun.spawnSync(["git", ...NON_RECURSIVE_GIT_CONFIG, "rev-parse", "--verify", "HEAD^{commit}"], {
|
|
195
|
+
cwd: absolutePath,
|
|
196
|
+
stdout: "pipe",
|
|
197
|
+
stderr: "pipe",
|
|
198
|
+
env: { ...(0, import_git_process_environment.workerGitProcessEnvironment)(), GIT_CEILING_DIRECTORIES: root }
|
|
199
|
+
});
|
|
200
|
+
if (result.exitCode !== 0) return null;
|
|
201
|
+
const head = result.stdout.toString().trim();
|
|
202
|
+
return OBJECT_ID_PATTERN.test(head) ? head : null;
|
|
203
|
+
}
|
|
204
|
+
function checkoutGitDirectory(root) {
|
|
205
|
+
const dotGit = import_node_path.default.join(root, ".git");
|
|
206
|
+
const status = lstatIfExists(dotGit);
|
|
207
|
+
if (!status) return null;
|
|
208
|
+
if (status.isDirectory()) return dotGit;
|
|
209
|
+
if (!status.isFile()) return null;
|
|
210
|
+
const match = /^gitdir:[ \t]*(.+?)\s*$/u.exec(import_node_fs.default.readFileSync(dotGit, "utf8").split(/\r?\n/u)[0] ?? "");
|
|
211
|
+
return match ? import_node_path.default.resolve(root, match[1]) : null;
|
|
212
|
+
}
|
|
213
|
+
function gitlinkStaleHeadsPath(root) {
|
|
214
|
+
const gitDirectory = checkoutGitDirectory(root);
|
|
215
|
+
return gitDirectory ? import_node_path.default.join(gitDirectory, "r5d", GITLINK_STALE_HEADS_FILE) : null;
|
|
216
|
+
}
|
|
217
|
+
function readGitlinkStaleHeads(root) {
|
|
218
|
+
const heads = /* @__PURE__ */ new Map();
|
|
219
|
+
const markerPath = gitlinkStaleHeadsPath(root);
|
|
220
|
+
if (!markerPath || !lstatIfExists(markerPath)?.isFile()) return heads;
|
|
221
|
+
let parsed;
|
|
222
|
+
try {
|
|
223
|
+
parsed = JSON.parse(import_node_fs.default.readFileSync(markerPath, "utf8"));
|
|
224
|
+
} catch {
|
|
225
|
+
return heads;
|
|
226
|
+
}
|
|
227
|
+
if (!parsed || typeof parsed !== "object" || parsed.version !== 1) return heads;
|
|
228
|
+
const recorded = parsed.heads;
|
|
229
|
+
if (!recorded || typeof recorded !== "object") return heads;
|
|
230
|
+
for (const [relativePath, objectId] of Object.entries(recorded)) {
|
|
231
|
+
const normalized = normalizeRelativePath(relativePath);
|
|
232
|
+
if (normalized && typeof objectId === "string" && OBJECT_ID_PATTERN.test(objectId)) heads.set(normalized, objectId);
|
|
233
|
+
}
|
|
234
|
+
return heads;
|
|
235
|
+
}
|
|
236
|
+
function writeGitlinkStaleHeads(root, heads) {
|
|
237
|
+
const markerPath = gitlinkStaleHeadsPath(root);
|
|
238
|
+
if (!markerPath) return;
|
|
239
|
+
const directory = import_node_path.default.dirname(markerPath);
|
|
240
|
+
if (heads.size === 0) {
|
|
241
|
+
if (!lstatIfExists(markerPath)) return;
|
|
242
|
+
import_node_fs.default.rmSync(markerPath, { force: true });
|
|
243
|
+
fsyncPath(directory, import_node_fs.default.constants.O_RDONLY);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
const directoryStatus = lstatIfExists(directory);
|
|
247
|
+
if (!directoryStatus) import_node_fs.default.mkdirSync(directory, { mode: 448 });
|
|
248
|
+
else if (!directoryStatus.isDirectory() || directoryStatus.isSymbolicLink()) {
|
|
249
|
+
throw new Error(`Checkout private state is not a regular directory: ${directory}`);
|
|
250
|
+
}
|
|
251
|
+
const temporaryPath = import_node_path.default.join(directory, `.${GITLINK_STALE_HEADS_FILE}.${(0, import_node_crypto.randomUUID)()}.tmp`);
|
|
252
|
+
const descriptor = import_node_fs.default.openSync(temporaryPath, import_node_fs.default.constants.O_WRONLY | import_node_fs.default.constants.O_CREAT | import_node_fs.default.constants.O_EXCL, 384);
|
|
253
|
+
try {
|
|
254
|
+
import_node_fs.default.writeFileSync(descriptor, `${JSON.stringify({ version: 1, heads: Object.fromEntries([...heads].sort()) })}
|
|
255
|
+
`);
|
|
256
|
+
import_node_fs.default.fsyncSync(descriptor);
|
|
257
|
+
} finally {
|
|
258
|
+
import_node_fs.default.closeSync(descriptor);
|
|
259
|
+
}
|
|
260
|
+
import_node_fs.default.renameSync(temporaryPath, markerPath);
|
|
261
|
+
fsyncPath(directory, import_node_fs.default.constants.O_RDONLY);
|
|
262
|
+
}
|
|
263
|
+
function addEntry(entries, root, relativePath, recurseDirectories, gitlinks) {
|
|
156
264
|
const normalized = normalizeRelativePath(relativePath);
|
|
157
265
|
if (!normalized || entries.has(normalized)) return;
|
|
266
|
+
const gitlink = gitlinks?.get(normalized);
|
|
267
|
+
if (gitlink !== void 0) {
|
|
268
|
+
entries.set(normalized, { kind: "gitlink", objectId: gitlink });
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
158
271
|
const inspected = inspectRelativePathWithoutFollowingAncestors(root, normalized);
|
|
159
272
|
if (inspected.kind !== "entry") return;
|
|
160
273
|
const { absolutePath, stat } = inspected;
|
|
@@ -173,7 +286,7 @@ function addEntry(entries, root, relativePath, recurseDirectories) {
|
|
|
173
286
|
entries.set(normalized, { kind: "directory", mode: stat.mode });
|
|
174
287
|
if (!recurseDirectories) return;
|
|
175
288
|
for (const child of import_node_fs.default.readdirSync(absolutePath).sort()) {
|
|
176
|
-
addEntry(entries, root, import_node_path.default.posix.join(normalized, child), true);
|
|
289
|
+
addEntry(entries, root, import_node_path.default.posix.join(normalized, child), true, gitlinks);
|
|
177
290
|
}
|
|
178
291
|
}
|
|
179
292
|
function addParentDirectories(entries, root) {
|
|
@@ -191,57 +304,59 @@ function addParentDirectories(entries, root) {
|
|
|
191
304
|
}
|
|
192
305
|
}
|
|
193
306
|
}
|
|
194
|
-
function
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
],
|
|
210
|
-
{
|
|
211
|
-
cwd: root,
|
|
212
|
-
stdout: "pipe",
|
|
213
|
-
stderr: "pipe",
|
|
214
|
-
env: (0, import_git_process_environment.workerGitProcessEnvironment)()
|
|
307
|
+
function inspectGitWorkingTree(root) {
|
|
308
|
+
const entries = /* @__PURE__ */ new Map();
|
|
309
|
+
const listing = listGitCheckout(root);
|
|
310
|
+
const indexGitlinks = /* @__PURE__ */ new Map();
|
|
311
|
+
for (const record of listing.indexRecords) {
|
|
312
|
+
if (record.mode === "160000" && !indexGitlinks.has(record.relativePath)) indexGitlinks.set(record.relativePath, record.objectId);
|
|
313
|
+
}
|
|
314
|
+
const submodules = /* @__PURE__ */ new Map();
|
|
315
|
+
let staleHeads = null;
|
|
316
|
+
for (const { relativePath, nestedRepository } of listing.roots) {
|
|
317
|
+
if (entries.has(relativePath)) continue;
|
|
318
|
+
const indexObjectId = indexGitlinks.get(relativePath);
|
|
319
|
+
if (indexObjectId === void 0 && !nestedRepository) {
|
|
320
|
+
addEntry(entries, root, relativePath, true);
|
|
321
|
+
continue;
|
|
215
322
|
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
if (
|
|
323
|
+
const inspected = inspectRelativePathWithoutFollowingAncestors(root, relativePath);
|
|
324
|
+
if (inspected.kind !== "entry") continue;
|
|
325
|
+
if (!inspected.stat.isDirectory() || inspected.stat.isSymbolicLink()) {
|
|
326
|
+
addEntry(entries, root, relativePath, true);
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
const head = populatedCheckoutHead(root, inspected.absolutePath);
|
|
330
|
+
submodules.set(relativePath, head);
|
|
331
|
+
let objectId = indexObjectId ?? head;
|
|
332
|
+
if (head && indexObjectId !== void 0 && head !== indexObjectId) {
|
|
333
|
+
staleHeads ??= readGitlinkStaleHeads(root);
|
|
334
|
+
objectId = staleHeads.get(relativePath) === head ? indexObjectId : head;
|
|
335
|
+
}
|
|
336
|
+
if (objectId) entries.set(relativePath, { kind: "gitlink", objectId });
|
|
226
337
|
}
|
|
227
|
-
|
|
338
|
+
addParentDirectories(entries, root);
|
|
339
|
+
return { entries, indexRecords: listing.indexRecords, submodules };
|
|
228
340
|
}
|
|
229
|
-
function
|
|
230
|
-
const
|
|
231
|
-
|
|
341
|
+
function normalizedGitlinks(gitlinks) {
|
|
342
|
+
const normalized = /* @__PURE__ */ new Map();
|
|
343
|
+
for (const [relativePath, objectId] of gitlinks ?? []) {
|
|
344
|
+
const relative = normalizeRelativePath(relativePath);
|
|
345
|
+
if (!relative) throw new Error(`Invalid working-tree gitlink path: ${relativePath}`);
|
|
346
|
+
if (!OBJECT_ID_PATTERN.test(objectId)) throw new Error(`Invalid working-tree gitlink object id for ${relativePath}: ${objectId}`);
|
|
347
|
+
normalized.set(relative, objectId);
|
|
348
|
+
}
|
|
349
|
+
return normalized;
|
|
232
350
|
}
|
|
233
351
|
function inspectWorkingTree(root, mode, options = {}) {
|
|
234
352
|
const entries = /* @__PURE__ */ new Map();
|
|
235
353
|
root = import_node_path.default.resolve(root);
|
|
236
354
|
if (!inspectDirectoryRoot(root, { allowMissing: true, label: "Working-tree root" })) return entries;
|
|
237
|
-
if (mode === "
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
if (omittedGitlinks?.has(relativePath) && directoryIsEmpty(root, relativePath)) continue;
|
|
243
|
-
addEntry(entries, root, relativePath, true);
|
|
244
|
-
}
|
|
355
|
+
if (mode === "git") return inspectGitWorkingTree(root).entries;
|
|
356
|
+
const gitlinks = normalizedGitlinks(options.gitlinks);
|
|
357
|
+
for (const child of import_node_fs.default.readdirSync(root).sort()) addEntry(entries, root, child, true, gitlinks);
|
|
358
|
+
for (const [relativePath, objectId] of gitlinks) {
|
|
359
|
+
if (entries.get(relativePath)?.kind !== "gitlink") entries.set(relativePath, { kind: "gitlink", objectId });
|
|
245
360
|
}
|
|
246
361
|
addParentDirectories(entries, root);
|
|
247
362
|
return entries;
|
|
@@ -391,6 +506,9 @@ function copyEntry(sourceRoot, targetRoot, relativePath, entry, durability) {
|
|
|
391
506
|
}
|
|
392
507
|
return;
|
|
393
508
|
}
|
|
509
|
+
if (entry.kind === "gitlink") {
|
|
510
|
+
throw new Error(`Working-tree gitlink cannot be copied as content: ${relativePath}`);
|
|
511
|
+
}
|
|
394
512
|
if (!source.stat.isFile() || source.stat.isSymbolicLink() || source.stat.size !== entry.size) {
|
|
395
513
|
throw new Error(`Working-tree source changed while it was being mirrored: ${sourcePath}`);
|
|
396
514
|
}
|
|
@@ -404,6 +522,110 @@ function copyEntry(sourceRoot, targetRoot, relativePath, entry, durability) {
|
|
|
404
522
|
copyRegularFile(sourcePath, targetPath, entry, durability);
|
|
405
523
|
}
|
|
406
524
|
}
|
|
525
|
+
function inspectMirrorTarget(targetRoot, deletionMode) {
|
|
526
|
+
if (deletionMode !== "git") return { existing: inspectWorkingTree(targetRoot, "all"), git: null };
|
|
527
|
+
const inspection = inspectGitWorkingTree(targetRoot);
|
|
528
|
+
const occupied = /* @__PURE__ */ new Set();
|
|
529
|
+
for (const relativePath of inspection.submodules.keys()) {
|
|
530
|
+
const inspected = inspectRelativePathWithoutFollowingAncestors(targetRoot, relativePath);
|
|
531
|
+
if (inspected.kind === "entry" && inspected.stat.isDirectory() && import_node_fs.default.readdirSync(inspected.absolutePath).length > 0) {
|
|
532
|
+
occupied.add(relativePath);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
return { existing: inspection.entries, git: { indexRecords: inspection.indexRecords, submodules: inspection.submodules, occupied } };
|
|
536
|
+
}
|
|
537
|
+
function shieldedDesiredPaths(desired, git) {
|
|
538
|
+
const shielded = /* @__PURE__ */ new Set();
|
|
539
|
+
if (git.submodules.size === 0) return shielded;
|
|
540
|
+
const roots = new Set(git.submodules.keys());
|
|
541
|
+
for (const [relativePath, entry] of desired) {
|
|
542
|
+
if (hasAncestorIn(relativePath, roots)) {
|
|
543
|
+
shielded.add(relativePath);
|
|
544
|
+
continue;
|
|
545
|
+
}
|
|
546
|
+
if (!roots.has(relativePath) || entry.kind === "gitlink") continue;
|
|
547
|
+
if (entry.kind === "directory" || git.occupied.has(relativePath)) shielded.add(relativePath);
|
|
548
|
+
}
|
|
549
|
+
return shielded;
|
|
550
|
+
}
|
|
551
|
+
function directoryShieldsSubmodule(relativePath, entry, git) {
|
|
552
|
+
if (!git || entry.kind !== "directory") return false;
|
|
553
|
+
const prefix = `${relativePath}/`;
|
|
554
|
+
for (const root of git.submodules.keys()) if (root.startsWith(prefix)) return true;
|
|
555
|
+
return false;
|
|
556
|
+
}
|
|
557
|
+
function planIndexChanges(desired, shielded, git) {
|
|
558
|
+
const removed = [];
|
|
559
|
+
const written = [];
|
|
560
|
+
const desiredGitlinks = /* @__PURE__ */ new Map();
|
|
561
|
+
for (const [relativePath, entry] of desired) {
|
|
562
|
+
if (entry.kind === "gitlink" && !shielded.has(relativePath)) desiredGitlinks.set(relativePath, entry.objectId);
|
|
563
|
+
}
|
|
564
|
+
const desiredGitlinkRoots = new Set(desiredGitlinks.keys());
|
|
565
|
+
const currentGitlinks = /* @__PURE__ */ new Map();
|
|
566
|
+
for (const record of git.indexRecords) {
|
|
567
|
+
const desiredObjectId = desiredGitlinks.get(record.relativePath);
|
|
568
|
+
if (record.mode === "160000") {
|
|
569
|
+
if (record.stage === 0) currentGitlinks.set(record.relativePath, record.objectId);
|
|
570
|
+
if (desiredObjectId !== void 0) {
|
|
571
|
+
if (desiredObjectId !== record.objectId) removed.push(record);
|
|
572
|
+
continue;
|
|
573
|
+
}
|
|
574
|
+
const desiredEntry = desired.get(record.relativePath);
|
|
575
|
+
if (desiredEntry?.kind !== "directory" && desiredEntry?.kind !== "gitlink" && !git.occupied.has(record.relativePath)) {
|
|
576
|
+
removed.push(record);
|
|
577
|
+
}
|
|
578
|
+
continue;
|
|
579
|
+
}
|
|
580
|
+
if (desiredObjectId !== void 0 || hasAncestorIn(record.relativePath, desiredGitlinkRoots)) removed.push(record);
|
|
581
|
+
}
|
|
582
|
+
for (const [relativePath, objectId] of desiredGitlinks) {
|
|
583
|
+
if (currentGitlinks.get(relativePath) !== objectId) written.push({ relativePath, objectId });
|
|
584
|
+
}
|
|
585
|
+
return { removed, written };
|
|
586
|
+
}
|
|
587
|
+
function indexInfoRecord(record) {
|
|
588
|
+
const stage = record.stage === 0 ? "" : ` ${record.stage}`;
|
|
589
|
+
return Buffer.concat([Buffer.from(`${record.mode} ${record.objectId}${stage} `), Buffer.from(record.relativePath), Buffer.from([0])]);
|
|
590
|
+
}
|
|
591
|
+
function updateIndex(targetRoot, changes) {
|
|
592
|
+
if (changes.drop.length === 0 && changes.put.length === 0) return;
|
|
593
|
+
const feed = Buffer.concat([
|
|
594
|
+
...changes.drop.map(
|
|
595
|
+
({ relativePath, objectId }) => indexInfoRecord({ mode: "0", objectId: "0".repeat(objectId.length), stage: 0, relativePath })
|
|
596
|
+
),
|
|
597
|
+
...changes.put.map(indexInfoRecord)
|
|
598
|
+
]);
|
|
599
|
+
gitOutput(
|
|
600
|
+
targetRoot,
|
|
601
|
+
["-c", "core.fsync=index", "-c", "core.fsyncMethod=fsync", "update-index", "-z", "--index-info"],
|
|
602
|
+
"Update Git working-tree index",
|
|
603
|
+
{ stdin: feed }
|
|
604
|
+
);
|
|
605
|
+
const gitDirectory = checkoutGitDirectory(targetRoot);
|
|
606
|
+
if (!gitDirectory) return;
|
|
607
|
+
const indexPath = import_node_path.default.join(gitDirectory, "index");
|
|
608
|
+
if (lstatIfExists(indexPath)?.isFile()) fsyncPath(indexPath, import_node_fs.default.constants.O_RDONLY | import_node_fs.default.constants.O_NOFOLLOW);
|
|
609
|
+
fsyncPath(gitDirectory, import_node_fs.default.constants.O_RDONLY);
|
|
610
|
+
}
|
|
611
|
+
function recordGitlinkStaleHeads(targetRoot, desired, shielded, git) {
|
|
612
|
+
const heads = readGitlinkStaleHeads(targetRoot);
|
|
613
|
+
let changed = false;
|
|
614
|
+
for (const [relativePath, head] of git.submodules) {
|
|
615
|
+
const entry = desired.get(relativePath);
|
|
616
|
+
const next = entry?.kind === "gitlink" && !shielded.has(relativePath) && head && head !== entry.objectId ? head : null;
|
|
617
|
+
if ((heads.get(relativePath) ?? null) === next) continue;
|
|
618
|
+
if (next) heads.set(relativePath, next);
|
|
619
|
+
else heads.delete(relativePath);
|
|
620
|
+
changed = true;
|
|
621
|
+
}
|
|
622
|
+
for (const relativePath of [...heads.keys()]) {
|
|
623
|
+
if (git.submodules.has(relativePath)) continue;
|
|
624
|
+
heads.delete(relativePath);
|
|
625
|
+
changed = true;
|
|
626
|
+
}
|
|
627
|
+
if (changed) writeGitlinkStaleHeads(targetRoot, heads);
|
|
628
|
+
}
|
|
407
629
|
function mirrorWorkingTree(input) {
|
|
408
630
|
const sourceRoot = import_node_path.default.resolve(input.sourceRoot);
|
|
409
631
|
const targetRoot = import_node_path.default.resolve(input.targetRoot);
|
|
@@ -412,19 +634,34 @@ function mirrorWorkingTree(input) {
|
|
|
412
634
|
import_node_fs.default.mkdirSync(targetRoot, { recursive: true });
|
|
413
635
|
}
|
|
414
636
|
inspectDirectoryRoot(targetRoot, { allowMissing: false, label: "Working-tree target root" });
|
|
415
|
-
const desired = inspectWorkingTree(sourceRoot, input.sourceMode);
|
|
416
|
-
const
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
for (const relativePath of [...existing.keys()].sort(
|
|
420
|
-
if (
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
(
|
|
424
|
-
|
|
637
|
+
const desired = inspectWorkingTree(sourceRoot, input.sourceMode, { gitlinks: input.sourceGitlinks });
|
|
638
|
+
const target = inspectMirrorTarget(targetRoot, input.deletionMode);
|
|
639
|
+
const shielded = target.git ? shieldedDesiredPaths(desired, target.git) : /* @__PURE__ */ new Set();
|
|
640
|
+
const index = target.git ? planIndexChanges(desired, shielded, target.git) : null;
|
|
641
|
+
for (const relativePath of [...target.existing.keys()].sort(deepestFirst)) {
|
|
642
|
+
if (desired.has(relativePath)) continue;
|
|
643
|
+
const entry = target.existing.get(relativePath);
|
|
644
|
+
if (entry.kind === "gitlink" || directoryShieldsSubmodule(relativePath, entry, target.git)) continue;
|
|
645
|
+
removeEntry(targetRoot, relativePath);
|
|
646
|
+
}
|
|
647
|
+
const gitlinks = [];
|
|
648
|
+
for (const [relativePath, entry] of [...desired.entries()].sort(([left], [right]) => shallowestFirst(left, right))) {
|
|
649
|
+
if (shielded.has(relativePath)) continue;
|
|
650
|
+
if (entry.kind === "gitlink") {
|
|
651
|
+
ensureDirectory(targetRoot, relativePath, 493);
|
|
652
|
+
gitlinks.push({ relativePath, objectId: entry.objectId });
|
|
653
|
+
continue;
|
|
654
|
+
}
|
|
425
655
|
copyEntry(sourceRoot, targetRoot, relativePath, entry, durability);
|
|
426
656
|
}
|
|
427
|
-
|
|
657
|
+
if (index && target.git) {
|
|
658
|
+
updateIndex(targetRoot, { drop: index.removed, put: index.written.map((gitlink) => ({ mode: "160000", stage: 0, ...gitlink })) });
|
|
659
|
+
recordGitlinkStaleHeads(targetRoot, desired, shielded, target.git);
|
|
660
|
+
}
|
|
661
|
+
return {
|
|
662
|
+
paths: [...desired.keys()].filter((relativePath) => !shielded.has(relativePath) && desired.get(relativePath)?.kind !== "directory").sort(),
|
|
663
|
+
gitlinks
|
|
664
|
+
};
|
|
428
665
|
}
|
|
429
666
|
function normalizeWorkingTreeRelativePath(value) {
|
|
430
667
|
return normalizeRelativePath(value);
|
|
@@ -464,23 +701,25 @@ function fsyncPath(targetPath, flags) {
|
|
|
464
701
|
function planWorkingTreeMirror(input) {
|
|
465
702
|
const sourceRoot = import_node_path.default.resolve(input.sourceRoot);
|
|
466
703
|
const targetRoot = import_node_path.default.resolve(input.targetRoot);
|
|
467
|
-
const
|
|
468
|
-
const
|
|
469
|
-
|
|
470
|
-
|
|
704
|
+
const inspectedDesired = inspectWorkingTree(sourceRoot, input.sourceMode, { gitlinks: input.sourceGitlinks });
|
|
705
|
+
const target = inspectMirrorTarget(targetRoot, input.deletionMode);
|
|
706
|
+
const shielded = target.git ? shieldedDesiredPaths(inspectedDesired, target.git) : /* @__PURE__ */ new Set();
|
|
707
|
+
const desired = new Map([...inspectedDesired].filter(([relativePath]) => !shielded.has(relativePath)));
|
|
471
708
|
const replaced = /* @__PURE__ */ new Map();
|
|
472
709
|
const absent = [];
|
|
473
|
-
for (const [relativePath, entry] of existing) {
|
|
474
|
-
if (
|
|
710
|
+
for (const [relativePath, entry] of target.existing) {
|
|
711
|
+
if (inspectedDesired.has(relativePath) || entry.kind === "gitlink") continue;
|
|
712
|
+
if (directoryShieldsSubmodule(relativePath, entry, target.git)) continue;
|
|
713
|
+
replaced.set(relativePath, entry);
|
|
475
714
|
}
|
|
476
715
|
for (const [relativePath, entry] of desired) {
|
|
477
|
-
const
|
|
478
|
-
if (
|
|
716
|
+
const targetEntry = inspectRelativePathWithoutFollowingAncestors(targetRoot, relativePath);
|
|
717
|
+
if (targetEntry.kind !== "entry") {
|
|
479
718
|
absent.push(relativePath);
|
|
480
719
|
continue;
|
|
481
720
|
}
|
|
482
|
-
const { absolutePath, stat } =
|
|
483
|
-
if (entry.kind === "directory") {
|
|
721
|
+
const { absolutePath, stat } = targetEntry;
|
|
722
|
+
if (entry.kind === "directory" || entry.kind === "gitlink") {
|
|
484
723
|
if (stat.isDirectory()) continue;
|
|
485
724
|
} else if (entry.kind === "symlink") {
|
|
486
725
|
if (stat.isSymbolicLink() && import_node_fs.default.readlinkSync(absolutePath) === entry.target) continue;
|
|
@@ -500,7 +739,8 @@ function planWorkingTreeMirror(input) {
|
|
|
500
739
|
return {
|
|
501
740
|
desired,
|
|
502
741
|
replaced,
|
|
503
|
-
absent: absent.filter((relativePath) => !hasAncestorIn(relativePath, replacedDirectories)).sort(shallowestFirst)
|
|
742
|
+
absent: absent.filter((relativePath) => !hasAncestorIn(relativePath, replacedDirectories)).sort(shallowestFirst),
|
|
743
|
+
index: target.git ? planIndexChanges(inspectedDesired, shielded, target.git) : { removed: [], written: [] }
|
|
504
744
|
};
|
|
505
745
|
}
|
|
506
746
|
function captureWorkingTreeMirrorPlan(input) {
|
|
@@ -513,6 +753,7 @@ function captureWorkingTreeMirrorPlan(input) {
|
|
|
513
753
|
}
|
|
514
754
|
const trees = [];
|
|
515
755
|
for (const [relativePath, entry] of [...input.plan.replaced.entries()].sort(([left], [right]) => shallowestFirst(left, right))) {
|
|
756
|
+
if (entry.kind === "gitlink") continue;
|
|
516
757
|
if (entry.kind !== "directory") {
|
|
517
758
|
copyEntry(targetRoot, snapshotRoot, relativePath, entry, durability);
|
|
518
759
|
continue;
|
|
@@ -531,7 +772,12 @@ function captureWorkingTreeMirrorPlan(input) {
|
|
|
531
772
|
});
|
|
532
773
|
trees.push(relativePath);
|
|
533
774
|
}
|
|
534
|
-
|
|
775
|
+
const { index } = input.plan;
|
|
776
|
+
return {
|
|
777
|
+
absent: [...input.plan.absent].sort(shallowestFirst),
|
|
778
|
+
trees: trees.sort(shallowestFirst),
|
|
779
|
+
...index.removed.length > 0 || index.written.length > 0 ? { index } : {}
|
|
780
|
+
};
|
|
535
781
|
}
|
|
536
782
|
function restoreWorkingTreeMirrorScope(input) {
|
|
537
783
|
const snapshotRoot = import_node_path.default.resolve(input.snapshotRoot);
|
|
@@ -556,6 +802,7 @@ function restoreWorkingTreeMirrorScope(input) {
|
|
|
556
802
|
const touched = /* @__PURE__ */ new Set();
|
|
557
803
|
for (const relativePath of [...absent].sort(deepestFirst)) {
|
|
558
804
|
const inspected = inspectRelativePathWithoutFollowingAncestors(targetRoot, relativePath);
|
|
805
|
+
if (inspected.kind === "entry" && inspected.stat.isDirectory() && lstatIfExists(import_node_path.default.join(inspected.absolutePath, ".git"))) continue;
|
|
559
806
|
if (inspected.kind === "entry") import_node_fs.default.rmSync(inspected.absolutePath, { recursive: true, force: true });
|
|
560
807
|
touched.add(relativePath);
|
|
561
808
|
}
|
|
@@ -593,7 +840,7 @@ function restoreWorkingTreeMirrorScope(input) {
|
|
|
593
840
|
if (current.kind === "entry" && !current.stat.isDirectory()) {
|
|
594
841
|
import_node_fs.default.rmSync(current.absolutePath, { recursive: true, force: true });
|
|
595
842
|
}
|
|
596
|
-
ensureDirectory(targetRoot, relativePath, entry.mode);
|
|
843
|
+
ensureDirectory(targetRoot, relativePath, entry.kind === "directory" ? entry.mode : 493);
|
|
597
844
|
mirrorWorkingTree({
|
|
598
845
|
sourceRoot: import_node_path.default.join(snapshotRoot, ...relativePath.split("/")),
|
|
599
846
|
targetRoot: import_node_path.default.join(targetRoot, ...relativePath.split("/")),
|
|
@@ -601,6 +848,7 @@ function restoreWorkingTreeMirrorScope(input) {
|
|
|
601
848
|
deletionMode: "all"
|
|
602
849
|
});
|
|
603
850
|
}
|
|
851
|
+
if (input.scope.index) updateIndex(targetRoot, { drop: input.scope.index.written, put: input.scope.index.removed });
|
|
604
852
|
return { paths: [...touched].sort(shallowestFirst) };
|
|
605
853
|
}
|
|
606
854
|
function fsyncWorkingTreePaths(root, relativePaths) {
|