diff --git a/commitizen/cli.py b/commitizen/cli.py index a442803d7f..cf3d6c5eef 100644 --- a/commitizen/cli.py +++ b/commitizen/cli.py @@ -92,6 +92,10 @@ def __call__( ), "formatter_class": argparse.RawDescriptionHelpFormatter, "arguments": [ + { + "name": "--config", + "help": "the path of configuration file", + }, {"name": "--debug", "action": "store_true", "help": "use debug mode"}, { "name": ["-n", "--name"], @@ -534,9 +538,7 @@ def parse_no_raise(comma_separated_no_raise: str) -> list[int]: def main(): - conf = config.read_cfg() parser = cli(data) - argcomplete.autocomplete(parser) # Show help if no arg provided if len(sys.argv) == 1: @@ -576,6 +578,11 @@ def main(): extra_args = " ".join(unknown_args[1:]) arguments["extra_cli_args"] = extra_args + if args.config: + conf = config.read_cfg(args.config) + else: + conf = config.read_cfg() + if args.name: conf.update({"name": args.name}) elif not args.name and not conf.path: diff --git a/commitizen/config/__init__.py b/commitizen/config/__init__.py index 09e38ca96e..a9395fca7d 100644 --- a/commitizen/config/__init__.py +++ b/commitizen/config/__init__.py @@ -3,6 +3,7 @@ from pathlib import Path from commitizen import defaults, git +from commitizen.exceptions import ConfigFileNotFound, ConfigFileIsEmpty from .base_config import BaseConfig from .json_config import JsonConfig @@ -10,19 +11,26 @@ from .yaml_config import YAMLConfig -def read_cfg() -> BaseConfig: +def read_cfg(filepath: str | None = None) -> BaseConfig: conf = BaseConfig() - git_project_root = git.find_git_project_root() - cfg_search_paths = [Path(".")] - if git_project_root: - cfg_search_paths.append(git_project_root) + if filepath is not None: + if not Path(filepath).exists(): + raise ConfigFileNotFound() + + cfg_paths = (path for path in (Path(filepath),)) + else: + git_project_root = git.find_git_project_root() + cfg_search_paths = [Path(".")] + if git_project_root: + cfg_search_paths.append(git_project_root) + + cfg_paths = ( + path / Path(filename) + for path in cfg_search_paths + for filename in defaults.config_files + ) - cfg_paths = ( - path / Path(filename) - for path in cfg_search_paths - for filename in defaults.config_files - ) for filename in cfg_paths: if not filename.exists(): continue @@ -39,7 +47,9 @@ def read_cfg() -> BaseConfig: elif "yaml" in filename.suffix: _conf = YAMLConfig(data=data, path=filename) - if _conf.is_empty_config: + if filepath is not None and _conf.is_empty_config: + raise ConfigFileIsEmpty() + elif _conf.is_empty_config: continue else: conf = _conf diff --git a/commitizen/exceptions.py b/commitizen/exceptions.py index 31b867b8f9..9cb1517680 100644 --- a/commitizen/exceptions.py +++ b/commitizen/exceptions.py @@ -34,6 +34,8 @@ class ExitCode(enum.IntEnum): VERSION_PROVIDER_UNKNOWN = 27 VERSION_SCHEME_UNKNOWN = 28 CHANGELOG_FORMAT_UNKNOWN = 29 + CONFIG_FILE_NOT_FOUND = 30 + CONFIG_FILE_IS_EMPTY = 31 class CommitizenException(Exception): @@ -189,3 +191,13 @@ class VersionSchemeUnknown(CommitizenException): class ChangelogFormatUnknown(CommitizenException): exit_code = ExitCode.CHANGELOG_FORMAT_UNKNOWN message = "Unknown changelog format identifier" + + +class ConfigFileNotFound(CommitizenException): + exit_code = ExitCode.CONFIG_FILE_NOT_FOUND + message = "Cannot found the config file, please check your file path again." + + +class ConfigFileIsEmpty(CommitizenException): + exit_code = ExitCode.CONFIG_FILE_IS_EMPTY + message = "Config file is empty, please check your file path again." diff --git a/docs/README.md b/docs/README.md index e48062e168..8c7dc51e48 100644 --- a/docs/README.md +++ b/docs/README.md @@ -107,6 +107,7 @@ For more information about the topic go to https://conventionalcommits.org/ optional arguments: -h, --help show this help message and exit + --config the path of configuration file --debug use debug mode -n NAME, --name NAME use the given commitizen (default: cz_conventional_commits) -nr NO_RAISE, --no-raise NO_RAISE diff --git a/tests/test_cli.py b/tests/test_cli.py index 93f6c16ddd..345f0b1b00 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -12,6 +12,7 @@ NoCommandFoundError, NotAGitProjectError, InvalidCommandArgumentError, + ConfigFileNotFound, ) @@ -25,6 +26,15 @@ def test_sysexit_no_argv(mocker: MockFixture, capsys): assert out.startswith("usage") +def test_cz_config_file_without_correct_file_path(mocker: MockFixture, capsys): + testargs = ["cz", "--config", "./config/pyproject.toml", "example"] + mocker.patch.object(sys, "argv", testargs) + + with pytest.raises(ConfigFileNotFound) as excinfo: + cli.main() + assert "Cannot found the config file" in str(excinfo.value) + + def test_cz_with_arg_but_without_command(mocker: MockFixture): testargs = ["cz", "--name", "cz_jira"] mocker.patch.object(sys, "argv", testargs) diff --git a/tests/test_conf.py b/tests/test_conf.py index a12bcdd35d..786f12b36b 100644 --- a/tests/test_conf.py +++ b/tests/test_conf.py @@ -9,7 +9,7 @@ import yaml from commitizen import config, defaults, git -from commitizen.exceptions import InvalidConfigurationError +from commitizen.exceptions import InvalidConfigurationError, ConfigFileIsEmpty PYPROJECT = """ [tool.commitizen] @@ -44,6 +44,27 @@ } } +JSON_STR = r""" + { + "commitizen": { + "name": "cz_jira", + "version": "1.0.0", + "version_files": [ + "commitizen/__version__.py", + "pyproject.toml" + ] + } + } +""" + +YAML_STR = """ +commitizen: + name: cz_jira + version: 1.0.0 + version_files: + - commitizen/__version__.py + - pyproject.toml +""" _settings: dict[str, Any] = { "name": "cz_jira", @@ -158,6 +179,40 @@ def test_load_empty_pyproject_toml_and_cz_toml_with_config(_, tmpdir): cfg = config.read_cfg() assert cfg.settings == _settings + def test_load_pyproject_toml_from_config_argument(_, tmpdir): + with tmpdir.as_cwd(): + _not_root_path = tmpdir.mkdir("not_in_root").join("pyproject.toml") + _not_root_path.write(PYPROJECT) + + cfg = config.read_cfg(filepath="./not_in_root/pyproject.toml") + assert cfg.settings == _settings + + def test_load_cz_json_not_from_config_argument(_, tmpdir): + with tmpdir.as_cwd(): + _not_root_path = tmpdir.mkdir("not_in_root").join(".cz.json") + _not_root_path.write(JSON_STR) + + cfg = config.read_cfg(filepath="./not_in_root/.cz.json") + json_cfg_by_class = config.JsonConfig(data=JSON_STR, path=_not_root_path) + assert cfg.settings == json_cfg_by_class.settings + + def test_load_cz_yaml_not_from_config_argument(_, tmpdir): + with tmpdir.as_cwd(): + _not_root_path = tmpdir.mkdir("not_in_root").join(".cz.yaml") + _not_root_path.write(YAML_STR) + + cfg = config.read_cfg(filepath="./not_in_root/.cz.yaml") + yaml_cfg_by_class = config.YAMLConfig(data=YAML_STR, path=_not_root_path) + assert cfg.settings == yaml_cfg_by_class._settings + + def test_load_empty_pyproject_toml_from_config_argument(_, tmpdir): + with tmpdir.as_cwd(): + _not_root_path = tmpdir.mkdir("not_in_root").join("pyproject.toml") + _not_root_path.write("") + + with pytest.raises(ConfigFileIsEmpty): + config.read_cfg(filepath="./not_in_root/pyproject.toml") + class TestTomlConfig: def test_init_empty_config_content(self, tmpdir):