Skip to content

feat(commit): implement questions 'filter' support with handlers #1207

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
29 changes: 27 additions & 2 deletions commitizen/commands/commit.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,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 (
break_multiple_line,
get_backup_file_path,
required_validator,
required_validator_scope,
required_validator_subject_strip,
required_validator_title_strip,
)
from commitizen.exceptions import (
CommitError,
CommitMessageLengthExceededError,
Expand Down Expand Up @@ -51,9 +58,27 @@ def read_backup_message(self) -> str | None:
def prompt_commit_questions(self) -> str:
# Prompt user for the commit message
cz = self.cz
questions = cz.questions()
questions = [dict(question) for question in cz.questions()]

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"] == "break_multiple_line":
question["filter"] = break_multiple_line
elif question["filter"] == "required_validator":
question["filter"] = required_validator
elif question["filter"] == "required_validator_scope":
question["filter"] = required_validator_scope
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
else:
raise NotAllowed(f"Unknown value filter: {question['filter']}")

try:
answers = questionary.prompt(questions, style=cz.style)
except ValueError as err:
Expand Down
4 changes: 2 additions & 2 deletions commitizen/cz/conventional_commits/conventional_commits.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

from commitizen import defaults
from commitizen.cz.base import BaseCommitizen
from commitizen.cz.utils import multiple_line_breaker, required_validator
from commitizen.cz.utils import break_multiple_line, required_validator
from commitizen.defaults import Questions

__all__ = ["ConventionalCommitsCz"]
Expand Down Expand Up @@ -129,7 +129,7 @@ def questions(self) -> Questions:
"message": (
"Provide additional contextual information about the code changes: (press [enter] to skip)\n"
),
"filter": multiple_line_breaker,
"filter": break_multiple_line,
},
{
"type": "confirm",
Expand Down
25 changes: 23 additions & 2 deletions commitizen/cz/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,34 @@
from commitizen.cz import exceptions


def required_validator(answer, msg=None):
def required_validator(answer: str, msg=None) -> str:
if not answer:
raise exceptions.AnswerRequiredError(msg)
return answer


def multiple_line_breaker(answer, sep="|"):
def required_validator_scope(
answer: str,
msg: str = "! Error: Scope is required",
) -> str:
return required_validator(answer, msg)


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


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


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


Expand Down
37 changes: 19 additions & 18 deletions docs/customization.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,13 +110,13 @@ And the correspondent example for a yaml file:
commitizen:
name: cz_customize
customize:
message_template: "{{change_type}}:{% if show_message %} {{message}}{% endif %}"
message_template: '{{change_type}}:{% if show_message %} {{message}}{% endif %}'
example: 'feature: this feature enable customize through config file'
schema: "<type>: <body>"
schema_pattern: "(feature|bug fix):(\\s.*)"
bump_pattern: "^(break|new|fix|hotfix)"
commit_parser: "^(?P<change_type>feature|bug fix):\\s(?P<message>.*)?"
changelog_pattern: "^(feature|bug fix)?(!)?"
schema: '<type>: <body>'
schema_pattern: '(feature|bug fix):(\\s.*)'
bump_pattern: '^(break|new|fix|hotfix)'
commit_parser: '^(?P<change_type>feature|bug fix):\\s(?P<message>.*)?'
changelog_pattern: '^(feature|bug fix)?(!)?'
change_type_map:
feature: Feat
bug fix: Fix
Expand All @@ -125,7 +125,7 @@ commitizen:
new: MINOR
fix: PATCH
hotfix: PATCH
change_type_order: ["BREAKING CHANGE", "feat", "fix", "refactor", "perf"]
change_type_order: ['BREAKING CHANGE', 'feat', 'fix', 'refactor', 'perf']
info_path: cz_customize_info.txt
info: This is customized info
questions:
Expand Down Expand Up @@ -168,17 +168,18 @@ commitizen:

#### Detailed `questions` content

| Parameter | Type | Default | Description |
| ----------- | ------ | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `type` | `str` | `None` | The type of questions. Valid types: `list`, `select`, `input`, etc. The `select` type provides an interactive searchable list interface. [See More][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 = list` or `type = select`. 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)** |
| `multiline` | `bool` | `False` | (OPTIONAL) Enable multiline support when `type = input`. |
| `use_search_filter` | `bool` | `False` | (OPTIONAL) Enable search/filter functionality for list/select type questions. This allows users to type and filter through the choices. |
| `use_jk_keys` | `bool` | `True` | (OPTIONAL) Enable/disable j/k keys for navigation in list/select type questions. Set to false if you prefer arrow keys only. |
| Parameter | Type | Default | Description |
| ------------------- | ------ | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `type` | `str` | `None` | The type of questions. Valid types: `list`, `select`, `input`, etc. The `select` type provides an interactive searchable list interface. [See More][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 = list` or `type = select`. 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. The string is the name of a `commitizen.cz.utils.NAME(answer...)` function like `break_multiple_line` |
| `multiline` | `bool` | `False` | (OPTIONAL) Enable multiline support when `type = input`. |
| `multiline` | `bool` | `False` | (OPTIONAL) Enable multiline support when `type = input`. |
| `use_search_filter` | `bool` | `False` | (OPTIONAL) Enable search/filter functionality for list/select type questions. This allows users to type and filter through the choices. |
| `use_jk_keys` | `bool` | `True` | (OPTIONAL) Enable/disable j/k keys for navigation in list/select type questions. Set to false if you prefer arrow keys only. |

[different-question-types]: https://github.com/tmbo/questionary#different-question-types

Expand Down
62 changes: 62 additions & 0 deletions tests/commands/test_commit_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,68 @@ def test_commit_when_nothing_to_commit(config, mocker: MockFixture):
assert "No files added to staging!" in str(excinfo.value)


@pytest.mark.usefixtures("staging_is_clean")
def test_commit_when_nothing_added_to_commit(config, mocker: MockFixture):
prompt_mock = mocker.patch("questionary.prompt")
prompt_mock.return_value = {
"prefix": "feat",
"subject": "user created",
"scope": "",
"is_breaking_change": False,
"body": "",
"footer": "",
}

commit_mock = mocker.patch("commitizen.git.commit")
commit_mock.return_value = cmd.Command(
'nothing added to commit but untracked files present (use "git add" to track)',
"",
b"",
b"",
0,
)

error_mock = mocker.patch("commitizen.out.error")

commands.Commit(config, {"all": False})()

prompt_mock.assert_called_once()
error_mock.assert_called_once()

assert "nothing added" in error_mock.call_args[0][0]


@pytest.mark.usefixtures("staging_is_clean")
def test_commit_when_no_changes_added_to_commit(config, mocker: MockFixture):
prompt_mock = mocker.patch("questionary.prompt")
prompt_mock.return_value = {
"prefix": "feat",
"subject": "user created",
"scope": "",
"is_breaking_change": False,
"body": "",
"footer": "",
}

commit_mock = mocker.patch("commitizen.git.commit")
commit_mock.return_value = cmd.Command(
'no changes added to commit (use "git add" and/or "git commit -a")',
"",
b"",
b"",
0,
)

error_mock = mocker.patch("commitizen.out.error")

commands.Commit(config, {"all": False})()

prompt_mock.assert_called_once()
error_mock.assert_called_once()

assert "no changes added to commit" in error_mock.call_args[0][0]


@pytest.mark.usefixtures("staging_is_clean")
def test_commit_with_allow_empty(config, mocker: MockFixture):
prompt_mock = mocker.patch("questionary.prompt")
Expand Down
Loading