@ricsam/r5d-worker 0.0.131 → 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.
Files changed (47) hide show
  1. package/dist/cjs/atomic-rename.cjs +303 -0
  2. package/dist/cjs/git-blob-hash.cjs +41 -0
  3. package/dist/cjs/main.cjs +187 -38
  4. package/dist/cjs/package.json +1 -1
  5. package/dist/cjs/three-way-merge.cjs +346 -0
  6. package/dist/cjs/working-tree-mirror.cjs +1049 -64
  7. package/dist/cjs/workspace-command-sync-policy.cjs +8 -4
  8. package/dist/cjs/workspace-command-targets.cjs +63 -0
  9. package/dist/cjs/workspace-filesystem-job-types.cjs +11 -1
  10. package/dist/cjs/workspace-filesystem-jobs.cjs +2 -0
  11. package/dist/cjs/workspace-git-sync.cjs +846 -61
  12. package/dist/cjs/workspace-hydration-ledger.cjs +66 -0
  13. package/dist/cjs/workspace-hydration-merge.cjs +433 -0
  14. package/dist/cjs/workspace-hydration-recovery-state.cjs +53 -0
  15. package/dist/cjs/workspace-merge-projection.cjs +81 -10
  16. package/dist/cjs/workspace-project-config-policy.cjs +19 -12
  17. package/dist/mjs/atomic-rename.mjs +261 -0
  18. package/dist/mjs/git-blob-hash.mjs +16 -0
  19. package/dist/mjs/main.mjs +196 -39
  20. package/dist/mjs/package.json +1 -1
  21. package/dist/mjs/three-way-merge.mjs +318 -0
  22. package/dist/mjs/working-tree-mirror.mjs +1035 -64
  23. package/dist/mjs/workspace-command-sync-policy.mjs +8 -4
  24. package/dist/mjs/workspace-command-targets.mjs +37 -0
  25. package/dist/mjs/workspace-filesystem-job-types.mjs +11 -1
  26. package/dist/mjs/workspace-filesystem-jobs.mjs +4 -0
  27. package/dist/mjs/workspace-git-sync.mjs +854 -62
  28. package/dist/mjs/workspace-hydration-ledger.mjs +42 -0
  29. package/dist/mjs/workspace-hydration-merge.mjs +399 -0
  30. package/dist/mjs/workspace-hydration-recovery-state.mjs +29 -0
  31. package/dist/mjs/workspace-merge-projection.mjs +85 -11
  32. package/dist/mjs/workspace-project-config-policy.mjs +16 -10
  33. package/dist/types/atomic-rename.d.ts +78 -0
  34. package/dist/types/git-blob-hash.d.ts +10 -0
  35. package/dist/types/main.d.ts +21 -2
  36. package/dist/types/three-way-merge.d.ts +77 -0
  37. package/dist/types/working-tree-mirror.d.ts +270 -7
  38. package/dist/types/workspace-command-sync-policy.d.ts +12 -6
  39. package/dist/types/workspace-command-targets.d.ts +37 -0
  40. package/dist/types/workspace-filesystem-job-types.d.ts +46 -4
  41. package/dist/types/workspace-git-sync.d.ts +125 -3
  42. package/dist/types/workspace-hydration-ledger.d.ts +43 -0
  43. package/dist/types/workspace-hydration-merge.d.ts +95 -0
  44. package/dist/types/workspace-hydration-recovery-state.d.ts +10 -0
  45. package/dist/types/workspace-merge-projection.d.ts +19 -1
  46. package/dist/types/workspace-project-config-policy.d.ts +17 -3
  47. 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}/`)) entries.delete(existing);
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) entries.set(relativePath, { kind: "gitlink", 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") entries.set(relativePath, { kind: "gitlink", objectId });
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 copyRegularFile(sourcePath, targetPath, entry, durability) {
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
- temporaryPath,
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
- fs.renameSync(temporaryPath, targetPath);
423
- if (durability === "per_entry") {
424
- const parent = fs.openSync(path.dirname(targetPath), fs.constants.O_RDONLY);
425
- try {
426
- fs.fsyncSync(parent);
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
- removeEntry(targetRoot, relativePath);
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 (!filesEqual(sourcePath, targetPath, entry)) {
477
- const target = inspectRelativePathWithoutFollowingAncestors(targetRoot, relativePath);
478
- if (target.kind === "blocked") {
479
- throw new Error(`Refusing to copy through a symlink or non-directory: ${target.blockingPath}`);
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
- const targetStat = target.kind === "entry" ? target.stat : null;
482
- if (targetStat && !targetStat.isFile()) removeEntry(targetRoot, relativePath);
483
- copyRegularFile(sourcePath, targetPath, entry, durability);
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 inspectMirrorTarget(targetRoot, deletionMode) {
487
- if (deletionMode !== "git") return { existing: inspectWorkingTree(targetRoot, "all"), git: null };
488
- const inspection = inspectGitWorkingTree(targetRoot);
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 mirrorWorkingTree(input) {
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 target = inspectMirrorTarget(targetRoot, input.deletionMode);
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
- const index = target.git ? planIndexChanges(desired, shielded, target.git) : null;
602
- for (const relativePath of [...target.existing.keys()].sort(deepestFirst)) {
603
- if (desired.has(relativePath)) continue;
604
- const entry = target.existing.get(relativePath);
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
- removeEntry(targetRoot, relativePath);
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
- ensureDirectory(targetRoot, relativePath, 493);
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
- recordGitlinkStaleHeads(targetRoot, desired, shielded, target.git);
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
- const sourceRoot = path.resolve(input.sourceRoot);
664
- const targetRoot = path.resolve(input.targetRoot);
665
- const inspectedDesired = inspectWorkingTree(sourceRoot, input.sourceMode, { gitlinks: input.sourceGitlinks });
666
- const target = inspectMirrorTarget(targetRoot, input.deletionMode);
667
- const shielded = target.git ? shieldedDesiredPaths(inspectedDesired, target.git) : /* @__PURE__ */ new Set();
668
- const desired = new Map([...inspectedDesired].filter(([relativePath]) => !shielded.has(relativePath)));
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 target.existing) {
672
- if (inspectedDesired.has(relativePath) || entry.kind === "gitlink") continue;
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 (entry.kind === "directory" || entry.kind === "gitlink") {
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
- if (!fs.lstatSync(temporaryPath).isFile()) continue;
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
- restoreWorkingTreeMirrorScope
1807
+ prepareWorkingTreeMirror,
1808
+ projectedFileEntry,
1809
+ projectedFileEntryMatches,
1810
+ restoreWorkingTreeMirrorScope,
1811
+ rollbackWorkingTreeMirrorJournal,
1812
+ sameWorkingTreeInode,
1813
+ setWorkingTreeMirrorRaceHook,
1814
+ workingTreeAtomicRenameSupport,
1815
+ workingTreeDisplacedRetentionDirectory,
1816
+ workingTreeStatsMatch
846
1817
  };