Skip to content

Commit

Permalink
feat(commit): implement questions 'filter' support with handlers
Browse files Browse the repository at this point in the history
Supported APIs:
  - multiple_line_breaker
  - required_validator
  - required_validator_scope
  - required_validator_subject_strip
  - required_validator_title_strip

Example YAML configurations:
---
commitizen:
  name: cz_customize
  customize:
    questions:
      - ...
      - type: input
        name: scope
        message: 'Scope of the change :'
        filter: 'required_validator_scope'
        default: ''
      - type: input
        name: subject
        message: 'Title of the commit (starting in lower case and without period) :'
        filter: 'required_validator_subject_strip'
        default: ''
      - type: input
        name: body
        message: 'Additional contextual message (Empty to skip) :'
        default: 'Issue: #...'
        filter: 'multiple_line_breaker'
---

Signed-off-by: Adrian DC <[email protected]>
  • Loading branch information
AdrianDC committed Aug 25, 2024
1 parent 5e08775 commit 968a900
Show file tree
Hide file tree
Showing 4 changed files with 113 additions and 5 deletions.
26 changes: 25 additions & 1 deletion commitizen/commands/commit.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,14 @@
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,
required_validator,
required_validator_scope,
required_validator_subject_strip,
required_validator_title_strip,
)
from commitizen.exceptions import (
CommitError,
CommitMessageLengthExceededError,
Expand Down Expand Up @@ -52,6 +59,23 @@ def prompt_commit_questions(self) -> str:

for question in filter(lambda q: q["type"] == "list", questions):
question["use_shortcuts"] = self.config.settings["use_shortcuts"]

for question in filter(
lambda q: isinstance(q.get("filter", None), str), questions
):
if question["filter"] == "multiple_line_breaker":
question["filter"] = multiple_line_breaker
elif question["filter"] == "required_validator":
question["filter"] = required_validator

Check warning on line 69 in commitizen/commands/commit.py

View check run for this annotation

Codecov / codecov/patch

commitizen/commands/commit.py#L69

Added line #L69 was not covered by tests
elif question["filter"] == "required_validator_scope":
question["filter"] = required_validator_scope

Check warning on line 71 in commitizen/commands/commit.py

View check run for this annotation

Codecov / codecov/patch

commitizen/commands/commit.py#L71

Added line #L71 was not covered by tests
elif question["filter"] == "required_validator_subject_strip":
question["filter"] = required_validator_subject_strip
elif question["filter"] == "required_validator_title_strip":
question["filter"] = required_validator_title_strip

Check warning on line 75 in commitizen/commands/commit.py

View check run for this annotation

Codecov / codecov/patch

commitizen/commands/commit.py#L74-L75

Added lines #L74 - L75 were not covered by tests
else:
raise NotAllowed(f"Unknown value filter: {question['filter']}")

Check warning on line 77 in commitizen/commands/commit.py

View check run for this annotation

Codecov / codecov/patch

commitizen/commands/commit.py#L77

Added line #L77 was not covered by tests

try:
answers = questionary.prompt(questions, style=cz.style)
except ValueError as err:
Expand Down
12 changes: 12 additions & 0 deletions commitizen/cz/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,18 @@ def required_validator(answer, msg=None):
return answer


def required_validator_scope(answer, msg="! Error: Scope is required"):
return required_validator(answer, msg)

Check warning on line 16 in commitizen/cz/utils.py

View check run for this annotation

Codecov / codecov/patch

commitizen/cz/utils.py#L16

Added line #L16 was not covered by tests


def required_validator_subject_strip(answer, msg="! Error: Subject is required"):
return required_validator(answer.strip(".").strip(), msg)

Check warning on line 20 in commitizen/cz/utils.py

View check run for this annotation

Codecov / codecov/patch

commitizen/cz/utils.py#L20

Added line #L20 was not covered by tests


def required_validator_title_strip(answer, msg="! Error: Title is required"):
return required_validator(answer.strip(".").strip(), msg)

Check warning on line 24 in commitizen/cz/utils.py

View check run for this annotation

Codecov / codecov/patch

commitizen/cz/utils.py#L24

Added line #L24 was not covered by tests


def multiple_line_breaker(answer, sep="|"):
return "\n".join(line.strip() for line in answer.split(sep) if line)

Expand Down
2 changes: 1 addition & 1 deletion docs/customization.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 the name of a `commitizen.cz.utils.NAME(answer...)` function like `multiple_line_breaker` |
[different-question-types]: https://github.com/tmbo/questionary#different-question-types

#### Shortcut keys
Expand Down
78 changes: 75 additions & 3 deletions tests/test_cz_customize.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
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, required_validator_subject_strip
from commitizen.exceptions import MissingCzCustomizeConfigError

TOML_STR = r"""
Expand Down Expand Up @@ -36,10 +39,17 @@
]
message = "Select the type of change you are committing"
[[tool.commitizen.customize.questions]]
type = "input"
name = "subject"
message = "Subject."
filter = "required_validator_subject_strip"
[[tool.commitizen.customize.questions]]
type = "input"
name = "message"
message = "Body."
filter = "multiple_line_breaker"
[[tool.commitizen.customize.questions]]
type = "confirm"
Expand Down Expand Up @@ -89,10 +99,17 @@
],
"message": "Select the type of change you are committing"
},
{
"type": "input",
"name": "subject",
"message": "Subject.",
"filter": "required_validator_subject_strip"
},
{
"type": "input",
"name": "message",
"message": "Body."
"message": "Body.",
"filter": "multiple_line_breaker"
},
{
"type": "confirm",
Expand Down Expand Up @@ -139,9 +156,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: required_validator_subject_strip
- type: input
name: message
message: Body.
filter: multiple_line_breaker
- type: confirm
name: show_message
message: Do you want to add body message in commit?
Expand Down Expand Up @@ -330,6 +352,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"),
Expand Down Expand Up @@ -437,7 +466,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 = [
Expand All @@ -450,7 +479,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": "required_validator_subject_strip",
},
{
"type": "input",
"name": "message",
"message": "Body.",
"filter": "multiple_line_breaker",
},
{
"type": "confirm",
"name": "show_message",
Expand All @@ -460,6 +500,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 prompts_questions[1]["filter"] == required_validator_subject_strip
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()
Expand Down

0 comments on commit 968a900

Please sign in to comment.