Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 27 additions & 2 deletions commitizen/commands/commit.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,14 @@

from commitizen import factory, git, out
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 @@ -69,9 +76,27 @@ def _read_backup_message(self) -> str | None:

def _get_message_by_prompt_commit_questions(self) -> str:
# Prompt user for the commit message
questions = self.cz.questions()
questions = [dict(question) for question in self.cz.questions()]

for question in (q for q in questions if q["type"] == "list"):
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=self.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 @@ -5,7 +5,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

if TYPE_CHECKING:
from collections.abc import Mapping
Expand Down Expand Up @@ -136,7 +136,7 @@ def questions(self) -> list[CzQuestion]:
"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
23 changes: 22 additions & 1 deletion commitizen/cz/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,28 @@ def required_validator(answer: str, msg: object = None) -> str:
return answer


def multiple_line_breaker(answer: str, sep: str = "|") -> str:
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
2 changes: 1 addition & 1 deletion docs/customization/config_file.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ Example:
| `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)** |
| `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`. |
| `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. |
Expand Down
152 changes: 146 additions & 6 deletions tests/test_cz_customize.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,32 @@
from pathlib import Path

import pytest
from pytest_mock import MockFixture

from commitizen import cmd, commands
from commitizen.config import BaseConfig
from commitizen.config.json_config import JsonConfig
from commitizen.config.toml_config import TomlConfig
from commitizen.config.yaml_config import YAMLConfig
from commitizen.cz.customize import CustomizeCommitsCz
from commitizen.exceptions import MissingCzCustomizeConfigError
from commitizen.cz.utils import (
break_multiple_line,
required_validator,
required_validator_scope,
required_validator_subject_strip,
required_validator_title_strip,
)
from commitizen.exceptions import MissingCzCustomizeConfigError, NotAllowed

TOML_STR = r"""
[tool.commitizen]
name = "cz_customize"
version = "1.0.0"
version_files = [
"commitizen/__version__.py",
"pyproject.toml"
]

[tool.commitizen.customize]
message_template = "{{change_type}}:{% if show_message %} {{message}}{% endif %}"
example = "feature: this feature enables customization through a config file"
Expand All @@ -34,10 +51,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 = "break_multiple_line"

[[tool.commitizen.customize.questions]]
type = "confirm"
Expand All @@ -48,7 +72,7 @@
JSON_STR = r"""
{
"commitizen": {
"name": "cz_jira",
"name": "cz_customize",
"version": "1.0.0",
"version_files": [
"commitizen/__version__.py",
Expand Down Expand Up @@ -87,10 +111,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": "break_multiple_line"
},
{
"type": "confirm",
Expand All @@ -105,7 +136,7 @@

YAML_STR = """
commitizen:
name: cz_jira
name: cz_customize
version: 1.0.0
version_files:
- commitizen/__version__.py
Expand Down Expand Up @@ -137,9 +168,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: break_multiple_line
- type: confirm
name: show_message
message: Do you want to add body message in commit?
Expand Down Expand Up @@ -328,6 +364,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=Path("not_exist.toml")),
Expand All @@ -344,6 +387,15 @@ def config(request):
return request.param


@pytest.fixture(
params=[
YAMLConfig(data=YAML_STR, path=Path("not_exist.yaml")),
]
)
def config_filters(request):
return request.param


@pytest.fixture(
params=[
TomlConfig(data=TOML_STR_INFO_PATH, path=Path("not_exist.toml")),
Expand Down Expand Up @@ -516,7 +568,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 @@ -529,7 +581,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": "break_multiple_line",
},
{
"type": "confirm",
"name": "show_message",
Expand All @@ -539,6 +602,83 @@ def test_questions(config):
assert list(questions) == expected_questions


@pytest.mark.usefixtures("staging_is_clean")
def test_questions_filter_default(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"] == break_multiple_line
assert prompts_questions[3]["type"] == "confirm"
assert prompts_questions[3]["name"] == "show_message"


@pytest.mark.usefixtures("staging_is_clean")
def test_questions_filter_values(config_filters, 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)

commit_cmd = commands.Commit(config_filters, {})

assert isinstance(commit_cmd.cz, CustomizeCommitsCz)

for filter_desc in [
("break_multiple_line", break_multiple_line),
("required_validator", required_validator),
("required_validator_scope", required_validator_scope),
("required_validator_subject_strip", required_validator_subject_strip),
("required_validator_title_strip", required_validator_title_strip),
]:
commit_cmd.cz.custom_settings["questions"][1]["filter"] = filter_desc[0] # type: ignore[index]
commit_cmd()

assert filter_desc[1]("input")

prompts_questions = prompt_mock.call_args[0][0]
assert prompts_questions[1]["filter"] == filter_desc[1]

for filter_name in [
"",
"faulty_value",
]:
commit_cmd.cz.custom_settings["questions"][1]["filter"] = filter_name # type: ignore[index]

with pytest.raises(NotAllowed):
commit_cmd()


def test_questions_unicode(config_with_unicode):
cz = CustomizeCommitsCz(config_with_unicode)
questions = cz.questions()
Expand Down
6 changes: 3 additions & 3 deletions tests/test_cz_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ def test_required_validator():
utils.required_validator("")


def test_multiple_line_breaker():
def test_break_multiple_line():
message = "this is the first line | and this is the second line "
result = utils.multiple_line_breaker(message)
result = utils.break_multiple_line(message)
assert result == "this is the first line\nand this is the second line"

result = utils.multiple_line_breaker(message, "is")
result = utils.break_multiple_line(message, "is")
assert result == "th\n\nthe first line | and th\n\nthe second line"


Expand Down
Loading