Skip to content
  • Sponsor commitizen-tools/commitizen

  • Notifications You must be signed in to change notification settings
  • Fork 289

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
@@ -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,
@@ -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:
4 changes: 2 additions & 2 deletions commitizen/cz/conventional_commits/conventional_commits.py
Original file line number Diff line number Diff line change
@@ -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"]
@@ -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",
25 changes: 23 additions & 2 deletions commitizen/cz/utils.py
Original file line number Diff line number Diff line change
@@ -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)


37 changes: 19 additions & 18 deletions docs/customization.md
Original file line number Diff line number Diff line change
@@ -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
@@ -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:
@@ -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

62 changes: 62 additions & 0 deletions tests/commands/test_commit_command.py
Original file line number Diff line number Diff line change
@@ -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")
Loading