From 092ec64e004e20b58d0318e9e83afd52b9a42f4f Mon Sep 17 00:00:00 2001 From: TTW Date: Wed, 18 Mar 2026 13:57:04 +0800 Subject: [PATCH 1/7] fix(commit): honor message length limit from cli and config CLI > config > default (0) for not limit --- commitizen/commands/commit.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/commitizen/commands/commit.py b/commitizen/commands/commit.py index 6668c0d65..f561cff22 100644 --- a/commitizen/commands/commit.py +++ b/commitizen/commands/commit.py @@ -54,6 +54,13 @@ def __init__(self, config: BaseConfig, arguments: CommitArgs) -> None: self.arguments = arguments self.backup_file_path = get_backup_file_path() + message_length_limit = arguments.get("message_length_limit") + self.message_length_limit: int = ( + message_length_limit + if message_length_limit is not None + else config.settings["message_length_limit"] + ) + def _read_backup_message(self) -> str | None: # Check the commit backup file exists if not self.backup_file_path.is_file(): @@ -85,19 +92,14 @@ def _get_message_by_prompt_commit_questions(self) -> str: return message def _validate_subject_length(self, message: str) -> None: - message_length_limit = self.arguments.get( - "message_length_limit", self.config.settings.get("message_length_limit", 0) - ) # By the contract, message_length_limit is set to 0 for no limit - if ( - message_length_limit is None or message_length_limit <= 0 - ): # do nothing for no limit + if self.message_length_limit <= 0: return subject = message.partition("\n")[0].strip() - if len(subject) > message_length_limit: + if len(subject) > self.message_length_limit: raise CommitMessageLengthExceededError( - f"Length of commit message exceeds limit ({len(subject)}/{message_length_limit}), subject: '{subject}'" + f"Length of commit message exceeds limit ({len(subject)}/{self.message_length_limit}), subject: '{subject}'" ) def manual_edit(self, message: str) -> str: From 2be6bcb93de598b23dda71242e49abd8b36efb5e Mon Sep 17 00:00:00 2001 From: TTW Date: Wed, 18 Mar 2026 13:58:27 +0800 Subject: [PATCH 2/7] fix(check): honor message length limit from cli and config CLI > config > default (0) for not limit --- commitizen/commands/check.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/commitizen/commands/check.py b/commitizen/commands/check.py index ab5e671d6..9449a7082 100644 --- a/commitizen/commands/check.py +++ b/commitizen/commands/check.py @@ -46,8 +46,12 @@ def __init__(self, config: BaseConfig, arguments: CheckArgs, *args: object) -> N ) self.use_default_range = bool(arguments.get("use_default_range")) - self.max_msg_length = arguments.get( - "message_length_limit", config.settings.get("message_length_limit", 0) + + message_length_limit = arguments.get("message_length_limit") + self.message_length_limit: int = ( + message_length_limit + if message_length_limit is not None + else config.settings["message_length_limit"] ) # we need to distinguish between None and [], which is a valid value @@ -100,7 +104,7 @@ def __call__(self) -> None: pattern=pattern, allow_abort=self.allow_abort, allowed_prefixes=self.allowed_prefixes, - max_msg_length=self.max_msg_length, + max_msg_length=self.message_length_limit, commit_hash=commit.rev, ) ).is_valid From 6d7bba1cf77dc3e762642e5e4d1547d7f948b5e8 Mon Sep 17 00:00:00 2001 From: TTW Date: Wed, 18 Mar 2026 15:13:24 +0800 Subject: [PATCH 3/7] test(commit): add coverage for message length limit precedence --- tests/commands/test_commit_command.py | 64 ++++++++++++++++++++------- 1 file changed, 48 insertions(+), 16 deletions(-) diff --git a/tests/commands/test_commit_command.py b/tests/commands/test_commit_command.py index 2322cb3cb..034ba5872 100644 --- a/tests/commands/test_commit_command.py +++ b/tests/commands/test_commit_command.py @@ -336,34 +336,66 @@ def test_commit_when_nothing_added_to_commit(config, mocker: MockFixture, out): error_mock.assert_called_once_with(out) -@pytest.mark.usefixtures("staging_is_clean", "commit_mock") -def test_commit_command_with_config_message_length_limit( - config, success_mock: MockType, prompt_mock_feat: MockType -): +def _commit_first_line_len(prompt_mock_feat: MockType) -> int: prefix = prompt_mock_feat.return_value["prefix"] subject = prompt_mock_feat.return_value["subject"] - message_length = len(f"{prefix}: {subject}") + scope = prompt_mock_feat.return_value["scope"] + + formatted_scope = f"({scope})" if scope else "" + first_line = f"{prefix}{formatted_scope}: {subject}" + return len(first_line) - commands.Commit(config, {"message_length_limit": message_length})() + +@pytest.mark.usefixtures("staging_is_clean", "commit_mock", "prompt_mock_feat") +def test_commit_message_length_cli_at_limit_succeeds( + config, success_mock: MockType, prompt_mock_feat: MockType +): + message_len = _commit_first_line_len(prompt_mock_feat) + commands.Commit(config, {"message_length_limit": message_len})() success_mock.assert_called_once() + +@pytest.mark.usefixtures("staging_is_clean", "commit_mock", "prompt_mock_feat") +def test_commit_message_length_cli_below_limit_raises( + config, prompt_mock_feat: MockType +): + message_len = _commit_first_line_len(prompt_mock_feat) with pytest.raises(CommitMessageLengthExceededError): - commands.Commit(config, {"message_length_limit": message_length - 1})() + commands.Commit(config, {"message_length_limit": message_len - 1})() - config.settings["message_length_limit"] = message_length - success_mock.reset_mock() - commands.Commit(config, {})() + +@pytest.mark.usefixtures("staging_is_clean", "commit_mock", "prompt_mock_feat") +def test_commit_message_length_uses_config_when_cli_unset( + config, success_mock: MockType, prompt_mock_feat: MockType +): + config.settings["message_length_limit"] = _commit_first_line_len(prompt_mock_feat) + commands.Commit(config, {"message_length_limit": None})() success_mock.assert_called_once() - config.settings["message_length_limit"] = message_length - 1 + +@pytest.mark.usefixtures("staging_is_clean", "commit_mock", "prompt_mock_feat") +def test_commit_message_length_config_exceeded_when_cli_unset( + config, prompt_mock_feat: MockType +): + config.settings["message_length_limit"] = _commit_first_line_len(prompt_mock_feat) - 1 with pytest.raises(CommitMessageLengthExceededError): - commands.Commit(config, {})() + commands.Commit(config, {"message_length_limit": None})() + - # Test config message length limit is overridden by CLI argument - success_mock.reset_mock() - commands.Commit(config, {"message_length_limit": message_length})() +@pytest.mark.usefixtures("staging_is_clean", "commit_mock", "prompt_mock_feat") +def test_commit_message_length_cli_overrides_stricter_config( + config, success_mock: MockType, prompt_mock_feat: MockType +): + message_len = _commit_first_line_len(prompt_mock_feat) + config.settings["message_length_limit"] = message_len - 1 + commands.Commit(config, {"message_length_limit": message_len})() success_mock.assert_called_once() - success_mock.reset_mock() + +@pytest.mark.usefixtures("staging_is_clean", "commit_mock", "prompt_mock_feat") +def test_commit_message_length_cli_zero_disables_limit( + config, success_mock: MockType, prompt_mock_feat: MockType +): + config.settings["message_length_limit"] = _commit_first_line_len(prompt_mock_feat) - 1 commands.Commit(config, {"message_length_limit": 0})() success_mock.assert_called_once() From b0deaf64afa0ff87054b56dbd847f17f2fe88cce Mon Sep 17 00:00:00 2001 From: TTW Date: Wed, 18 Mar 2026 16:16:56 +0800 Subject: [PATCH 4/7] test(check): add coverage for message length limit precedence --- tests/commands/test_check_command.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/tests/commands/test_check_command.py b/tests/commands/test_check_command.py index f3f860313..072ed7330 100644 --- a/tests/commands/test_check_command.py +++ b/tests/commands/test_check_command.py @@ -351,23 +351,25 @@ def test_check_command_with_amend_prefix_default(config, success_mock): success_mock.assert_called_once() -def test_check_command_with_config_message_length_limit(config, success_mock): +def test_check_command_with_config_message_length_limit_and_cli_none(config, success_mock: MockType): message = "fix(scope): some commit message" config.settings["message_length_limit"] = len(message) + 1 commands.Check( config=config, - arguments={"message": message}, + arguments={"message": message, "message_length_limit": None}, )() success_mock.assert_called_once() -def test_check_command_with_config_message_length_limit_exceeded(config): +def test_check_command_with_config_message_length_limit_exceeded_and_cli_none( + config, +): message = "fix(scope): some commit message" config.settings["message_length_limit"] = len(message) - 1 with pytest.raises(CommitMessageLengthExceededError): commands.Check( config=config, - arguments={"message": message}, + arguments={"message": message, "message_length_limit": None}, )() @@ -376,7 +378,7 @@ def test_check_command_cli_overrides_config_message_length_limit( ): message = "fix(scope): some commit message" config.settings["message_length_limit"] = len(message) - 1 - for message_length_limit in [len(message) + 1, 0]: + for message_length_limit in [len(message), 0]: success_mock.reset_mock() commands.Check( config=config, From e61f70cca2006d26a6d3bf26eb35e437a314f96c Mon Sep 17 00:00:00 2001 From: TTW Date: Wed, 18 Mar 2026 16:21:02 +0800 Subject: [PATCH 5/7] test(cli-config): add integration tests for message length limit config --- tests/test_cli_config_integration.py | 87 ++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 tests/test_cli_config_integration.py diff --git a/tests/test_cli_config_integration.py b/tests/test_cli_config_integration.py new file mode 100644 index 000000000..156c59f1a --- /dev/null +++ b/tests/test_cli_config_integration.py @@ -0,0 +1,87 @@ +from __future__ import annotations + +from pathlib import Path + +import pytest + +from commitizen.exceptions import CommitMessageLengthExceededError, DryRunExit +from tests.utils import UtilFixture + + +def _write_pyproject_with_message_length_limit( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, message_length_limit: int +) -> None: + (tmp_path / "pyproject.toml").write_text( + "[tool.commitizen]\n" + 'name = "cz_conventional_commits"\n' + f"message_length_limit = {message_length_limit}\n", + encoding="utf-8", + ) + monkeypatch.chdir(tmp_path) + + +def _mock_commit_prompt(mocker, *, subject: str) -> None: + mocker.patch( + "questionary.prompt", + return_value={ + "prefix": "feat", + "subject": subject, + "scope": "", + "is_breaking_change": False, + "body": "", + "footer": "", + }, + ) + + +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_cli_check_reads_message_length_limit_from_pyproject( + util: UtilFixture, monkeypatch: pytest.MonkeyPatch, tmp_path: Path +): + _write_pyproject_with_message_length_limit(tmp_path, monkeypatch, 10) + + long_message_file = tmp_path / "long_message.txt" + long_message_file.write_text("feat: this is definitely too long", encoding="utf-8") + + with pytest.raises(CommitMessageLengthExceededError): + util.run_cli("check", "--commit-msg-file", str(long_message_file)) + + +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_cli_commit_reads_message_length_limit_from_pyproject( + util: UtilFixture, + monkeypatch: pytest.MonkeyPatch, + mocker, + tmp_path: Path, +): + _write_pyproject_with_message_length_limit(tmp_path, monkeypatch, 10) + _mock_commit_prompt(mocker, subject="this is definitely too long") + mocker.patch("commitizen.git.is_staging_clean", return_value=False) + + with pytest.raises(CommitMessageLengthExceededError): + util.run_cli("commit", "--dry-run") + + +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_cli_check_cli_overrides_message_length_limit_from_pyproject( + util: UtilFixture, monkeypatch: pytest.MonkeyPatch, tmp_path: Path +): + _write_pyproject_with_message_length_limit(tmp_path, monkeypatch, 10) + + util.run_cli("check", "-l", "0", "--message", "feat: this is definitely too long") + + +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_cli_commit_cli_overrides_message_length_limit_from_pyproject( + util: UtilFixture, + monkeypatch: pytest.MonkeyPatch, + mocker, + tmp_path: Path, +): + _write_pyproject_with_message_length_limit(tmp_path, monkeypatch, 10) + _mock_commit_prompt(mocker, subject="this is definitely too long") + mocker.patch("commitizen.git.is_staging_clean", return_value=False) + + with pytest.raises(DryRunExit): + util.run_cli("commit", "--dry-run", "-l", "100") + From e6e74751f222dfe01ff3174ea4f29e28e9ee68f9 Mon Sep 17 00:00:00 2001 From: TTW Date: Wed, 18 Mar 2026 16:35:12 +0800 Subject: [PATCH 6/7] fix(types): allow None for message_length_limit cli arg --- commitizen/commands/check.py | 2 +- commitizen/commands/commit.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/commitizen/commands/check.py b/commitizen/commands/check.py index 9449a7082..01faf0eb1 100644 --- a/commitizen/commands/check.py +++ b/commitizen/commands/check.py @@ -21,7 +21,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 diff --git a/commitizen/commands/commit.py b/commitizen/commands/commit.py index f561cff22..4a0bd5886 100644 --- a/commitizen/commands/commit.py +++ b/commitizen/commands/commit.py @@ -35,7 +35,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 From f5c4b4880bd3204d75ee1ba702d784bcf16c0624 Mon Sep 17 00:00:00 2001 From: TTW Date: Wed, 18 Mar 2026 16:35:47 +0800 Subject: [PATCH 7/7] refactor(test): apply type-check-only imports --- tests/commands/test_check_command.py | 4 +++- tests/commands/test_commit_command.py | 8 ++++++-- tests/test_cli_config_integration.py | 9 ++++++--- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/tests/commands/test_check_command.py b/tests/commands/test_check_command.py index 072ed7330..5abd0688a 100644 --- a/tests/commands/test_check_command.py +++ b/tests/commands/test_check_command.py @@ -351,7 +351,9 @@ def test_check_command_with_amend_prefix_default(config, success_mock): success_mock.assert_called_once() -def test_check_command_with_config_message_length_limit_and_cli_none(config, success_mock: MockType): +def test_check_command_with_config_message_length_limit_and_cli_none( + config, success_mock: MockType +): message = "fix(scope): some commit message" config.settings["message_length_limit"] = len(message) + 1 commands.Check( diff --git a/tests/commands/test_commit_command.py b/tests/commands/test_commit_command.py index 034ba5872..497b57607 100644 --- a/tests/commands/test_commit_command.py +++ b/tests/commands/test_commit_command.py @@ -377,7 +377,9 @@ def test_commit_message_length_uses_config_when_cli_unset( def test_commit_message_length_config_exceeded_when_cli_unset( config, prompt_mock_feat: MockType ): - config.settings["message_length_limit"] = _commit_first_line_len(prompt_mock_feat) - 1 + config.settings["message_length_limit"] = ( + _commit_first_line_len(prompt_mock_feat) - 1 + ) with pytest.raises(CommitMessageLengthExceededError): commands.Commit(config, {"message_length_limit": None})() @@ -396,6 +398,8 @@ def test_commit_message_length_cli_overrides_stricter_config( def test_commit_message_length_cli_zero_disables_limit( config, success_mock: MockType, prompt_mock_feat: MockType ): - config.settings["message_length_limit"] = _commit_first_line_len(prompt_mock_feat) - 1 + config.settings["message_length_limit"] = ( + _commit_first_line_len(prompt_mock_feat) - 1 + ) commands.Commit(config, {"message_length_limit": 0})() success_mock.assert_called_once() diff --git a/tests/test_cli_config_integration.py b/tests/test_cli_config_integration.py index 156c59f1a..2406549ba 100644 --- a/tests/test_cli_config_integration.py +++ b/tests/test_cli_config_integration.py @@ -1,11 +1,15 @@ from __future__ import annotations -from pathlib import Path +from typing import TYPE_CHECKING import pytest from commitizen.exceptions import CommitMessageLengthExceededError, DryRunExit -from tests.utils import UtilFixture + +if TYPE_CHECKING: + from pathlib import Path + + from tests.utils import UtilFixture def _write_pyproject_with_message_length_limit( @@ -84,4 +88,3 @@ def test_cli_commit_cli_overrides_message_length_limit_from_pyproject( with pytest.raises(DryRunExit): util.run_cli("commit", "--dry-run", "-l", "100") -