diff --git a/desloppify/app/commands/move/apply.py b/desloppify/app/commands/move/apply.py index d242f32bc..4876dfa95 100644 --- a/desloppify/app/commands/move/apply.py +++ b/desloppify/app/commands/move/apply.py @@ -17,6 +17,11 @@ from desloppify.base.output.terminal import colorize +def _ensure_move_destination_absent(dest_abs: str) -> None: + if Path(dest_abs).exists(): + raise FileExistsError(f"Destination already exists: {dest_abs}") + + def _rollback_written_files(written_files: dict[str, str]) -> None: failed = restore_files_best_effort(written_files, safe_write_text) for filepath in failed: @@ -55,6 +60,7 @@ def apply_file_move( Path(dest_abs).parent.mkdir(parents=True, exist_ok=True) written_files: dict[str, str] = {} try: + _ensure_move_destination_absent(dest_abs) shutil.move(source_abs, dest_abs) if dest_abs in new_contents: @@ -85,6 +91,7 @@ def apply_directory_move( Path(dest_abs).parent.mkdir(parents=True, exist_ok=True) written_files: dict[str, str] = {} try: + _ensure_move_destination_absent(dest_abs) shutil.move(source_abs, dest_abs) for src_file, changes in internal_changes.items(): diff --git a/desloppify/tests/commands/test_transitive_engine.py b/desloppify/tests/commands/test_transitive_engine.py index 50652577c..90392815d 100644 --- a/desloppify/tests/commands/test_transitive_engine.py +++ b/desloppify/tests/commands/test_transitive_engine.py @@ -753,6 +753,19 @@ def test_file_move_with_importer_changes(self, tmp_path): ) assert importer.read_text() == "from b import thing" + def test_file_move_fails_if_destination_already_exists(self, tmp_path): + src = tmp_path / "a.py" + dest = tmp_path / "b.py" + src.write_text("source content") + dest.write_text("existing content") + + with pytest.raises(FileExistsError, match="Destination already exists"): + move_apply_mod.apply_file_move(str(src), str(dest), {}, []) + + assert src.exists() + assert src.read_text() == "source content" + assert dest.read_text() == "existing content" + def test_file_move_rollback_on_write_error(self, tmp_path): """If writing an importer fails, the move is rolled back.""" src = tmp_path / "a.py" @@ -802,6 +815,22 @@ def test_directory_move_with_internal_changes(self, tmp_path): ) assert (dest / "a.py").read_text() == "from new_pkg.b import f" + def test_directory_move_fails_if_destination_already_exists(self, tmp_path): + src = tmp_path / "pkg" + src.mkdir() + (src / "mod.py").write_text("source content") + + dest = tmp_path / "new_pkg" + dest.mkdir() + (dest / "mod.py").write_text("existing content") + + with pytest.raises(FileExistsError, match="Destination already exists"): + move_apply_mod.apply_directory_move(str(src), str(dest), src, {}, {}) + + assert src.exists() + assert (src / "mod.py").read_text() == "source content" + assert (dest / "mod.py").read_text() == "existing content" + # ===================================================================== # Module 5: shared_phases.py