Summary
On a case-insensitive filesystem (macOS/APFS and HFS+ by default, and Windows),
a rename that only changes the case of a name is rejected with
File name already exists. Since lowercasing is one of the core
transformations, this makes the tool largely unusable on those platforms for
names that differ from their slug by case alone.
$ ls
README.MD
$ rename-simple README.MD
[E] .: README.MD -> File name already exists
1 entry matched, 0 entry renamed, 1 error.
The rename is perfectly legal there — mv README.MD readme.md works — and it
cannot lose data: source and destination are the same inode, so the operation
is a pure rename.
Cause
filter_conflicts treats any existing destination as a conflict
(src/main.rs:328). On a case-insensitive filesystem readme.md resolves to
the very same entry as README.MD, so exists() answers true.
resolve_existing then classifies it via compare_entries, which reports
EntryMatch::SameEntry because the (dev, ino) pair matches
(src/lib.rs:742), and same-entry clashes are reported as a plain error
(src/main.rs:365). That branch is correct for hard links — deleting or
renaming there would be destructive — but a case-only rename of a single entry
is not the same situation.
Suggested fix
EntryMatch::SameEntry already carries the information needed: when the
destination is the source, no data can be lost. The rename can be attempted
instead of refused, letting the filesystem arbitrate.
The catch is rename_no_clobber (src/main.rs:386): on Linux it uses
renameat2(RENAME_NOREPLACE), which fails with EEXIST on a same-entry
rename, so the fallback path (try_exists + fs::rename) is the one that
would have to run for this case. Hard links must keep failing — a fix must
distinguish "the destination path resolves to the source" (safe, and what
happens on a case-insensitive filesystem) from "two distinct names for one
inode" (link1 → link2, must stay an error).
Why this is not fixed yet
CI runs on ubuntu-latest only (.github/workflows/ci.yml), and the bug
cannot be reproduced on a case-sensitive filesystem. Fixing it blind would mean
shipping an untested change to the only code path that is allowed to touch the
filesystem. Adding a macos-latest job to the CI matrix is a prerequisite.
Found during the code review that led to 0.7.1. Pre-existing; not a regression
from the -D work.
Summary
On a case-insensitive filesystem (macOS/APFS and HFS+ by default, and Windows),
a rename that only changes the case of a name is rejected with
File name already exists. Since lowercasing is one of the coretransformations, this makes the tool largely unusable on those platforms for
names that differ from their slug by case alone.
The rename is perfectly legal there —
mv README.MD readme.mdworks — and itcannot lose data: source and destination are the same inode, so the operation
is a pure rename.
Cause
filter_conflictstreats any existing destination as a conflict(
src/main.rs:328). On a case-insensitive filesystemreadme.mdresolves tothe very same entry as
README.MD, soexists()answerstrue.resolve_existingthen classifies it viacompare_entries, which reportsEntryMatch::SameEntrybecause the(dev, ino)pair matches(
src/lib.rs:742), and same-entry clashes are reported as a plain error(
src/main.rs:365). That branch is correct for hard links — deleting orrenaming there would be destructive — but a case-only rename of a single entry
is not the same situation.
Suggested fix
EntryMatch::SameEntryalready carries the information needed: when thedestination is the source, no data can be lost. The rename can be attempted
instead of refused, letting the filesystem arbitrate.
The catch is
rename_no_clobber(src/main.rs:386): on Linux it usesrenameat2(RENAME_NOREPLACE), which fails withEEXISTon a same-entryrename, so the fallback path (
try_exists+fs::rename) is the one thatwould have to run for this case. Hard links must keep failing — a fix must
distinguish "the destination path resolves to the source" (safe, and what
happens on a case-insensitive filesystem) from "two distinct names for one
inode" (
link1→link2, must stay an error).Why this is not fixed yet
CI runs on
ubuntu-latestonly (.github/workflows/ci.yml), and the bugcannot be reproduced on a case-sensitive filesystem. Fixing it blind would mean
shipping an untested change to the only code path that is allowed to touch the
filesystem. Adding a
macos-latestjob to the CI matrix is a prerequisite.Found during the code review that led to 0.7.1. Pre-existing; not a regression
from the
-Dwork.