diff --git a/copier/cli.py b/copier/cli.py index bd60c0870..29cd60977 100644 --- a/copier/cli.py +++ b/copier/cli.py @@ -129,18 +129,6 @@ class _Subcommand(cli.Application): ), ) pretend = cli.Flag(["-n", "--pretend"], help="Run but do not make any changes") - force = cli.Flag( - ["-f", "--force"], - help="Same as `--defaults --overwrite`.", - ) - defaults = cli.Flag( - ["-l", "--defaults"], - help="Use default answers to questions, which might be null if not specified.", - ) - overwrite = cli.Flag( - ["-w", "--overwrite"], - help="Overwrite files that already exist, without asking.", - ) skip = cli.SwitchAttr( ["-s", "--skip"], str, @@ -185,8 +173,6 @@ def _worker(self, src_path: OptStr = None, dst_path: str = ".", **kwargs) -> Wor dst_path=Path(dst_path), answers_file=self.answers_file, exclude=self.exclude, - defaults=self.force or self.defaults, - overwrite=self.force or self.overwrite, pretend=self.pretend, skip_if_exists=self.skip, quiet=self.quiet, @@ -212,6 +198,18 @@ class CopierCopySubApp(_Subcommand): default=True, help="On error, do not delete destination if it was created by Copier.", ) + defaults = cli.Flag( + ["-l", "--defaults"], + help="Use default answers to questions, which might be null if not specified.", + ) + force = cli.Flag( + ["-f", "--force"], + help="Same as `--defaults --overwrite`.", + ) + overwrite = cli.Flag( + ["-w", "--overwrite"], + help="Overwrite files that already exist, without asking.", + ) @handle_exceptions def main(self, template_src: str, destination_path: str) -> int: @@ -230,6 +228,8 @@ def main(self, template_src: str, destination_path: str) -> int: template_src, destination_path, cleanup_on_error=self.cleanup_on_error, + defaults=self.force or self.defaults, + overwrite=self.force or self.overwrite, ).run_copy() return 0 @@ -260,6 +260,19 @@ class CopierRecopySubApp(_Subcommand): """ ) + defaults = cli.Flag( + ["-l", "--defaults"], + help="Use default answers to questions, which might be null if not specified.", + ) + force = cli.Flag( + ["-f", "--force"], + help="Same as `--defaults --overwrite`.", + ) + overwrite = cli.Flag( + ["-w", "--overwrite"], + help="Overwrite files that already exist, without asking.", + ) + @handle_exceptions def main(self, destination_path: cli.ExistingDirectory = ".") -> int: """Call [run_recopy][copier.main.Worker.run_recopy]. @@ -274,6 +287,8 @@ def main(self, destination_path: cli.ExistingDirectory = ".") -> int: """ self._worker( dst_path=destination_path, + defaults=self.force or self.defaults, + overwrite=self.force or self.overwrite, ).run_recopy() return 0 @@ -318,6 +333,10 @@ class CopierUpdateSubApp(_Subcommand): "accuracy, decrease for resilience." ), ) + defaults = cli.Flag( + ["-l", "-f", "--defaults"], + help="Use default answers to questions, which might be null if not specified.", + ) @handle_exceptions def main(self, destination_path: cli.ExistingDirectory = ".") -> int: @@ -335,6 +354,8 @@ def main(self, destination_path: cli.ExistingDirectory = ".") -> int: dst_path=destination_path, conflict=self.conflict, context_lines=self.context_lines, + defaults=self.defaults, + overwrite=True, ).run_update() return 0 diff --git a/copier/main.py b/copier/main.py index 1f40055d8..3b917025c 100644 --- a/copier/main.py +++ b/copier/main.py @@ -755,6 +755,11 @@ def run_update(self) -> None: f"Your are downgrading from {self.subproject.template.version} to {self.template.version}. " "Downgrades are not supported." ) + if not self.overwrite: + # Only git-tracked subprojects can be updated, so the user can + # review the diff before committing; so we can safely avoid + # asking for confirmation + raise UserMessageError("Enable overwrite to update a subproject.") if not self.quiet: # TODO Unify printing tools print( diff --git a/tests/test_conditional_file_name.py b/tests/test_conditional_file_name.py index 96a2ef0eb..4c19941aa 100644 --- a/tests/test_conditional_file_name.py +++ b/tests/test_conditional_file_name.py @@ -101,7 +101,7 @@ def test_answer_changes( git("commit", "-mv1") if interactive: - tui = spawn(COPIER_PATH + ("update", "--overwrite", str(dst)), timeout=10) + tui = spawn(COPIER_PATH + ("update", str(dst)), timeout=10) expect_prompt(tui, "condition", "bool") tui.expect_exact("(Y/n)") tui.sendline("n") diff --git a/tests/test_updatediff.py b/tests/test_updatediff.py index f8c91d606..ce26c6635 100644 --- a/tests/test_updatediff.py +++ b/tests/test_updatediff.py @@ -11,6 +11,7 @@ from plumbum.cmd import git from copier.cli import CopierApp +from copier.errors import UserMessageError from copier.main import Worker, run_copy, run_update from copier.types import Literal @@ -201,7 +202,7 @@ def test_updatediff(tmp_path_factory: pytest.TempPathFactory) -> None: ) commit("-m", "I prefer grog") # Update target to latest tag and check it's updated in answers file - CopierApp.run(["copier", "update", "--defaults", "--overwrite"], exit=False) + CopierApp.run(["copier", "update", "--defaults"], exit=False) assert answers.read_text() == dedent( f"""\ # Changes here will be overwritten by Copier @@ -223,7 +224,7 @@ def test_updatediff(tmp_path_factory: pytest.TempPathFactory) -> None: commit("-m", "Update template to v0.0.2") # Update target to latest commit, which is still untagged CopierApp.run( - ["copier", "update", "--defaults", "--overwrite", "--vcs-ref=HEAD"], + ["copier", "update", "--defaults", "--vcs-ref=HEAD"], exit=False, ) # Check no new migrations were executed @@ -258,7 +259,7 @@ def test_updatediff(tmp_path_factory: pytest.TempPathFactory) -> None: assert not git("status", "--porcelain") # No more updates exist, so updating again should change nothing CopierApp.run( - ["copier", "update", "--defaults", "--overwrite", "--vcs-ref=HEAD"], + ["copier", "update", "--defaults", "--vcs-ref=HEAD"], exit=False, ) assert not git("status", "--porcelain") @@ -575,7 +576,7 @@ def test_overwrite_answers_file_always( git("commit", "-m1") # When updating, the only thing to overwrite is the copier answers file, # which shouldn't ask, so also this shouldn't hang with overwrite=False - run_update(defaults=True, answers_file=answers_file) + run_update(defaults=True, overwrite=True, answers_file=answers_file) answers = yaml.safe_load( (dst / (answers_file or ".copier-answers.yml")).read_bytes() ) @@ -641,7 +642,11 @@ def test_file_removed(tmp_path_factory: pytest.TempPathFactory) -> None: git("tag", "2") # Subproject updates with local.cwd(dst): - run_update(conflict="rej") + with pytest.raises( + UserMessageError, match="Enable overwrite to update a subproject." + ): + run_update(conflict="rej") + run_update(conflict="rej", overwrite=True) # Check what must still exist assert (dst / ".copier-answers.yml").is_file() assert (dst / "I.txt").is_file() @@ -730,7 +735,7 @@ def test_update_inline_changed_answers_and_questions( git("commit", "-am2") # Update from template, inline, with answer changes if interactive: - tui = spawn(COPIER_PATH + ("update", "-w", "--conflict=inline"), timeout=10) + tui = spawn(COPIER_PATH + ("update", "--conflict=inline"), timeout=10) tui.expect_exact("b (bool)") tui.expect_exact("(Y/n)") tui.sendline() @@ -941,7 +946,6 @@ def function_two(): else: COPIER_CMD( "update", - "--overwrite", str(dst), "--conflict=inline", f"--context-lines={context_lines}",