From 13c65bf2261c6224bc6042bd7dc2c1dda8744b78 Mon Sep 17 00:00:00 2001 From: catfish Date: Mon, 11 Aug 2025 23:54:31 +0800 Subject: [PATCH 1/2] feat: implement message length limit for commit messages --- commitizen/cli.py | 2 - commitizen/commands/check.py | 16 ++++++- commitizen/commands/commit.py | 7 ++- commitizen/defaults.py | 2 + tests/commands/test_check_command.py | 62 ++++++++++++++++++++++++++- tests/commands/test_commit_command.py | 59 +++++++++++++++++++++++++ tests/test_conf.py | 2 + 7 files changed, 144 insertions(+), 6 deletions(-) diff --git a/commitizen/cli.py b/commitizen/cli.py index 6f7556df49..a700ebd7bd 100644 --- a/commitizen/cli.py +++ b/commitizen/cli.py @@ -163,7 +163,6 @@ def __call__( { "name": ["-l", "--message-length-limit"], "type": int, - "default": 0, "help": "length limit of the commit message; 0 for no limit", }, { @@ -495,7 +494,6 @@ def __call__( { "name": ["-l", "--message-length-limit"], "type": int, - "default": 0, "help": "length limit of the commit message; 0 for no limit", }, ], diff --git a/commitizen/commands/check.py b/commitizen/commands/check.py index 69147bcfbe..58acb187bb 100644 --- a/commitizen/commands/check.py +++ b/commitizen/commands/check.py @@ -7,6 +7,7 @@ from commitizen import factory, git, out from commitizen.config import BaseConfig from commitizen.exceptions import ( + CommitMessageLengthExceededError, InvalidCommandArgumentError, InvalidCommitMessageError, NoCommitsFoundError, @@ -40,7 +41,13 @@ def __init__(self, config: BaseConfig, arguments: CheckArgs, *args: object) -> N self.allow_abort = bool( arguments.get("allow_abort", config.settings["allow_abort"]) ) - self.max_msg_length = arguments.get("message_length_limit", 0) + + # Use command line argument if provided, otherwise use config setting + cmd_length_limit = arguments.get("message_length_limit") + if cmd_length_limit is None: + self.max_msg_length = config.settings.get("message_length_limit", 0) + else: + self.max_msg_length = cmd_length_limit # we need to distinguish between None and [], which is a valid value allowed_prefixes = arguments.get("allowed_prefixes") @@ -154,6 +161,11 @@ def _validate_commit_message( if self.max_msg_length: 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_msg}"\n' + f"message length limit: {self.max_msg_length} (actual: {msg_len})" + ) return bool(pattern.match(commit_msg)) diff --git a/commitizen/commands/commit.py b/commitizen/commands/commit.py index 19bb72fb00..9dbd226e3d 100644 --- a/commitizen/commands/commit.py +++ b/commitizen/commands/commit.py @@ -81,7 +81,12 @@ 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) + + + message_length_limit = self.arguments.get("message_length_limit") + if message_length_limit is None: + message_length_limit = self.config.settings.get("message_length_limit", 0) + if 0 < message_length_limit < message_len: raise CommitMessageLengthExceededError( f"Length of commit message exceeds limit ({message_len}/{message_length_limit})" diff --git a/commitizen/defaults.py b/commitizen/defaults.py index 94d4d97b22..792cc770a6 100644 --- a/commitizen/defaults.py +++ b/commitizen/defaults.py @@ -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 name: str post_bump_hooks: list[str] | None pre_bump_hooks: list[str] | None @@ -108,6 +109,7 @@ class Settings(TypedDict, total=False): "always_signoff": False, "template": None, # default provided by plugin "extras": {}, + "message_length_limit": 0, # 0 for no limit } MAJOR = "MAJOR" diff --git a/tests/commands/test_check_command.py b/tests/commands/test_check_command.py index d95a173d8a..1a325d7f1b 100644 --- a/tests/commands/test_check_command.py +++ b/tests/commands/test_check_command.py @@ -8,6 +8,7 @@ from commitizen import cli, commands, git from commitizen.exceptions import ( + CommitMessageLengthExceededError, InvalidCommandArgumentError, InvalidCommitMessageError, NoCommitsFoundError, @@ -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": 0}, + ) + + check_cmd() + success_mock.assert_called_once() diff --git a/tests/commands/test_commit_command.py b/tests/commands/test_commit_command.py index 930e1a7a9b..6a247f7783 100644 --- a/tests/commands/test_commit_command.py +++ b/tests/commands/test_commit_command.py @@ -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": 0})() + success_mock.assert_called_once() diff --git a/tests/test_conf.py b/tests/test_conf.py index f89a0049f4..3afd37dc3a 100644 --- a/tests/test_conf.py +++ b/tests/test_conf.py @@ -95,6 +95,7 @@ "always_signoff": False, "template": None, "extras": {}, + "message_length_limit": 0, } _new_settings: dict[str, Any] = { @@ -126,6 +127,7 @@ "always_signoff": False, "template": None, "extras": {}, + "message_length_limit": 0, } From d639a865ffbc7b33a8587ef0cd0a898f2413ad3b Mon Sep 17 00:00:00 2001 From: catfish Date: Tue, 12 Aug 2025 13:06:27 +0800 Subject: [PATCH 2/2] docs(config): add message length limit configuration option --- commitizen/commands/commit.py | 1 - docs/config.md | 8 ++++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/commitizen/commands/commit.py b/commitizen/commands/commit.py index 9dbd226e3d..0ce9180d7c 100644 --- a/commitizen/commands/commit.py +++ b/commitizen/commands/commit.py @@ -82,7 +82,6 @@ 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") if message_length_limit is None: message_length_limit = self.config.settings.get("message_length_limit", 0) diff --git a/docs/config.md b/docs/config.md index 649881daec..a2ac0cb8b3 100644 --- a/docs/config.md +++ b/docs/config.md @@ -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`