From f334af9c6d8acc8904ef4619c864fd0d43cba654 Mon Sep 17 00:00:00 2001 From: catfish Date: Mon, 11 Aug 2025 23:54:31 +0800 Subject: [PATCH 1/2] feat: add config option for line length warning --- commitizen/cli.py | 2 - commitizen/commands/check.py | 21 +++++++--- commitizen/commands/commit.py | 7 +++- commitizen/defaults.py | 2 + poetry.lock | 2 +- pyproject.toml | 2 +- tests/commands/test_check_command.py | 59 ++++++++++++++++++++++++++- tests/commands/test_commit_command.py | 59 +++++++++++++++++++++++++++ tests/test_conf.py | 2 + 9 files changed, 143 insertions(+), 13 deletions(-) diff --git a/commitizen/cli.py b/commitizen/cli.py index e9689d75f..c11e9078d 100644 --- a/commitizen/cli.py +++ b/commitizen/cli.py @@ -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", }, { @@ -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", }, ], diff --git a/commitizen/commands/check.py b/commitizen/commands/check.py index d45e388f9..a6101f7df 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, @@ -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 @@ -41,8 +42,11 @@ def __init__(self, config: BaseConfig, arguments: CheckArgs, *args: object) -> N self.allow_abort = bool( 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") @@ -88,7 +92,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 @@ -153,7 +157,7 @@ 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 @@ -161,9 +165,14 @@ def _validate_commit_message( 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)) diff --git a/commitizen/commands/commit.py b/commitizen/commands/commit.py index d16b4d8d8..7144bced8 100644 --- a/commitizen/commands/commit.py +++ b/commitizen/commands/commit.py @@ -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 @@ -80,8 +80,11 @@ def _get_message_by_prompt_commit_questions(self) -> str: raise NoAnswersError() message = self.cz.message(answers) - if limit := self.arguments.get("message_length_limit", 0): + if limit := self.arguments.get( + "message_length_limit", self.config.settings.get("message_length_limit", 0) + ): self._validate_subject_length(message=message, length_limit=limit) + return message def _validate_subject_length(self, *, message: str, length_limit: int) -> None: diff --git a/commitizen/defaults.py b/commitizen/defaults.py index 68e580f52..9b3b76a68 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 | None name: str post_bump_hooks: list[str] | None pre_bump_hooks: list[str] | None @@ -111,6 +112,7 @@ class Settings(TypedDict, total=False): "template": None, # default provided by plugin "extras": {}, "breaking_change_exclamation_in_title": False, + "message_length_limit": None, # None for no limit } MAJOR = "MAJOR" diff --git a/poetry.lock b/poetry.lock index 1a731e4f0..49df9dcd5 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1955,4 +1955,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.1" python-versions = ">=3.9,<4.0" -content-hash = "bdc8773ed978a4265a2a099265db7e116d2f65c467c4980d984e546716cea244" +content-hash = "cd5648d8aad7b58913b1c0e4cd4f04c98d5bcfa7e4ef8e7bb994a59492d7d4a2" diff --git a/pyproject.toml b/pyproject.toml index f16ad596b..4ff252ebf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,7 +18,7 @@ dependencies = [ "colorama (>=0.4.1,<1.0)", "termcolor (>=1.1.0,<4.0.0)", "packaging>=19", - "tomlkit (>=0.5.3,<1.0.0)", + "tomlkit (>=0.8.0,<1.0.0)", "jinja2>=2.10.3", "pyyaml>=3.08", "argcomplete >=1.12.1,<3.7", diff --git a/tests/commands/test_check_command.py b/tests/commands/test_check_command.py index 365a556dd..d2a82a903 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,7 +450,7 @@ 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() @@ -460,3 +461,59 @@ def test_check_command_with_amend_prefix_default(config, mocker: MockFixture): check_cmd() success_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}, + ) diff --git a/tests/commands/test_commit_command.py b/tests/commands/test_commit_command.py index 8d0181f3e..3e408576f 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": None})() + success_mock.assert_called_once() diff --git a/tests/test_conf.py b/tests/test_conf.py index 47633c7e0..bbbed41e0 100644 --- a/tests/test_conf.py +++ b/tests/test_conf.py @@ -106,6 +106,7 @@ "template": None, "extras": {}, "breaking_change_exclamation_in_title": False, + "message_length_limit": None, } _new_settings: dict[str, Any] = { @@ -145,6 +146,7 @@ "template": None, "extras": {}, "breaking_change_exclamation_in_title": False, + "message_length_limit": None, } From 3f554129b341c6de60e141d4507d48319f7230a4 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 --- docs/config.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/config.md b/docs/config.md index c00a6f72a..94aa2076b 100644 --- a/docs/config.md +++ b/docs/config.md @@ -119,6 +119,14 @@ Automatically retry failed commit when running `cz commit`. [Read more][retry_af 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`