From 3cd7f0a307d4926ae1de309d96e3217cd4b9830f Mon Sep 17 00:00:00 2001 From: Lukas Geiger Date: Sat, 26 Sep 2026 18:33:44 +0200 Subject: [PATCH] fix(batch-rename): recognize case-only self-rename on case-insensitive FS Split out of PR #6 at team-lead's request (measured Windows regression there was traced to the conftest.py Qt-teardown attempt, reverted; this fix is unrelated and independently verified). test_case_only_rename_is_actionable_not_unchanged and test_batch_rename_dialog_case_mode_and_execution failed on macOS (AssertionError: Status sollte 'ok' sein, war aber 'collision'), passing on Linux and Windows. Root cause: generate_preview()'s "is this a case-only self-rename" check compared os.path.normcase(new_path) == os.path.normcase(original_path). os.path.normcase() reflects the OS's PATH-STRING convention (lowercases on Windows, no-op on POSIX) -- it says nothing about the actual filesystem's case sensitivity. macOS is the classic mismatch: its path convention is POSIX/case-sensitive (normcase is a no-op there), but its default filesystem (APFS) is case-INsensitive, so "sample.txt" and "SAMPLE.txt" are the same file on disk despite differing normcase strings. The self-rename bypass therefore never triggered, and the subsequent os.path.exists(item.new_path) check found the (same) file already "there" and flagged it as an external collision. Fix: after the normcase check, also compare via os.path.samefile() when both paths exist -- this checks actual inode/device identity and correctly recognizes the case-only self-rename on any case-insensitive filesystem regardless of the OS's path-string convention. Guarded against a not-yet-existing target with a double os.path.exists() check before the call plus try/except OSError as a safety net. --- src/core/batch_rename_service.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/core/batch_rename_service.py b/src/core/batch_rename_service.py index 5ddcb3f..4fe18f5 100644 --- a/src/core/batch_rename_service.py +++ b/src/core/batch_rename_service.py @@ -236,9 +236,23 @@ def generate_preview( norm_target = os.path.normcase(os.path.abspath(item.new_path)) norm_src = os.path.normcase(os.path.abspath(item.original_path)) - # Case-Only Rename der eigenen Datei ist zulässig + # Case-Only Rename der eigenen Datei ist zulässig. os.path.normcase() + # allein reicht dafür nicht: normcase folgt der Pfad-STRING-Konvention + # des Betriebssystems (Windows -> lowercase, POSIX -> No-Op) und weiß + # nichts über das tatsächliche Dateisystem. Unter macOS ist die + # Pfadkonvention case-sensitiv (normcase verändert nichts), das dort + # standardmäßige APFS aber case-insensitiv -- "sample.txt" und + # "SAMPLE.txt" sind auf Dateisystemebene dieselbe Datei, obwohl ihre + # normcase-Strings sich unterscheiden. os.path.samefile() vergleicht + # dagegen tatsächliche Inode/Device-Identität und erkennt das korrekt. if norm_target == norm_src: continue + if os.path.exists(item.new_path) and os.path.exists(item.original_path): + try: + if os.path.samefile(item.new_path, item.original_path): + continue + except OSError: + pass # Ziel wird von einer anderen, nicht wegziehenden Batch-Datei blockiert if norm_target in blocked_original_paths: