diff --git a/commitizen/commands/commit.py b/commitizen/commands/commit.py index 4a52a96d4..7f8e4fa5b 100644 --- a/commitizen/commands/commit.py +++ b/commitizen/commands/commit.py @@ -8,7 +8,10 @@ from commitizen import factory, git, out from commitizen.config import BaseConfig from commitizen.cz.exceptions import CzException -from commitizen.cz.utils import get_backup_file_path +from commitizen.cz.utils import ( + get_backup_file_path, + multiple_line_breaker, +) from commitizen.exceptions import ( CommitError, CommitMessageLengthExceededError, @@ -52,6 +55,19 @@ def prompt_commit_questions(self) -> str: for question in filter(lambda q: q["type"] == "list", questions): question["use_shortcuts"] = self.config.settings["use_shortcuts"] + + # Import allowed modules for 'filter' + global commitizen + import commitizen.cz.utils + + for question in filter( + lambda q: isinstance(q.get("filter", None), str), questions + ): + question_filter = [ + multiple_line_breaker(question["filter"].replace("\\n", "\n")) + ] + question["filter"] = eval("\n".join(question_filter)) + try: answers = questionary.prompt(questions, style=cz.style) except ValueError as err: diff --git a/docs/customization.md b/docs/customization.md index 3e9ae7f50..590db86d0 100644 --- a/docs/customization.md +++ b/docs/customization.md @@ -175,7 +175,7 @@ commitizen: | `message` | `str` | `None` | Detail description for the question. | | `choices` | `list` | `None` | (OPTIONAL) The choices when `type = list`. Either use a list of values or a list of dictionaries with `name` and `value` keys. Keyboard shortcuts can be defined via `key`. See examples above. | | `default` | `Any` | `None` | (OPTIONAL) The default value for this question. | -| `filter` | `str` | `None` | (Optional) Validator for user's answer. **(Work in Progress)** | +| `filter` | `str` | `None` | (OPTIONAL) Validator for user's answer. The string is evaluated into a Python function, either use `commitizen.cz.utils.*` or lambda functions like `lambda text: text.strip(".").strip()` | [different-question-types]: https://github.com/tmbo/questionary#different-question-types #### Shortcut keys diff --git a/tests/test_cz_customize.py b/tests/test_cz_customize.py index cd55e0f8f..7f3e32877 100644 --- a/tests/test_cz_customize.py +++ b/tests/test_cz_customize.py @@ -1,7 +1,12 @@ +from types import LambdaType + import pytest +from pytest_mock import MockFixture +from commitizen import cmd, commands from commitizen.config import BaseConfig, JsonConfig, TomlConfig, YAMLConfig from commitizen.cz.customize import CustomizeCommitsCz +from commitizen.cz.utils import multiple_line_breaker from commitizen.exceptions import MissingCzCustomizeConfigError TOML_STR = r""" @@ -36,10 +41,17 @@ ] message = "Select the type of change you are committing" + [[tool.commitizen.customize.questions]] + type = "input" + name = "subject" + message = "Subject." + filter = "lambda text: commitizen.cz.utils.required_validator(text.strip(\".\").strip(), msg=\"! Error: Subject is required\")" + [[tool.commitizen.customize.questions]] type = "input" name = "message" message = "Body." + filter = "commitizen.cz.utils.multiple_line_breaker" [[tool.commitizen.customize.questions]] type = "confirm" @@ -89,10 +101,17 @@ ], "message": "Select the type of change you are committing" }, + { + "type": "input", + "name": "subject", + "message": "Subject.", + "filter": "lambda text: commitizen.cz.utils.required_validator(text.strip(\".\").strip(), msg=\"! Error: Subject is required\")" + }, { "type": "input", "name": "message", - "message": "Body." + "message": "Body.", + "filter": "commitizen.cz.utils.multiple_line_breaker" }, { "type": "confirm", @@ -139,9 +158,14 @@ - value: bug fix name: 'bug fix: A bug fix.' message: Select the type of change you are committing + - type: input + name: subject + message: Subject. + filter: 'lambda text: commitizen.cz.utils.required_validator(text.strip(".").strip(), msg="! Error: Subject is required")' - type: input name: message message: Body. + filter: 'commitizen.cz.utils.multiple_line_breaker' - type: confirm name: show_message message: Do you want to add body message in commit? @@ -330,6 +354,13 @@ """ +@pytest.fixture +def staging_is_clean(mocker: MockFixture, tmp_git_project): + is_staging_clean_mock = mocker.patch("commitizen.git.is_staging_clean") + is_staging_clean_mock.return_value = False + return tmp_git_project + + @pytest.fixture( params=[ TomlConfig(data=TOML_STR, path="not_exist.toml"), @@ -437,7 +468,7 @@ def test_change_type_order_unicode(config_with_unicode): ] -def test_questions(config): +def test_questions_default(config): cz = CustomizeCommitsCz(config) questions = cz.questions() expected_questions = [ @@ -450,7 +481,18 @@ def test_questions(config): ], "message": "Select the type of change you are committing", }, - {"type": "input", "name": "message", "message": "Body."}, + { + "type": "input", + "name": "subject", + "message": "Subject.", + "filter": 'lambda text: commitizen.cz.utils.required_validator(text.strip(".").strip(), msg="! Error: Subject is required")', + }, + { + "type": "input", + "name": "message", + "message": "Body.", + "filter": "commitizen.cz.utils.multiple_line_breaker", + }, { "type": "confirm", "name": "show_message", @@ -460,6 +502,38 @@ def test_questions(config): assert list(questions) == expected_questions +@pytest.mark.usefixtures("staging_is_clean") +def test_questions_filter(config, mocker: MockFixture): + is_staging_clean_mock = mocker.patch("commitizen.git.is_staging_clean") + is_staging_clean_mock.return_value = False + + prompt_mock = mocker.patch("questionary.prompt") + prompt_mock.return_value = { + "change_type": "feature", + "subject": "user created", + "message": "body of the commit", + "show_message": True, + } + + commit_mock = mocker.patch("commitizen.git.commit") + commit_mock.return_value = cmd.Command("success", "", b"", b"", 0) + + commands.Commit(config, {})() + + prompts_questions = prompt_mock.call_args[0][0] + assert prompts_questions[0]["type"] == "list" + assert prompts_questions[0]["name"] == "change_type" + assert prompts_questions[0]["use_shortcuts"] is False + assert prompts_questions[1]["type"] == "input" + assert prompts_questions[1]["name"] == "subject" + assert type(prompts_questions[1]["filter"]) is LambdaType + assert prompts_questions[2]["type"] == "input" + assert prompts_questions[2]["name"] == "message" + assert prompts_questions[2]["filter"] == multiple_line_breaker + assert prompts_questions[3]["type"] == "confirm" + assert prompts_questions[3]["name"] == "show_message" + + def test_questions_unicode(config_with_unicode): cz = CustomizeCommitsCz(config_with_unicode) questions = cz.questions()