Problem
Naive serial renames lose data whenever the rename set has collisions or cycles between source and destination names.
Chain — rename a → b then b → c:
mv a b # original b is clobbered by a
mv b c # what was a is now at c; original b is gone
Swap — rename a → b and b → a:
mv a b # original b destroyed
mv b a # one file is gone forever
These cases come up in any "shift everything" rename and in regex-driven renames where source and target sets overlap.
Proposed fix
Apply renames in two phases that decouple source and target namespaces:
- Phase 1 (quarantine): rename every source to a unique temp name in the same directory (e.g.
.find-replace.tmp.{nonce}.{seq}).
- Phase 2 (install): rename every temp to its final destination.
Because the temp namespace is disjoint from both the source set and the target set, ordering within each phase doesn't matter — chains, swaps, and arbitrary permutations all work.
Safety details
- Use
renameat2(RENAME_NOREPLACE) on Linux so phase 2 atomically fails if a destination unexpectedly exists (closes the TOCTOU window). Fall back to a pre-check + rename(2) on platforms without it.
fsync the parent directory after each phase so the operation is durable across power loss.
- Pre-flight: verify no two sources collide, no two destinations collide, and any destination not in the source set doesn't already exist. Abort before phase 1 on conflict.
- On phase 1 partial failure, roll back the temps already created (sources are still vacant, so this is always possible).
- On phase 2 partial failure, leave the remaining files in temp names and surface the error clearly; a
--resume path can finish them.
- Optional but recommended: write a journal file (
.find-replace.journal) listing (original, temp, final) triples before phase 1 so a crashed run can be resumed or rolled back.
Why this lands first
Pure safety improvement — no flag, no behavior change visible to correct uses, no API surface. Establishes the rename infrastructure that regex mode will depend on.
Acceptance
Problem
Naive serial renames lose data whenever the rename set has collisions or cycles between source and destination names.
Chain — rename
a → bthenb → c:Swap — rename
a → bandb → a:These cases come up in any "shift everything" rename and in regex-driven renames where source and target sets overlap.
Proposed fix
Apply renames in two phases that decouple source and target namespaces:
.find-replace.tmp.{nonce}.{seq}).Because the temp namespace is disjoint from both the source set and the target set, ordering within each phase doesn't matter — chains, swaps, and arbitrary permutations all work.
Safety details
renameat2(RENAME_NOREPLACE)on Linux so phase 2 atomically fails if a destination unexpectedly exists (closes the TOCTOU window). Fall back to a pre-check +rename(2)on platforms without it.fsyncthe parent directory after each phase so the operation is durable across power loss.--resumepath can finish them..find-replace.journal) listing(original, temp, final)triples before phase 1 so a crashed run can be resumed or rolled back.Why this lands first
Pure safety improvement — no flag, no behavior change visible to correct uses, no API surface. Establishes the rename infrastructure that regex mode will depend on.
Acceptance
renameat2(RENAME_NOREPLACE)used on Linux; fallback documentedfsync'd after each phase