diff --git a/commitizen/commands/init.py b/commitizen/commands/init.py index 2d5a6a8e01..1c459d5ed4 100644 --- a/commitizen/commands/init.py +++ b/commitizen/commands/init.py @@ -1,7 +1,11 @@ +import os + import questionary +import yaml from packaging.version import Version -from commitizen import factory, out +from commitizen import cmd, factory, out +from commitizen.__version__ import __version__ from commitizen.config import BaseConfig, TomlConfig from commitizen.cz import registry from commitizen.defaults import config_files @@ -20,7 +24,6 @@ def __call__(self): # No config for commitizen exist if not self.config.path: config_path = self._ask_config_path() - if "toml" in config_path: self.config = TomlConfig(data="", path=config_path) @@ -31,11 +34,14 @@ def __call__(self): values_to_add["version"] = Version(tag).public values_to_add["tag_format"] = self._ask_tag_format(tag) self._update_config_file(values_to_add) + + if questionary.confirm("Do you want to install pre-commit hook?").ask(): + self._install_pre_commit_hook() + out.write("You can bump the version and create changelog running:\n") out.info("cz bump --changelog") out.success("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: @@ -99,6 +105,50 @@ def _ask_tag_format(self, latest_tag) -> str: tag_format = "$version" return tag_format + def _install_pre_commit_hook(self): + pre_commit_config_filename = ".pre-commit-config.yaml" + cz_hook_config = { + "repo": "https://github.com/commitizen-tools/commitizen", + "rev": f"v{__version__}", + "hooks": [{"id": "commitizen", "stages": ["commit-msg"]}], + } + + config_data = {} + if not os.path.isfile(pre_commit_config_filename): + # .pre-commit-config does not exist + config_data["repos"] = [cz_hook_config] + else: + # breakpoint() + with open(pre_commit_config_filename) as config_file: + yaml_data = yaml.safe_load(config_file) + if yaml_data: + config_data = yaml_data + + if "repos" in config_data: + for pre_commit_hook in config_data["repos"]: + if "commitizen" in pre_commit_hook["repo"]: + out.write("commitizen already in pre-commit config") + break + else: + config_data["repos"].append(cz_hook_config) + else: + # .pre-commit-config exists but there's no "repos" key + config_data["repos"] = [cz_hook_config] + + with open(pre_commit_config_filename, "w") as config_file: + yaml.safe_dump(config_data, stream=config_file) + + c = cmd.run("pre-commit install --hook-type commit-msg") + if c.return_code == 127: + out.error( + "pre-commit is not installed in current environement.\n" + "Run 'pre-commit install --hook-type commit-msg' again after it's installed" + ) + elif c.return_code != 0: + out.error(c.err) + else: + out.write("commitizen pre-commit hook is now installed in your '.git'\n") + def _update_config_file(self, values): for key, value in values.items(): self.config.set_key(key, value) diff --git a/tests/commands/test_init_command.py b/tests/commands/test_init_command.py index 39a0794005..555e0d816c 100644 --- a/tests/commands/test_init_command.py +++ b/tests/commands/test_init_command.py @@ -1,6 +1,10 @@ +import os + import pytest +import yaml from commitizen import commands +from commitizen.__version__ import __version__ from commitizen.exceptions import NoAnswersError @@ -12,7 +16,22 @@ def ask(self): return self.expected_return -def test_init(tmpdir, mocker, config): +pre_commit_config_filename = ".pre-commit-config.yaml" +cz_hook_config = { + "repo": "https://github.com/commitizen-tools/commitizen", + "rev": f"v{__version__}", + "hooks": [{"id": "commitizen", "stages": ["commit-msg"]}], +} + +expected_config = ( + "[tool.commitizen]\n" + 'name = "cz_conventional_commits"\n' + 'version = "0.0.1"\n' + 'tag_format = "$version"\n' +) + + +def test_init_without_setup_pre_commit_hook(tmpdir, mocker, config): mocker.patch( "questionary.select", side_effect=[ @@ -20,23 +39,19 @@ def test_init(tmpdir, mocker, config): FakeQuestion("cz_conventional_commits"), ], ) - mocker.patch("questionary.confirm", return_value=FakeQuestion("y")) - mocker.patch("questionary.text", return_value=FakeQuestion("y")) - expected_config = ( - "[tool.commitizen]\n" - 'name = "cz_conventional_commits"\n' - 'version = "0.0.1"\n' - 'tag_format = "y"\n' - ) + mocker.patch("questionary.confirm", return_value=FakeQuestion(True)) + mocker.patch("questionary.text", return_value=FakeQuestion("$version")) + mocker.patch("questionary.confirm", return_value=FakeQuestion(False)) with tmpdir.as_cwd(): commands.Init(config)() with open("pyproject.toml", "r") as toml_file: config_data = toml_file.read() - assert config_data == expected_config + assert not os.path.isfile(pre_commit_config_filename) + def test_init_when_config_already_exists(config, capsys): # Set config path @@ -67,3 +82,85 @@ def test_init_without_choosing_tag(config, mocker, tmpdir): with tmpdir.as_cwd(): with pytest.raises(NoAnswersError): commands.Init(config)() + + +class TestPreCommitCases: + @pytest.fixture(scope="function", autouse=True) + def default_choices(_, mocker): + mocker.patch( + "questionary.select", + side_effect=[ + FakeQuestion("pyproject.toml"), + FakeQuestion("cz_conventional_commits"), + ], + ) + mocker.patch("questionary.confirm", return_value=FakeQuestion(True)) + mocker.patch("questionary.text", return_value=FakeQuestion("$version")) + mocker.patch("questionary.confirm", return_value=FakeQuestion(True)) + + def test_no_existing_pre_commit_conifg(_, tmpdir, config): + with tmpdir.as_cwd(): + commands.Init(config)() + + with open("pyproject.toml", "r") as toml_file: + config_data = toml_file.read() + assert config_data == expected_config + + with open(pre_commit_config_filename, "r") as pre_commit_file: + pre_commit_config_data = yaml.safe_load(pre_commit_file.read()) + assert pre_commit_config_data == {"repos": [cz_hook_config]} + + def test_empty_pre_commit_config(_, tmpdir, config): + with tmpdir.as_cwd(): + p = tmpdir.join(pre_commit_config_filename) + p.write("") + + commands.Init(config)() + + with open("pyproject.toml", "r") as toml_file: + config_data = toml_file.read() + assert config_data == expected_config + + with open(pre_commit_config_filename, "r") as pre_commit_file: + pre_commit_config_data = yaml.safe_load(pre_commit_file.read()) + assert pre_commit_config_data == {"repos": [cz_hook_config]} + + def test_pre_commit_config_without_cz_hook(_, tmpdir, config): + existing_hook_config = { + "repo": "https://github.com/pre-commit/pre-commit-hooks", + "rev": "v1.2.3", + "hooks": [{"id", "trailing-whitespace"}], + } + + with tmpdir.as_cwd(): + p = tmpdir.join(pre_commit_config_filename) + p.write(yaml.safe_dump({"repos": [existing_hook_config]})) + + commands.Init(config)() + + with open("pyproject.toml", "r") as toml_file: + config_data = toml_file.read() + assert config_data == expected_config + + with open(pre_commit_config_filename, "r") as pre_commit_file: + pre_commit_config_data = yaml.safe_load(pre_commit_file.read()) + assert pre_commit_config_data == { + "repos": [existing_hook_config, cz_hook_config] + } + + def test_cz_hook_exists_in_pre_commit_config(_, tmpdir, config): + with tmpdir.as_cwd(): + p = tmpdir.join(pre_commit_config_filename) + p.write(yaml.safe_dump({"repos": [cz_hook_config]})) + + commands.Init(config)() + + with open("pyproject.toml", "r") as toml_file: + config_data = toml_file.read() + assert config_data == expected_config + + with open(pre_commit_config_filename, "r") as pre_commit_file: + pre_commit_config_data = yaml.safe_load(pre_commit_file.read()) + + # check that config is not duplicated + assert pre_commit_config_data == {"repos": [cz_hook_config]} diff --git a/tests/test_conf.py b/tests/test_conf.py index 0a4760e64e..241f2117d5 100644 --- a/tests/test_conf.py +++ b/tests/test_conf.py @@ -54,63 +54,54 @@ @pytest.fixture -def configure_supported_files(): - original = defaults.config_files.copy() +def config_files_manager(request, tmpdir): + with tmpdir.as_cwd(): + filename = request.param + with open(filename, "w") as f: + if "toml" in filename: + f.write(PYPROJECT) + yield - # patch the defaults to include tests - defaults.config_files = [os.path.join("tests", f) for f in defaults.config_files] - yield - defaults.config_files = original - - -@pytest.fixture -def config_files_manager(request): - filename = request.param - filepath = os.path.join("tests", filename) - with open(filepath, "w") as f: - if "toml" in filename: - f.write(PYPROJECT) - yield - os.remove(filepath) - - -@pytest.fixture -def empty_pyproject_ok_cz(): - pyproject = "tests/pyproject.toml" - with open(pyproject, "w") as f: - f.write("") - yield - os.remove(pyproject) - - -@pytest.mark.parametrize( - "config_files_manager", defaults.config_files.copy(), indirect=True -) -def test_load_conf(config_files_manager, configure_supported_files): - cfg = config.read_cfg() - assert cfg.settings == _settings +def test_find_git_project_root(tmpdir): + assert git.find_git_project_root() == Path(os.getcwd()) -def test_conf_returns_default_when_no_files(configure_supported_files): - cfg = config.read_cfg() - assert cfg.settings == defaults.DEFAULT_SETTINGS + with tmpdir.as_cwd() as _: + assert git.find_git_project_root() is None @pytest.mark.parametrize( "config_files_manager", defaults.config_files.copy(), indirect=True ) -def test_set_key(configure_supported_files, config_files_manager): +def test_set_key(config_files_manager): _conf = config.read_cfg() _conf.set_key("version", "2.0.0") cfg = config.read_cfg() assert cfg.settings == _new_settings -def test_find_git_project_root(tmpdir): - assert git.find_git_project_root() == Path(os.getcwd()) - - with tmpdir.as_cwd() as _: - assert git.find_git_project_root() is None +class TestReadCfg: + @pytest.mark.parametrize( + "config_files_manager", defaults.config_files.copy(), indirect=True + ) + def test_load_conf(_, config_files_manager): + cfg = config.read_cfg() + assert cfg.settings == _settings + + def test_conf_returns_default_when_no_files(_, tmpdir): + with tmpdir.as_cwd(): + cfg = config.read_cfg() + assert cfg.settings == defaults.DEFAULT_SETTINGS + + def test_load_empty_pyproject_toml_and_cz_toml_with_config(_, tmpdir): + with tmpdir.as_cwd(): + p = tmpdir.join("pyproject.toml") + p.write("") + p = tmpdir.join(".cz.toml") + p.write(PYPROJECT) + + cfg = config.read_cfg() + assert cfg.settings == _settings class TestTomlConfig: diff --git a/tests/test_git.py b/tests/test_git.py index 0fa22df12e..92abc2f040 100644 --- a/tests/test_git.py +++ b/tests/test_git.py @@ -1,13 +1,7 @@ import pytest from commitizen import git -from tests.utils import create_file_and_commit - - -class FakeCommand: - def __init__(self, out=None, err=None): - self.out = out - self.err = err +from tests.utils import FakeCommand, create_file_and_commit def test_git_object_eq(): diff --git a/tests/utils.py b/tests/utils.py index 64598b8df1..7f5b2b87f3 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -5,6 +5,13 @@ from commitizen import cmd, git +class FakeCommand: + def __init__(self, out=None, err=None, return_code=0): + self.out = out + self.err = err + self.return_code = return_code + + def create_file_and_commit(message: str, filename: Optional[str] = None): if not filename: filename = str(uuid.uuid4())