@ricsam/r5d-worker 0.0.132 → 0.0.133
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/atomic-rename.cjs +303 -0
- package/dist/cjs/git-blob-hash.cjs +41 -0
- package/dist/cjs/main.cjs +187 -38
- package/dist/cjs/package.json +1 -1
- package/dist/cjs/three-way-merge.cjs +346 -0
- package/dist/cjs/working-tree-mirror.cjs +1049 -64
- package/dist/cjs/workspace-command-sync-policy.cjs +8 -4
- package/dist/cjs/workspace-command-targets.cjs +63 -0
- package/dist/cjs/workspace-filesystem-job-types.cjs +11 -1
- package/dist/cjs/workspace-filesystem-jobs.cjs +2 -0
- package/dist/cjs/workspace-git-sync.cjs +846 -61
- package/dist/cjs/workspace-hydration-ledger.cjs +66 -0
- package/dist/cjs/workspace-hydration-merge.cjs +433 -0
- package/dist/cjs/workspace-hydration-recovery-state.cjs +53 -0
- package/dist/cjs/workspace-merge-projection.cjs +81 -10
- package/dist/cjs/workspace-project-config-policy.cjs +19 -12
- package/dist/mjs/atomic-rename.mjs +261 -0
- package/dist/mjs/git-blob-hash.mjs +16 -0
- package/dist/mjs/main.mjs +196 -39
- package/dist/mjs/package.json +1 -1
- package/dist/mjs/three-way-merge.mjs +318 -0
- package/dist/mjs/working-tree-mirror.mjs +1035 -64
- package/dist/mjs/workspace-command-sync-policy.mjs +8 -4
- package/dist/mjs/workspace-command-targets.mjs +37 -0
- package/dist/mjs/workspace-filesystem-job-types.mjs +11 -1
- package/dist/mjs/workspace-filesystem-jobs.mjs +4 -0
- package/dist/mjs/workspace-git-sync.mjs +854 -62
- package/dist/mjs/workspace-hydration-ledger.mjs +42 -0
- package/dist/mjs/workspace-hydration-merge.mjs +399 -0
- package/dist/mjs/workspace-hydration-recovery-state.mjs +29 -0
- package/dist/mjs/workspace-merge-projection.mjs +85 -11
- package/dist/mjs/workspace-project-config-policy.mjs +16 -10
- package/dist/types/atomic-rename.d.ts +78 -0
- package/dist/types/git-blob-hash.d.ts +10 -0
- package/dist/types/main.d.ts +21 -2
- package/dist/types/three-way-merge.d.ts +77 -0
- package/dist/types/working-tree-mirror.d.ts +270 -7
- package/dist/types/workspace-command-sync-policy.d.ts +12 -6
- package/dist/types/workspace-command-targets.d.ts +37 -0
- package/dist/types/workspace-filesystem-job-types.d.ts +46 -4
- package/dist/types/workspace-git-sync.d.ts +125 -3
- package/dist/types/workspace-hydration-ledger.d.ts +43 -0
- package/dist/types/workspace-hydration-merge.d.ts +95 -0
- package/dist/types/workspace-hydration-recovery-state.d.ts +10 -0
- package/dist/types/workspace-merge-projection.d.ts +19 -1
- package/dist/types/workspace-project-config-policy.d.ts +17 -3
- package/package.json +2 -2
|
@@ -1,7 +1,43 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
4
4
|
import { workerGitProcessEnvironment } from "./git-process-environment.mjs";
|
|
5
|
+
import { AtomicRenameError, atomicRenamePrimitivesFor, isAtomicRenameUnsupported } from "./atomic-rename.mjs";
|
|
6
|
+
function projectedFileEntry(stat) {
|
|
7
|
+
return { kind: "file", size: stat.size, mode: stat.mode, mtimeMs: stat.mtimeMs, ctimeMs: stat.ctimeMs, ino: stat.ino };
|
|
8
|
+
}
|
|
9
|
+
function projectedFileEntryMatches(entry, stat) {
|
|
10
|
+
return stat.isFile() && stat.ino === entry.ino && stat.size === entry.size && stat.mode === entry.mode && stat.mtimeMs === entry.mtimeMs && stat.ctimeMs === entry.ctimeMs;
|
|
11
|
+
}
|
|
12
|
+
function workingTreeStatsMatch(left, right) {
|
|
13
|
+
return left.isFile() === right.isFile() && left.isDirectory() === right.isDirectory() && left.isSymbolicLink() === right.isSymbolicLink() && left.ino === right.ino && left.dev === right.dev && left.size === right.size && left.mode === right.mode && left.mtimeMs === right.mtimeMs && left.ctimeMs === right.ctimeMs;
|
|
14
|
+
}
|
|
15
|
+
function sameWorkingTreeInode(left, right) {
|
|
16
|
+
return left.ino === right.ino && left.dev === right.dev;
|
|
17
|
+
}
|
|
18
|
+
function displacedEntryMatches(expected, displaced) {
|
|
19
|
+
return sameWorkingTreeInode(expected, displaced) && displaced.mode === expected.mode && displaced.size === expected.size && displaced.mtimeMs === expected.mtimeMs;
|
|
20
|
+
}
|
|
21
|
+
class WorkingTreeTargetChangedError extends Error {
|
|
22
|
+
constructor(relativePath, options = {}) {
|
|
23
|
+
const retained = options.retained ?? [];
|
|
24
|
+
super(
|
|
25
|
+
`Working-tree target changed while it was being hydrated: ${relativePath}` + (options.detail ? ` (${options.detail})` : "") + (retained.length > 0 ? `; displaced entries retained at ${retained.map(({ path: retainedPath }) => retainedPath).join(", ")}` : "")
|
|
26
|
+
);
|
|
27
|
+
this.relativePath = relativePath;
|
|
28
|
+
this.name = "WorkingTreeTargetChangedError";
|
|
29
|
+
this.retained = retained;
|
|
30
|
+
}
|
|
31
|
+
relativePath;
|
|
32
|
+
retained;
|
|
33
|
+
}
|
|
34
|
+
let raceHook = null;
|
|
35
|
+
function setWorkingTreeMirrorRaceHook(hook) {
|
|
36
|
+
raceHook = hook;
|
|
37
|
+
}
|
|
38
|
+
function raceWindow(phase, relativePath, targetPath) {
|
|
39
|
+
if (raceHook) raceHook({ phase, relativePath, targetPath });
|
|
40
|
+
}
|
|
5
41
|
const HFS_IGNORED_CODE_POINTS = /[\u200c-\u200f\u202a-\u202e\u206a-\u206f\ufeff]/gu;
|
|
6
42
|
const MIRROR_TEMPORARY_FILE_PREFIX = ".r5d-working-tree-mirror-";
|
|
7
43
|
const OBJECT_ID_PATTERN = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/u;
|
|
@@ -76,6 +112,9 @@ function inspectRelativePathWithoutFollowingAncestors(root, relativePath) {
|
|
|
76
112
|
}
|
|
77
113
|
return { kind: "missing" };
|
|
78
114
|
}
|
|
115
|
+
function inspectWorkingTreePath(root, relativePath) {
|
|
116
|
+
return inspectRelativePathWithoutFollowingAncestors(path.resolve(root), relativePath);
|
|
117
|
+
}
|
|
79
118
|
function inspectDirectoryRoot(root, options) {
|
|
80
119
|
let stat;
|
|
81
120
|
try {
|
|
@@ -220,7 +259,7 @@ function writeGitlinkStaleHeads(root, heads) {
|
|
|
220
259
|
fs.renameSync(temporaryPath, markerPath);
|
|
221
260
|
fsyncPath(directory, fs.constants.O_RDONLY);
|
|
222
261
|
}
|
|
223
|
-
function addEntry(entries, root, relativePath, recurseDirectories, gitlinks) {
|
|
262
|
+
function addEntry(entries, root, relativePath, recurseDirectories, gitlinks, stats) {
|
|
224
263
|
const normalized = normalizeRelativePath(relativePath);
|
|
225
264
|
if (!normalized || entries.has(normalized)) return;
|
|
226
265
|
const gitlink = gitlinks?.get(normalized);
|
|
@@ -233,23 +272,29 @@ function addEntry(entries, root, relativePath, recurseDirectories, gitlinks) {
|
|
|
233
272
|
const { absolutePath, stat } = inspected;
|
|
234
273
|
if (stat.isSymbolicLink()) {
|
|
235
274
|
for (const existing of [...entries.keys()]) {
|
|
236
|
-
if (existing.startsWith(`${normalized}/`))
|
|
275
|
+
if (existing.startsWith(`${normalized}/`)) {
|
|
276
|
+
entries.delete(existing);
|
|
277
|
+
stats?.delete(existing);
|
|
278
|
+
}
|
|
237
279
|
}
|
|
238
280
|
entries.set(normalized, { kind: "symlink", mode: stat.mode, target: fs.readlinkSync(absolutePath) });
|
|
281
|
+
stats?.set(normalized, stat);
|
|
239
282
|
return;
|
|
240
283
|
}
|
|
241
284
|
if (stat.isFile()) {
|
|
242
285
|
entries.set(normalized, { kind: "file", mode: stat.mode, size: stat.size });
|
|
286
|
+
stats?.set(normalized, stat);
|
|
243
287
|
return;
|
|
244
288
|
}
|
|
245
289
|
if (!stat.isDirectory()) return;
|
|
246
290
|
entries.set(normalized, { kind: "directory", mode: stat.mode });
|
|
291
|
+
stats?.set(normalized, stat);
|
|
247
292
|
if (!recurseDirectories) return;
|
|
248
293
|
for (const child of fs.readdirSync(absolutePath).sort()) {
|
|
249
|
-
addEntry(entries, root, path.posix.join(normalized, child), true, gitlinks);
|
|
294
|
+
addEntry(entries, root, path.posix.join(normalized, child), true, gitlinks, stats);
|
|
250
295
|
}
|
|
251
296
|
}
|
|
252
|
-
function addParentDirectories(entries, root) {
|
|
297
|
+
function addParentDirectories(entries, root, stats) {
|
|
253
298
|
for (const relativePath of [...entries.keys()]) {
|
|
254
299
|
let parent = path.posix.dirname(relativePath);
|
|
255
300
|
while (parent !== ".") {
|
|
@@ -259,12 +304,13 @@ function addParentDirectories(entries, root) {
|
|
|
259
304
|
throw new Error(`Working-tree entry has an unsafe parent directory: ${parent}`);
|
|
260
305
|
}
|
|
261
306
|
entries.set(parent, { kind: "directory", mode: inspected.stat.mode });
|
|
307
|
+
stats?.set(parent, inspected.stat);
|
|
262
308
|
}
|
|
263
309
|
parent = path.posix.dirname(parent);
|
|
264
310
|
}
|
|
265
311
|
}
|
|
266
312
|
}
|
|
267
|
-
function inspectGitWorkingTree(root) {
|
|
313
|
+
function inspectGitWorkingTree(root, stats) {
|
|
268
314
|
const entries = /* @__PURE__ */ new Map();
|
|
269
315
|
const listing = listGitCheckout(root);
|
|
270
316
|
const indexGitlinks = /* @__PURE__ */ new Map();
|
|
@@ -277,13 +323,13 @@ function inspectGitWorkingTree(root) {
|
|
|
277
323
|
if (entries.has(relativePath)) continue;
|
|
278
324
|
const indexObjectId = indexGitlinks.get(relativePath);
|
|
279
325
|
if (indexObjectId === void 0 && !nestedRepository) {
|
|
280
|
-
addEntry(entries, root, relativePath, true);
|
|
326
|
+
addEntry(entries, root, relativePath, true, void 0, stats);
|
|
281
327
|
continue;
|
|
282
328
|
}
|
|
283
329
|
const inspected = inspectRelativePathWithoutFollowingAncestors(root, relativePath);
|
|
284
330
|
if (inspected.kind !== "entry") continue;
|
|
285
331
|
if (!inspected.stat.isDirectory() || inspected.stat.isSymbolicLink()) {
|
|
286
|
-
addEntry(entries, root, relativePath, true);
|
|
332
|
+
addEntry(entries, root, relativePath, true, void 0, stats);
|
|
287
333
|
continue;
|
|
288
334
|
}
|
|
289
335
|
const head = populatedCheckoutHead(root, inspected.absolutePath);
|
|
@@ -293,9 +339,12 @@ function inspectGitWorkingTree(root) {
|
|
|
293
339
|
staleHeads ??= readGitlinkStaleHeads(root);
|
|
294
340
|
objectId = staleHeads.get(relativePath) === head ? indexObjectId : head;
|
|
295
341
|
}
|
|
296
|
-
if (objectId)
|
|
342
|
+
if (objectId) {
|
|
343
|
+
entries.set(relativePath, { kind: "gitlink", objectId });
|
|
344
|
+
stats?.set(relativePath, inspected.stat);
|
|
345
|
+
}
|
|
297
346
|
}
|
|
298
|
-
addParentDirectories(entries, root);
|
|
347
|
+
addParentDirectories(entries, root, stats);
|
|
299
348
|
return { entries, indexRecords: listing.indexRecords, submodules };
|
|
300
349
|
}
|
|
301
350
|
function normalizedGitlinks(gitlinks) {
|
|
@@ -312,13 +361,16 @@ function inspectWorkingTree(root, mode, options = {}) {
|
|
|
312
361
|
const entries = /* @__PURE__ */ new Map();
|
|
313
362
|
root = path.resolve(root);
|
|
314
363
|
if (!inspectDirectoryRoot(root, { allowMissing: true, label: "Working-tree root" })) return entries;
|
|
315
|
-
if (mode === "git") return inspectGitWorkingTree(root).entries;
|
|
364
|
+
if (mode === "git") return inspectGitWorkingTree(root, options.stats).entries;
|
|
316
365
|
const gitlinks = normalizedGitlinks(options.gitlinks);
|
|
317
|
-
for (const child of fs.readdirSync(root).sort()) addEntry(entries, root, child, true, gitlinks);
|
|
366
|
+
for (const child of fs.readdirSync(root).sort()) addEntry(entries, root, child, true, gitlinks, options.stats);
|
|
318
367
|
for (const [relativePath, objectId] of gitlinks) {
|
|
319
|
-
if (entries.get(relativePath)?.kind !== "gitlink")
|
|
368
|
+
if (entries.get(relativePath)?.kind !== "gitlink") {
|
|
369
|
+
entries.set(relativePath, { kind: "gitlink", objectId });
|
|
370
|
+
options.stats?.delete(relativePath);
|
|
371
|
+
}
|
|
320
372
|
}
|
|
321
|
-
addParentDirectories(entries, root);
|
|
373
|
+
addParentDirectories(entries, root, options.stats);
|
|
322
374
|
return entries;
|
|
323
375
|
}
|
|
324
376
|
function removeEntry(targetRoot, relativePath) {
|
|
@@ -329,6 +381,443 @@ function removeEntry(targetRoot, relativePath) {
|
|
|
329
381
|
}
|
|
330
382
|
fs.rmSync(inspected.absolutePath, { recursive: true, force: true });
|
|
331
383
|
}
|
|
384
|
+
function assertTargetExpectation(targetRoot, relativePath, guards) {
|
|
385
|
+
if (!guards?.expectations?.has(relativePath)) return;
|
|
386
|
+
const expected = guards.expectations.get(relativePath) ?? null;
|
|
387
|
+
const inspected = inspectRelativePathWithoutFollowingAncestors(targetRoot, relativePath);
|
|
388
|
+
if (expected === null) {
|
|
389
|
+
if (inspected.kind === "missing") return;
|
|
390
|
+
throw new WorkingTreeTargetChangedError(relativePath);
|
|
391
|
+
}
|
|
392
|
+
if (expected.isDirectory() && !expected.isSymbolicLink()) {
|
|
393
|
+
if (inspected.kind === "entry" && inspected.stat.isDirectory() && !inspected.stat.isSymbolicLink()) return;
|
|
394
|
+
throw new WorkingTreeTargetChangedError(relativePath);
|
|
395
|
+
}
|
|
396
|
+
if (inspected.kind !== "entry" || !workingTreeStatsMatch(expected, inspected.stat)) throw new WorkingTreeTargetChangedError(relativePath);
|
|
397
|
+
}
|
|
398
|
+
const DISPLACED_RETENTION_DIRECTORY = "hydration-displaced";
|
|
399
|
+
const RETAINED_NAME_MAX_ENCODED_LENGTH = 150;
|
|
400
|
+
const YIELD_ATTEMPTS = 4;
|
|
401
|
+
const passStates = /* @__PURE__ */ new WeakMap();
|
|
402
|
+
function newPassState(targetRoot) {
|
|
403
|
+
const retention = retentionDirectoryFor(targetRoot);
|
|
404
|
+
const primitives = atomicRenamePrimitivesFor(retention ?? targetRoot);
|
|
405
|
+
return { targetRoot, retention, primitives, exchangeUnavailable: primitives.support.exchange === "unavailable" };
|
|
406
|
+
}
|
|
407
|
+
function workingTreeAtomicRenameSupport(targetRoot) {
|
|
408
|
+
return newPassState(path.resolve(targetRoot)).primitives.support;
|
|
409
|
+
}
|
|
410
|
+
function guardedPassState(guards, targetRoot) {
|
|
411
|
+
let state = passStates.get(guards);
|
|
412
|
+
if (!state) {
|
|
413
|
+
state = newPassState(targetRoot);
|
|
414
|
+
passStates.set(guards, state);
|
|
415
|
+
}
|
|
416
|
+
return state;
|
|
417
|
+
}
|
|
418
|
+
function workingTreeDisplacedRetentionDirectory(targetRoot) {
|
|
419
|
+
const gitDirectory = checkoutGitDirectory(path.resolve(targetRoot));
|
|
420
|
+
return gitDirectory ? path.join(gitDirectory, "r5d", DISPLACED_RETENTION_DIRECTORY) : null;
|
|
421
|
+
}
|
|
422
|
+
function retentionDirectoryFor(targetRoot) {
|
|
423
|
+
const directory = workingTreeDisplacedRetentionDirectory(targetRoot);
|
|
424
|
+
if (!directory) return null;
|
|
425
|
+
try {
|
|
426
|
+
fs.mkdirSync(directory, { recursive: true, mode: 448 });
|
|
427
|
+
const status = fs.lstatSync(directory);
|
|
428
|
+
if (!status.isDirectory() || status.isSymbolicLink()) return null;
|
|
429
|
+
if (status.dev !== fs.lstatSync(targetRoot).dev) return null;
|
|
430
|
+
return directory;
|
|
431
|
+
} catch {
|
|
432
|
+
return null;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
function encodeRetainedName(relativePath) {
|
|
436
|
+
let encoded = "";
|
|
437
|
+
for (const byte of Buffer.from(relativePath, "utf8")) {
|
|
438
|
+
const isPlain = byte >= 48 && byte <= 57 || byte >= 65 && byte <= 90 || byte >= 97 && byte <= 122 || byte === 45 || byte === 46 || byte === 95;
|
|
439
|
+
encoded += isPlain ? String.fromCharCode(byte) : `%${byte.toString(16).padStart(2, "0")}`;
|
|
440
|
+
}
|
|
441
|
+
return encoded.length > RETAINED_NAME_MAX_ENCODED_LENGTH ? encoded.slice(0, RETAINED_NAME_MAX_ENCODED_LENGTH) : encoded;
|
|
442
|
+
}
|
|
443
|
+
function stagingPathFor(state, targetPath, relativePath) {
|
|
444
|
+
if (state.retention) return path.join(state.retention, `${randomUUID()}.${encodeRetainedName(relativePath)}`);
|
|
445
|
+
return path.join(path.dirname(targetPath), `${MIRROR_TEMPORARY_FILE_PREFIX}${randomUUID()}.tmp`);
|
|
446
|
+
}
|
|
447
|
+
function switchExpectation(targetRoot, relativePath, guards) {
|
|
448
|
+
if (guards.expectations?.has(relativePath)) return guards.expectations.get(relativePath) ?? null;
|
|
449
|
+
const inspected = inspectRelativePathWithoutFollowingAncestors(targetRoot, relativePath);
|
|
450
|
+
if (inspected.kind === "blocked") throw new Error(`Refusing to copy through a symlink or non-directory: ${inspected.blockingPath}`);
|
|
451
|
+
return inspected.kind === "entry" ? inspected.stat : null;
|
|
452
|
+
}
|
|
453
|
+
function settledAfter(targetPath, stagedStat, stagedContent) {
|
|
454
|
+
const current = lstatIfExists(targetPath);
|
|
455
|
+
return current && displacedEntryMatches(stagedStat, current) && entryContent(targetPath, current) === stagedContent ? current : stagedStat;
|
|
456
|
+
}
|
|
457
|
+
function entryContent(filePath, expected, expectedBlob) {
|
|
458
|
+
try {
|
|
459
|
+
if (expected.isSymbolicLink()) {
|
|
460
|
+
const before = fs.lstatSync(filePath);
|
|
461
|
+
if (!workingTreeStatsMatch(expected, before)) return null;
|
|
462
|
+
const bytes = Buffer.from(fs.readlinkSync(filePath));
|
|
463
|
+
if (!workingTreeStatsMatch(before, fs.lstatSync(filePath))) return null;
|
|
464
|
+
const digest = createHash("sha256").update(`blob ${bytes.length}\0`).update(bytes).digest("hex");
|
|
465
|
+
if (expectedBlob && (expectedBlob.length === 64 ? digest : createHash("sha1").update(`blob ${bytes.length}\0`).update(bytes).digest("hex")) !== expectedBlob)
|
|
466
|
+
return null;
|
|
467
|
+
return digest;
|
|
468
|
+
}
|
|
469
|
+
if (!expected.isFile()) return null;
|
|
470
|
+
const descriptor = fs.openSync(filePath, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
|
|
471
|
+
try {
|
|
472
|
+
const before = fs.fstatSync(descriptor);
|
|
473
|
+
if (!workingTreeStatsMatch(expected, before)) return null;
|
|
474
|
+
const hash = createHash("sha256").update(`blob ${before.size}\0`);
|
|
475
|
+
const knownHash = expectedBlob && expectedBlob.length !== 64 ? createHash("sha1").update(`blob ${before.size}\0`) : null;
|
|
476
|
+
const buffer = Buffer.allocUnsafe(64 * 1024);
|
|
477
|
+
for (let offset = 0; offset < before.size; ) {
|
|
478
|
+
const count = fs.readSync(descriptor, buffer, 0, Math.min(buffer.length, before.size - offset), offset);
|
|
479
|
+
if (count <= 0) return null;
|
|
480
|
+
const bytes = buffer.subarray(0, count);
|
|
481
|
+
hash.update(bytes);
|
|
482
|
+
knownHash?.update(bytes);
|
|
483
|
+
offset += count;
|
|
484
|
+
}
|
|
485
|
+
if (!workingTreeStatsMatch(before, fs.fstatSync(descriptor)) || !workingTreeStatsMatch(before, fs.lstatSync(filePath))) return null;
|
|
486
|
+
const digest = hash.digest("hex");
|
|
487
|
+
if (expectedBlob && (knownHash ? knownHash.digest("hex") : digest) !== expectedBlob) return null;
|
|
488
|
+
return digest;
|
|
489
|
+
} finally {
|
|
490
|
+
fs.closeSync(descriptor);
|
|
491
|
+
}
|
|
492
|
+
} catch (error) {
|
|
493
|
+
if (isPathStateCode(error.code) || error.code === "ELOOP") return null;
|
|
494
|
+
throw error;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
function fsyncRetainedEntry(retainedPath) {
|
|
498
|
+
const status = fs.lstatSync(retainedPath);
|
|
499
|
+
if (status.isDirectory() && !status.isSymbolicLink()) {
|
|
500
|
+
for (const child of fs.readdirSync(retainedPath)) fsyncRetainedEntry(path.join(retainedPath, child));
|
|
501
|
+
fsyncPath(retainedPath, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
|
|
502
|
+
} else if (status.isFile()) {
|
|
503
|
+
fsyncPath(retainedPath, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
|
|
504
|
+
}
|
|
505
|
+
fsyncPath(path.dirname(retainedPath), fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
|
|
506
|
+
}
|
|
507
|
+
function retainForeign(journal, relativePath, retainedPath, reason) {
|
|
508
|
+
fsyncRetainedEntry(retainedPath);
|
|
509
|
+
const retained = { relativePath, path: retainedPath, reason };
|
|
510
|
+
if (journal) (journal.retained ??= []).push(retained);
|
|
511
|
+
return retained;
|
|
512
|
+
}
|
|
513
|
+
function isPathStateCode(code) {
|
|
514
|
+
return code === "EEXIST" || code === "ENOENT" || code === "ENOTDIR" || code === "ENOTEMPTY" || code === "EISDIR";
|
|
515
|
+
}
|
|
516
|
+
function publishNoReplace(state, stagedPath, stagedStat, relativePath, targetPath) {
|
|
517
|
+
if (stagedStat.isSymbolicLink() && state.primitives.support.noReplace === "link") {
|
|
518
|
+
throw new WorkingTreeTargetChangedError(relativePath, { detail: "filesystem cannot publish a symlink with provable ownership" });
|
|
519
|
+
}
|
|
520
|
+
try {
|
|
521
|
+
state.primitives.renameNoReplace(stagedPath, targetPath);
|
|
522
|
+
} catch (error) {
|
|
523
|
+
if (error instanceof AtomicRenameError && isPathStateCode(error.code)) throw new WorkingTreeTargetChangedError(relativePath);
|
|
524
|
+
throw error;
|
|
525
|
+
}
|
|
526
|
+
return null;
|
|
527
|
+
}
|
|
528
|
+
function restoreDisplacedNoReplace(state, displacedPath, displaced, targetPath) {
|
|
529
|
+
try {
|
|
530
|
+
if (displaced.isSymbolicLink() && state.primitives.support.noReplace === "link") {
|
|
531
|
+
return false;
|
|
532
|
+
}
|
|
533
|
+
state.primitives.renameNoReplace(displacedPath, targetPath);
|
|
534
|
+
return true;
|
|
535
|
+
} catch (error) {
|
|
536
|
+
const code = error instanceof AtomicRenameError ? error.code : error.code;
|
|
537
|
+
if (isPathStateCode(code) || code === "ENOTSUP") return false;
|
|
538
|
+
throw error;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
function discardStaged(input, reason) {
|
|
542
|
+
const current = lstatIfExists(input.stagedPath);
|
|
543
|
+
if (!current) return null;
|
|
544
|
+
if (input.onFailure === "unlink" && input.stagedContent && displacedEntryMatches(input.stagedStat, current) && entryContent(input.stagedPath, current) === input.stagedContent) {
|
|
545
|
+
fs.rmSync(input.stagedPath, { force: true });
|
|
546
|
+
return null;
|
|
547
|
+
}
|
|
548
|
+
return retainForeign(input.journal, input.relativePath, input.stagedPath, reason);
|
|
549
|
+
}
|
|
550
|
+
function discardStagedAndThrow(input, error) {
|
|
551
|
+
const retained = discardStaged(input, "unrestorable");
|
|
552
|
+
if (retained && error instanceof WorkingTreeTargetChangedError) {
|
|
553
|
+
throw new WorkingTreeTargetChangedError(input.relativePath, { detail: error.message, retained: [...error.retained, retained] });
|
|
554
|
+
}
|
|
555
|
+
throw error;
|
|
556
|
+
}
|
|
557
|
+
function switchStagedEntry(input) {
|
|
558
|
+
const { state, targetRoot, relativePath, targetPath, stagedPath, stagedStat, expected } = input;
|
|
559
|
+
const stagedContent = input.stagedContent ?? entryContent(stagedPath, stagedStat);
|
|
560
|
+
if (!stagedContent) {
|
|
561
|
+
const retained = retainForeign(input.journal, relativePath, stagedPath, "unrestorable");
|
|
562
|
+
throw new WorkingTreeTargetChangedError(relativePath, { retained: [retained] });
|
|
563
|
+
}
|
|
564
|
+
input.stagedContent = stagedContent;
|
|
565
|
+
if (stagedStat.isSymbolicLink() && state.primitives.support.noReplace === "link") {
|
|
566
|
+
discardStagedAndThrow(
|
|
567
|
+
input,
|
|
568
|
+
new WorkingTreeTargetChangedError(relativePath, { detail: "filesystem cannot publish a symlink with provable ownership" })
|
|
569
|
+
);
|
|
570
|
+
}
|
|
571
|
+
const inspected = inspectRelativePathWithoutFollowingAncestors(targetRoot, relativePath);
|
|
572
|
+
if (inspected.kind === "blocked") {
|
|
573
|
+
discardStagedAndThrow(input, new Error(`Refusing to copy through a symlink or non-directory: ${inspected.blockingPath}`));
|
|
574
|
+
}
|
|
575
|
+
if (expected === null) {
|
|
576
|
+
if (inspected.kind !== "missing") {
|
|
577
|
+
discardStagedAndThrow(input, new WorkingTreeTargetChangedError(relativePath));
|
|
578
|
+
}
|
|
579
|
+
raceWindow(input.phase, relativePath, targetPath);
|
|
580
|
+
let created;
|
|
581
|
+
try {
|
|
582
|
+
created = publishNoReplace(state, stagedPath, stagedStat, relativePath, targetPath);
|
|
583
|
+
} catch (error) {
|
|
584
|
+
discardStagedAndThrow(input, error);
|
|
585
|
+
}
|
|
586
|
+
raceWindow("after_switch", relativePath, targetPath);
|
|
587
|
+
return { after: created ?? settledAfter(targetPath, stagedStat, stagedContent), afterContent: stagedContent, displaced: null };
|
|
588
|
+
}
|
|
589
|
+
if (inspected.kind !== "entry" || !workingTreeStatsMatch(expected, inspected.stat)) {
|
|
590
|
+
discardStagedAndThrow(input, new WorkingTreeTargetChangedError(relativePath));
|
|
591
|
+
}
|
|
592
|
+
const expectedContent = entryContent(targetPath, expected, input.expectedBlob);
|
|
593
|
+
if (!expectedContent || input.expectedContent && input.expectedContent !== expectedContent) {
|
|
594
|
+
discardStagedAndThrow(input, new WorkingTreeTargetChangedError(relativePath));
|
|
595
|
+
}
|
|
596
|
+
input.expectedContent = expectedContent;
|
|
597
|
+
raceWindow(input.phase, relativePath, targetPath);
|
|
598
|
+
if (!state.exchangeUnavailable) {
|
|
599
|
+
try {
|
|
600
|
+
state.primitives.exchange(stagedPath, targetPath);
|
|
601
|
+
} catch (error) {
|
|
602
|
+
if (isAtomicRenameUnsupported(error)) {
|
|
603
|
+
state.exchangeUnavailable = true;
|
|
604
|
+
return switchWithoutExchange(input);
|
|
605
|
+
}
|
|
606
|
+
if (error instanceof AtomicRenameError && isPathStateCode(error.code)) {
|
|
607
|
+
discardStagedAndThrow(input, new WorkingTreeTargetChangedError(relativePath, { detail: `${error.code} at the switch` }));
|
|
608
|
+
}
|
|
609
|
+
discardStagedAndThrow(input, error);
|
|
610
|
+
}
|
|
611
|
+
raceWindow("after_switch", relativePath, targetPath);
|
|
612
|
+
const displaced = fs.lstatSync(stagedPath);
|
|
613
|
+
if (displacedEntryMatches(expected, displaced) && entryContent(stagedPath, displaced) === expectedContent) {
|
|
614
|
+
return {
|
|
615
|
+
after: settledAfter(targetPath, stagedStat, stagedContent),
|
|
616
|
+
afterContent: stagedContent,
|
|
617
|
+
displaced: { path: stagedPath, stat: displaced, content: expectedContent }
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
return yieldDisplacedEntry(input, displaced);
|
|
621
|
+
}
|
|
622
|
+
return switchWithoutExchange(input);
|
|
623
|
+
}
|
|
624
|
+
function yieldDisplacedEntry(input, displaced) {
|
|
625
|
+
const { state, relativePath, targetPath, stagedPath, stagedStat } = input;
|
|
626
|
+
raceWindow("before_yield", relativePath, targetPath);
|
|
627
|
+
let installedBefore = stagedStat;
|
|
628
|
+
let toInstall = displaced;
|
|
629
|
+
for (let attempt = 0; attempt < YIELD_ATTEMPTS; attempt += 1) {
|
|
630
|
+
try {
|
|
631
|
+
state.primitives.exchange(stagedPath, targetPath);
|
|
632
|
+
} catch (error) {
|
|
633
|
+
if (!(error instanceof AtomicRenameError && isPathStateCode(error.code))) throw error;
|
|
634
|
+
if (restoreDisplacedNoReplace(state, stagedPath, toInstall, targetPath)) {
|
|
635
|
+
throw new WorkingTreeTargetChangedError(relativePath, { detail: "another writer replaced the entry; its bytes were kept" });
|
|
636
|
+
}
|
|
637
|
+
const retained2 = retainForeign(input.journal, relativePath, stagedPath, "later_writer");
|
|
638
|
+
throw new WorkingTreeTargetChangedError(relativePath, { retained: [retained2] });
|
|
639
|
+
}
|
|
640
|
+
const returned = fs.lstatSync(stagedPath);
|
|
641
|
+
if (sameWorkingTreeInode(returned, installedBefore)) {
|
|
642
|
+
if (sameWorkingTreeInode(returned, stagedStat)) {
|
|
643
|
+
discardStagedAndThrow(
|
|
644
|
+
input,
|
|
645
|
+
new WorkingTreeTargetChangedError(relativePath, { detail: "another writer replaced the entry; its bytes were kept" })
|
|
646
|
+
);
|
|
647
|
+
}
|
|
648
|
+
const retained2 = retainForeign(input.journal, relativePath, stagedPath, "later_writer");
|
|
649
|
+
throw new WorkingTreeTargetChangedError(relativePath, { retained: [retained2] });
|
|
650
|
+
}
|
|
651
|
+
installedBefore = toInstall;
|
|
652
|
+
toInstall = returned;
|
|
653
|
+
}
|
|
654
|
+
const retained = retainForeign(input.journal, relativePath, stagedPath, "unrestorable");
|
|
655
|
+
throw new WorkingTreeTargetChangedError(relativePath, { detail: "the path kept changing while the pass yielded", retained: [retained] });
|
|
656
|
+
}
|
|
657
|
+
function switchWithoutExchange(input) {
|
|
658
|
+
const { state, relativePath, targetPath, stagedPath, stagedStat, expected } = input;
|
|
659
|
+
const park = stagingPathFor(state, targetPath, relativePath);
|
|
660
|
+
try {
|
|
661
|
+
fs.renameSync(targetPath, park);
|
|
662
|
+
} catch (error) {
|
|
663
|
+
if (isPathStateCode(error.code))
|
|
664
|
+
discardStagedAndThrow(input, new WorkingTreeTargetChangedError(relativePath, { detail: "removed before the switch" }));
|
|
665
|
+
discardStagedAndThrow(input, error);
|
|
666
|
+
}
|
|
667
|
+
raceWindow("after_switch", relativePath, targetPath);
|
|
668
|
+
const displaced = fs.lstatSync(park);
|
|
669
|
+
if (displacedEntryMatches(expected, displaced) && entryContent(park, displaced) === input.expectedContent) {
|
|
670
|
+
let created;
|
|
671
|
+
try {
|
|
672
|
+
created = publishNoReplace(state, stagedPath, stagedStat, relativePath, targetPath);
|
|
673
|
+
} catch (error) {
|
|
674
|
+
if (error instanceof WorkingTreeTargetChangedError) {
|
|
675
|
+
const retained2 = retainForeign(input.journal, relativePath, park, "reappeared");
|
|
676
|
+
discardStagedAndThrow(
|
|
677
|
+
input,
|
|
678
|
+
new WorkingTreeTargetChangedError(relativePath, {
|
|
679
|
+
detail: "an entry was created while the path was being replaced",
|
|
680
|
+
retained: [retained2]
|
|
681
|
+
})
|
|
682
|
+
);
|
|
683
|
+
}
|
|
684
|
+
if (!restoreDisplacedNoReplace(state, park, displaced, targetPath)) retainForeign(input.journal, relativePath, park, "unrestorable");
|
|
685
|
+
discardStagedAndThrow(input, error);
|
|
686
|
+
}
|
|
687
|
+
return {
|
|
688
|
+
after: created ?? settledAfter(targetPath, stagedStat, input.stagedContent),
|
|
689
|
+
afterContent: input.stagedContent,
|
|
690
|
+
displaced: { path: park, stat: displaced, content: input.expectedContent }
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
raceWindow("before_yield", relativePath, targetPath);
|
|
694
|
+
const stagedRetained = discardStaged(input, "unrestorable");
|
|
695
|
+
if (restoreDisplacedNoReplace(state, park, displaced, targetPath)) {
|
|
696
|
+
throw new WorkingTreeTargetChangedError(relativePath, {
|
|
697
|
+
detail: "another writer replaced the entry; its bytes were kept",
|
|
698
|
+
retained: stagedRetained ? [stagedRetained] : []
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
const retained = retainForeign(input.journal, relativePath, park, "later_writer");
|
|
702
|
+
throw new WorkingTreeTargetChangedError(relativePath, { retained: [...stagedRetained ? [stagedRetained] : [], retained] });
|
|
703
|
+
}
|
|
704
|
+
function removeSwitchedEntry(input) {
|
|
705
|
+
const { state, targetRoot, relativePath, targetPath, expected } = input;
|
|
706
|
+
const inspected = inspectRelativePathWithoutFollowingAncestors(targetRoot, relativePath);
|
|
707
|
+
if (inspected.kind === "blocked") {
|
|
708
|
+
throw new Error(`Refusing to remove a working-tree path through a symlink or non-directory: ${inspected.blockingPath}`);
|
|
709
|
+
}
|
|
710
|
+
if (inspected.kind !== "entry" || !workingTreeStatsMatch(expected, inspected.stat)) throw new WorkingTreeTargetChangedError(relativePath);
|
|
711
|
+
const content = entryContent(targetPath, expected, input.expectedBlob);
|
|
712
|
+
if (!content || input.expectedContent && content !== input.expectedContent) throw new WorkingTreeTargetChangedError(relativePath);
|
|
713
|
+
raceWindow(input.phase, relativePath, targetPath);
|
|
714
|
+
const park = stagingPathFor(state, targetPath, relativePath);
|
|
715
|
+
try {
|
|
716
|
+
fs.renameSync(targetPath, park);
|
|
717
|
+
} catch (error) {
|
|
718
|
+
if (isPathStateCode(error.code))
|
|
719
|
+
throw new WorkingTreeTargetChangedError(relativePath, { detail: "removed before the switch" });
|
|
720
|
+
throw error;
|
|
721
|
+
}
|
|
722
|
+
raceWindow("after_remove", relativePath, targetPath);
|
|
723
|
+
const displaced = fs.lstatSync(park);
|
|
724
|
+
if (displacedEntryMatches(expected, displaced) && entryContent(park, displaced) === content)
|
|
725
|
+
return { path: park, stat: displaced, content };
|
|
726
|
+
raceWindow("before_yield", relativePath, targetPath);
|
|
727
|
+
if (restoreDisplacedNoReplace(state, park, displaced, targetPath)) {
|
|
728
|
+
throw new WorkingTreeTargetChangedError(relativePath, { detail: "another writer replaced the entry; its bytes were kept" });
|
|
729
|
+
}
|
|
730
|
+
const retained = retainForeign(input.journal, relativePath, park, "later_writer");
|
|
731
|
+
throw new WorkingTreeTargetChangedError(relativePath, { retained: [retained] });
|
|
732
|
+
}
|
|
733
|
+
function removeEntryGuarded(targetRoot, relativePath, replaced, guards) {
|
|
734
|
+
if (!guards) {
|
|
735
|
+
removeEntry(targetRoot, relativePath);
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
const state = guardedPassState(guards, targetRoot);
|
|
739
|
+
const expected = switchExpectation(targetRoot, relativePath, guards);
|
|
740
|
+
const targetPath = path.join(targetRoot, ...relativePath.split("/"));
|
|
741
|
+
assertInside(targetRoot, targetPath);
|
|
742
|
+
const inspected = inspectRelativePathWithoutFollowingAncestors(targetRoot, relativePath);
|
|
743
|
+
if (inspected.kind === "blocked") {
|
|
744
|
+
throw new Error(`Refusing to remove a working-tree path through a symlink or non-directory: ${inspected.blockingPath}`);
|
|
745
|
+
}
|
|
746
|
+
if (expected === null) {
|
|
747
|
+
if (inspected.kind !== "missing") throw new WorkingTreeTargetChangedError(relativePath);
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
if (expected.isDirectory() && !expected.isSymbolicLink()) {
|
|
751
|
+
if (inspected.kind === "entry") {
|
|
752
|
+
if (!inspected.stat.isDirectory() || inspected.stat.isSymbolicLink()) throw new WorkingTreeTargetChangedError(relativePath);
|
|
753
|
+
raceWindow("before_remove", relativePath, targetPath);
|
|
754
|
+
try {
|
|
755
|
+
fs.rmdirSync(inspected.absolutePath);
|
|
756
|
+
} catch (error) {
|
|
757
|
+
const code = error.code;
|
|
758
|
+
if (code === "ENOTEMPTY" || code === "EEXIST" || code === "ENOTDIR") throw new WorkingTreeTargetChangedError(relativePath);
|
|
759
|
+
if (code !== "ENOENT") throw error;
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
if (guards.expectations?.has(relativePath)) guards.expectations.set(relativePath, null);
|
|
763
|
+
if (replaced) guards.journal?.entries.push({ relativePath, replaced, after: null });
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
const displaced = removeSwitchedEntry({
|
|
767
|
+
state,
|
|
768
|
+
targetRoot,
|
|
769
|
+
relativePath,
|
|
770
|
+
targetPath,
|
|
771
|
+
expected,
|
|
772
|
+
expectedBlob: guards.preBlobs?.get(relativePath),
|
|
773
|
+
journal: guards.journal,
|
|
774
|
+
phase: "before_remove"
|
|
775
|
+
});
|
|
776
|
+
if (guards.expectations?.has(relativePath)) guards.expectations.set(relativePath, null);
|
|
777
|
+
const recorded = replaced ?? treeEntryFromStat(displaced.stat, displaced.path);
|
|
778
|
+
if (guards.journal && recorded) guards.journal.entries.push({ relativePath, replaced: recorded, after: null, displaced });
|
|
779
|
+
else releaseRetainedEntry(void 0, relativePath, displaced);
|
|
780
|
+
}
|
|
781
|
+
function ensureDirectoryGuarded(targetRoot, relativePath, guards) {
|
|
782
|
+
if (!guards) {
|
|
783
|
+
ensureDirectory(targetRoot, relativePath, 493);
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
const normalized = normalizeRelativePath(relativePath);
|
|
787
|
+
if (!normalized) throw new Error(`Invalid working-tree directory path: ${relativePath}`);
|
|
788
|
+
let current = path.resolve(targetRoot);
|
|
789
|
+
let relative = "";
|
|
790
|
+
for (const segment of normalized.split("/")) {
|
|
791
|
+
current = path.join(current, segment);
|
|
792
|
+
relative = relative ? `${relative}/${segment}` : segment;
|
|
793
|
+
assertInside(targetRoot, current);
|
|
794
|
+
const stat = lstatIfExists(current);
|
|
795
|
+
if (stat && (!stat.isDirectory() || stat.isSymbolicLink())) throw new WorkingTreeTargetChangedError(relative);
|
|
796
|
+
if (stat) continue;
|
|
797
|
+
try {
|
|
798
|
+
fs.mkdirSync(current, { mode: 493 });
|
|
799
|
+
} catch (error) {
|
|
800
|
+
if (error.code !== "EEXIST") throw error;
|
|
801
|
+
const now = lstatIfExists(current);
|
|
802
|
+
if (!now || !now.isDirectory() || now.isSymbolicLink()) throw new WorkingTreeTargetChangedError(relative);
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
function replaceWithSymlink(targetRoot, relativePath, target) {
|
|
807
|
+
const targetPath = path.join(targetRoot, ...relativePath.split("/"));
|
|
808
|
+
assertInside(targetRoot, targetPath);
|
|
809
|
+
const temporaryPath = path.join(path.dirname(targetPath), `${MIRROR_TEMPORARY_FILE_PREFIX}${randomUUID()}.tmp`);
|
|
810
|
+
fs.symlinkSync(target, temporaryPath);
|
|
811
|
+
try {
|
|
812
|
+
const current = inspectRelativePathWithoutFollowingAncestors(targetRoot, relativePath);
|
|
813
|
+
if (current.kind === "blocked") throw new Error(`Refusing to copy through a symlink or non-directory: ${current.blockingPath}`);
|
|
814
|
+
if (current.kind === "entry" && current.stat.isDirectory() && !current.stat.isSymbolicLink()) removeEntry(targetRoot, relativePath);
|
|
815
|
+
fs.renameSync(temporaryPath, targetPath);
|
|
816
|
+
} catch (error) {
|
|
817
|
+
fs.rmSync(temporaryPath, { force: true });
|
|
818
|
+
throw error;
|
|
819
|
+
}
|
|
820
|
+
}
|
|
332
821
|
function ensureDirectory(targetRoot, relativePath, mode) {
|
|
333
822
|
const normalized = normalizeRelativePath(relativePath);
|
|
334
823
|
if (!normalized) throw new Error(`Invalid working-tree directory path: ${relativePath}`);
|
|
@@ -386,9 +875,8 @@ function filesEqual(leftPath, rightPath, entry) {
|
|
|
386
875
|
fs.closeSync(right);
|
|
387
876
|
}
|
|
388
877
|
}
|
|
389
|
-
function
|
|
878
|
+
function stageRegularFile(sourcePath, stagedPath, entry, durability) {
|
|
390
879
|
const source = fs.openSync(sourcePath, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
|
|
391
|
-
const temporaryPath = path.join(path.dirname(targetPath), `${MIRROR_TEMPORARY_FILE_PREFIX}${randomUUID()}.tmp`);
|
|
392
880
|
let target;
|
|
393
881
|
try {
|
|
394
882
|
const sourceStat = fs.fstatSync(source);
|
|
@@ -396,7 +884,7 @@ function copyRegularFile(sourcePath, targetPath, entry, durability) {
|
|
|
396
884
|
throw new Error(`Working-tree source changed while it was being mirrored: ${sourcePath}`);
|
|
397
885
|
}
|
|
398
886
|
target = fs.openSync(
|
|
399
|
-
|
|
887
|
+
stagedPath,
|
|
400
888
|
fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_NOFOLLOW,
|
|
401
889
|
entry.mode & 511
|
|
402
890
|
);
|
|
@@ -417,22 +905,54 @@ function copyRegularFile(sourcePath, targetPath, entry, durability) {
|
|
|
417
905
|
if (!finalSourceStat.isFile() || finalSourceStat.size !== sourceStat.size || (finalSourceStat.mode & 511) !== (sourceStat.mode & 511) || finalSourceStat.mtimeMs !== sourceStat.mtimeMs || finalSourceStat.ctimeMs !== sourceStat.ctimeMs) {
|
|
418
906
|
throw new Error(`Working-tree source changed while it was being mirrored: ${sourcePath}`);
|
|
419
907
|
}
|
|
908
|
+
const stagedStat = fs.fstatSync(target);
|
|
420
909
|
fs.closeSync(target);
|
|
421
910
|
target = void 0;
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
} finally {
|
|
428
|
-
fs.closeSync(parent);
|
|
429
|
-
}
|
|
911
|
+
return { sourceStat: finalSourceStat, stagedStat };
|
|
912
|
+
} catch (error) {
|
|
913
|
+
if (target !== void 0) {
|
|
914
|
+
fs.closeSync(target);
|
|
915
|
+
target = void 0;
|
|
430
916
|
}
|
|
917
|
+
fs.rmSync(stagedPath, { force: true });
|
|
918
|
+
throw error;
|
|
431
919
|
} finally {
|
|
432
920
|
if (target !== void 0) fs.closeSync(target);
|
|
433
921
|
fs.closeSync(source);
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
function writeStagedContent(stagedPath, override, durability) {
|
|
925
|
+
const target = fs.openSync(
|
|
926
|
+
stagedPath,
|
|
927
|
+
fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_NOFOLLOW,
|
|
928
|
+
override.mode & 511
|
|
929
|
+
);
|
|
930
|
+
try {
|
|
931
|
+
let written = 0;
|
|
932
|
+
while (written < override.content.length) {
|
|
933
|
+
written += fs.writeSync(target, override.content, written, override.content.length - written, written);
|
|
934
|
+
}
|
|
935
|
+
fs.fchmodSync(target, override.mode & 511);
|
|
936
|
+
if (durability === "per_entry") fs.fsyncSync(target);
|
|
937
|
+
return fs.fstatSync(target);
|
|
938
|
+
} catch (error) {
|
|
939
|
+
fs.rmSync(stagedPath, { force: true });
|
|
940
|
+
throw error;
|
|
941
|
+
} finally {
|
|
942
|
+
fs.closeSync(target);
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
function copyRegularFile(sourcePath, targetPath, entry, durability) {
|
|
946
|
+
const temporaryPath = path.join(path.dirname(targetPath), `${MIRROR_TEMPORARY_FILE_PREFIX}${randomUUID()}.tmp`);
|
|
947
|
+
const { sourceStat } = stageRegularFile(sourcePath, temporaryPath, entry, durability);
|
|
948
|
+
try {
|
|
949
|
+
fs.renameSync(temporaryPath, targetPath);
|
|
950
|
+
} catch (error) {
|
|
434
951
|
fs.rmSync(temporaryPath, { force: true });
|
|
952
|
+
throw error;
|
|
435
953
|
}
|
|
954
|
+
if (durability === "per_entry") fsyncPath(path.dirname(targetPath), fs.constants.O_RDONLY);
|
|
955
|
+
return sourceStat;
|
|
436
956
|
}
|
|
437
957
|
function copyEntry(sourceRoot, targetRoot, relativePath, entry, durability) {
|
|
438
958
|
const targetPath = path.join(targetRoot, ...relativePath.split("/"));
|
|
@@ -449,7 +969,7 @@ function copyEntry(sourceRoot, targetRoot, relativePath, entry, durability) {
|
|
|
449
969
|
throw new Error(`Working-tree source changed while it was being mirrored: ${sourcePath}`);
|
|
450
970
|
}
|
|
451
971
|
ensureDirectory(targetRoot, relativePath, entry.mode);
|
|
452
|
-
return;
|
|
972
|
+
return null;
|
|
453
973
|
}
|
|
454
974
|
if (entry.kind === "symlink") {
|
|
455
975
|
if (!source.stat.isSymbolicLink() || fs.readlinkSync(sourcePath) !== entry.target) {
|
|
@@ -461,11 +981,8 @@ function copyEntry(sourceRoot, targetRoot, relativePath, entry, durability) {
|
|
|
461
981
|
} catch {
|
|
462
982
|
unchanged = false;
|
|
463
983
|
}
|
|
464
|
-
if (!unchanged)
|
|
465
|
-
|
|
466
|
-
fs.symlinkSync(entry.target, targetPath);
|
|
467
|
-
}
|
|
468
|
-
return;
|
|
984
|
+
if (!unchanged) replaceWithSymlink(targetRoot, relativePath, entry.target);
|
|
985
|
+
return { kind: "symlink", target: entry.target };
|
|
469
986
|
}
|
|
470
987
|
if (entry.kind === "gitlink") {
|
|
471
988
|
throw new Error(`Working-tree gitlink cannot be copied as content: ${relativePath}`);
|
|
@@ -473,19 +990,183 @@ function copyEntry(sourceRoot, targetRoot, relativePath, entry, durability) {
|
|
|
473
990
|
if (!source.stat.isFile() || source.stat.isSymbolicLink() || source.stat.size !== entry.size) {
|
|
474
991
|
throw new Error(`Working-tree source changed while it was being mirrored: ${sourcePath}`);
|
|
475
992
|
}
|
|
476
|
-
if (
|
|
477
|
-
const
|
|
478
|
-
if (
|
|
479
|
-
throw new Error(`
|
|
993
|
+
if (filesEqual(sourcePath, targetPath, entry)) {
|
|
994
|
+
const settled = fs.lstatSync(sourcePath);
|
|
995
|
+
if (!settled.isFile() || settled.ino !== source.stat.ino || settled.size !== source.stat.size || settled.mode !== source.stat.mode) {
|
|
996
|
+
throw new Error(`Working-tree source changed while it was being mirrored: ${sourcePath}`);
|
|
480
997
|
}
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
998
|
+
if (settled.mtimeMs !== source.stat.mtimeMs || settled.ctimeMs !== source.stat.ctimeMs) {
|
|
999
|
+
throw new Error(`Working-tree source changed while it was being mirrored: ${sourcePath}`);
|
|
1000
|
+
}
|
|
1001
|
+
return projectedFileEntry(settled);
|
|
1002
|
+
}
|
|
1003
|
+
const target = inspectRelativePathWithoutFollowingAncestors(targetRoot, relativePath);
|
|
1004
|
+
if (target.kind === "blocked") {
|
|
1005
|
+
throw new Error(`Refusing to copy through a symlink or non-directory: ${target.blockingPath}`);
|
|
484
1006
|
}
|
|
1007
|
+
if (target.kind === "entry" && !target.stat.isFile()) removeEntry(targetRoot, relativePath);
|
|
1008
|
+
return projectedFileEntry(copyRegularFile(sourcePath, targetPath, entry, durability));
|
|
485
1009
|
}
|
|
486
|
-
function
|
|
487
|
-
|
|
488
|
-
const
|
|
1010
|
+
function copyEntryGuarded(sourceRoot, targetRoot, relativePath, entry, durability, guards) {
|
|
1011
|
+
const state = guardedPassState(guards, targetRoot);
|
|
1012
|
+
const targetPath = path.join(targetRoot, ...relativePath.split("/"));
|
|
1013
|
+
assertInside(targetRoot, targetPath);
|
|
1014
|
+
const parent = path.posix.dirname(relativePath);
|
|
1015
|
+
if (parent !== ".") ensureDirectoryGuarded(targetRoot, parent, guards);
|
|
1016
|
+
const override = guards.overrides?.get(relativePath);
|
|
1017
|
+
if (override && entry.kind !== "file") throw new Error(`Working-tree override must replace a regular file: ${relativePath}`);
|
|
1018
|
+
let sourcePath = null;
|
|
1019
|
+
let sourceStat = null;
|
|
1020
|
+
if (!override) {
|
|
1021
|
+
const source = inspectRelativePathWithoutFollowingAncestors(sourceRoot, relativePath);
|
|
1022
|
+
if (source.kind !== "entry") throw new Error(`Working-tree source became unsafe while it was being mirrored: ${relativePath}`);
|
|
1023
|
+
sourcePath = source.absolutePath;
|
|
1024
|
+
sourceStat = source.stat;
|
|
1025
|
+
}
|
|
1026
|
+
if (entry.kind === "directory") {
|
|
1027
|
+
if (!sourceStat.isDirectory() || sourceStat.isSymbolicLink()) {
|
|
1028
|
+
throw new Error(`Working-tree source changed while it was being mirrored: ${sourcePath}`);
|
|
1029
|
+
}
|
|
1030
|
+
const target2 = inspectRelativePathWithoutFollowingAncestors(targetRoot, relativePath);
|
|
1031
|
+
if (target2.kind === "blocked") throw new Error(`Refusing to copy through a symlink or non-directory: ${target2.blockingPath}`);
|
|
1032
|
+
if (target2.kind === "entry" && target2.stat.isDirectory() && !target2.stat.isSymbolicLink()) return null;
|
|
1033
|
+
if (target2.kind === "entry") {
|
|
1034
|
+
if (guards.expectations && !guards.expectations.has(relativePath)) throw new WorkingTreeTargetChangedError(relativePath);
|
|
1035
|
+
removeEntryGuarded(targetRoot, relativePath, treeEntryFromStat(target2.stat, target2.absolutePath), guards);
|
|
1036
|
+
} else assertTargetExpectation(targetRoot, relativePath, guards);
|
|
1037
|
+
createDirectoryGuarded(targetRoot, relativePath, entry.mode, guards);
|
|
1038
|
+
return null;
|
|
1039
|
+
}
|
|
1040
|
+
if (entry.kind === "gitlink") {
|
|
1041
|
+
throw new Error(`Working-tree gitlink cannot be copied as content: ${relativePath}`);
|
|
1042
|
+
}
|
|
1043
|
+
const target = inspectRelativePathWithoutFollowingAncestors(targetRoot, relativePath);
|
|
1044
|
+
if (target.kind === "blocked") throw new Error(`Refusing to copy through a symlink or non-directory: ${target.blockingPath}`);
|
|
1045
|
+
if (entry.kind === "symlink") {
|
|
1046
|
+
if (!sourceStat.isSymbolicLink() || fs.readlinkSync(sourcePath) !== entry.target) {
|
|
1047
|
+
throw new Error(`Working-tree source changed while it was being mirrored: ${sourcePath}`);
|
|
1048
|
+
}
|
|
1049
|
+
if (target.kind === "entry" && target.stat.isSymbolicLink() && fs.readlinkSync(target.absolutePath) === entry.target) {
|
|
1050
|
+
return { kind: "symlink", target: entry.target };
|
|
1051
|
+
}
|
|
1052
|
+
} else if (!override) {
|
|
1053
|
+
if (!sourceStat.isFile() || sourceStat.isSymbolicLink() || sourceStat.size !== entry.size) {
|
|
1054
|
+
throw new Error(`Working-tree source changed while it was being mirrored: ${sourcePath}`);
|
|
1055
|
+
}
|
|
1056
|
+
if (filesEqual(sourcePath, targetPath, entry)) {
|
|
1057
|
+
const settled = fs.lstatSync(sourcePath);
|
|
1058
|
+
if (!settled.isFile() || settled.ino !== sourceStat.ino || settled.size !== sourceStat.size || settled.mode !== sourceStat.mode) {
|
|
1059
|
+
throw new Error(`Working-tree source changed while it was being mirrored: ${sourcePath}`);
|
|
1060
|
+
}
|
|
1061
|
+
if (settled.mtimeMs !== sourceStat.mtimeMs || settled.ctimeMs !== sourceStat.ctimeMs) {
|
|
1062
|
+
throw new Error(`Working-tree source changed while it was being mirrored: ${sourcePath}`);
|
|
1063
|
+
}
|
|
1064
|
+
return projectedFileEntry(settled);
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
let replaced = target.kind === "entry" ? treeEntryFromStat(target.stat, target.absolutePath) : null;
|
|
1068
|
+
let expected = switchExpectation(targetRoot, relativePath, guards);
|
|
1069
|
+
if (target.kind === "entry" && target.stat.isDirectory() && !target.stat.isSymbolicLink()) {
|
|
1070
|
+
removeEntryGuarded(targetRoot, relativePath, replaced, guards);
|
|
1071
|
+
replaced = null;
|
|
1072
|
+
expected = null;
|
|
1073
|
+
}
|
|
1074
|
+
const stagedPath = stagingPathFor(state, targetPath, relativePath);
|
|
1075
|
+
let stagedStat;
|
|
1076
|
+
let projected = null;
|
|
1077
|
+
if (entry.kind === "symlink") {
|
|
1078
|
+
fs.symlinkSync(entry.target, stagedPath);
|
|
1079
|
+
stagedStat = fs.lstatSync(stagedPath);
|
|
1080
|
+
projected = { kind: "symlink", target: entry.target };
|
|
1081
|
+
} else if (override) {
|
|
1082
|
+
stagedStat = writeStagedContent(stagedPath, override, durability);
|
|
1083
|
+
} else {
|
|
1084
|
+
const staged = stageRegularFile(sourcePath, stagedPath, entry, durability);
|
|
1085
|
+
stagedStat = staged.stagedStat;
|
|
1086
|
+
projected = projectedFileEntry(staged.sourceStat);
|
|
1087
|
+
}
|
|
1088
|
+
const outcome = switchStagedEntry({
|
|
1089
|
+
state,
|
|
1090
|
+
targetRoot,
|
|
1091
|
+
relativePath,
|
|
1092
|
+
targetPath,
|
|
1093
|
+
stagedPath,
|
|
1094
|
+
stagedStat,
|
|
1095
|
+
expected,
|
|
1096
|
+
expectedBlob: guards.preBlobs?.get(relativePath),
|
|
1097
|
+
journal: guards.journal,
|
|
1098
|
+
phase: "before_switch",
|
|
1099
|
+
onFailure: "unlink"
|
|
1100
|
+
});
|
|
1101
|
+
if (durability === "per_entry") fsyncPath(path.dirname(targetPath), fs.constants.O_RDONLY);
|
|
1102
|
+
if (guards.journal)
|
|
1103
|
+
guards.journal.entries.push({
|
|
1104
|
+
relativePath,
|
|
1105
|
+
replaced,
|
|
1106
|
+
after: outcome.after,
|
|
1107
|
+
afterContent: outcome.afterContent,
|
|
1108
|
+
...outcome.displaced ? { displaced: outcome.displaced } : {}
|
|
1109
|
+
});
|
|
1110
|
+
else if (outcome.displaced) releaseRetainedEntry(void 0, relativePath, outcome.displaced);
|
|
1111
|
+
if (guards.expectations?.has(relativePath)) guards.expectations.set(relativePath, outcome.after);
|
|
1112
|
+
return projected;
|
|
1113
|
+
}
|
|
1114
|
+
function createDirectoryGuarded(targetRoot, relativePath, mode, guards) {
|
|
1115
|
+
if (!guards) {
|
|
1116
|
+
ensureDirectory(targetRoot, relativePath, mode);
|
|
1117
|
+
return;
|
|
1118
|
+
}
|
|
1119
|
+
const targetPath = path.join(targetRoot, ...relativePath.split("/"));
|
|
1120
|
+
assertInside(targetRoot, targetPath);
|
|
1121
|
+
try {
|
|
1122
|
+
fs.mkdirSync(targetPath, { mode: mode & 511 });
|
|
1123
|
+
} catch (error) {
|
|
1124
|
+
if (isPathStateCode(error.code)) throw new WorkingTreeTargetChangedError(relativePath);
|
|
1125
|
+
throw error;
|
|
1126
|
+
}
|
|
1127
|
+
if (!guards.journal) return;
|
|
1128
|
+
const created = inspectRelativePathWithoutFollowingAncestors(targetRoot, relativePath);
|
|
1129
|
+
if (created.kind !== "entry" || !created.stat.isDirectory() || created.stat.isSymbolicLink()) {
|
|
1130
|
+
throw new WorkingTreeTargetChangedError(relativePath);
|
|
1131
|
+
}
|
|
1132
|
+
guards.journal.entries.push({ relativePath, replaced: null, after: created.stat });
|
|
1133
|
+
}
|
|
1134
|
+
function retainedEntryIntact(retained, current) {
|
|
1135
|
+
return retained.content !== void 0 && sameWorkingTreeInode(retained.stat, current) && current.mode === retained.stat.mode && current.size === retained.stat.size && current.mtimeMs === retained.stat.mtimeMs && current.ctimeMs === retained.stat.ctimeMs && entryContent(retained.path, current) === retained.content;
|
|
1136
|
+
}
|
|
1137
|
+
function settleRetainedEntries(journal, targetRoot) {
|
|
1138
|
+
raceWindow("before_settle", "", targetRoot);
|
|
1139
|
+
for (const entry of journal.entries) {
|
|
1140
|
+
if (!entry.displaced) continue;
|
|
1141
|
+
const current = lstatIfExists(entry.displaced.path);
|
|
1142
|
+
if (!current || !retainedEntryIntact(entry.displaced, current)) {
|
|
1143
|
+
throw new WorkingTreeTargetChangedError(entry.relativePath, {
|
|
1144
|
+
detail: "written through a descriptor opened before hydration replaced the path"
|
|
1145
|
+
});
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
releaseRetainedEntries(journal);
|
|
1149
|
+
}
|
|
1150
|
+
function releaseRetainedEntries(journal) {
|
|
1151
|
+
for (const entry of journal.entries) {
|
|
1152
|
+
if (!entry.displaced) continue;
|
|
1153
|
+
releaseRetainedEntry(journal, entry.relativePath, entry.displaced);
|
|
1154
|
+
delete entry.displaced;
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
function releaseRetainedEntry(journal, relativePath, displaced) {
|
|
1158
|
+
const current = lstatIfExists(displaced.path);
|
|
1159
|
+
if (!current) return;
|
|
1160
|
+
if (!retainedEntryIntact(displaced, current)) {
|
|
1161
|
+
const retained = retainForeign(journal, relativePath, displaced.path, "unrestorable");
|
|
1162
|
+
if (!journal) throw new WorkingTreeTargetChangedError(relativePath, { retained: [retained] });
|
|
1163
|
+
return;
|
|
1164
|
+
}
|
|
1165
|
+
fs.rmSync(displaced.path, { force: true });
|
|
1166
|
+
}
|
|
1167
|
+
function inspectMirrorTarget(targetRoot, deletionMode, stats) {
|
|
1168
|
+
if (deletionMode !== "git") return { existing: inspectWorkingTree(targetRoot, "all", { stats }), git: null };
|
|
1169
|
+
const inspection = inspectGitWorkingTree(targetRoot, stats);
|
|
489
1170
|
const occupied = /* @__PURE__ */ new Set();
|
|
490
1171
|
for (const relativePath of inspection.submodules.keys()) {
|
|
491
1172
|
const inspected = inspectRelativePathWithoutFollowingAncestors(targetRoot, relativePath);
|
|
@@ -587,43 +1268,109 @@ function recordGitlinkStaleHeads(targetRoot, desired, shielded, git) {
|
|
|
587
1268
|
}
|
|
588
1269
|
if (changed) writeGitlinkStaleHeads(targetRoot, heads);
|
|
589
1270
|
}
|
|
590
|
-
function
|
|
1271
|
+
function prepareWorkingTreeMirror(input) {
|
|
591
1272
|
const sourceRoot = path.resolve(input.sourceRoot);
|
|
592
1273
|
const targetRoot = path.resolve(input.targetRoot);
|
|
593
|
-
const durability = input.durability ?? "per_entry";
|
|
594
1274
|
if (!inspectDirectoryRoot(targetRoot, { allowMissing: true, label: "Working-tree target root" })) {
|
|
595
1275
|
fs.mkdirSync(targetRoot, { recursive: true });
|
|
596
1276
|
}
|
|
597
1277
|
inspectDirectoryRoot(targetRoot, { allowMissing: false, label: "Working-tree target root" });
|
|
598
1278
|
const desired = inspectWorkingTree(sourceRoot, input.sourceMode, { gitlinks: input.sourceGitlinks });
|
|
599
|
-
const
|
|
1279
|
+
const existingStats = /* @__PURE__ */ new Map();
|
|
1280
|
+
const target = inspectMirrorTarget(targetRoot, input.deletionMode, existingStats);
|
|
600
1281
|
const shielded = target.git ? shieldedDesiredPaths(desired, target.git) : /* @__PURE__ */ new Set();
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
1282
|
+
return {
|
|
1283
|
+
sourceRoot,
|
|
1284
|
+
targetRoot,
|
|
1285
|
+
deletionMode: input.deletionMode,
|
|
1286
|
+
desired,
|
|
1287
|
+
existing: target.existing,
|
|
1288
|
+
existingStats,
|
|
1289
|
+
git: target.git,
|
|
1290
|
+
shielded
|
|
1291
|
+
};
|
|
1292
|
+
}
|
|
1293
|
+
function applyWorkingTreeMirror(prepared, options = {}) {
|
|
1294
|
+
const { sourceRoot, targetRoot, desired, existing, shielded } = prepared;
|
|
1295
|
+
const durability = options.durability ?? "per_entry";
|
|
1296
|
+
const guards = options.guards;
|
|
1297
|
+
const selection = options.selection;
|
|
1298
|
+
const target = { existing, git: prepared.git };
|
|
1299
|
+
const indexDesired = selection?.desiredForIndex ?? desired;
|
|
1300
|
+
const index = target.git ? planIndexChanges(indexDesired, shielded, target.git) : null;
|
|
1301
|
+
const neededDirectories = /* @__PURE__ */ new Set();
|
|
1302
|
+
if (selection) {
|
|
1303
|
+
for (const relativePath of selection.write) {
|
|
1304
|
+
let parent = path.posix.dirname(relativePath);
|
|
1305
|
+
while (parent !== ".") {
|
|
1306
|
+
neededDirectories.add(parent);
|
|
1307
|
+
parent = path.posix.dirname(parent);
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
for (const relativePath of [...existing.keys()].sort(deepestFirst)) {
|
|
1312
|
+
if (selection ? !selection.remove.has(relativePath) : desired.has(relativePath)) continue;
|
|
1313
|
+
const entry = existing.get(relativePath);
|
|
605
1314
|
if (entry.kind === "gitlink" || directoryShieldsSubmodule(relativePath, entry, target.git)) continue;
|
|
606
|
-
|
|
1315
|
+
if (selection && entry.kind === "directory") {
|
|
1316
|
+
const inspected = inspectRelativePathWithoutFollowingAncestors(targetRoot, relativePath);
|
|
1317
|
+
if (inspected.kind !== "entry" || !inspected.stat.isDirectory() || inspected.stat.isSymbolicLink()) continue;
|
|
1318
|
+
try {
|
|
1319
|
+
fs.rmdirSync(inspected.absolutePath);
|
|
1320
|
+
} catch (error) {
|
|
1321
|
+
const code = error.code;
|
|
1322
|
+
if (code === "ENOTEMPTY" || code === "EEXIST" || code === "ENOENT") continue;
|
|
1323
|
+
throw error;
|
|
1324
|
+
}
|
|
1325
|
+
if (guards?.expectations?.has(relativePath)) guards.expectations.set(relativePath, null);
|
|
1326
|
+
guards?.journal?.entries.push({ relativePath, replaced: entry, after: null });
|
|
1327
|
+
continue;
|
|
1328
|
+
}
|
|
1329
|
+
removeEntryGuarded(targetRoot, relativePath, entry, guards);
|
|
607
1330
|
}
|
|
608
1331
|
const gitlinks = [];
|
|
609
1332
|
for (const [relativePath, entry] of [...desired.entries()].sort(([left], [right]) => shallowestFirst(left, right))) {
|
|
610
1333
|
if (shielded.has(relativePath)) continue;
|
|
1334
|
+
if (selection && entry.kind === "directory" && !neededDirectories.has(relativePath)) continue;
|
|
1335
|
+
if (selection && entry.kind !== "directory" && !selection.write.has(relativePath)) continue;
|
|
611
1336
|
if (entry.kind === "gitlink") {
|
|
612
|
-
|
|
1337
|
+
if (guards) {
|
|
1338
|
+
const parent = path.posix.dirname(relativePath);
|
|
1339
|
+
if (parent !== ".") ensureDirectoryGuarded(targetRoot, parent, guards);
|
|
1340
|
+
const current = inspectRelativePathWithoutFollowingAncestors(targetRoot, relativePath);
|
|
1341
|
+
if (current.kind === "blocked") throw new Error(`Refusing to copy through a symlink or non-directory: ${current.blockingPath}`);
|
|
1342
|
+
if (current.kind === "entry" && (!current.stat.isDirectory() || current.stat.isSymbolicLink())) {
|
|
1343
|
+
if (guards.expectations && !guards.expectations.has(relativePath)) throw new WorkingTreeTargetChangedError(relativePath);
|
|
1344
|
+
removeEntryGuarded(targetRoot, relativePath, treeEntryFromStat(current.stat, current.absolutePath), guards);
|
|
1345
|
+
createDirectoryGuarded(targetRoot, relativePath, 493, guards);
|
|
1346
|
+
} else if (current.kind !== "entry") {
|
|
1347
|
+
assertTargetExpectation(targetRoot, relativePath, guards);
|
|
1348
|
+
createDirectoryGuarded(targetRoot, relativePath, 493, guards);
|
|
1349
|
+
}
|
|
1350
|
+
} else {
|
|
1351
|
+
ensureDirectory(targetRoot, relativePath, 493);
|
|
1352
|
+
}
|
|
613
1353
|
gitlinks.push({ relativePath, objectId: entry.objectId });
|
|
1354
|
+
options.projected?.set(relativePath, { kind: "gitlink", objectId: entry.objectId });
|
|
614
1355
|
continue;
|
|
615
1356
|
}
|
|
616
|
-
copyEntry(sourceRoot, targetRoot, relativePath, entry, durability);
|
|
1357
|
+
const projected = guards ? copyEntryGuarded(sourceRoot, targetRoot, relativePath, entry, durability, guards) : copyEntry(sourceRoot, targetRoot, relativePath, entry, durability);
|
|
1358
|
+
if (projected && options.projected) options.projected.set(relativePath, projected);
|
|
617
1359
|
}
|
|
1360
|
+
if (guards?.journal) settleRetainedEntries(guards.journal, targetRoot);
|
|
618
1361
|
if (index && target.git) {
|
|
619
1362
|
updateIndex(targetRoot, { drop: index.removed, put: index.written.map((gitlink) => ({ mode: "160000", stage: 0, ...gitlink })) });
|
|
620
|
-
|
|
1363
|
+
if (guards?.journal && (index.removed.length > 0 || index.written.length > 0)) guards.journal.indexApplied = index;
|
|
1364
|
+
recordGitlinkStaleHeads(targetRoot, indexDesired, shielded, target.git);
|
|
621
1365
|
}
|
|
622
1366
|
return {
|
|
623
1367
|
paths: [...desired.keys()].filter((relativePath) => !shielded.has(relativePath) && desired.get(relativePath)?.kind !== "directory").sort(),
|
|
624
1368
|
gitlinks
|
|
625
1369
|
};
|
|
626
1370
|
}
|
|
1371
|
+
function mirrorWorkingTree(input) {
|
|
1372
|
+
return applyWorkingTreeMirror(prepareWorkingTreeMirror(input), { durability: input.durability, projected: input.projected });
|
|
1373
|
+
}
|
|
627
1374
|
function normalizeWorkingTreeRelativePath(value) {
|
|
628
1375
|
return normalizeRelativePath(value);
|
|
629
1376
|
}
|
|
@@ -660,16 +1407,23 @@ function fsyncPath(targetPath, flags) {
|
|
|
660
1407
|
}
|
|
661
1408
|
}
|
|
662
1409
|
function planWorkingTreeMirror(input) {
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
const
|
|
667
|
-
const
|
|
668
|
-
const
|
|
1410
|
+
return planPreparedWorkingTreeMirror(prepareWorkingTreeMirror(input));
|
|
1411
|
+
}
|
|
1412
|
+
function planPreparedWorkingTreeMirror(prepared, options = {}) {
|
|
1413
|
+
const { sourceRoot, targetRoot, existing, shielded } = prepared;
|
|
1414
|
+
const inspectedDesired = prepared.desired;
|
|
1415
|
+
const selection = options.selection;
|
|
1416
|
+
const target = { existing, git: prepared.git };
|
|
1417
|
+
const desired = new Map(
|
|
1418
|
+
[...inspectedDesired].filter(
|
|
1419
|
+
([relativePath, entry]) => !shielded.has(relativePath) && (!selection || entry.kind === "directory" || selection.write.has(relativePath))
|
|
1420
|
+
)
|
|
1421
|
+
);
|
|
669
1422
|
const replaced = /* @__PURE__ */ new Map();
|
|
670
1423
|
const absent = [];
|
|
671
|
-
for (const [relativePath, entry] of
|
|
672
|
-
if (
|
|
1424
|
+
for (const [relativePath, entry] of existing) {
|
|
1425
|
+
if (selection ? !selection.remove.has(relativePath) : inspectedDesired.has(relativePath)) continue;
|
|
1426
|
+
if (entry.kind === "gitlink") continue;
|
|
673
1427
|
if (directoryShieldsSubmodule(relativePath, entry, target.git)) continue;
|
|
674
1428
|
replaced.set(relativePath, entry);
|
|
675
1429
|
}
|
|
@@ -680,7 +1434,8 @@ function planWorkingTreeMirror(input) {
|
|
|
680
1434
|
continue;
|
|
681
1435
|
}
|
|
682
1436
|
const { absolutePath, stat } = targetEntry;
|
|
683
|
-
if (
|
|
1437
|
+
if (options.overrides?.has(relativePath)) {
|
|
1438
|
+
} else if (entry.kind === "directory" || entry.kind === "gitlink") {
|
|
684
1439
|
if (stat.isDirectory()) continue;
|
|
685
1440
|
} else if (entry.kind === "symlink") {
|
|
686
1441
|
if (stat.isSymbolicLink() && fs.readlinkSync(absolutePath) === entry.target) continue;
|
|
@@ -701,7 +1456,208 @@ function planWorkingTreeMirror(input) {
|
|
|
701
1456
|
desired,
|
|
702
1457
|
replaced,
|
|
703
1458
|
absent: absent.filter((relativePath) => !hasAncestorIn(relativePath, replacedDirectories)).sort(shallowestFirst),
|
|
704
|
-
index: target.git ? planIndexChanges(inspectedDesired, shielded, target.git) : { removed: [], written: [] }
|
|
1459
|
+
index: target.git ? planIndexChanges(selection?.desiredForIndex ?? inspectedDesired, shielded, target.git) : { removed: [], written: [] }
|
|
1460
|
+
};
|
|
1461
|
+
}
|
|
1462
|
+
function rollbackWorkingTreeMirrorJournal(input) {
|
|
1463
|
+
const targetRoot = path.resolve(input.targetRoot);
|
|
1464
|
+
const snapshotRoot = path.resolve(input.snapshotRoot);
|
|
1465
|
+
const { journal } = input;
|
|
1466
|
+
const state = newPassState(targetRoot);
|
|
1467
|
+
const touched = /* @__PURE__ */ new Set();
|
|
1468
|
+
const abandoned = [];
|
|
1469
|
+
const restoredTrees = /* @__PURE__ */ new Set();
|
|
1470
|
+
const targetPathOf = (relativePath) => {
|
|
1471
|
+
const targetPath = path.join(targetRoot, ...relativePath.split("/"));
|
|
1472
|
+
assertInside(targetRoot, targetPath);
|
|
1473
|
+
return targetPath;
|
|
1474
|
+
};
|
|
1475
|
+
const stageFromSnapshot = (relativePath, replaced) => {
|
|
1476
|
+
const captured = inspectRelativePathWithoutFollowingAncestors(snapshotRoot, relativePath);
|
|
1477
|
+
if (captured.kind !== "entry") throw new Error(`Working-tree snapshot does not hold the replaced entry: ${relativePath}`);
|
|
1478
|
+
const stagedPath = stagingPathFor(state, targetPathOf(relativePath), relativePath);
|
|
1479
|
+
if (replaced.kind === "symlink") {
|
|
1480
|
+
if (!captured.stat.isSymbolicLink()) throw new Error(`Working-tree snapshot does not hold the replaced entry: ${relativePath}`);
|
|
1481
|
+
fs.symlinkSync(fs.readlinkSync(captured.absolutePath), stagedPath);
|
|
1482
|
+
return { path: stagedPath, stat: fs.lstatSync(stagedPath) };
|
|
1483
|
+
}
|
|
1484
|
+
if (!captured.stat.isFile()) throw new Error(`Working-tree snapshot does not hold the replaced entry: ${relativePath}`);
|
|
1485
|
+
return { path: stagedPath, stat: stageRegularFile(captured.absolutePath, stagedPath, replaced, "per_entry").stagedStat };
|
|
1486
|
+
};
|
|
1487
|
+
const restoreReplaced = (entry, expected) => {
|
|
1488
|
+
const replaced = entry.replaced;
|
|
1489
|
+
const { relativePath } = entry;
|
|
1490
|
+
if (replaced.kind === "gitlink") return true;
|
|
1491
|
+
if (replaced.kind === "directory") return restoreCapturedDirectory(relativePath, replaced);
|
|
1492
|
+
let staged;
|
|
1493
|
+
let onFailure = "unlink";
|
|
1494
|
+
if (entry.displaced && lstatIfExists(entry.displaced.path)) {
|
|
1495
|
+
const current = fs.lstatSync(entry.displaced.path);
|
|
1496
|
+
if (!sameWorkingTreeInode(current, entry.displaced.stat))
|
|
1497
|
+
throw new Error(`Retained working-tree entry was replaced: ${entry.displaced.path}`);
|
|
1498
|
+
if (!retainedEntryIntact(entry.displaced, current)) onFailure = "retain";
|
|
1499
|
+
staged = { path: entry.displaced.path, stat: current };
|
|
1500
|
+
} else {
|
|
1501
|
+
staged = stageFromSnapshot(relativePath, replaced);
|
|
1502
|
+
}
|
|
1503
|
+
try {
|
|
1504
|
+
const outcome = switchStagedEntry({
|
|
1505
|
+
state,
|
|
1506
|
+
targetRoot,
|
|
1507
|
+
relativePath,
|
|
1508
|
+
targetPath: targetPathOf(relativePath),
|
|
1509
|
+
stagedPath: staged.path,
|
|
1510
|
+
stagedStat: staged.stat,
|
|
1511
|
+
expected,
|
|
1512
|
+
expectedContent: expected === null ? void 0 : entry.afterContent,
|
|
1513
|
+
journal,
|
|
1514
|
+
phase: "before_rollback_switch",
|
|
1515
|
+
onFailure
|
|
1516
|
+
});
|
|
1517
|
+
if (outcome.displaced) releaseRetainedEntry(journal, relativePath, outcome.displaced);
|
|
1518
|
+
delete entry.displaced;
|
|
1519
|
+
return true;
|
|
1520
|
+
} catch (error) {
|
|
1521
|
+
if (!(error instanceof WorkingTreeTargetChangedError)) throw error;
|
|
1522
|
+
if (onFailure === "retain") delete entry.displaced;
|
|
1523
|
+
else if (entry.displaced && !lstatIfExists(entry.displaced.path)) delete entry.displaced;
|
|
1524
|
+
return false;
|
|
1525
|
+
}
|
|
1526
|
+
};
|
|
1527
|
+
const restoreCapturedDirectory = (relativePath, replaced) => {
|
|
1528
|
+
const captured = inspectRelativePathWithoutFollowingAncestors(snapshotRoot, relativePath);
|
|
1529
|
+
if (captured.kind !== "entry" || !captured.stat.isDirectory()) {
|
|
1530
|
+
throw new Error(`Working-tree snapshot does not hold the captured directory: ${relativePath}`);
|
|
1531
|
+
}
|
|
1532
|
+
try {
|
|
1533
|
+
fs.mkdirSync(targetPathOf(relativePath), { mode: replaced.mode & 511 });
|
|
1534
|
+
} catch (error) {
|
|
1535
|
+
if (isPathStateCode(error.code)) return false;
|
|
1536
|
+
throw error;
|
|
1537
|
+
}
|
|
1538
|
+
let complete = true;
|
|
1539
|
+
const children = inspectWorkingTree(captured.absolutePath, "all");
|
|
1540
|
+
for (const [childRelative, child] of [...children.entries()].sort(([left], [right]) => shallowestFirst(left, right))) {
|
|
1541
|
+
const childPath = path.posix.join(relativePath, childRelative);
|
|
1542
|
+
if (child.kind === "gitlink") continue;
|
|
1543
|
+
if (child.kind === "directory") {
|
|
1544
|
+
try {
|
|
1545
|
+
fs.mkdirSync(targetPathOf(childPath), { mode: child.mode & 511 });
|
|
1546
|
+
} catch (error) {
|
|
1547
|
+
if (!isPathStateCode(error.code)) throw error;
|
|
1548
|
+
const now = inspectRelativePathWithoutFollowingAncestors(targetRoot, childPath);
|
|
1549
|
+
if (now.kind !== "entry" || !now.stat.isDirectory() || now.stat.isSymbolicLink()) {
|
|
1550
|
+
abandoned.push(childPath);
|
|
1551
|
+
complete = false;
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
continue;
|
|
1555
|
+
}
|
|
1556
|
+
const staged = stageFromSnapshot(childPath, child);
|
|
1557
|
+
try {
|
|
1558
|
+
switchStagedEntry({
|
|
1559
|
+
state,
|
|
1560
|
+
targetRoot,
|
|
1561
|
+
relativePath: childPath,
|
|
1562
|
+
targetPath: targetPathOf(childPath),
|
|
1563
|
+
stagedPath: staged.path,
|
|
1564
|
+
stagedStat: staged.stat,
|
|
1565
|
+
expected: null,
|
|
1566
|
+
journal,
|
|
1567
|
+
phase: "before_rollback_switch",
|
|
1568
|
+
onFailure: "unlink"
|
|
1569
|
+
});
|
|
1570
|
+
} catch (error) {
|
|
1571
|
+
if (!(error instanceof WorkingTreeTargetChangedError)) throw error;
|
|
1572
|
+
abandoned.push(childPath);
|
|
1573
|
+
complete = false;
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
restoredTrees.add(relativePath);
|
|
1577
|
+
return complete;
|
|
1578
|
+
};
|
|
1579
|
+
for (const entry of [...journal.entries].reverse()) {
|
|
1580
|
+
const { relativePath } = entry;
|
|
1581
|
+
if (hasAncestorIn(relativePath, restoredTrees)) {
|
|
1582
|
+
touched.add(relativePath);
|
|
1583
|
+
continue;
|
|
1584
|
+
}
|
|
1585
|
+
const current = inspectRelativePathWithoutFollowingAncestors(targetRoot, relativePath);
|
|
1586
|
+
if (current.kind === "blocked") {
|
|
1587
|
+
abandoned.push(relativePath);
|
|
1588
|
+
continue;
|
|
1589
|
+
}
|
|
1590
|
+
if (entry.after === null) {
|
|
1591
|
+
if (current.kind !== "missing") {
|
|
1592
|
+
abandoned.push(relativePath);
|
|
1593
|
+
continue;
|
|
1594
|
+
}
|
|
1595
|
+
if (entry.replaced && !restoreReplaced(entry, null)) {
|
|
1596
|
+
abandoned.push(relativePath);
|
|
1597
|
+
continue;
|
|
1598
|
+
}
|
|
1599
|
+
touched.add(relativePath);
|
|
1600
|
+
continue;
|
|
1601
|
+
}
|
|
1602
|
+
if (current.kind !== "entry" || !workingTreeStatsMatch(entry.after, current.stat) || entry.afterContent !== void 0 && entryContent(current.absolutePath, current.stat) !== entry.afterContent) {
|
|
1603
|
+
if (!(current.kind === "entry" && entry.after.isDirectory() && current.stat.isDirectory())) {
|
|
1604
|
+
abandoned.push(relativePath);
|
|
1605
|
+
continue;
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
if (current.stat.isDirectory() && !current.stat.isSymbolicLink()) {
|
|
1609
|
+
try {
|
|
1610
|
+
fs.rmdirSync(current.absolutePath);
|
|
1611
|
+
} catch (error) {
|
|
1612
|
+
const code = error.code;
|
|
1613
|
+
if (code === "ENOTEMPTY" || code === "EEXIST" || code === "ENOTDIR") {
|
|
1614
|
+
abandoned.push(relativePath);
|
|
1615
|
+
continue;
|
|
1616
|
+
}
|
|
1617
|
+
if (code !== "ENOENT") throw error;
|
|
1618
|
+
}
|
|
1619
|
+
if (entry.replaced && !restoreReplaced(entry, null)) {
|
|
1620
|
+
abandoned.push(relativePath);
|
|
1621
|
+
continue;
|
|
1622
|
+
}
|
|
1623
|
+
touched.add(relativePath);
|
|
1624
|
+
continue;
|
|
1625
|
+
}
|
|
1626
|
+
if (entry.replaced) {
|
|
1627
|
+
if (!restoreReplaced(entry, entry.after)) {
|
|
1628
|
+
abandoned.push(relativePath);
|
|
1629
|
+
continue;
|
|
1630
|
+
}
|
|
1631
|
+
touched.add(relativePath);
|
|
1632
|
+
continue;
|
|
1633
|
+
}
|
|
1634
|
+
try {
|
|
1635
|
+
const displaced = removeSwitchedEntry({
|
|
1636
|
+
state,
|
|
1637
|
+
targetRoot,
|
|
1638
|
+
relativePath,
|
|
1639
|
+
targetPath: targetPathOf(relativePath),
|
|
1640
|
+
expected: entry.after,
|
|
1641
|
+
expectedContent: entry.afterContent,
|
|
1642
|
+
journal,
|
|
1643
|
+
phase: "before_rollback_remove"
|
|
1644
|
+
});
|
|
1645
|
+
releaseRetainedEntry(journal, relativePath, displaced);
|
|
1646
|
+
} catch (error) {
|
|
1647
|
+
if (!(error instanceof WorkingTreeTargetChangedError)) throw error;
|
|
1648
|
+
abandoned.push(relativePath);
|
|
1649
|
+
continue;
|
|
1650
|
+
}
|
|
1651
|
+
touched.add(relativePath);
|
|
1652
|
+
}
|
|
1653
|
+
releaseRetainedEntries(journal);
|
|
1654
|
+
if (journal.indexApplied) {
|
|
1655
|
+
updateIndex(targetRoot, { drop: journal.indexApplied.written, put: journal.indexApplied.removed });
|
|
1656
|
+
}
|
|
1657
|
+
return {
|
|
1658
|
+
paths: [...touched].sort(shallowestFirst),
|
|
1659
|
+
abandoned: [...new Set(abandoned)].sort(shallowestFirst),
|
|
1660
|
+
...journal.retained && journal.retained.length > 0 ? { retained: [...journal.retained] } : {}
|
|
705
1661
|
};
|
|
706
1662
|
}
|
|
707
1663
|
function captureWorkingTreeMirrorPlan(input) {
|
|
@@ -782,7 +1738,8 @@ function restoreWorkingTreeMirrorScope(input) {
|
|
|
782
1738
|
for (const name of fs.readdirSync(absoluteDirectory)) {
|
|
783
1739
|
if (!MIRROR_TEMPORARY_FILE_PATTERN.test(name)) continue;
|
|
784
1740
|
const temporaryPath = path.join(absoluteDirectory, name);
|
|
785
|
-
|
|
1741
|
+
const temporaryStatus = fs.lstatSync(temporaryPath);
|
|
1742
|
+
if (!temporaryStatus.isFile() && !temporaryStatus.isSymbolicLink()) continue;
|
|
786
1743
|
fs.rmSync(temporaryPath, { force: true });
|
|
787
1744
|
touched.add(directory === "." ? name : path.posix.join(directory, name));
|
|
788
1745
|
}
|
|
@@ -835,12 +1792,26 @@ function fsyncWorkingTreePaths(root, relativePaths) {
|
|
|
835
1792
|
}
|
|
836
1793
|
}
|
|
837
1794
|
export {
|
|
1795
|
+
WorkingTreeTargetChangedError,
|
|
1796
|
+
applyWorkingTreeMirror,
|
|
838
1797
|
assertWorkingTreeHasNoPortableGitMetadataAliases,
|
|
839
1798
|
captureWorkingTreeMirrorPlan,
|
|
1799
|
+
displacedEntryMatches,
|
|
840
1800
|
fsyncWorkingTreePaths,
|
|
841
1801
|
inspectWorkingTree,
|
|
1802
|
+
inspectWorkingTreePath,
|
|
842
1803
|
mirrorWorkingTree,
|
|
843
1804
|
normalizeWorkingTreeRelativePath,
|
|
1805
|
+
planPreparedWorkingTreeMirror,
|
|
844
1806
|
planWorkingTreeMirror,
|
|
845
|
-
|
|
1807
|
+
prepareWorkingTreeMirror,
|
|
1808
|
+
projectedFileEntry,
|
|
1809
|
+
projectedFileEntryMatches,
|
|
1810
|
+
restoreWorkingTreeMirrorScope,
|
|
1811
|
+
rollbackWorkingTreeMirrorJournal,
|
|
1812
|
+
sameWorkingTreeInode,
|
|
1813
|
+
setWorkingTreeMirrorRaceHook,
|
|
1814
|
+
workingTreeAtomicRenameSupport,
|
|
1815
|
+
workingTreeDisplacedRetentionDirectory,
|
|
1816
|
+
workingTreeStatsMatch
|
|
846
1817
|
};
|