@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
@@ -28,20 +28,70 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
28
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
29
  var working_tree_mirror_exports = {};
30
30
  __export(working_tree_mirror_exports, {
31
+ WorkingTreeTargetChangedError: () => WorkingTreeTargetChangedError,
32
+ applyWorkingTreeMirror: () => applyWorkingTreeMirror,
31
33
  assertWorkingTreeHasNoPortableGitMetadataAliases: () => assertWorkingTreeHasNoPortableGitMetadataAliases,
32
34
  captureWorkingTreeMirrorPlan: () => captureWorkingTreeMirrorPlan,
35
+ displacedEntryMatches: () => displacedEntryMatches,
33
36
  fsyncWorkingTreePaths: () => fsyncWorkingTreePaths,
34
37
  inspectWorkingTree: () => inspectWorkingTree,
38
+ inspectWorkingTreePath: () => inspectWorkingTreePath,
35
39
  mirrorWorkingTree: () => mirrorWorkingTree,
36
40
  normalizeWorkingTreeRelativePath: () => normalizeWorkingTreeRelativePath,
41
+ planPreparedWorkingTreeMirror: () => planPreparedWorkingTreeMirror,
37
42
  planWorkingTreeMirror: () => planWorkingTreeMirror,
38
- restoreWorkingTreeMirrorScope: () => restoreWorkingTreeMirrorScope
43
+ prepareWorkingTreeMirror: () => prepareWorkingTreeMirror,
44
+ projectedFileEntry: () => projectedFileEntry,
45
+ projectedFileEntryMatches: () => projectedFileEntryMatches,
46
+ restoreWorkingTreeMirrorScope: () => restoreWorkingTreeMirrorScope,
47
+ rollbackWorkingTreeMirrorJournal: () => rollbackWorkingTreeMirrorJournal,
48
+ sameWorkingTreeInode: () => sameWorkingTreeInode,
49
+ setWorkingTreeMirrorRaceHook: () => setWorkingTreeMirrorRaceHook,
50
+ workingTreeAtomicRenameSupport: () => workingTreeAtomicRenameSupport,
51
+ workingTreeDisplacedRetentionDirectory: () => workingTreeDisplacedRetentionDirectory,
52
+ workingTreeStatsMatch: () => workingTreeStatsMatch
39
53
  });
40
54
  module.exports = __toCommonJS(working_tree_mirror_exports);
41
55
  var import_node_fs = __toESM(require("node:fs"), 1);
42
56
  var import_node_path = __toESM(require("node:path"), 1);
43
57
  var import_node_crypto = require("node:crypto");
44
58
  var import_git_process_environment = require("./git-process-environment.cjs");
59
+ var import_atomic_rename = require("./atomic-rename.cjs");
60
+ function projectedFileEntry(stat) {
61
+ return { kind: "file", size: stat.size, mode: stat.mode, mtimeMs: stat.mtimeMs, ctimeMs: stat.ctimeMs, ino: stat.ino };
62
+ }
63
+ function projectedFileEntryMatches(entry, stat) {
64
+ return stat.isFile() && stat.ino === entry.ino && stat.size === entry.size && stat.mode === entry.mode && stat.mtimeMs === entry.mtimeMs && stat.ctimeMs === entry.ctimeMs;
65
+ }
66
+ function workingTreeStatsMatch(left, right) {
67
+ 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;
68
+ }
69
+ function sameWorkingTreeInode(left, right) {
70
+ return left.ino === right.ino && left.dev === right.dev;
71
+ }
72
+ function displacedEntryMatches(expected, displaced) {
73
+ return sameWorkingTreeInode(expected, displaced) && displaced.mode === expected.mode && displaced.size === expected.size && displaced.mtimeMs === expected.mtimeMs;
74
+ }
75
+ class WorkingTreeTargetChangedError extends Error {
76
+ constructor(relativePath, options = {}) {
77
+ const retained = options.retained ?? [];
78
+ super(
79
+ `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(", ")}` : "")
80
+ );
81
+ this.relativePath = relativePath;
82
+ this.name = "WorkingTreeTargetChangedError";
83
+ this.retained = retained;
84
+ }
85
+ relativePath;
86
+ retained;
87
+ }
88
+ let raceHook = null;
89
+ function setWorkingTreeMirrorRaceHook(hook) {
90
+ raceHook = hook;
91
+ }
92
+ function raceWindow(phase, relativePath, targetPath) {
93
+ if (raceHook) raceHook({ phase, relativePath, targetPath });
94
+ }
45
95
  const HFS_IGNORED_CODE_POINTS = /[\u200c-\u200f\u202a-\u202e\u206a-\u206f\ufeff]/gu;
46
96
  const MIRROR_TEMPORARY_FILE_PREFIX = ".r5d-working-tree-mirror-";
47
97
  const OBJECT_ID_PATTERN = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/u;
@@ -116,6 +166,9 @@ function inspectRelativePathWithoutFollowingAncestors(root, relativePath) {
116
166
  }
117
167
  return { kind: "missing" };
118
168
  }
169
+ function inspectWorkingTreePath(root, relativePath) {
170
+ return inspectRelativePathWithoutFollowingAncestors(import_node_path.default.resolve(root), relativePath);
171
+ }
119
172
  function inspectDirectoryRoot(root, options) {
120
173
  let stat;
121
174
  try {
@@ -260,7 +313,7 @@ function writeGitlinkStaleHeads(root, heads) {
260
313
  import_node_fs.default.renameSync(temporaryPath, markerPath);
261
314
  fsyncPath(directory, import_node_fs.default.constants.O_RDONLY);
262
315
  }
263
- function addEntry(entries, root, relativePath, recurseDirectories, gitlinks) {
316
+ function addEntry(entries, root, relativePath, recurseDirectories, gitlinks, stats) {
264
317
  const normalized = normalizeRelativePath(relativePath);
265
318
  if (!normalized || entries.has(normalized)) return;
266
319
  const gitlink = gitlinks?.get(normalized);
@@ -273,23 +326,29 @@ function addEntry(entries, root, relativePath, recurseDirectories, gitlinks) {
273
326
  const { absolutePath, stat } = inspected;
274
327
  if (stat.isSymbolicLink()) {
275
328
  for (const existing of [...entries.keys()]) {
276
- if (existing.startsWith(`${normalized}/`)) entries.delete(existing);
329
+ if (existing.startsWith(`${normalized}/`)) {
330
+ entries.delete(existing);
331
+ stats?.delete(existing);
332
+ }
277
333
  }
278
334
  entries.set(normalized, { kind: "symlink", mode: stat.mode, target: import_node_fs.default.readlinkSync(absolutePath) });
335
+ stats?.set(normalized, stat);
279
336
  return;
280
337
  }
281
338
  if (stat.isFile()) {
282
339
  entries.set(normalized, { kind: "file", mode: stat.mode, size: stat.size });
340
+ stats?.set(normalized, stat);
283
341
  return;
284
342
  }
285
343
  if (!stat.isDirectory()) return;
286
344
  entries.set(normalized, { kind: "directory", mode: stat.mode });
345
+ stats?.set(normalized, stat);
287
346
  if (!recurseDirectories) return;
288
347
  for (const child of import_node_fs.default.readdirSync(absolutePath).sort()) {
289
- addEntry(entries, root, import_node_path.default.posix.join(normalized, child), true, gitlinks);
348
+ addEntry(entries, root, import_node_path.default.posix.join(normalized, child), true, gitlinks, stats);
290
349
  }
291
350
  }
292
- function addParentDirectories(entries, root) {
351
+ function addParentDirectories(entries, root, stats) {
293
352
  for (const relativePath of [...entries.keys()]) {
294
353
  let parent = import_node_path.default.posix.dirname(relativePath);
295
354
  while (parent !== ".") {
@@ -299,12 +358,13 @@ function addParentDirectories(entries, root) {
299
358
  throw new Error(`Working-tree entry has an unsafe parent directory: ${parent}`);
300
359
  }
301
360
  entries.set(parent, { kind: "directory", mode: inspected.stat.mode });
361
+ stats?.set(parent, inspected.stat);
302
362
  }
303
363
  parent = import_node_path.default.posix.dirname(parent);
304
364
  }
305
365
  }
306
366
  }
307
- function inspectGitWorkingTree(root) {
367
+ function inspectGitWorkingTree(root, stats) {
308
368
  const entries = /* @__PURE__ */ new Map();
309
369
  const listing = listGitCheckout(root);
310
370
  const indexGitlinks = /* @__PURE__ */ new Map();
@@ -317,13 +377,13 @@ function inspectGitWorkingTree(root) {
317
377
  if (entries.has(relativePath)) continue;
318
378
  const indexObjectId = indexGitlinks.get(relativePath);
319
379
  if (indexObjectId === void 0 && !nestedRepository) {
320
- addEntry(entries, root, relativePath, true);
380
+ addEntry(entries, root, relativePath, true, void 0, stats);
321
381
  continue;
322
382
  }
323
383
  const inspected = inspectRelativePathWithoutFollowingAncestors(root, relativePath);
324
384
  if (inspected.kind !== "entry") continue;
325
385
  if (!inspected.stat.isDirectory() || inspected.stat.isSymbolicLink()) {
326
- addEntry(entries, root, relativePath, true);
386
+ addEntry(entries, root, relativePath, true, void 0, stats);
327
387
  continue;
328
388
  }
329
389
  const head = populatedCheckoutHead(root, inspected.absolutePath);
@@ -333,9 +393,12 @@ function inspectGitWorkingTree(root) {
333
393
  staleHeads ??= readGitlinkStaleHeads(root);
334
394
  objectId = staleHeads.get(relativePath) === head ? indexObjectId : head;
335
395
  }
336
- if (objectId) entries.set(relativePath, { kind: "gitlink", objectId });
396
+ if (objectId) {
397
+ entries.set(relativePath, { kind: "gitlink", objectId });
398
+ stats?.set(relativePath, inspected.stat);
399
+ }
337
400
  }
338
- addParentDirectories(entries, root);
401
+ addParentDirectories(entries, root, stats);
339
402
  return { entries, indexRecords: listing.indexRecords, submodules };
340
403
  }
341
404
  function normalizedGitlinks(gitlinks) {
@@ -352,13 +415,16 @@ function inspectWorkingTree(root, mode, options = {}) {
352
415
  const entries = /* @__PURE__ */ new Map();
353
416
  root = import_node_path.default.resolve(root);
354
417
  if (!inspectDirectoryRoot(root, { allowMissing: true, label: "Working-tree root" })) return entries;
355
- if (mode === "git") return inspectGitWorkingTree(root).entries;
418
+ if (mode === "git") return inspectGitWorkingTree(root, options.stats).entries;
356
419
  const gitlinks = normalizedGitlinks(options.gitlinks);
357
- for (const child of import_node_fs.default.readdirSync(root).sort()) addEntry(entries, root, child, true, gitlinks);
420
+ for (const child of import_node_fs.default.readdirSync(root).sort()) addEntry(entries, root, child, true, gitlinks, options.stats);
358
421
  for (const [relativePath, objectId] of gitlinks) {
359
- if (entries.get(relativePath)?.kind !== "gitlink") entries.set(relativePath, { kind: "gitlink", objectId });
422
+ if (entries.get(relativePath)?.kind !== "gitlink") {
423
+ entries.set(relativePath, { kind: "gitlink", objectId });
424
+ options.stats?.delete(relativePath);
425
+ }
360
426
  }
361
- addParentDirectories(entries, root);
427
+ addParentDirectories(entries, root, options.stats);
362
428
  return entries;
363
429
  }
364
430
  function removeEntry(targetRoot, relativePath) {
@@ -369,6 +435,443 @@ function removeEntry(targetRoot, relativePath) {
369
435
  }
370
436
  import_node_fs.default.rmSync(inspected.absolutePath, { recursive: true, force: true });
371
437
  }
438
+ function assertTargetExpectation(targetRoot, relativePath, guards) {
439
+ if (!guards?.expectations?.has(relativePath)) return;
440
+ const expected = guards.expectations.get(relativePath) ?? null;
441
+ const inspected = inspectRelativePathWithoutFollowingAncestors(targetRoot, relativePath);
442
+ if (expected === null) {
443
+ if (inspected.kind === "missing") return;
444
+ throw new WorkingTreeTargetChangedError(relativePath);
445
+ }
446
+ if (expected.isDirectory() && !expected.isSymbolicLink()) {
447
+ if (inspected.kind === "entry" && inspected.stat.isDirectory() && !inspected.stat.isSymbolicLink()) return;
448
+ throw new WorkingTreeTargetChangedError(relativePath);
449
+ }
450
+ if (inspected.kind !== "entry" || !workingTreeStatsMatch(expected, inspected.stat)) throw new WorkingTreeTargetChangedError(relativePath);
451
+ }
452
+ const DISPLACED_RETENTION_DIRECTORY = "hydration-displaced";
453
+ const RETAINED_NAME_MAX_ENCODED_LENGTH = 150;
454
+ const YIELD_ATTEMPTS = 4;
455
+ const passStates = /* @__PURE__ */ new WeakMap();
456
+ function newPassState(targetRoot) {
457
+ const retention = retentionDirectoryFor(targetRoot);
458
+ const primitives = (0, import_atomic_rename.atomicRenamePrimitivesFor)(retention ?? targetRoot);
459
+ return { targetRoot, retention, primitives, exchangeUnavailable: primitives.support.exchange === "unavailable" };
460
+ }
461
+ function workingTreeAtomicRenameSupport(targetRoot) {
462
+ return newPassState(import_node_path.default.resolve(targetRoot)).primitives.support;
463
+ }
464
+ function guardedPassState(guards, targetRoot) {
465
+ let state = passStates.get(guards);
466
+ if (!state) {
467
+ state = newPassState(targetRoot);
468
+ passStates.set(guards, state);
469
+ }
470
+ return state;
471
+ }
472
+ function workingTreeDisplacedRetentionDirectory(targetRoot) {
473
+ const gitDirectory = checkoutGitDirectory(import_node_path.default.resolve(targetRoot));
474
+ return gitDirectory ? import_node_path.default.join(gitDirectory, "r5d", DISPLACED_RETENTION_DIRECTORY) : null;
475
+ }
476
+ function retentionDirectoryFor(targetRoot) {
477
+ const directory = workingTreeDisplacedRetentionDirectory(targetRoot);
478
+ if (!directory) return null;
479
+ try {
480
+ import_node_fs.default.mkdirSync(directory, { recursive: true, mode: 448 });
481
+ const status = import_node_fs.default.lstatSync(directory);
482
+ if (!status.isDirectory() || status.isSymbolicLink()) return null;
483
+ if (status.dev !== import_node_fs.default.lstatSync(targetRoot).dev) return null;
484
+ return directory;
485
+ } catch {
486
+ return null;
487
+ }
488
+ }
489
+ function encodeRetainedName(relativePath) {
490
+ let encoded = "";
491
+ for (const byte of Buffer.from(relativePath, "utf8")) {
492
+ const isPlain = byte >= 48 && byte <= 57 || byte >= 65 && byte <= 90 || byte >= 97 && byte <= 122 || byte === 45 || byte === 46 || byte === 95;
493
+ encoded += isPlain ? String.fromCharCode(byte) : `%${byte.toString(16).padStart(2, "0")}`;
494
+ }
495
+ return encoded.length > RETAINED_NAME_MAX_ENCODED_LENGTH ? encoded.slice(0, RETAINED_NAME_MAX_ENCODED_LENGTH) : encoded;
496
+ }
497
+ function stagingPathFor(state, targetPath, relativePath) {
498
+ if (state.retention) return import_node_path.default.join(state.retention, `${(0, import_node_crypto.randomUUID)()}.${encodeRetainedName(relativePath)}`);
499
+ return import_node_path.default.join(import_node_path.default.dirname(targetPath), `${MIRROR_TEMPORARY_FILE_PREFIX}${(0, import_node_crypto.randomUUID)()}.tmp`);
500
+ }
501
+ function switchExpectation(targetRoot, relativePath, guards) {
502
+ if (guards.expectations?.has(relativePath)) return guards.expectations.get(relativePath) ?? null;
503
+ const inspected = inspectRelativePathWithoutFollowingAncestors(targetRoot, relativePath);
504
+ if (inspected.kind === "blocked") throw new Error(`Refusing to copy through a symlink or non-directory: ${inspected.blockingPath}`);
505
+ return inspected.kind === "entry" ? inspected.stat : null;
506
+ }
507
+ function settledAfter(targetPath, stagedStat, stagedContent) {
508
+ const current = lstatIfExists(targetPath);
509
+ return current && displacedEntryMatches(stagedStat, current) && entryContent(targetPath, current) === stagedContent ? current : stagedStat;
510
+ }
511
+ function entryContent(filePath, expected, expectedBlob) {
512
+ try {
513
+ if (expected.isSymbolicLink()) {
514
+ const before = import_node_fs.default.lstatSync(filePath);
515
+ if (!workingTreeStatsMatch(expected, before)) return null;
516
+ const bytes = Buffer.from(import_node_fs.default.readlinkSync(filePath));
517
+ if (!workingTreeStatsMatch(before, import_node_fs.default.lstatSync(filePath))) return null;
518
+ const digest = (0, import_node_crypto.createHash)("sha256").update(`blob ${bytes.length}\0`).update(bytes).digest("hex");
519
+ if (expectedBlob && (expectedBlob.length === 64 ? digest : (0, import_node_crypto.createHash)("sha1").update(`blob ${bytes.length}\0`).update(bytes).digest("hex")) !== expectedBlob)
520
+ return null;
521
+ return digest;
522
+ }
523
+ if (!expected.isFile()) return null;
524
+ const descriptor = import_node_fs.default.openSync(filePath, import_node_fs.default.constants.O_RDONLY | import_node_fs.default.constants.O_NOFOLLOW);
525
+ try {
526
+ const before = import_node_fs.default.fstatSync(descriptor);
527
+ if (!workingTreeStatsMatch(expected, before)) return null;
528
+ const hash = (0, import_node_crypto.createHash)("sha256").update(`blob ${before.size}\0`);
529
+ const knownHash = expectedBlob && expectedBlob.length !== 64 ? (0, import_node_crypto.createHash)("sha1").update(`blob ${before.size}\0`) : null;
530
+ const buffer = Buffer.allocUnsafe(64 * 1024);
531
+ for (let offset = 0; offset < before.size; ) {
532
+ const count = import_node_fs.default.readSync(descriptor, buffer, 0, Math.min(buffer.length, before.size - offset), offset);
533
+ if (count <= 0) return null;
534
+ const bytes = buffer.subarray(0, count);
535
+ hash.update(bytes);
536
+ knownHash?.update(bytes);
537
+ offset += count;
538
+ }
539
+ if (!workingTreeStatsMatch(before, import_node_fs.default.fstatSync(descriptor)) || !workingTreeStatsMatch(before, import_node_fs.default.lstatSync(filePath))) return null;
540
+ const digest = hash.digest("hex");
541
+ if (expectedBlob && (knownHash ? knownHash.digest("hex") : digest) !== expectedBlob) return null;
542
+ return digest;
543
+ } finally {
544
+ import_node_fs.default.closeSync(descriptor);
545
+ }
546
+ } catch (error) {
547
+ if (isPathStateCode(error.code) || error.code === "ELOOP") return null;
548
+ throw error;
549
+ }
550
+ }
551
+ function fsyncRetainedEntry(retainedPath) {
552
+ const status = import_node_fs.default.lstatSync(retainedPath);
553
+ if (status.isDirectory() && !status.isSymbolicLink()) {
554
+ for (const child of import_node_fs.default.readdirSync(retainedPath)) fsyncRetainedEntry(import_node_path.default.join(retainedPath, child));
555
+ fsyncPath(retainedPath, import_node_fs.default.constants.O_RDONLY | import_node_fs.default.constants.O_NOFOLLOW);
556
+ } else if (status.isFile()) {
557
+ fsyncPath(retainedPath, import_node_fs.default.constants.O_RDONLY | import_node_fs.default.constants.O_NOFOLLOW);
558
+ }
559
+ fsyncPath(import_node_path.default.dirname(retainedPath), import_node_fs.default.constants.O_RDONLY | import_node_fs.default.constants.O_NOFOLLOW);
560
+ }
561
+ function retainForeign(journal, relativePath, retainedPath, reason) {
562
+ fsyncRetainedEntry(retainedPath);
563
+ const retained = { relativePath, path: retainedPath, reason };
564
+ if (journal) (journal.retained ??= []).push(retained);
565
+ return retained;
566
+ }
567
+ function isPathStateCode(code) {
568
+ return code === "EEXIST" || code === "ENOENT" || code === "ENOTDIR" || code === "ENOTEMPTY" || code === "EISDIR";
569
+ }
570
+ function publishNoReplace(state, stagedPath, stagedStat, relativePath, targetPath) {
571
+ if (stagedStat.isSymbolicLink() && state.primitives.support.noReplace === "link") {
572
+ throw new WorkingTreeTargetChangedError(relativePath, { detail: "filesystem cannot publish a symlink with provable ownership" });
573
+ }
574
+ try {
575
+ state.primitives.renameNoReplace(stagedPath, targetPath);
576
+ } catch (error) {
577
+ if (error instanceof import_atomic_rename.AtomicRenameError && isPathStateCode(error.code)) throw new WorkingTreeTargetChangedError(relativePath);
578
+ throw error;
579
+ }
580
+ return null;
581
+ }
582
+ function restoreDisplacedNoReplace(state, displacedPath, displaced, targetPath) {
583
+ try {
584
+ if (displaced.isSymbolicLink() && state.primitives.support.noReplace === "link") {
585
+ return false;
586
+ }
587
+ state.primitives.renameNoReplace(displacedPath, targetPath);
588
+ return true;
589
+ } catch (error) {
590
+ const code = error instanceof import_atomic_rename.AtomicRenameError ? error.code : error.code;
591
+ if (isPathStateCode(code) || code === "ENOTSUP") return false;
592
+ throw error;
593
+ }
594
+ }
595
+ function discardStaged(input, reason) {
596
+ const current = lstatIfExists(input.stagedPath);
597
+ if (!current) return null;
598
+ if (input.onFailure === "unlink" && input.stagedContent && displacedEntryMatches(input.stagedStat, current) && entryContent(input.stagedPath, current) === input.stagedContent) {
599
+ import_node_fs.default.rmSync(input.stagedPath, { force: true });
600
+ return null;
601
+ }
602
+ return retainForeign(input.journal, input.relativePath, input.stagedPath, reason);
603
+ }
604
+ function discardStagedAndThrow(input, error) {
605
+ const retained = discardStaged(input, "unrestorable");
606
+ if (retained && error instanceof WorkingTreeTargetChangedError) {
607
+ throw new WorkingTreeTargetChangedError(input.relativePath, { detail: error.message, retained: [...error.retained, retained] });
608
+ }
609
+ throw error;
610
+ }
611
+ function switchStagedEntry(input) {
612
+ const { state, targetRoot, relativePath, targetPath, stagedPath, stagedStat, expected } = input;
613
+ const stagedContent = input.stagedContent ?? entryContent(stagedPath, stagedStat);
614
+ if (!stagedContent) {
615
+ const retained = retainForeign(input.journal, relativePath, stagedPath, "unrestorable");
616
+ throw new WorkingTreeTargetChangedError(relativePath, { retained: [retained] });
617
+ }
618
+ input.stagedContent = stagedContent;
619
+ if (stagedStat.isSymbolicLink() && state.primitives.support.noReplace === "link") {
620
+ discardStagedAndThrow(
621
+ input,
622
+ new WorkingTreeTargetChangedError(relativePath, { detail: "filesystem cannot publish a symlink with provable ownership" })
623
+ );
624
+ }
625
+ const inspected = inspectRelativePathWithoutFollowingAncestors(targetRoot, relativePath);
626
+ if (inspected.kind === "blocked") {
627
+ discardStagedAndThrow(input, new Error(`Refusing to copy through a symlink or non-directory: ${inspected.blockingPath}`));
628
+ }
629
+ if (expected === null) {
630
+ if (inspected.kind !== "missing") {
631
+ discardStagedAndThrow(input, new WorkingTreeTargetChangedError(relativePath));
632
+ }
633
+ raceWindow(input.phase, relativePath, targetPath);
634
+ let created;
635
+ try {
636
+ created = publishNoReplace(state, stagedPath, stagedStat, relativePath, targetPath);
637
+ } catch (error) {
638
+ discardStagedAndThrow(input, error);
639
+ }
640
+ raceWindow("after_switch", relativePath, targetPath);
641
+ return { after: created ?? settledAfter(targetPath, stagedStat, stagedContent), afterContent: stagedContent, displaced: null };
642
+ }
643
+ if (inspected.kind !== "entry" || !workingTreeStatsMatch(expected, inspected.stat)) {
644
+ discardStagedAndThrow(input, new WorkingTreeTargetChangedError(relativePath));
645
+ }
646
+ const expectedContent = entryContent(targetPath, expected, input.expectedBlob);
647
+ if (!expectedContent || input.expectedContent && input.expectedContent !== expectedContent) {
648
+ discardStagedAndThrow(input, new WorkingTreeTargetChangedError(relativePath));
649
+ }
650
+ input.expectedContent = expectedContent;
651
+ raceWindow(input.phase, relativePath, targetPath);
652
+ if (!state.exchangeUnavailable) {
653
+ try {
654
+ state.primitives.exchange(stagedPath, targetPath);
655
+ } catch (error) {
656
+ if ((0, import_atomic_rename.isAtomicRenameUnsupported)(error)) {
657
+ state.exchangeUnavailable = true;
658
+ return switchWithoutExchange(input);
659
+ }
660
+ if (error instanceof import_atomic_rename.AtomicRenameError && isPathStateCode(error.code)) {
661
+ discardStagedAndThrow(input, new WorkingTreeTargetChangedError(relativePath, { detail: `${error.code} at the switch` }));
662
+ }
663
+ discardStagedAndThrow(input, error);
664
+ }
665
+ raceWindow("after_switch", relativePath, targetPath);
666
+ const displaced = import_node_fs.default.lstatSync(stagedPath);
667
+ if (displacedEntryMatches(expected, displaced) && entryContent(stagedPath, displaced) === expectedContent) {
668
+ return {
669
+ after: settledAfter(targetPath, stagedStat, stagedContent),
670
+ afterContent: stagedContent,
671
+ displaced: { path: stagedPath, stat: displaced, content: expectedContent }
672
+ };
673
+ }
674
+ return yieldDisplacedEntry(input, displaced);
675
+ }
676
+ return switchWithoutExchange(input);
677
+ }
678
+ function yieldDisplacedEntry(input, displaced) {
679
+ const { state, relativePath, targetPath, stagedPath, stagedStat } = input;
680
+ raceWindow("before_yield", relativePath, targetPath);
681
+ let installedBefore = stagedStat;
682
+ let toInstall = displaced;
683
+ for (let attempt = 0; attempt < YIELD_ATTEMPTS; attempt += 1) {
684
+ try {
685
+ state.primitives.exchange(stagedPath, targetPath);
686
+ } catch (error) {
687
+ if (!(error instanceof import_atomic_rename.AtomicRenameError && isPathStateCode(error.code))) throw error;
688
+ if (restoreDisplacedNoReplace(state, stagedPath, toInstall, targetPath)) {
689
+ throw new WorkingTreeTargetChangedError(relativePath, { detail: "another writer replaced the entry; its bytes were kept" });
690
+ }
691
+ const retained2 = retainForeign(input.journal, relativePath, stagedPath, "later_writer");
692
+ throw new WorkingTreeTargetChangedError(relativePath, { retained: [retained2] });
693
+ }
694
+ const returned = import_node_fs.default.lstatSync(stagedPath);
695
+ if (sameWorkingTreeInode(returned, installedBefore)) {
696
+ if (sameWorkingTreeInode(returned, stagedStat)) {
697
+ discardStagedAndThrow(
698
+ input,
699
+ new WorkingTreeTargetChangedError(relativePath, { detail: "another writer replaced the entry; its bytes were kept" })
700
+ );
701
+ }
702
+ const retained2 = retainForeign(input.journal, relativePath, stagedPath, "later_writer");
703
+ throw new WorkingTreeTargetChangedError(relativePath, { retained: [retained2] });
704
+ }
705
+ installedBefore = toInstall;
706
+ toInstall = returned;
707
+ }
708
+ const retained = retainForeign(input.journal, relativePath, stagedPath, "unrestorable");
709
+ throw new WorkingTreeTargetChangedError(relativePath, { detail: "the path kept changing while the pass yielded", retained: [retained] });
710
+ }
711
+ function switchWithoutExchange(input) {
712
+ const { state, relativePath, targetPath, stagedPath, stagedStat, expected } = input;
713
+ const park = stagingPathFor(state, targetPath, relativePath);
714
+ try {
715
+ import_node_fs.default.renameSync(targetPath, park);
716
+ } catch (error) {
717
+ if (isPathStateCode(error.code))
718
+ discardStagedAndThrow(input, new WorkingTreeTargetChangedError(relativePath, { detail: "removed before the switch" }));
719
+ discardStagedAndThrow(input, error);
720
+ }
721
+ raceWindow("after_switch", relativePath, targetPath);
722
+ const displaced = import_node_fs.default.lstatSync(park);
723
+ if (displacedEntryMatches(expected, displaced) && entryContent(park, displaced) === input.expectedContent) {
724
+ let created;
725
+ try {
726
+ created = publishNoReplace(state, stagedPath, stagedStat, relativePath, targetPath);
727
+ } catch (error) {
728
+ if (error instanceof WorkingTreeTargetChangedError) {
729
+ const retained2 = retainForeign(input.journal, relativePath, park, "reappeared");
730
+ discardStagedAndThrow(
731
+ input,
732
+ new WorkingTreeTargetChangedError(relativePath, {
733
+ detail: "an entry was created while the path was being replaced",
734
+ retained: [retained2]
735
+ })
736
+ );
737
+ }
738
+ if (!restoreDisplacedNoReplace(state, park, displaced, targetPath)) retainForeign(input.journal, relativePath, park, "unrestorable");
739
+ discardStagedAndThrow(input, error);
740
+ }
741
+ return {
742
+ after: created ?? settledAfter(targetPath, stagedStat, input.stagedContent),
743
+ afterContent: input.stagedContent,
744
+ displaced: { path: park, stat: displaced, content: input.expectedContent }
745
+ };
746
+ }
747
+ raceWindow("before_yield", relativePath, targetPath);
748
+ const stagedRetained = discardStaged(input, "unrestorable");
749
+ if (restoreDisplacedNoReplace(state, park, displaced, targetPath)) {
750
+ throw new WorkingTreeTargetChangedError(relativePath, {
751
+ detail: "another writer replaced the entry; its bytes were kept",
752
+ retained: stagedRetained ? [stagedRetained] : []
753
+ });
754
+ }
755
+ const retained = retainForeign(input.journal, relativePath, park, "later_writer");
756
+ throw new WorkingTreeTargetChangedError(relativePath, { retained: [...stagedRetained ? [stagedRetained] : [], retained] });
757
+ }
758
+ function removeSwitchedEntry(input) {
759
+ const { state, targetRoot, relativePath, targetPath, expected } = input;
760
+ const inspected = inspectRelativePathWithoutFollowingAncestors(targetRoot, relativePath);
761
+ if (inspected.kind === "blocked") {
762
+ throw new Error(`Refusing to remove a working-tree path through a symlink or non-directory: ${inspected.blockingPath}`);
763
+ }
764
+ if (inspected.kind !== "entry" || !workingTreeStatsMatch(expected, inspected.stat)) throw new WorkingTreeTargetChangedError(relativePath);
765
+ const content = entryContent(targetPath, expected, input.expectedBlob);
766
+ if (!content || input.expectedContent && content !== input.expectedContent) throw new WorkingTreeTargetChangedError(relativePath);
767
+ raceWindow(input.phase, relativePath, targetPath);
768
+ const park = stagingPathFor(state, targetPath, relativePath);
769
+ try {
770
+ import_node_fs.default.renameSync(targetPath, park);
771
+ } catch (error) {
772
+ if (isPathStateCode(error.code))
773
+ throw new WorkingTreeTargetChangedError(relativePath, { detail: "removed before the switch" });
774
+ throw error;
775
+ }
776
+ raceWindow("after_remove", relativePath, targetPath);
777
+ const displaced = import_node_fs.default.lstatSync(park);
778
+ if (displacedEntryMatches(expected, displaced) && entryContent(park, displaced) === content)
779
+ return { path: park, stat: displaced, content };
780
+ raceWindow("before_yield", relativePath, targetPath);
781
+ if (restoreDisplacedNoReplace(state, park, displaced, targetPath)) {
782
+ throw new WorkingTreeTargetChangedError(relativePath, { detail: "another writer replaced the entry; its bytes were kept" });
783
+ }
784
+ const retained = retainForeign(input.journal, relativePath, park, "later_writer");
785
+ throw new WorkingTreeTargetChangedError(relativePath, { retained: [retained] });
786
+ }
787
+ function removeEntryGuarded(targetRoot, relativePath, replaced, guards) {
788
+ if (!guards) {
789
+ removeEntry(targetRoot, relativePath);
790
+ return;
791
+ }
792
+ const state = guardedPassState(guards, targetRoot);
793
+ const expected = switchExpectation(targetRoot, relativePath, guards);
794
+ const targetPath = import_node_path.default.join(targetRoot, ...relativePath.split("/"));
795
+ assertInside(targetRoot, targetPath);
796
+ const inspected = inspectRelativePathWithoutFollowingAncestors(targetRoot, relativePath);
797
+ if (inspected.kind === "blocked") {
798
+ throw new Error(`Refusing to remove a working-tree path through a symlink or non-directory: ${inspected.blockingPath}`);
799
+ }
800
+ if (expected === null) {
801
+ if (inspected.kind !== "missing") throw new WorkingTreeTargetChangedError(relativePath);
802
+ return;
803
+ }
804
+ if (expected.isDirectory() && !expected.isSymbolicLink()) {
805
+ if (inspected.kind === "entry") {
806
+ if (!inspected.stat.isDirectory() || inspected.stat.isSymbolicLink()) throw new WorkingTreeTargetChangedError(relativePath);
807
+ raceWindow("before_remove", relativePath, targetPath);
808
+ try {
809
+ import_node_fs.default.rmdirSync(inspected.absolutePath);
810
+ } catch (error) {
811
+ const code = error.code;
812
+ if (code === "ENOTEMPTY" || code === "EEXIST" || code === "ENOTDIR") throw new WorkingTreeTargetChangedError(relativePath);
813
+ if (code !== "ENOENT") throw error;
814
+ }
815
+ }
816
+ if (guards.expectations?.has(relativePath)) guards.expectations.set(relativePath, null);
817
+ if (replaced) guards.journal?.entries.push({ relativePath, replaced, after: null });
818
+ return;
819
+ }
820
+ const displaced = removeSwitchedEntry({
821
+ state,
822
+ targetRoot,
823
+ relativePath,
824
+ targetPath,
825
+ expected,
826
+ expectedBlob: guards.preBlobs?.get(relativePath),
827
+ journal: guards.journal,
828
+ phase: "before_remove"
829
+ });
830
+ if (guards.expectations?.has(relativePath)) guards.expectations.set(relativePath, null);
831
+ const recorded = replaced ?? treeEntryFromStat(displaced.stat, displaced.path);
832
+ if (guards.journal && recorded) guards.journal.entries.push({ relativePath, replaced: recorded, after: null, displaced });
833
+ else releaseRetainedEntry(void 0, relativePath, displaced);
834
+ }
835
+ function ensureDirectoryGuarded(targetRoot, relativePath, guards) {
836
+ if (!guards) {
837
+ ensureDirectory(targetRoot, relativePath, 493);
838
+ return;
839
+ }
840
+ const normalized = normalizeRelativePath(relativePath);
841
+ if (!normalized) throw new Error(`Invalid working-tree directory path: ${relativePath}`);
842
+ let current = import_node_path.default.resolve(targetRoot);
843
+ let relative = "";
844
+ for (const segment of normalized.split("/")) {
845
+ current = import_node_path.default.join(current, segment);
846
+ relative = relative ? `${relative}/${segment}` : segment;
847
+ assertInside(targetRoot, current);
848
+ const stat = lstatIfExists(current);
849
+ if (stat && (!stat.isDirectory() || stat.isSymbolicLink())) throw new WorkingTreeTargetChangedError(relative);
850
+ if (stat) continue;
851
+ try {
852
+ import_node_fs.default.mkdirSync(current, { mode: 493 });
853
+ } catch (error) {
854
+ if (error.code !== "EEXIST") throw error;
855
+ const now = lstatIfExists(current);
856
+ if (!now || !now.isDirectory() || now.isSymbolicLink()) throw new WorkingTreeTargetChangedError(relative);
857
+ }
858
+ }
859
+ }
860
+ function replaceWithSymlink(targetRoot, relativePath, target) {
861
+ const targetPath = import_node_path.default.join(targetRoot, ...relativePath.split("/"));
862
+ assertInside(targetRoot, targetPath);
863
+ const temporaryPath = import_node_path.default.join(import_node_path.default.dirname(targetPath), `${MIRROR_TEMPORARY_FILE_PREFIX}${(0, import_node_crypto.randomUUID)()}.tmp`);
864
+ import_node_fs.default.symlinkSync(target, temporaryPath);
865
+ try {
866
+ const current = inspectRelativePathWithoutFollowingAncestors(targetRoot, relativePath);
867
+ if (current.kind === "blocked") throw new Error(`Refusing to copy through a symlink or non-directory: ${current.blockingPath}`);
868
+ if (current.kind === "entry" && current.stat.isDirectory() && !current.stat.isSymbolicLink()) removeEntry(targetRoot, relativePath);
869
+ import_node_fs.default.renameSync(temporaryPath, targetPath);
870
+ } catch (error) {
871
+ import_node_fs.default.rmSync(temporaryPath, { force: true });
872
+ throw error;
873
+ }
874
+ }
372
875
  function ensureDirectory(targetRoot, relativePath, mode) {
373
876
  const normalized = normalizeRelativePath(relativePath);
374
877
  if (!normalized) throw new Error(`Invalid working-tree directory path: ${relativePath}`);
@@ -426,9 +929,8 @@ function filesEqual(leftPath, rightPath, entry) {
426
929
  import_node_fs.default.closeSync(right);
427
930
  }
428
931
  }
429
- function copyRegularFile(sourcePath, targetPath, entry, durability) {
932
+ function stageRegularFile(sourcePath, stagedPath, entry, durability) {
430
933
  const source = import_node_fs.default.openSync(sourcePath, import_node_fs.default.constants.O_RDONLY | import_node_fs.default.constants.O_NOFOLLOW);
431
- const temporaryPath = import_node_path.default.join(import_node_path.default.dirname(targetPath), `${MIRROR_TEMPORARY_FILE_PREFIX}${(0, import_node_crypto.randomUUID)()}.tmp`);
432
934
  let target;
433
935
  try {
434
936
  const sourceStat = import_node_fs.default.fstatSync(source);
@@ -436,7 +938,7 @@ function copyRegularFile(sourcePath, targetPath, entry, durability) {
436
938
  throw new Error(`Working-tree source changed while it was being mirrored: ${sourcePath}`);
437
939
  }
438
940
  target = import_node_fs.default.openSync(
439
- temporaryPath,
941
+ stagedPath,
440
942
  import_node_fs.default.constants.O_WRONLY | import_node_fs.default.constants.O_CREAT | import_node_fs.default.constants.O_EXCL | import_node_fs.default.constants.O_NOFOLLOW,
441
943
  entry.mode & 511
442
944
  );
@@ -457,22 +959,54 @@ function copyRegularFile(sourcePath, targetPath, entry, durability) {
457
959
  if (!finalSourceStat.isFile() || finalSourceStat.size !== sourceStat.size || (finalSourceStat.mode & 511) !== (sourceStat.mode & 511) || finalSourceStat.mtimeMs !== sourceStat.mtimeMs || finalSourceStat.ctimeMs !== sourceStat.ctimeMs) {
458
960
  throw new Error(`Working-tree source changed while it was being mirrored: ${sourcePath}`);
459
961
  }
962
+ const stagedStat = import_node_fs.default.fstatSync(target);
460
963
  import_node_fs.default.closeSync(target);
461
964
  target = void 0;
462
- import_node_fs.default.renameSync(temporaryPath, targetPath);
463
- if (durability === "per_entry") {
464
- const parent = import_node_fs.default.openSync(import_node_path.default.dirname(targetPath), import_node_fs.default.constants.O_RDONLY);
465
- try {
466
- import_node_fs.default.fsyncSync(parent);
467
- } finally {
468
- import_node_fs.default.closeSync(parent);
469
- }
965
+ return { sourceStat: finalSourceStat, stagedStat };
966
+ } catch (error) {
967
+ if (target !== void 0) {
968
+ import_node_fs.default.closeSync(target);
969
+ target = void 0;
470
970
  }
971
+ import_node_fs.default.rmSync(stagedPath, { force: true });
972
+ throw error;
471
973
  } finally {
472
974
  if (target !== void 0) import_node_fs.default.closeSync(target);
473
975
  import_node_fs.default.closeSync(source);
976
+ }
977
+ }
978
+ function writeStagedContent(stagedPath, override, durability) {
979
+ const target = import_node_fs.default.openSync(
980
+ stagedPath,
981
+ import_node_fs.default.constants.O_WRONLY | import_node_fs.default.constants.O_CREAT | import_node_fs.default.constants.O_EXCL | import_node_fs.default.constants.O_NOFOLLOW,
982
+ override.mode & 511
983
+ );
984
+ try {
985
+ let written = 0;
986
+ while (written < override.content.length) {
987
+ written += import_node_fs.default.writeSync(target, override.content, written, override.content.length - written, written);
988
+ }
989
+ import_node_fs.default.fchmodSync(target, override.mode & 511);
990
+ if (durability === "per_entry") import_node_fs.default.fsyncSync(target);
991
+ return import_node_fs.default.fstatSync(target);
992
+ } catch (error) {
993
+ import_node_fs.default.rmSync(stagedPath, { force: true });
994
+ throw error;
995
+ } finally {
996
+ import_node_fs.default.closeSync(target);
997
+ }
998
+ }
999
+ function copyRegularFile(sourcePath, targetPath, entry, durability) {
1000
+ const temporaryPath = import_node_path.default.join(import_node_path.default.dirname(targetPath), `${MIRROR_TEMPORARY_FILE_PREFIX}${(0, import_node_crypto.randomUUID)()}.tmp`);
1001
+ const { sourceStat } = stageRegularFile(sourcePath, temporaryPath, entry, durability);
1002
+ try {
1003
+ import_node_fs.default.renameSync(temporaryPath, targetPath);
1004
+ } catch (error) {
474
1005
  import_node_fs.default.rmSync(temporaryPath, { force: true });
1006
+ throw error;
475
1007
  }
1008
+ if (durability === "per_entry") fsyncPath(import_node_path.default.dirname(targetPath), import_node_fs.default.constants.O_RDONLY);
1009
+ return sourceStat;
476
1010
  }
477
1011
  function copyEntry(sourceRoot, targetRoot, relativePath, entry, durability) {
478
1012
  const targetPath = import_node_path.default.join(targetRoot, ...relativePath.split("/"));
@@ -489,7 +1023,7 @@ function copyEntry(sourceRoot, targetRoot, relativePath, entry, durability) {
489
1023
  throw new Error(`Working-tree source changed while it was being mirrored: ${sourcePath}`);
490
1024
  }
491
1025
  ensureDirectory(targetRoot, relativePath, entry.mode);
492
- return;
1026
+ return null;
493
1027
  }
494
1028
  if (entry.kind === "symlink") {
495
1029
  if (!source.stat.isSymbolicLink() || import_node_fs.default.readlinkSync(sourcePath) !== entry.target) {
@@ -501,11 +1035,8 @@ function copyEntry(sourceRoot, targetRoot, relativePath, entry, durability) {
501
1035
  } catch {
502
1036
  unchanged = false;
503
1037
  }
504
- if (!unchanged) {
505
- removeEntry(targetRoot, relativePath);
506
- import_node_fs.default.symlinkSync(entry.target, targetPath);
507
- }
508
- return;
1038
+ if (!unchanged) replaceWithSymlink(targetRoot, relativePath, entry.target);
1039
+ return { kind: "symlink", target: entry.target };
509
1040
  }
510
1041
  if (entry.kind === "gitlink") {
511
1042
  throw new Error(`Working-tree gitlink cannot be copied as content: ${relativePath}`);
@@ -513,19 +1044,183 @@ function copyEntry(sourceRoot, targetRoot, relativePath, entry, durability) {
513
1044
  if (!source.stat.isFile() || source.stat.isSymbolicLink() || source.stat.size !== entry.size) {
514
1045
  throw new Error(`Working-tree source changed while it was being mirrored: ${sourcePath}`);
515
1046
  }
516
- if (!filesEqual(sourcePath, targetPath, entry)) {
517
- const target = inspectRelativePathWithoutFollowingAncestors(targetRoot, relativePath);
518
- if (target.kind === "blocked") {
519
- throw new Error(`Refusing to copy through a symlink or non-directory: ${target.blockingPath}`);
1047
+ if (filesEqual(sourcePath, targetPath, entry)) {
1048
+ const settled = import_node_fs.default.lstatSync(sourcePath);
1049
+ if (!settled.isFile() || settled.ino !== source.stat.ino || settled.size !== source.stat.size || settled.mode !== source.stat.mode) {
1050
+ throw new Error(`Working-tree source changed while it was being mirrored: ${sourcePath}`);
520
1051
  }
521
- const targetStat = target.kind === "entry" ? target.stat : null;
522
- if (targetStat && !targetStat.isFile()) removeEntry(targetRoot, relativePath);
523
- copyRegularFile(sourcePath, targetPath, entry, durability);
1052
+ if (settled.mtimeMs !== source.stat.mtimeMs || settled.ctimeMs !== source.stat.ctimeMs) {
1053
+ throw new Error(`Working-tree source changed while it was being mirrored: ${sourcePath}`);
1054
+ }
1055
+ return projectedFileEntry(settled);
1056
+ }
1057
+ const target = inspectRelativePathWithoutFollowingAncestors(targetRoot, relativePath);
1058
+ if (target.kind === "blocked") {
1059
+ throw new Error(`Refusing to copy through a symlink or non-directory: ${target.blockingPath}`);
524
1060
  }
1061
+ if (target.kind === "entry" && !target.stat.isFile()) removeEntry(targetRoot, relativePath);
1062
+ return projectedFileEntry(copyRegularFile(sourcePath, targetPath, entry, durability));
525
1063
  }
526
- function inspectMirrorTarget(targetRoot, deletionMode) {
527
- if (deletionMode !== "git") return { existing: inspectWorkingTree(targetRoot, "all"), git: null };
528
- const inspection = inspectGitWorkingTree(targetRoot);
1064
+ function copyEntryGuarded(sourceRoot, targetRoot, relativePath, entry, durability, guards) {
1065
+ const state = guardedPassState(guards, targetRoot);
1066
+ const targetPath = import_node_path.default.join(targetRoot, ...relativePath.split("/"));
1067
+ assertInside(targetRoot, targetPath);
1068
+ const parent = import_node_path.default.posix.dirname(relativePath);
1069
+ if (parent !== ".") ensureDirectoryGuarded(targetRoot, parent, guards);
1070
+ const override = guards.overrides?.get(relativePath);
1071
+ if (override && entry.kind !== "file") throw new Error(`Working-tree override must replace a regular file: ${relativePath}`);
1072
+ let sourcePath = null;
1073
+ let sourceStat = null;
1074
+ if (!override) {
1075
+ const source = inspectRelativePathWithoutFollowingAncestors(sourceRoot, relativePath);
1076
+ if (source.kind !== "entry") throw new Error(`Working-tree source became unsafe while it was being mirrored: ${relativePath}`);
1077
+ sourcePath = source.absolutePath;
1078
+ sourceStat = source.stat;
1079
+ }
1080
+ if (entry.kind === "directory") {
1081
+ if (!sourceStat.isDirectory() || sourceStat.isSymbolicLink()) {
1082
+ throw new Error(`Working-tree source changed while it was being mirrored: ${sourcePath}`);
1083
+ }
1084
+ const target2 = inspectRelativePathWithoutFollowingAncestors(targetRoot, relativePath);
1085
+ if (target2.kind === "blocked") throw new Error(`Refusing to copy through a symlink or non-directory: ${target2.blockingPath}`);
1086
+ if (target2.kind === "entry" && target2.stat.isDirectory() && !target2.stat.isSymbolicLink()) return null;
1087
+ if (target2.kind === "entry") {
1088
+ if (guards.expectations && !guards.expectations.has(relativePath)) throw new WorkingTreeTargetChangedError(relativePath);
1089
+ removeEntryGuarded(targetRoot, relativePath, treeEntryFromStat(target2.stat, target2.absolutePath), guards);
1090
+ } else assertTargetExpectation(targetRoot, relativePath, guards);
1091
+ createDirectoryGuarded(targetRoot, relativePath, entry.mode, guards);
1092
+ return null;
1093
+ }
1094
+ if (entry.kind === "gitlink") {
1095
+ throw new Error(`Working-tree gitlink cannot be copied as content: ${relativePath}`);
1096
+ }
1097
+ const target = inspectRelativePathWithoutFollowingAncestors(targetRoot, relativePath);
1098
+ if (target.kind === "blocked") throw new Error(`Refusing to copy through a symlink or non-directory: ${target.blockingPath}`);
1099
+ if (entry.kind === "symlink") {
1100
+ if (!sourceStat.isSymbolicLink() || import_node_fs.default.readlinkSync(sourcePath) !== entry.target) {
1101
+ throw new Error(`Working-tree source changed while it was being mirrored: ${sourcePath}`);
1102
+ }
1103
+ if (target.kind === "entry" && target.stat.isSymbolicLink() && import_node_fs.default.readlinkSync(target.absolutePath) === entry.target) {
1104
+ return { kind: "symlink", target: entry.target };
1105
+ }
1106
+ } else if (!override) {
1107
+ if (!sourceStat.isFile() || sourceStat.isSymbolicLink() || sourceStat.size !== entry.size) {
1108
+ throw new Error(`Working-tree source changed while it was being mirrored: ${sourcePath}`);
1109
+ }
1110
+ if (filesEqual(sourcePath, targetPath, entry)) {
1111
+ const settled = import_node_fs.default.lstatSync(sourcePath);
1112
+ if (!settled.isFile() || settled.ino !== sourceStat.ino || settled.size !== sourceStat.size || settled.mode !== sourceStat.mode) {
1113
+ throw new Error(`Working-tree source changed while it was being mirrored: ${sourcePath}`);
1114
+ }
1115
+ if (settled.mtimeMs !== sourceStat.mtimeMs || settled.ctimeMs !== sourceStat.ctimeMs) {
1116
+ throw new Error(`Working-tree source changed while it was being mirrored: ${sourcePath}`);
1117
+ }
1118
+ return projectedFileEntry(settled);
1119
+ }
1120
+ }
1121
+ let replaced = target.kind === "entry" ? treeEntryFromStat(target.stat, target.absolutePath) : null;
1122
+ let expected = switchExpectation(targetRoot, relativePath, guards);
1123
+ if (target.kind === "entry" && target.stat.isDirectory() && !target.stat.isSymbolicLink()) {
1124
+ removeEntryGuarded(targetRoot, relativePath, replaced, guards);
1125
+ replaced = null;
1126
+ expected = null;
1127
+ }
1128
+ const stagedPath = stagingPathFor(state, targetPath, relativePath);
1129
+ let stagedStat;
1130
+ let projected = null;
1131
+ if (entry.kind === "symlink") {
1132
+ import_node_fs.default.symlinkSync(entry.target, stagedPath);
1133
+ stagedStat = import_node_fs.default.lstatSync(stagedPath);
1134
+ projected = { kind: "symlink", target: entry.target };
1135
+ } else if (override) {
1136
+ stagedStat = writeStagedContent(stagedPath, override, durability);
1137
+ } else {
1138
+ const staged = stageRegularFile(sourcePath, stagedPath, entry, durability);
1139
+ stagedStat = staged.stagedStat;
1140
+ projected = projectedFileEntry(staged.sourceStat);
1141
+ }
1142
+ const outcome = switchStagedEntry({
1143
+ state,
1144
+ targetRoot,
1145
+ relativePath,
1146
+ targetPath,
1147
+ stagedPath,
1148
+ stagedStat,
1149
+ expected,
1150
+ expectedBlob: guards.preBlobs?.get(relativePath),
1151
+ journal: guards.journal,
1152
+ phase: "before_switch",
1153
+ onFailure: "unlink"
1154
+ });
1155
+ if (durability === "per_entry") fsyncPath(import_node_path.default.dirname(targetPath), import_node_fs.default.constants.O_RDONLY);
1156
+ if (guards.journal)
1157
+ guards.journal.entries.push({
1158
+ relativePath,
1159
+ replaced,
1160
+ after: outcome.after,
1161
+ afterContent: outcome.afterContent,
1162
+ ...outcome.displaced ? { displaced: outcome.displaced } : {}
1163
+ });
1164
+ else if (outcome.displaced) releaseRetainedEntry(void 0, relativePath, outcome.displaced);
1165
+ if (guards.expectations?.has(relativePath)) guards.expectations.set(relativePath, outcome.after);
1166
+ return projected;
1167
+ }
1168
+ function createDirectoryGuarded(targetRoot, relativePath, mode, guards) {
1169
+ if (!guards) {
1170
+ ensureDirectory(targetRoot, relativePath, mode);
1171
+ return;
1172
+ }
1173
+ const targetPath = import_node_path.default.join(targetRoot, ...relativePath.split("/"));
1174
+ assertInside(targetRoot, targetPath);
1175
+ try {
1176
+ import_node_fs.default.mkdirSync(targetPath, { mode: mode & 511 });
1177
+ } catch (error) {
1178
+ if (isPathStateCode(error.code)) throw new WorkingTreeTargetChangedError(relativePath);
1179
+ throw error;
1180
+ }
1181
+ if (!guards.journal) return;
1182
+ const created = inspectRelativePathWithoutFollowingAncestors(targetRoot, relativePath);
1183
+ if (created.kind !== "entry" || !created.stat.isDirectory() || created.stat.isSymbolicLink()) {
1184
+ throw new WorkingTreeTargetChangedError(relativePath);
1185
+ }
1186
+ guards.journal.entries.push({ relativePath, replaced: null, after: created.stat });
1187
+ }
1188
+ function retainedEntryIntact(retained, current) {
1189
+ 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;
1190
+ }
1191
+ function settleRetainedEntries(journal, targetRoot) {
1192
+ raceWindow("before_settle", "", targetRoot);
1193
+ for (const entry of journal.entries) {
1194
+ if (!entry.displaced) continue;
1195
+ const current = lstatIfExists(entry.displaced.path);
1196
+ if (!current || !retainedEntryIntact(entry.displaced, current)) {
1197
+ throw new WorkingTreeTargetChangedError(entry.relativePath, {
1198
+ detail: "written through a descriptor opened before hydration replaced the path"
1199
+ });
1200
+ }
1201
+ }
1202
+ releaseRetainedEntries(journal);
1203
+ }
1204
+ function releaseRetainedEntries(journal) {
1205
+ for (const entry of journal.entries) {
1206
+ if (!entry.displaced) continue;
1207
+ releaseRetainedEntry(journal, entry.relativePath, entry.displaced);
1208
+ delete entry.displaced;
1209
+ }
1210
+ }
1211
+ function releaseRetainedEntry(journal, relativePath, displaced) {
1212
+ const current = lstatIfExists(displaced.path);
1213
+ if (!current) return;
1214
+ if (!retainedEntryIntact(displaced, current)) {
1215
+ const retained = retainForeign(journal, relativePath, displaced.path, "unrestorable");
1216
+ if (!journal) throw new WorkingTreeTargetChangedError(relativePath, { retained: [retained] });
1217
+ return;
1218
+ }
1219
+ import_node_fs.default.rmSync(displaced.path, { force: true });
1220
+ }
1221
+ function inspectMirrorTarget(targetRoot, deletionMode, stats) {
1222
+ if (deletionMode !== "git") return { existing: inspectWorkingTree(targetRoot, "all", { stats }), git: null };
1223
+ const inspection = inspectGitWorkingTree(targetRoot, stats);
529
1224
  const occupied = /* @__PURE__ */ new Set();
530
1225
  for (const relativePath of inspection.submodules.keys()) {
531
1226
  const inspected = inspectRelativePathWithoutFollowingAncestors(targetRoot, relativePath);
@@ -627,43 +1322,109 @@ function recordGitlinkStaleHeads(targetRoot, desired, shielded, git) {
627
1322
  }
628
1323
  if (changed) writeGitlinkStaleHeads(targetRoot, heads);
629
1324
  }
630
- function mirrorWorkingTree(input) {
1325
+ function prepareWorkingTreeMirror(input) {
631
1326
  const sourceRoot = import_node_path.default.resolve(input.sourceRoot);
632
1327
  const targetRoot = import_node_path.default.resolve(input.targetRoot);
633
- const durability = input.durability ?? "per_entry";
634
1328
  if (!inspectDirectoryRoot(targetRoot, { allowMissing: true, label: "Working-tree target root" })) {
635
1329
  import_node_fs.default.mkdirSync(targetRoot, { recursive: true });
636
1330
  }
637
1331
  inspectDirectoryRoot(targetRoot, { allowMissing: false, label: "Working-tree target root" });
638
1332
  const desired = inspectWorkingTree(sourceRoot, input.sourceMode, { gitlinks: input.sourceGitlinks });
639
- const target = inspectMirrorTarget(targetRoot, input.deletionMode);
1333
+ const existingStats = /* @__PURE__ */ new Map();
1334
+ const target = inspectMirrorTarget(targetRoot, input.deletionMode, existingStats);
640
1335
  const shielded = target.git ? shieldedDesiredPaths(desired, target.git) : /* @__PURE__ */ new Set();
641
- const index = target.git ? planIndexChanges(desired, shielded, target.git) : null;
642
- for (const relativePath of [...target.existing.keys()].sort(deepestFirst)) {
643
- if (desired.has(relativePath)) continue;
644
- const entry = target.existing.get(relativePath);
1336
+ return {
1337
+ sourceRoot,
1338
+ targetRoot,
1339
+ deletionMode: input.deletionMode,
1340
+ desired,
1341
+ existing: target.existing,
1342
+ existingStats,
1343
+ git: target.git,
1344
+ shielded
1345
+ };
1346
+ }
1347
+ function applyWorkingTreeMirror(prepared, options = {}) {
1348
+ const { sourceRoot, targetRoot, desired, existing, shielded } = prepared;
1349
+ const durability = options.durability ?? "per_entry";
1350
+ const guards = options.guards;
1351
+ const selection = options.selection;
1352
+ const target = { existing, git: prepared.git };
1353
+ const indexDesired = selection?.desiredForIndex ?? desired;
1354
+ const index = target.git ? planIndexChanges(indexDesired, shielded, target.git) : null;
1355
+ const neededDirectories = /* @__PURE__ */ new Set();
1356
+ if (selection) {
1357
+ for (const relativePath of selection.write) {
1358
+ let parent = import_node_path.default.posix.dirname(relativePath);
1359
+ while (parent !== ".") {
1360
+ neededDirectories.add(parent);
1361
+ parent = import_node_path.default.posix.dirname(parent);
1362
+ }
1363
+ }
1364
+ }
1365
+ for (const relativePath of [...existing.keys()].sort(deepestFirst)) {
1366
+ if (selection ? !selection.remove.has(relativePath) : desired.has(relativePath)) continue;
1367
+ const entry = existing.get(relativePath);
645
1368
  if (entry.kind === "gitlink" || directoryShieldsSubmodule(relativePath, entry, target.git)) continue;
646
- removeEntry(targetRoot, relativePath);
1369
+ if (selection && entry.kind === "directory") {
1370
+ const inspected = inspectRelativePathWithoutFollowingAncestors(targetRoot, relativePath);
1371
+ if (inspected.kind !== "entry" || !inspected.stat.isDirectory() || inspected.stat.isSymbolicLink()) continue;
1372
+ try {
1373
+ import_node_fs.default.rmdirSync(inspected.absolutePath);
1374
+ } catch (error) {
1375
+ const code = error.code;
1376
+ if (code === "ENOTEMPTY" || code === "EEXIST" || code === "ENOENT") continue;
1377
+ throw error;
1378
+ }
1379
+ if (guards?.expectations?.has(relativePath)) guards.expectations.set(relativePath, null);
1380
+ guards?.journal?.entries.push({ relativePath, replaced: entry, after: null });
1381
+ continue;
1382
+ }
1383
+ removeEntryGuarded(targetRoot, relativePath, entry, guards);
647
1384
  }
648
1385
  const gitlinks = [];
649
1386
  for (const [relativePath, entry] of [...desired.entries()].sort(([left], [right]) => shallowestFirst(left, right))) {
650
1387
  if (shielded.has(relativePath)) continue;
1388
+ if (selection && entry.kind === "directory" && !neededDirectories.has(relativePath)) continue;
1389
+ if (selection && entry.kind !== "directory" && !selection.write.has(relativePath)) continue;
651
1390
  if (entry.kind === "gitlink") {
652
- ensureDirectory(targetRoot, relativePath, 493);
1391
+ if (guards) {
1392
+ const parent = import_node_path.default.posix.dirname(relativePath);
1393
+ if (parent !== ".") ensureDirectoryGuarded(targetRoot, parent, guards);
1394
+ const current = inspectRelativePathWithoutFollowingAncestors(targetRoot, relativePath);
1395
+ if (current.kind === "blocked") throw new Error(`Refusing to copy through a symlink or non-directory: ${current.blockingPath}`);
1396
+ if (current.kind === "entry" && (!current.stat.isDirectory() || current.stat.isSymbolicLink())) {
1397
+ if (guards.expectations && !guards.expectations.has(relativePath)) throw new WorkingTreeTargetChangedError(relativePath);
1398
+ removeEntryGuarded(targetRoot, relativePath, treeEntryFromStat(current.stat, current.absolutePath), guards);
1399
+ createDirectoryGuarded(targetRoot, relativePath, 493, guards);
1400
+ } else if (current.kind !== "entry") {
1401
+ assertTargetExpectation(targetRoot, relativePath, guards);
1402
+ createDirectoryGuarded(targetRoot, relativePath, 493, guards);
1403
+ }
1404
+ } else {
1405
+ ensureDirectory(targetRoot, relativePath, 493);
1406
+ }
653
1407
  gitlinks.push({ relativePath, objectId: entry.objectId });
1408
+ options.projected?.set(relativePath, { kind: "gitlink", objectId: entry.objectId });
654
1409
  continue;
655
1410
  }
656
- copyEntry(sourceRoot, targetRoot, relativePath, entry, durability);
1411
+ const projected = guards ? copyEntryGuarded(sourceRoot, targetRoot, relativePath, entry, durability, guards) : copyEntry(sourceRoot, targetRoot, relativePath, entry, durability);
1412
+ if (projected && options.projected) options.projected.set(relativePath, projected);
657
1413
  }
1414
+ if (guards?.journal) settleRetainedEntries(guards.journal, targetRoot);
658
1415
  if (index && target.git) {
659
1416
  updateIndex(targetRoot, { drop: index.removed, put: index.written.map((gitlink) => ({ mode: "160000", stage: 0, ...gitlink })) });
660
- recordGitlinkStaleHeads(targetRoot, desired, shielded, target.git);
1417
+ if (guards?.journal && (index.removed.length > 0 || index.written.length > 0)) guards.journal.indexApplied = index;
1418
+ recordGitlinkStaleHeads(targetRoot, indexDesired, shielded, target.git);
661
1419
  }
662
1420
  return {
663
1421
  paths: [...desired.keys()].filter((relativePath) => !shielded.has(relativePath) && desired.get(relativePath)?.kind !== "directory").sort(),
664
1422
  gitlinks
665
1423
  };
666
1424
  }
1425
+ function mirrorWorkingTree(input) {
1426
+ return applyWorkingTreeMirror(prepareWorkingTreeMirror(input), { durability: input.durability, projected: input.projected });
1427
+ }
667
1428
  function normalizeWorkingTreeRelativePath(value) {
668
1429
  return normalizeRelativePath(value);
669
1430
  }
@@ -700,16 +1461,23 @@ function fsyncPath(targetPath, flags) {
700
1461
  }
701
1462
  }
702
1463
  function planWorkingTreeMirror(input) {
703
- const sourceRoot = import_node_path.default.resolve(input.sourceRoot);
704
- const targetRoot = import_node_path.default.resolve(input.targetRoot);
705
- const inspectedDesired = inspectWorkingTree(sourceRoot, input.sourceMode, { gitlinks: input.sourceGitlinks });
706
- const target = inspectMirrorTarget(targetRoot, input.deletionMode);
707
- const shielded = target.git ? shieldedDesiredPaths(inspectedDesired, target.git) : /* @__PURE__ */ new Set();
708
- const desired = new Map([...inspectedDesired].filter(([relativePath]) => !shielded.has(relativePath)));
1464
+ return planPreparedWorkingTreeMirror(prepareWorkingTreeMirror(input));
1465
+ }
1466
+ function planPreparedWorkingTreeMirror(prepared, options = {}) {
1467
+ const { sourceRoot, targetRoot, existing, shielded } = prepared;
1468
+ const inspectedDesired = prepared.desired;
1469
+ const selection = options.selection;
1470
+ const target = { existing, git: prepared.git };
1471
+ const desired = new Map(
1472
+ [...inspectedDesired].filter(
1473
+ ([relativePath, entry]) => !shielded.has(relativePath) && (!selection || entry.kind === "directory" || selection.write.has(relativePath))
1474
+ )
1475
+ );
709
1476
  const replaced = /* @__PURE__ */ new Map();
710
1477
  const absent = [];
711
- for (const [relativePath, entry] of target.existing) {
712
- if (inspectedDesired.has(relativePath) || entry.kind === "gitlink") continue;
1478
+ for (const [relativePath, entry] of existing) {
1479
+ if (selection ? !selection.remove.has(relativePath) : inspectedDesired.has(relativePath)) continue;
1480
+ if (entry.kind === "gitlink") continue;
713
1481
  if (directoryShieldsSubmodule(relativePath, entry, target.git)) continue;
714
1482
  replaced.set(relativePath, entry);
715
1483
  }
@@ -720,7 +1488,8 @@ function planWorkingTreeMirror(input) {
720
1488
  continue;
721
1489
  }
722
1490
  const { absolutePath, stat } = targetEntry;
723
- if (entry.kind === "directory" || entry.kind === "gitlink") {
1491
+ if (options.overrides?.has(relativePath)) {
1492
+ } else if (entry.kind === "directory" || entry.kind === "gitlink") {
724
1493
  if (stat.isDirectory()) continue;
725
1494
  } else if (entry.kind === "symlink") {
726
1495
  if (stat.isSymbolicLink() && import_node_fs.default.readlinkSync(absolutePath) === entry.target) continue;
@@ -741,7 +1510,208 @@ function planWorkingTreeMirror(input) {
741
1510
  desired,
742
1511
  replaced,
743
1512
  absent: absent.filter((relativePath) => !hasAncestorIn(relativePath, replacedDirectories)).sort(shallowestFirst),
744
- index: target.git ? planIndexChanges(inspectedDesired, shielded, target.git) : { removed: [], written: [] }
1513
+ index: target.git ? planIndexChanges(selection?.desiredForIndex ?? inspectedDesired, shielded, target.git) : { removed: [], written: [] }
1514
+ };
1515
+ }
1516
+ function rollbackWorkingTreeMirrorJournal(input) {
1517
+ const targetRoot = import_node_path.default.resolve(input.targetRoot);
1518
+ const snapshotRoot = import_node_path.default.resolve(input.snapshotRoot);
1519
+ const { journal } = input;
1520
+ const state = newPassState(targetRoot);
1521
+ const touched = /* @__PURE__ */ new Set();
1522
+ const abandoned = [];
1523
+ const restoredTrees = /* @__PURE__ */ new Set();
1524
+ const targetPathOf = (relativePath) => {
1525
+ const targetPath = import_node_path.default.join(targetRoot, ...relativePath.split("/"));
1526
+ assertInside(targetRoot, targetPath);
1527
+ return targetPath;
1528
+ };
1529
+ const stageFromSnapshot = (relativePath, replaced) => {
1530
+ const captured = inspectRelativePathWithoutFollowingAncestors(snapshotRoot, relativePath);
1531
+ if (captured.kind !== "entry") throw new Error(`Working-tree snapshot does not hold the replaced entry: ${relativePath}`);
1532
+ const stagedPath = stagingPathFor(state, targetPathOf(relativePath), relativePath);
1533
+ if (replaced.kind === "symlink") {
1534
+ if (!captured.stat.isSymbolicLink()) throw new Error(`Working-tree snapshot does not hold the replaced entry: ${relativePath}`);
1535
+ import_node_fs.default.symlinkSync(import_node_fs.default.readlinkSync(captured.absolutePath), stagedPath);
1536
+ return { path: stagedPath, stat: import_node_fs.default.lstatSync(stagedPath) };
1537
+ }
1538
+ if (!captured.stat.isFile()) throw new Error(`Working-tree snapshot does not hold the replaced entry: ${relativePath}`);
1539
+ return { path: stagedPath, stat: stageRegularFile(captured.absolutePath, stagedPath, replaced, "per_entry").stagedStat };
1540
+ };
1541
+ const restoreReplaced = (entry, expected) => {
1542
+ const replaced = entry.replaced;
1543
+ const { relativePath } = entry;
1544
+ if (replaced.kind === "gitlink") return true;
1545
+ if (replaced.kind === "directory") return restoreCapturedDirectory(relativePath, replaced);
1546
+ let staged;
1547
+ let onFailure = "unlink";
1548
+ if (entry.displaced && lstatIfExists(entry.displaced.path)) {
1549
+ const current = import_node_fs.default.lstatSync(entry.displaced.path);
1550
+ if (!sameWorkingTreeInode(current, entry.displaced.stat))
1551
+ throw new Error(`Retained working-tree entry was replaced: ${entry.displaced.path}`);
1552
+ if (!retainedEntryIntact(entry.displaced, current)) onFailure = "retain";
1553
+ staged = { path: entry.displaced.path, stat: current };
1554
+ } else {
1555
+ staged = stageFromSnapshot(relativePath, replaced);
1556
+ }
1557
+ try {
1558
+ const outcome = switchStagedEntry({
1559
+ state,
1560
+ targetRoot,
1561
+ relativePath,
1562
+ targetPath: targetPathOf(relativePath),
1563
+ stagedPath: staged.path,
1564
+ stagedStat: staged.stat,
1565
+ expected,
1566
+ expectedContent: expected === null ? void 0 : entry.afterContent,
1567
+ journal,
1568
+ phase: "before_rollback_switch",
1569
+ onFailure
1570
+ });
1571
+ if (outcome.displaced) releaseRetainedEntry(journal, relativePath, outcome.displaced);
1572
+ delete entry.displaced;
1573
+ return true;
1574
+ } catch (error) {
1575
+ if (!(error instanceof WorkingTreeTargetChangedError)) throw error;
1576
+ if (onFailure === "retain") delete entry.displaced;
1577
+ else if (entry.displaced && !lstatIfExists(entry.displaced.path)) delete entry.displaced;
1578
+ return false;
1579
+ }
1580
+ };
1581
+ const restoreCapturedDirectory = (relativePath, replaced) => {
1582
+ const captured = inspectRelativePathWithoutFollowingAncestors(snapshotRoot, relativePath);
1583
+ if (captured.kind !== "entry" || !captured.stat.isDirectory()) {
1584
+ throw new Error(`Working-tree snapshot does not hold the captured directory: ${relativePath}`);
1585
+ }
1586
+ try {
1587
+ import_node_fs.default.mkdirSync(targetPathOf(relativePath), { mode: replaced.mode & 511 });
1588
+ } catch (error) {
1589
+ if (isPathStateCode(error.code)) return false;
1590
+ throw error;
1591
+ }
1592
+ let complete = true;
1593
+ const children = inspectWorkingTree(captured.absolutePath, "all");
1594
+ for (const [childRelative, child] of [...children.entries()].sort(([left], [right]) => shallowestFirst(left, right))) {
1595
+ const childPath = import_node_path.default.posix.join(relativePath, childRelative);
1596
+ if (child.kind === "gitlink") continue;
1597
+ if (child.kind === "directory") {
1598
+ try {
1599
+ import_node_fs.default.mkdirSync(targetPathOf(childPath), { mode: child.mode & 511 });
1600
+ } catch (error) {
1601
+ if (!isPathStateCode(error.code)) throw error;
1602
+ const now = inspectRelativePathWithoutFollowingAncestors(targetRoot, childPath);
1603
+ if (now.kind !== "entry" || !now.stat.isDirectory() || now.stat.isSymbolicLink()) {
1604
+ abandoned.push(childPath);
1605
+ complete = false;
1606
+ }
1607
+ }
1608
+ continue;
1609
+ }
1610
+ const staged = stageFromSnapshot(childPath, child);
1611
+ try {
1612
+ switchStagedEntry({
1613
+ state,
1614
+ targetRoot,
1615
+ relativePath: childPath,
1616
+ targetPath: targetPathOf(childPath),
1617
+ stagedPath: staged.path,
1618
+ stagedStat: staged.stat,
1619
+ expected: null,
1620
+ journal,
1621
+ phase: "before_rollback_switch",
1622
+ onFailure: "unlink"
1623
+ });
1624
+ } catch (error) {
1625
+ if (!(error instanceof WorkingTreeTargetChangedError)) throw error;
1626
+ abandoned.push(childPath);
1627
+ complete = false;
1628
+ }
1629
+ }
1630
+ restoredTrees.add(relativePath);
1631
+ return complete;
1632
+ };
1633
+ for (const entry of [...journal.entries].reverse()) {
1634
+ const { relativePath } = entry;
1635
+ if (hasAncestorIn(relativePath, restoredTrees)) {
1636
+ touched.add(relativePath);
1637
+ continue;
1638
+ }
1639
+ const current = inspectRelativePathWithoutFollowingAncestors(targetRoot, relativePath);
1640
+ if (current.kind === "blocked") {
1641
+ abandoned.push(relativePath);
1642
+ continue;
1643
+ }
1644
+ if (entry.after === null) {
1645
+ if (current.kind !== "missing") {
1646
+ abandoned.push(relativePath);
1647
+ continue;
1648
+ }
1649
+ if (entry.replaced && !restoreReplaced(entry, null)) {
1650
+ abandoned.push(relativePath);
1651
+ continue;
1652
+ }
1653
+ touched.add(relativePath);
1654
+ continue;
1655
+ }
1656
+ if (current.kind !== "entry" || !workingTreeStatsMatch(entry.after, current.stat) || entry.afterContent !== void 0 && entryContent(current.absolutePath, current.stat) !== entry.afterContent) {
1657
+ if (!(current.kind === "entry" && entry.after.isDirectory() && current.stat.isDirectory())) {
1658
+ abandoned.push(relativePath);
1659
+ continue;
1660
+ }
1661
+ }
1662
+ if (current.stat.isDirectory() && !current.stat.isSymbolicLink()) {
1663
+ try {
1664
+ import_node_fs.default.rmdirSync(current.absolutePath);
1665
+ } catch (error) {
1666
+ const code = error.code;
1667
+ if (code === "ENOTEMPTY" || code === "EEXIST" || code === "ENOTDIR") {
1668
+ abandoned.push(relativePath);
1669
+ continue;
1670
+ }
1671
+ if (code !== "ENOENT") throw error;
1672
+ }
1673
+ if (entry.replaced && !restoreReplaced(entry, null)) {
1674
+ abandoned.push(relativePath);
1675
+ continue;
1676
+ }
1677
+ touched.add(relativePath);
1678
+ continue;
1679
+ }
1680
+ if (entry.replaced) {
1681
+ if (!restoreReplaced(entry, entry.after)) {
1682
+ abandoned.push(relativePath);
1683
+ continue;
1684
+ }
1685
+ touched.add(relativePath);
1686
+ continue;
1687
+ }
1688
+ try {
1689
+ const displaced = removeSwitchedEntry({
1690
+ state,
1691
+ targetRoot,
1692
+ relativePath,
1693
+ targetPath: targetPathOf(relativePath),
1694
+ expected: entry.after,
1695
+ expectedContent: entry.afterContent,
1696
+ journal,
1697
+ phase: "before_rollback_remove"
1698
+ });
1699
+ releaseRetainedEntry(journal, relativePath, displaced);
1700
+ } catch (error) {
1701
+ if (!(error instanceof WorkingTreeTargetChangedError)) throw error;
1702
+ abandoned.push(relativePath);
1703
+ continue;
1704
+ }
1705
+ touched.add(relativePath);
1706
+ }
1707
+ releaseRetainedEntries(journal);
1708
+ if (journal.indexApplied) {
1709
+ updateIndex(targetRoot, { drop: journal.indexApplied.written, put: journal.indexApplied.removed });
1710
+ }
1711
+ return {
1712
+ paths: [...touched].sort(shallowestFirst),
1713
+ abandoned: [...new Set(abandoned)].sort(shallowestFirst),
1714
+ ...journal.retained && journal.retained.length > 0 ? { retained: [...journal.retained] } : {}
745
1715
  };
746
1716
  }
747
1717
  function captureWorkingTreeMirrorPlan(input) {
@@ -822,7 +1792,8 @@ function restoreWorkingTreeMirrorScope(input) {
822
1792
  for (const name of import_node_fs.default.readdirSync(absoluteDirectory)) {
823
1793
  if (!MIRROR_TEMPORARY_FILE_PATTERN.test(name)) continue;
824
1794
  const temporaryPath = import_node_path.default.join(absoluteDirectory, name);
825
- if (!import_node_fs.default.lstatSync(temporaryPath).isFile()) continue;
1795
+ const temporaryStatus = import_node_fs.default.lstatSync(temporaryPath);
1796
+ if (!temporaryStatus.isFile() && !temporaryStatus.isSymbolicLink()) continue;
826
1797
  import_node_fs.default.rmSync(temporaryPath, { force: true });
827
1798
  touched.add(directory === "." ? name : import_node_path.default.posix.join(directory, name));
828
1799
  }
@@ -876,12 +1847,26 @@ function fsyncWorkingTreePaths(root, relativePaths) {
876
1847
  }
877
1848
  // Annotate the CommonJS export names for ESM import in node:
878
1849
  0 && (module.exports = {
1850
+ WorkingTreeTargetChangedError,
1851
+ applyWorkingTreeMirror,
879
1852
  assertWorkingTreeHasNoPortableGitMetadataAliases,
880
1853
  captureWorkingTreeMirrorPlan,
1854
+ displacedEntryMatches,
881
1855
  fsyncWorkingTreePaths,
882
1856
  inspectWorkingTree,
1857
+ inspectWorkingTreePath,
883
1858
  mirrorWorkingTree,
884
1859
  normalizeWorkingTreeRelativePath,
1860
+ planPreparedWorkingTreeMirror,
885
1861
  planWorkingTreeMirror,
886
- restoreWorkingTreeMirrorScope
1862
+ prepareWorkingTreeMirror,
1863
+ projectedFileEntry,
1864
+ projectedFileEntryMatches,
1865
+ restoreWorkingTreeMirrorScope,
1866
+ rollbackWorkingTreeMirrorJournal,
1867
+ sameWorkingTreeInode,
1868
+ setWorkingTreeMirrorRaceHook,
1869
+ workingTreeAtomicRenameSupport,
1870
+ workingTreeDisplacedRetentionDirectory,
1871
+ workingTreeStatsMatch
887
1872
  });