diff --git a/commitizen/cli.py b/commitizen/cli.py index da170eefdd..e3af3a8073 100644 --- a/commitizen/cli.py +++ b/commitizen/cli.py @@ -135,6 +135,11 @@ } ], }, + { + "name": ["init"], + "help": "init commitizen configuration", + "func": commands.Init, + }, ], }, } diff --git a/commitizen/commands/__init__.py b/commitizen/commands/__init__.py index 051429f52f..e315a987c9 100644 --- a/commitizen/commands/__init__.py +++ b/commitizen/commands/__init__.py @@ -6,5 +6,17 @@ from .list_cz import ListCz from .schema import Schema from .version import Version +from .init import Init -__all__ = ("Bump", "Check", "Commit", "Example", "Info", "ListCz", "Schema", "Version") + +__all__ = ( + "Bump", + "Check", + "Commit", + "Example", + "Info", + "ListCz", + "Schema", + "Version", + "Init", +) diff --git a/commitizen/commands/init.py b/commitizen/commands/init.py new file mode 100644 index 0000000000..7fc39cdd01 --- /dev/null +++ b/commitizen/commands/init.py @@ -0,0 +1,109 @@ +from packaging.version import Version + +import questionary + +from commitizen import factory, out +from commitizen.cz import registry +from commitizen.config import BaseConfig, TomlConfig, IniConfig +from commitizen.git import get_latest_tag, get_all_tags +from commitizen.defaults import config_files + + +class Init: + def __init__(self, config: BaseConfig, *args): + self.config: BaseConfig = config + self.cz = factory.commiter_factory(self.config) + + def __call__(self): + values_to_add = {} + + # No config file exist + if not self.config.path: + config_path = self._ask_config_path() + + if "toml" in config_path: + self.config = TomlConfig(data="", path=config_path) + else: + self.config = IniConfig(data="", path=config_path) + + self.config.init_empty_config_file() + + values_to_add["name"] = self._ask_name() + tag = self._ask_tag() + values_to_add["version"] = Version(tag).public + values_to_add["tag_format"] = self._ask_tag_format(tag) + self._update_config_file(values_to_add) + out.write("The configuration are all set.") + else: + # TODO: handle the case that config file exist but no value + out.line(f"Config file {self.config.path} already exists") + + def _ask_config_path(self) -> str: + name = questionary.select( + "Please choose a supported config file: (default: pyproject.tml)", + choices=config_files, + default="pyproject.toml", + style=self.cz.style, + ).ask() + return name + + def _ask_name(self) -> str: + name = questionary.select( + "Please choose a cz: (default: cz_conventional_commits)", + choices=list(registry.keys()), + default="cz_conventional_commits", + style=self.cz.style, + ).ask() + return name + + def _ask_tag(self) -> str: + latest_tag = get_latest_tag() + if not latest_tag: + out.error("No Existing Tag. Set tag to v0.0.1") + return "0.0.1" + + is_correct_tag = questionary.confirm( + f"Is {latest_tag} the latest tag?", style=self.cz.style, default=False + ).ask() + if not is_correct_tag: + tags = get_all_tags() + if not tags: + out.error("No Existing Tag. Set tag to v0.0.1") + return "0.0.1" + + latest_tag = questionary.select( + "Please choose the latest tag: ", + choices=get_all_tags(), + style=self.cz.style, + ).ask() + + if not latest_tag: + out.error("Tag is required!") + raise SystemExit() + return latest_tag + + def _ask_tag_format(self, latest_tag) -> str: + is_correct_format = False + if latest_tag.startswith("v"): + tag_format = r"v$version" + is_correct_format = questionary.confirm( + f'Is "{tag_format}" the correct tag format?', style=self.cz.style + ).ask() + + if not is_correct_format: + tag_format = questionary.text( + 'Please enter the correct version format: (default: "$version")', + style=self.cz.style, + ).ask() + + if not tag_format: + tag_format = "$version" + return tag_format + + def _update_config_file(self, values): + if not values: + out.write("The configuration were all set. Nothing to add.") + raise SystemExit() + + for key, value in values.items(): + self.config.set_key(key, value) diff --git a/commitizen/config/ini_config.py b/commitizen/config/ini_config.py index 5c02e22e9b..8fcb525719 100644 --- a/commitizen/config/ini_config.py +++ b/commitizen/config/ini_config.py @@ -22,6 +22,10 @@ def __init__(self, *, data: str, path: str): self._parse_setting(data) self.add_path(path) + def init_empty_config_file(self): + with open(self.path, "w") as toml_file: + toml_file.write("[commitizen]") + def set_key(self, key, value): """Set or update a key in the conf. diff --git a/commitizen/config/toml_config.py b/commitizen/config/toml_config.py index 7e669b18e0..bbd7fd440c 100644 --- a/commitizen/config/toml_config.py +++ b/commitizen/config/toml_config.py @@ -10,6 +10,10 @@ def __init__(self, *, data: str, path: str): self._parse_setting(data) self.add_path(path) + def init_empty_config_file(self): + with open(self.path, "w") as toml_file: + toml_file.write("[tool.commitizen]") + def set_key(self, key, value): """Set or update a key in the conf. diff --git a/commitizen/git.py b/commitizen/git.py index db307d4510..bbd4d7b84b 100644 --- a/commitizen/git.py +++ b/commitizen/git.py @@ -1,5 +1,6 @@ import os from tempfile import NamedTemporaryFile +from typing import Optional, List from commitizen import cmd @@ -40,3 +41,17 @@ def is_staging_clean() -> bool: c = cmd.run("git diff --no-ext-diff --name-only") c_cached = cmd.run("git diff --no-ext-diff --cached --name-only") return not (bool(c.out) or bool(c_cached.out)) + + +def get_latest_tag() -> Optional[str]: + c = cmd.run("git describe --abbrev=0 --tags") + if c.err: + return None + return c.out.strip() + + +def get_all_tags() -> Optional[List[str]]: + c = cmd.run("git tag --list") + if c.err: + return [] + return [tag.strip() for tag in c.out.split("\n") if tag.strip()] diff --git a/docs/index.md b/docs/index.md index d1c5bbff92..ff10951fb0 100644 --- a/docs/index.md +++ b/docs/index.md @@ -95,6 +95,7 @@ commands: schema show commit schema bump bump semantic version based on the git log check validates that a commit message matches the commitizen schema + init init commitizen configuration ``` ## Contributing diff --git a/tests/test_commands.py b/tests/test_commands.py index e26d5be318..983b8a0073 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -166,14 +166,12 @@ def test_schema(config): def test_list_cz(config): with mock.patch("commitizen.out.write") as mocked_write: - commands.ListCz(config)() mocked_write.assert_called_once() def test_version(config): with mock.patch("commitizen.out.write") as mocked_write: - commands.Version(config)() mocked_write.assert_called_once() @@ -214,3 +212,13 @@ def test_check_conventional_commit(config, mocker): def test_check_command_when_commit_file_not_found(config): with pytest.raises(FileNotFoundError): commands.Check(config=config, arguments={"commit_msg_file": ""})() + + +def test_init_when_config_already_exists(config, capsys): + # Set config path + path = "tests/pyproject.toml" + config.add_path(path) + + commands.Init(config)() + captured = capsys.readouterr() + assert captured.out == f"Config file {path} already exists\n"