Skip to content
2 changes: 0 additions & 2 deletions commitizen/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,6 @@ def __call__(
{
"name": ["-l", "--message-length-limit"],
"type": int,
"default": 0,
"help": "length limit of the commit message; 0 for no limit",
},
{
Expand Down Expand Up @@ -499,7 +498,6 @@ def __call__(
{
"name": ["-l", "--message-length-limit"],
"type": int,
"default": 0,
"help": "length limit of the commit message; 0 for no limit",
},
],
Expand Down
20 changes: 14 additions & 6 deletions commitizen/commands/check.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from commitizen import factory, git, out
from commitizen.config import BaseConfig
from commitizen.exceptions import (
CommitMessageLengthExceededError,
InvalidCommandArgumentError,
InvalidCommitMessageError,
NoCommitsFoundError,
Expand All @@ -18,7 +19,7 @@ class CheckArgs(TypedDict, total=False):
commit_msg: str
rev_range: str
allow_abort: bool
message_length_limit: int
message_length_limit: int | None
allowed_prefixes: list[str]
message: str
use_default_range: bool
Expand All @@ -42,7 +43,9 @@ def __init__(self, config: BaseConfig, arguments: CheckArgs, *args: object) -> N
arguments.get("allow_abort", config.settings["allow_abort"])
)
self.use_default_range = bool(arguments.get("use_default_range"))
self.max_msg_length = arguments.get("message_length_limit", 0)
self.max_msg_length = arguments.get(
"message_length_limit", config.settings.get("message_length_limit", None)
)

# we need to distinguish between None and [], which is a valid value
allowed_prefixes = arguments.get("allowed_prefixes")
Expand Down Expand Up @@ -88,7 +91,7 @@ def __call__(self) -> None:
invalid_msgs_content = "\n".join(
f'commit "{commit.rev}": "{commit.message}"'
for commit in commits
if not self._validate_commit_message(commit.message, pattern)
if not self._validate_commit_message(commit.message, pattern, commit.rev)
)
if invalid_msgs_content:
# TODO: capitalize the first letter of the error message for consistency in v5
Expand Down Expand Up @@ -151,17 +154,22 @@ def _filter_comments(msg: str) -> str:
return "\n".join(lines)

def _validate_commit_message(
self, commit_msg: str, pattern: re.Pattern[str]
self, commit_msg: str, pattern: re.Pattern[str], commit_hash: str
) -> bool:
if not commit_msg:
return self.allow_abort

if any(map(commit_msg.startswith, self.allowed_prefixes)):
return True

if self.max_msg_length:
if self.max_msg_length is not None:
msg_len = len(commit_msg.partition("\n")[0].strip())
if msg_len > self.max_msg_length:
return False
raise CommitMessageLengthExceededError(
f"commit validation: failed!\n"
f"commit message length exceeds the limit.\n"
f'commit "{commit_hash}": "{commit_msg}"\n'
f"message length limit: {self.max_msg_length} (actual: {msg_len})"
)

return bool(pattern.match(commit_msg))
11 changes: 8 additions & 3 deletions commitizen/commands/commit.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ class CommitArgs(TypedDict, total=False):
dry_run: bool
edit: bool
extra_cli_args: str
message_length_limit: int
message_length_limit: int | None
no_retry: bool
signoff: bool
write_message_to_file: Path | None
Expand Down Expand Up @@ -81,8 +81,13 @@ def _prompt_commit_questions(self) -> str:

message = cz.message(answers)
message_len = len(message.partition("\n")[0].strip())
message_length_limit = self.arguments.get("message_length_limit", 0)
if 0 < message_length_limit < message_len:

message_length_limit = self.arguments.get(
"message_length_limit",
self.config.settings.get("message_length_limit", None),
)

if message_length_limit is not None and message_len > message_length_limit:
raise CommitMessageLengthExceededError(
f"Length of commit message exceeds limit ({message_len}/{message_length_limit})"
)
Expand Down
2 changes: 2 additions & 0 deletions commitizen/defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ class Settings(TypedDict, total=False):
ignored_tag_formats: Sequence[str]
legacy_tag_formats: Sequence[str]
major_version_zero: bool
message_length_limit: int | None
name: str
post_bump_hooks: list[str] | None
pre_bump_hooks: list[str] | None
Expand Down Expand Up @@ -108,6 +109,7 @@ class Settings(TypedDict, total=False):
"always_signoff": False,
"template": None, # default provided by plugin
"extras": {},
"message_length_limit": None, # None for no limit
}

MAJOR = "MAJOR"
Expand Down
8 changes: 8 additions & 0 deletions docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,14 @@ Default: `false`

Disallow empty commit messages, useful in CI. [Read more][allow_abort]

### `message_length_limit`

Type: `int`

Default: `0`

Maximum length of the commit message. Setting it to `0` disables the length limit. It can be overridden by the `-l/--message-length-limit` command line argument.

### `allowed_prefixes`

Type: `list`
Expand Down
62 changes: 61 additions & 1 deletion tests/commands/test_check_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

from commitizen import cli, commands, git
from commitizen.exceptions import (
CommitMessageLengthExceededError,
InvalidCommandArgumentError,
InvalidCommitMessageError,
NoCommitsFoundError,
Expand Down Expand Up @@ -449,6 +450,65 @@ def test_check_command_with_message_length_limit_exceeded(config, mocker: MockFi
arguments={"message": message, "message_length_limit": len(message) - 1},
)

with pytest.raises(InvalidCommitMessageError):
with pytest.raises(CommitMessageLengthExceededError):
check_cmd()
error_mock.assert_called_once()


def test_check_command_with_config_message_length_limit(config, mocker: MockFixture):
success_mock = mocker.patch("commitizen.out.success")
message = "fix(scope): some commit message"

config.settings["message_length_limit"] = len(message) + 1

check_cmd = commands.Check(
config=config,
arguments={"message": message},
)

check_cmd()
success_mock.assert_called_once()


def test_check_command_with_config_message_length_limit_exceeded(
config, mocker: MockFixture
):
error_mock = mocker.patch("commitizen.out.error")
message = "fix(scope): some commit message"

config.settings["message_length_limit"] = len(message) - 1

check_cmd = commands.Check(
config=config,
arguments={"message": message},
)

with pytest.raises(CommitMessageLengthExceededError):
check_cmd()
error_mock.assert_called_once()


def test_check_command_cli_overrides_config_message_length_limit(
config, mocker: MockFixture
):
success_mock = mocker.patch("commitizen.out.success")
message = "fix(scope): some commit message"

config.settings["message_length_limit"] = len(message) - 1

check_cmd = commands.Check(
config=config,
arguments={"message": message, "message_length_limit": len(message) + 1},
)

check_cmd()
success_mock.assert_called_once()

success_mock.reset_mock()
check_cmd = commands.Check(
config=config,
arguments={"message": message, "message_length_limit": None},
)

check_cmd()
success_mock.assert_called_once()
59 changes: 59 additions & 0 deletions tests/commands/test_commit_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -554,3 +554,62 @@ def test_commit_when_nothing_added_to_commit(config, mocker: MockFixture, out):

commit_mock.assert_called_once()
error_mock.assert_called_once_with(out)


@pytest.mark.usefixtures("staging_is_clean")
def test_commit_command_with_config_message_length_limit(config, mocker: MockFixture):
prompt_mock = mocker.patch("questionary.prompt")
prefix = "feat"
subject = "random subject"
message_length = len(prefix) + len(": ") + len(subject)
prompt_mock.return_value = {
"prefix": prefix,
"subject": subject,
"scope": "",
"is_breaking_change": False,
"body": "random body",
"footer": "random footer",
}

commit_mock = mocker.patch("commitizen.git.commit")
commit_mock.return_value = cmd.Command("success", "", b"", b"", 0)
success_mock = mocker.patch("commitizen.out.success")

config.settings["message_length_limit"] = message_length
commands.Commit(config, {})()
success_mock.assert_called_once()

config.settings["message_length_limit"] = message_length - 1
with pytest.raises(CommitMessageLengthExceededError):
commands.Commit(config, {})()


@pytest.mark.usefixtures("staging_is_clean")
def test_commit_command_cli_overrides_config_message_length_limit(
config, mocker: MockFixture
):
prompt_mock = mocker.patch("questionary.prompt")
prefix = "feat"
subject = "random subject"
message_length = len(prefix) + len(": ") + len(subject)
prompt_mock.return_value = {
"prefix": prefix,
"subject": subject,
"scope": "",
"is_breaking_change": False,
"body": "random body",
"footer": "random footer",
}

commit_mock = mocker.patch("commitizen.git.commit")
commit_mock.return_value = cmd.Command("success", "", b"", b"", 0)
success_mock = mocker.patch("commitizen.out.success")

config.settings["message_length_limit"] = message_length - 1

commands.Commit(config, {"message_length_limit": message_length})()
success_mock.assert_called_once()

success_mock.reset_mock()
commands.Commit(config, {"message_length_limit": None})()
success_mock.assert_called_once()
2 changes: 2 additions & 0 deletions tests/test_conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@
"always_signoff": False,
"template": None,
"extras": {},
"message_length_limit": None,
}

_new_settings: dict[str, Any] = {
Expand Down Expand Up @@ -126,6 +127,7 @@
"always_signoff": False,
"template": None,
"extras": {},
"message_length_limit": None,
}


Expand Down
Loading