diff --git a/commitizen/cz/__init__.py b/commitizen/cz/__init__.py index 88e936d0c6..5951b722d1 100644 --- a/commitizen/cz/__init__.py +++ b/commitizen/cz/__init__.py @@ -3,8 +3,13 @@ from commitizen.cz.conventional_commits import ConventionalCommitsCz from commitizen.cz.jira import JiraSmartCz +from commitizen.cz.customize import CustomizeCommitsCz -registry = {"cz_conventional_commits": ConventionalCommitsCz, "cz_jira": JiraSmartCz} +registry = { + "cz_conventional_commits": ConventionalCommitsCz, + "cz_jira": JiraSmartCz, + "cz_customize": CustomizeCommitsCz, +} plugins = { name: importlib.import_module(name).discover_this for finder, name, ispkg in pkgutil.iter_modules() diff --git a/commitizen/cz/customize/__init__.py b/commitizen/cz/customize/__init__.py new file mode 100644 index 0000000000..c5af001a79 --- /dev/null +++ b/commitizen/cz/customize/__init__.py @@ -0,0 +1 @@ +from .customize import CustomizeCommitsCz # noqa diff --git a/commitizen/cz/customize/customize.py b/commitizen/cz/customize/customize.py new file mode 100644 index 0000000000..e57caec11e --- /dev/null +++ b/commitizen/cz/customize/customize.py @@ -0,0 +1,45 @@ +from commitizen import defaults +from commitizen.cz.base import BaseCommitizen + +__all__ = ["CustomizeCommitsCz"] + + +class CustomizeCommitsCz(BaseCommitizen): + bump_pattern = defaults.bump_pattern + bump_map = defaults.bump_map + + def __init__(self, config: dict): + super(CustomizeCommitsCz, self).__init__(config) + self.custom_config = self.config.get("customize") + + custom_bump_pattern = self.custom_config.get("bump_pattern") + if custom_bump_pattern: + self.bump_pattern = custom_bump_pattern + + custom_bump_map = self.custom_config.get("bump_map") + if custom_bump_map: + self.bump_map = custom_bump_map + + def questions(self) -> list: + return self.custom_config.get("questions") + + def message(self, answers: dict) -> str: + message_template = self.custom_config.get("message_template") + return message_template.format(**answers) + + def example(self) -> str: + return self.custom_config.get("example") + + def schema(self) -> str: + return self.custom_config.get("schema") + + def info(self) -> str: + info_path = self.custom_config.get("info_path") + info = self.custom_config.get("info") + if info_path: + with open(info_path, "r") as f: + content = f.read() + return content + elif info: + return info + raise NotImplementedError("Not Implemented yet") diff --git a/commitizen/cz/customize/customize_info.txt b/commitizen/cz/customize/customize_info.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/config.md b/docs/config.md index dcbb218e86..e4852de2b3 100644 --- a/docs/config.md +++ b/docs/config.md @@ -65,3 +65,4 @@ The extra tab before the square brakets (`]`) at the end is required. | `tag_format` | `str` | `None` | Format for the git tag, useful for old projects, that use a convention like `"v1.2.1"`. [See more](https://woile.github.io/commitizen/bump#tag_format) | | `bump_message` | `str` | `None` | Create custom commit message, useful to skip ci. [See more](https://woile.github.io/commitizen/bump#bump_message) | | `style` | `list` | see above | Style for the prompts (It will merge this value with default style.) [See More (Styling your prompts with your favorite colors)](https://github.com/tmbo/questionary#additional-features) | +| `customize` | `dict` | `None` | **This is only supported when config through `toml`.** Custom rules for committing and bumping. [See more](https://woile.github.io/commitizen/customization/) | diff --git a/docs/customization.md b/docs/customization.md index f863d9fae3..5e0120f0b0 100644 --- a/docs/customization.md +++ b/docs/customization.md @@ -1,5 +1,7 @@ Customizing commitizen is not hard at all. +## Customize through customize class + The basic steps are: 1. Inheriting from `BaseCommitizen` @@ -9,7 +11,7 @@ The basic steps are: Check an [example](convcomms) on how to configure `BaseCommitizen`. -## Custom commit rules +### Custom commit rules Create a file starting with `cz_` for example `cz_jira.py`. This prefix is used to detect the plugin. Same method [flask uses] @@ -95,7 +97,7 @@ If you feel like it should be part of this repo, create a PR. [flask uses]: http://flask.pocoo.org/docs/0.12/extensiondev/ -## Custom bump rules +### Custom bump rules You need to define 2 parameters inside `BaseCommitizen`. @@ -123,7 +125,7 @@ cz -n cz_strange bump [convcomms]: https://github.com/Woile/commitizen/blob/master/commitizen/cz/conventional_commits/conventional_commits.py -## Raise Customize Exception +### Raise Customize Exception If you wannt `commitizen` to catch your exception and print the message, you'll have to inherit `CzException`. @@ -133,3 +135,64 @@ from commitizen.cz.exception import CzException class NoSubjectProvidedException(CzException): ... ``` + +## Customize in toml + +**This is only supported when configuring through `toml` (e.g., `pyproject.toml`, `.cz`, and `.cz.toml`)** + +The basic steps are: +1. Define your custom committing or bumpping rules in the configuration file. +2. Declare `name = "cz_customize"` in your configuration file, or add `-n cz_customize` when running commitizen. + +Example: + +```toml +[tool.commitizen] +name = "cz_customize" + +[tool.commitizen.customize] +message_template = "{change_type}: {message}" +example = "feature: this feature eanable customize through config file" +schema = ": " +bump_pattern = "^(break|new|fix|hotfix)" +bump_map = {"break" = "MAJOR", "new" = "MINOR", "fix" = "PATCH", "hotfix" = "PATCH"} +info_path = "cz_customize_info.txt" +info = """ +This is customized info +""" + +[[tool.commitizen.customize.questions]] +type = "list" +name = "change_type" +choices = ["feature", "bug fix"] +message = "Select the type of change you are committing" + +[[tool.commitizen.customize.questions]] +type = "input" +name = "message" +message = "Body." +``` + +### Customize configuration + +| Parameter | Type | Default | Description | +| --------- | ---- | ------- | ----------- | +| `question` | `dict` | `None` | Questions regarding the commit message. Detatiled below. | +| `message_template` | `str` | `None` | The template for generating message from the given answers. `message_template` should follow the python string formatting specification, and all the variables in this template should be defined in `name` in `questions`. | +| `example` | `str` | `None` | (OPTIONAL) Provide an example to help understand the style. Used by `cz example`. | +| `schema` | `str` | `None` | (OPTIONAL) Show the schema used. Used by `cz schema`. | +| `info_path` | `str` | `None` | (OPTIONAL) The path to the file that contains explanation of the commit rules. Used by `cz info`. If not provided `cz info`, will load `info` instead. | +| `info` | `str` | `None` | (OPTIONAL) Explanation of the commit rules. Used by `cz info`. | +| `bump_map` | `dict` | `None` | (OPTIONAL) Dictionary mapping the extracted information to a `SemVer` increment type (`MAJOR`, `MINOR`, `PATCH`) | +| `bump_pattern` | `str` | `None` | (OPTIONAL) Regex to extract information from commit (subject and body) | + +#### Detailed `question` content + +| Parameter | Type | Default | Description | +| --------- | ---- | ------- | ----------- | +| `type` | `str` | `None` | The type of questions. Valid type: `list`, `input` and etc. [See More](https://github.com/tmbo/questionary#different-question-types) | +| `name` | `str` | `None` | The key for the value answered by user. It's used in `message_template` | +| `message` | `str` | `None` | Detail description for the question. | +| `choices` | `list` | `None` | (OPTIONAL) The choices when `type = choice`. It should be list of dictionaries with `name` and `value`. (e.g., `[{value = "feature", name = "feature: A new feature."}, {value = "bug fix", name = "bug fix: A bug fix."}]`) | +| `default` | `Any` | `None` | (OPTIONAL) The default value for this question. | +| `filter` | `str` | `None` | (Optional) Validator for user's answer. **(Work in Progress)** | diff --git a/tests/test_cz_customize.py b/tests/test_cz_customize.py new file mode 100644 index 0000000000..ba034837c0 --- /dev/null +++ b/tests/test_cz_customize.py @@ -0,0 +1,94 @@ +import pytest +from tomlkit import parse + +from commitizen.cz.customize import CustomizeCommitsCz +from commitizen.config import Config + + +@pytest.fixture(scope="module") +def config(): + _conf = Config() + toml_str = """ + [tool.commitizen.customize] + # message_template should follow the python string formatting spec + message_template = "{change_type}: {message}" + example = "feature: this feature eanable customize through config file" + schema = ": " + bump_pattern = "^(break|new|fix|hotfix)" + bump_map = {"break" = "MAJOR", "new" = "MINOR", "fix" = "PATCH", "hotfix" = "PATCH"} + info = "This is a customized cz." + + [[tool.commitizen.customize.questions]] + type = "list" + name = "change_type" + choices = [ + {value = "feature", name = "feature: A new feature."}, + {value = "bug fix", name = "bug fix: A bug fix."} + ] + message = "Select the type of change you are committing" + + [[tool.commitizen.customize.questions]] + type = "input" + name = "message" + message = "Body." + """ + _conf.update(parse(toml_str)["tool"]["commitizen"]) + return _conf.config + + +def test_bump_pattern(config): + cz = CustomizeCommitsCz(config) + assert cz.bump_pattern == "^(break|new|fix|hotfix)" + + +def test_bump_map(config): + cz = CustomizeCommitsCz(config) + assert cz.bump_map == { + "break": "MAJOR", + "new": "MINOR", + "fix": "PATCH", + "hotfix": "PATCH", + } + + +def test_questions(config): + cz = CustomizeCommitsCz(config) + questions = cz.questions() + expected_questions = [ + { + "type": "list", + "name": "change_type", + "choices": [ + {"value": "feature", "name": "feature: A new feature."}, + {"value": "bug fix", "name": "bug fix: A bug fix."}, + ], + "message": "Select the type of change you are committing", + }, + {"type": "input", "name": "message", "message": "Body."}, + ] + assert list(questions) == expected_questions + + +def test_answer(config): + cz = CustomizeCommitsCz(config) + answers = { + "change_type": "feature", + "message": "this feature eanable customize through config file", + } + message = cz.message(answers) + assert message == "feature: this feature eanable customize through config file" + + +def test_example(config): + cz = CustomizeCommitsCz(config) + assert "feature: this feature eanable customize through config file" in cz.example() + + +def test_schema(config): + cz = CustomizeCommitsCz(config) + assert ": " in cz.schema() + + +def test_info(config): + cz = CustomizeCommitsCz(config) + assert "This is a customized cz." in cz.info()