From aa15fda8942b008e63542129fe082948b1b9fad9 Mon Sep 17 00:00:00 2001 From: Ying-Fang Date: Wed, 4 Feb 2026 16:20:18 -0600 Subject: [PATCH 1/5] feat(commit): add a tag(--body-length-limit) and a function for command commit --- commitizen/cli.py | 5 ++++ commitizen/commands/commit.py | 26 +++++++++++++++++++ commitizen/defaults.py | 2 ++ ...n_when_use_help_option_py_3_10_commit_.txt | 7 ++++- ...n_when_use_help_option_py_3_11_commit_.txt | 7 ++++- ...n_when_use_help_option_py_3_12_commit_.txt | 7 ++++- ...n_when_use_help_option_py_3_13_commit_.txt | 7 ++++- ...n_when_use_help_option_py_3_14_commit_.txt | 7 ++++- tests/test_conf.py | 2 ++ 9 files changed, 65 insertions(+), 5 deletions(-) diff --git a/commitizen/cli.py b/commitizen/cli.py index 79988fb5cb..fac79af3af 100644 --- a/commitizen/cli.py +++ b/commitizen/cli.py @@ -164,6 +164,11 @@ def __call__( "type": int, "help": "Set the length limit of the commit message; 0 for no limit.", }, + { + "name": ["--body-length-limit"], + "type": int, + "help": "Set the length limit of the commit body. Commit message in body will be rewrapped to this length; 0 for no limit.", + }, { "name": ["--"], "action": "store_true", diff --git a/commitizen/commands/commit.py b/commitizen/commands/commit.py index 5776af4201..06120bad59 100644 --- a/commitizen/commands/commit.py +++ b/commitizen/commands/commit.py @@ -5,6 +5,7 @@ import shutil import subprocess import tempfile +import textwrap from typing import TYPE_CHECKING, TypedDict import questionary @@ -37,6 +38,7 @@ class CommitArgs(TypedDict, total=False): edit: bool extra_cli_args: str message_length_limit: int + body_length_limit: int no_retry: bool signoff: bool write_message_to_file: Path | None @@ -84,6 +86,7 @@ def _get_message_by_prompt_commit_questions(self) -> str: message = self.cz.message(answers) self._validate_subject_length(message) + message = self._rewrap_body(message) return message def _validate_subject_length(self, message: str) -> None: @@ -102,6 +105,29 @@ def _validate_subject_length(self, message: str) -> None: f"Length of commit message exceeds limit ({len(subject)}/{message_length_limit}), subject: '{subject}'" ) + def _rewrap_body(self, message: str) -> str: + body_length_limit = self.arguments.get( + "body_length_limit", self.config.settings.get("body_length_limit", 0) + ) + # By the contract, body_length_limit is set to 0 for no limit + if ( + body_length_limit is None or body_length_limit <= 0 + ): # do nothing for no limit + return message + + message_parts = message.split("\n", 2) + if len(message_parts) < 3: + return message + + # First line is subject, second is blank line, rest is body + subject = message_parts[0] + blank_line = message_parts[1] + body = message_parts[2].strip() + wrapped_body = textwrap.fill( + body, width=body_length_limit, replace_whitespace=False + ) + return f"{subject}\n{blank_line}\n{wrapped_body}" + def manual_edit(self, message: str) -> str: editor = git.get_core_editor() if editor is None: diff --git a/commitizen/defaults.py b/commitizen/defaults.py index 4865ccc188..94bd166696 100644 --- a/commitizen/defaults.py +++ b/commitizen/defaults.py @@ -49,6 +49,7 @@ class Settings(TypedDict, total=False): legacy_tag_formats: Sequence[str] major_version_zero: bool message_length_limit: int + body_length_limit: int name: str post_bump_hooks: list[str] | None pre_bump_hooks: list[str] | None @@ -115,6 +116,7 @@ class Settings(TypedDict, total=False): "extras": {}, "breaking_change_exclamation_in_title": False, "message_length_limit": 0, # 0 for no limit + "body_length_limit": 0, # 0 for no limit } MAJOR = "MAJOR" diff --git a/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_10_commit_.txt b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_10_commit_.txt index bd256ccf8c..7cc8366eea 100644 --- a/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_10_commit_.txt +++ b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_10_commit_.txt @@ -1,6 +1,7 @@ usage: cz commit [-h] [--retry] [--no-retry] [--dry-run] [--write-message-to-file FILE_PATH] [-s] [-a] [-e] - [-l MESSAGE_LENGTH_LIMIT] [--] + [-l MESSAGE_LENGTH_LIMIT] + [--body-length-limit BODY_LENGTH_LIMIT] [--] Create new commit @@ -22,4 +23,8 @@ options: -l MESSAGE_LENGTH_LIMIT, --message-length-limit MESSAGE_LENGTH_LIMIT Set the length limit of the commit message; 0 for no limit. + --body-length-limit BODY_LENGTH_LIMIT + Set the length limit of the commit body. Commit + message in body will be rewrapped to this length; 0 + for no limit. -- Positional arguments separator (recommended). diff --git a/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_11_commit_.txt b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_11_commit_.txt index bd256ccf8c..7cc8366eea 100644 --- a/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_11_commit_.txt +++ b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_11_commit_.txt @@ -1,6 +1,7 @@ usage: cz commit [-h] [--retry] [--no-retry] [--dry-run] [--write-message-to-file FILE_PATH] [-s] [-a] [-e] - [-l MESSAGE_LENGTH_LIMIT] [--] + [-l MESSAGE_LENGTH_LIMIT] + [--body-length-limit BODY_LENGTH_LIMIT] [--] Create new commit @@ -22,4 +23,8 @@ options: -l MESSAGE_LENGTH_LIMIT, --message-length-limit MESSAGE_LENGTH_LIMIT Set the length limit of the commit message; 0 for no limit. + --body-length-limit BODY_LENGTH_LIMIT + Set the length limit of the commit body. Commit + message in body will be rewrapped to this length; 0 + for no limit. -- Positional arguments separator (recommended). diff --git a/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_12_commit_.txt b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_12_commit_.txt index bd256ccf8c..7cc8366eea 100644 --- a/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_12_commit_.txt +++ b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_12_commit_.txt @@ -1,6 +1,7 @@ usage: cz commit [-h] [--retry] [--no-retry] [--dry-run] [--write-message-to-file FILE_PATH] [-s] [-a] [-e] - [-l MESSAGE_LENGTH_LIMIT] [--] + [-l MESSAGE_LENGTH_LIMIT] + [--body-length-limit BODY_LENGTH_LIMIT] [--] Create new commit @@ -22,4 +23,8 @@ options: -l MESSAGE_LENGTH_LIMIT, --message-length-limit MESSAGE_LENGTH_LIMIT Set the length limit of the commit message; 0 for no limit. + --body-length-limit BODY_LENGTH_LIMIT + Set the length limit of the commit body. Commit + message in body will be rewrapped to this length; 0 + for no limit. -- Positional arguments separator (recommended). diff --git a/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_13_commit_.txt b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_13_commit_.txt index cbd5780f6d..df3df7c540 100644 --- a/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_13_commit_.txt +++ b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_13_commit_.txt @@ -1,6 +1,7 @@ usage: cz commit [-h] [--retry] [--no-retry] [--dry-run] [--write-message-to-file FILE_PATH] [-s] [-a] [-e] - [-l MESSAGE_LENGTH_LIMIT] [--] + [-l MESSAGE_LENGTH_LIMIT] + [--body-length-limit BODY_LENGTH_LIMIT] [--] Create new commit @@ -22,4 +23,8 @@ options: -l, --message-length-limit MESSAGE_LENGTH_LIMIT Set the length limit of the commit message; 0 for no limit. + --body-length-limit BODY_LENGTH_LIMIT + Set the length limit of the commit body. Commit + message in body will be rewrapped to this length; 0 + for no limit. -- Positional arguments separator (recommended). diff --git a/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_14_commit_.txt b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_14_commit_.txt index cbd5780f6d..df3df7c540 100644 --- a/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_14_commit_.txt +++ b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_14_commit_.txt @@ -1,6 +1,7 @@ usage: cz commit [-h] [--retry] [--no-retry] [--dry-run] [--write-message-to-file FILE_PATH] [-s] [-a] [-e] - [-l MESSAGE_LENGTH_LIMIT] [--] + [-l MESSAGE_LENGTH_LIMIT] + [--body-length-limit BODY_LENGTH_LIMIT] [--] Create new commit @@ -22,4 +23,8 @@ options: -l, --message-length-limit MESSAGE_LENGTH_LIMIT Set the length limit of the commit message; 0 for no limit. + --body-length-limit BODY_LENGTH_LIMIT + Set the length limit of the commit body. Commit + message in body will be rewrapped to this length; 0 + for no limit. -- Positional arguments separator (recommended). diff --git a/tests/test_conf.py b/tests/test_conf.py index f1ff76ff88..9d69b63042 100644 --- a/tests/test_conf.py +++ b/tests/test_conf.py @@ -112,6 +112,7 @@ "extras": {}, "breaking_change_exclamation_in_title": False, "message_length_limit": 0, + "body_length_limit": 0, } _new_settings: dict[str, Any] = { @@ -152,6 +153,7 @@ "extras": {}, "breaking_change_exclamation_in_title": False, "message_length_limit": 0, + "body_length_limit": 0, } From 1f74bb2e74d7b040aa57dba4461aca0b9948da97 Mon Sep 17 00:00:00 2001 From: Ying-Fang Date: Wed, 4 Feb 2026 17:47:57 -0600 Subject: [PATCH 2/5] test(commit): add some test for commit --body-length-limit flag --- commitizen/commands/commit.py | 8 +- tests/commands/test_commit_command.py | 227 ++++++++++++++++++++++++++ 2 files changed, 232 insertions(+), 3 deletions(-) diff --git a/commitizen/commands/commit.py b/commitizen/commands/commit.py index 06120bad59..2c541c8682 100644 --- a/commitizen/commands/commit.py +++ b/commitizen/commands/commit.py @@ -123,9 +123,11 @@ def _rewrap_body(self, message: str) -> str: subject = message_parts[0] blank_line = message_parts[1] body = message_parts[2].strip() - wrapped_body = textwrap.fill( - body, width=body_length_limit, replace_whitespace=False - ) + body_lines = body.split("\n") + wrapped_body_lines = [] + for line in body_lines: + wrapped_body_lines.append(textwrap.fill(line, width=body_length_limit)) + wrapped_body = "\n".join(wrapped_body_lines) return f"{subject}\n{blank_line}\n{wrapped_body}" def manual_edit(self, message: str) -> str: diff --git a/tests/commands/test_commit_command.py b/tests/commands/test_commit_command.py index c80a13823d..4fb3d048a3 100644 --- a/tests/commands/test_commit_command.py +++ b/tests/commands/test_commit_command.py @@ -365,3 +365,230 @@ def test_commit_command_with_config_message_length_limit( success_mock.reset_mock() commands.Commit(config, {"message_length_limit": 0})() success_mock.assert_called_once() + + +@pytest.mark.usefixtures("staging_is_clean") +def test_commit_command_with_body_length_limit_wrapping( + config, success_mock: MockType, mocker: MockFixture +): + """Test that long body lines are automatically wrapped to the specified limit.""" + mocker.patch( + "questionary.prompt", + return_value={ + "prefix": "feat", + "subject": "add feature", + "scope": "", + "is_breaking_change": False, + "body": "This is a very long line that exceeds 72 characters and should be automatically wrapped by the system to fit within the limit", + "footer": "", + }, + ) + + commit_mock = mocker.patch( + "commitizen.git.commit", return_value=cmd.Command("success", "", b"", b"", 0) + ) + + # Execute with body_length_limit + commands.Commit(config, {"body_length_limit": 72})() + success_mock.assert_called_once() + + # Verify wrapping occurred + committed_message = commit_mock.call_args[0][0] + lines = committed_message.split("\n") + assert lines[0] == "feat: add feature" + assert lines[1] == "" + body_lines = lines[2:] + for line in body_lines: + if line.strip(): + assert len(line) <= 72, ( + f"Line exceeds 72 chars: '{line}' ({len(line)} chars)" + ) + + +@pytest.mark.usefixtures("staging_is_clean") +def test_commit_command_with_body_length_limit_preserves_line_breaks( + config, success_mock: MockType, mocker: MockFixture +): + """Test that intentional line breaks (from | character) are preserved.""" + # Simulate what happens after multiple_line_breaker processes "line1 | line2 | line3" + mocker.patch( + "questionary.prompt", + return_value={ + "prefix": "feat", + "subject": "add feature", + "scope": "", + "is_breaking_change": False, + "body": "Line1 that is very long and exceeds the limit\nLine2 that is very long and exceeds the limit\nLine3 that is very long and exceeds the limit", + "footer": "", + }, + ) + + commit_mock = mocker.patch( + "commitizen.git.commit", return_value=cmd.Command("success", "", b"", b"", 0) + ) + + commands.Commit(config, {"body_length_limit": 45})() + success_mock.assert_called_once() + + committed_message = commit_mock.call_args[0][0] + lines = committed_message.split("\n") + + # Should have a subject, a blank line + assert lines[0] == "feat: add feature" + assert lines[1] == "" + # Each original line should be wrapped separately, preserving the line breaks + body_lines = lines[2:] + # All lines should be <= 45 chars + for line in body_lines: + if line.strip(): + assert len(line) == 45, ( + f"Line's length is not 45 chars: '{line}' ({len(line)} chars)" + ) + + +@pytest.mark.usefixtures("staging_is_clean") +def test_commit_command_with_body_length_limit_disabled( + config, success_mock: MockType, mocker: MockFixture +): + """Test that body_length_limit = 0 disables wrapping.""" + long_body = "This is a very long line that exceeds 72 characters and should NOT be wrapped when body_length_limit is set to 0" + + mocker.patch( + "questionary.prompt", + return_value={ + "prefix": "feat", + "subject": "add feature", + "scope": "", + "is_breaking_change": False, + "body": long_body, + "footer": "", + }, + ) + + commit_mock = mocker.patch( + "commitizen.git.commit", return_value=cmd.Command("success", "", b"", b"", 0) + ) + + # Execute with body_length_limit = 0 (disabled) + commands.Commit(config, {"body_length_limit": 0})() + + success_mock.assert_called_once() + + # Get the actual commit message + committed_message = commit_mock.call_args[0][0] + + # Verify the body was NOT wrapped (should contain the original long line) + assert long_body in committed_message, "Body should not be wrapped when limit is 0" + + +@pytest.mark.usefixtures("staging_is_clean") +def test_commit_command_with_body_length_limit_from_config( + config, success_mock: MockType, mocker: MockFixture +): + """Test that body_length_limit can be set via config.""" + mocker.patch( + "questionary.prompt", + return_value={ + "prefix": "feat", + "subject": "add feature", + "scope": "", + "is_breaking_change": False, + "body": "This is a very long line that exceeds 50 characters and should be wrapped", + "footer": "", + }, + ) + + commit_mock = mocker.patch( + "commitizen.git.commit", return_value=cmd.Command("success", "", b"", b"", 0) + ) + + # Set body_length_limit in config + config.settings["body_length_limit"] = 50 + + commands.Commit(config, {})() + + success_mock.assert_called_once() + + # Get the actual commit message + committed_message = commit_mock.call_args[0][0] + + # Verify all body lines are within the limit + lines = committed_message.split("\n") + body_lines = lines[2:] + for line in body_lines: + if line.strip(): + assert len(line) <= 50, ( + f"Line exceeds 50 chars: '{line}' ({len(line)} chars)" + ) + + +@pytest.mark.usefixtures("staging_is_clean") +def test_commit_command_body_length_limit_cli_overrides_config( + config, success_mock: MockType, mocker: MockFixture +): + """Test that CLI argument overrides config setting.""" + mocker.patch( + "questionary.prompt", + return_value={ + "prefix": "feat", + "subject": "add feature", + "scope": "", + "is_breaking_change": False, + "body": "This is a line that is longer than 40 characters but shorter than 80 characters", + "footer": "", + }, + ) + + commit_mock = mocker.patch( + "commitizen.git.commit", return_value=cmd.Command("success", "", b"", b"", 0) + ) + + # Set config to 40 (would wrap) + config.settings["body_length_limit"] = 40 + + # Override with CLI argument to 0 (should NOT wrap) + commands.Commit(config, {"body_length_limit": 0})() + + success_mock.assert_called_once() + + # Get the actual commit message + committed_message = commit_mock.call_args[0][0] + + # The line should NOT be wrapped (CLI override to 0 disables wrapping) + assert ( + "This is a line that is longer than 40 characters but shorter than 80 characters" + in committed_message + ) + + +@pytest.mark.usefixtures("staging_is_clean") +def test_commit_command_with_body_length_limit_no_body( + config, success_mock: MockType, mocker: MockFixture +): + """Test that commits without body work correctly with body_length_limit set.""" + mocker.patch( + "questionary.prompt", + return_value={ + "prefix": "feat", + "subject": "add feature", + "scope": "", + "is_breaking_change": False, + "body": "", # No body + "footer": "", + }, + ) + + commit_mock = mocker.patch( + "commitizen.git.commit", return_value=cmd.Command("success", "", b"", b"", 0) + ) + + # Execute commit with body_length_limit (should not crash) + commands.Commit(config, {"body_length_limit": 72})() + + success_mock.assert_called_once() + + # Get the actual commit message + committed_message = commit_mock.call_args[0][0] + + # Should just be the subject line + assert committed_message.strip() == "feat: add feature" From faacc870aa9dea830b44be53241471b9e0821e5b Mon Sep 17 00:00:00 2001 From: Ying-Fang Date: Sat, 7 Feb 2026 04:22:41 -0600 Subject: [PATCH 3/5] refactor(commit): refactor test case and logic for tag --body-length-limit in commit command --- commitizen/commands/commit.py | 26 +- tests/commands/test_commit_command.py | 259 ++++-------------- ...mit_command_body_length_limit_disabled.txt | 3 + ...mmit_command_body_length_limit_no_body.txt | 1 + ...ody_length_limit_preserves_line_breaks.txt | 5 + ...mit_command_body_length_limit_wrapping.txt | 4 + 6 files changed, 77 insertions(+), 221 deletions(-) create mode 100644 tests/commands/test_commit_command/test_commit_command_body_length_limit_disabled.txt create mode 100644 tests/commands/test_commit_command/test_commit_command_body_length_limit_no_body.txt create mode 100644 tests/commands/test_commit_command/test_commit_command_body_length_limit_preserves_line_breaks.txt create mode 100644 tests/commands/test_commit_command/test_commit_command_body_length_limit_wrapping.txt diff --git a/commitizen/commands/commit.py b/commitizen/commands/commit.py index 2c541c8682..65fbf5fa41 100644 --- a/commitizen/commands/commit.py +++ b/commitizen/commands/commit.py @@ -6,6 +6,7 @@ import subprocess import tempfile import textwrap +from itertools import chain from typing import TYPE_CHECKING, TypedDict import questionary @@ -86,7 +87,7 @@ def _get_message_by_prompt_commit_questions(self) -> str: message = self.cz.message(answers) self._validate_subject_length(message) - message = self._rewrap_body(message) + message = self._wrap_body(message) return message def _validate_subject_length(self, message: str) -> None: @@ -105,30 +106,23 @@ def _validate_subject_length(self, message: str) -> None: f"Length of commit message exceeds limit ({len(subject)}/{message_length_limit}), subject: '{subject}'" ) - def _rewrap_body(self, message: str) -> str: + def _wrap_body(self, message: str) -> str: body_length_limit = self.arguments.get( "body_length_limit", self.config.settings.get("body_length_limit", 0) ) # By the contract, body_length_limit is set to 0 for no limit - if ( - body_length_limit is None or body_length_limit <= 0 - ): # do nothing for no limit + if body_length_limit <= 0: return message - message_parts = message.split("\n", 2) - if len(message_parts) < 3: + lines = message.split("\n") + if len(lines) < 3: return message # First line is subject, second is blank line, rest is body - subject = message_parts[0] - blank_line = message_parts[1] - body = message_parts[2].strip() - body_lines = body.split("\n") - wrapped_body_lines = [] - for line in body_lines: - wrapped_body_lines.append(textwrap.fill(line, width=body_length_limit)) - wrapped_body = "\n".join(wrapped_body_lines) - return f"{subject}\n{blank_line}\n{wrapped_body}" + wrapped_body_lines = [ + textwrap.wrap(line, width=body_length_limit) for line in lines[2:] + ] + return "\n".join(chain(lines[:2], chain.from_iterable(wrapped_body_lines))) def manual_edit(self, message: str) -> str: editor = git.get_core_editor() diff --git a/tests/commands/test_commit_command.py b/tests/commands/test_commit_command.py index 4fb3d048a3..483c1d2c28 100644 --- a/tests/commands/test_commit_command.py +++ b/tests/commands/test_commit_command.py @@ -368,124 +368,46 @@ def test_commit_command_with_config_message_length_limit( @pytest.mark.usefixtures("staging_is_clean") -def test_commit_command_with_body_length_limit_wrapping( - config, success_mock: MockType, mocker: MockFixture -): - """Test that long body lines are automatically wrapped to the specified limit.""" - mocker.patch( - "questionary.prompt", - return_value={ - "prefix": "feat", - "subject": "add feature", - "scope": "", - "is_breaking_change": False, - "body": "This is a very long line that exceeds 72 characters and should be automatically wrapped by the system to fit within the limit", - "footer": "", - }, - ) - - commit_mock = mocker.patch( - "commitizen.git.commit", return_value=cmd.Command("success", "", b"", b"", 0) - ) - - # Execute with body_length_limit - commands.Commit(config, {"body_length_limit": 72})() - success_mock.assert_called_once() - - # Verify wrapping occurred - committed_message = commit_mock.call_args[0][0] - lines = committed_message.split("\n") - assert lines[0] == "feat: add feature" - assert lines[1] == "" - body_lines = lines[2:] - for line in body_lines: - if line.strip(): - assert len(line) <= 72, ( - f"Line exceeds 72 chars: '{line}' ({len(line)} chars)" - ) - - -@pytest.mark.usefixtures("staging_is_clean") -def test_commit_command_with_body_length_limit_preserves_line_breaks( - config, success_mock: MockType, mocker: MockFixture -): - """Test that intentional line breaks (from | character) are preserved.""" - # Simulate what happens after multiple_line_breaker processes "line1 | line2 | line3" - mocker.patch( - "questionary.prompt", - return_value={ - "prefix": "feat", - "subject": "add feature", - "scope": "", - "is_breaking_change": False, - "body": "Line1 that is very long and exceeds the limit\nLine2 that is very long and exceeds the limit\nLine3 that is very long and exceeds the limit", - "footer": "", - }, - ) - - commit_mock = mocker.patch( - "commitizen.git.commit", return_value=cmd.Command("success", "", b"", b"", 0) - ) - - commands.Commit(config, {"body_length_limit": 45})() - success_mock.assert_called_once() - - committed_message = commit_mock.call_args[0][0] - lines = committed_message.split("\n") - - # Should have a subject, a blank line - assert lines[0] == "feat: add feature" - assert lines[1] == "" - # Each original line should be wrapped separately, preserving the line breaks - body_lines = lines[2:] - # All lines should be <= 45 chars - for line in body_lines: - if line.strip(): - assert len(line) == 45, ( - f"Line's length is not 45 chars: '{line}' ({len(line)} chars)" - ) - - -@pytest.mark.usefixtures("staging_is_clean") -def test_commit_command_with_body_length_limit_disabled( - config, success_mock: MockType, mocker: MockFixture -): - """Test that body_length_limit = 0 disables wrapping.""" - long_body = "This is a very long line that exceeds 72 characters and should NOT be wrapped when body_length_limit is set to 0" - - mocker.patch( - "questionary.prompt", - return_value={ - "prefix": "feat", - "subject": "add feature", - "scope": "", - "is_breaking_change": False, - "body": long_body, - "footer": "", - }, - ) - - commit_mock = mocker.patch( - "commitizen.git.commit", return_value=cmd.Command("success", "", b"", b"", 0) - ) - - # Execute with body_length_limit = 0 (disabled) - commands.Commit(config, {"body_length_limit": 0})() - - success_mock.assert_called_once() - - # Get the actual commit message - committed_message = commit_mock.call_args[0][0] - - # Verify the body was NOT wrapped (should contain the original long line) - assert long_body in committed_message, "Body should not be wrapped when limit is 0" - - -@pytest.mark.usefixtures("staging_is_clean") -def test_commit_command_with_body_length_limit_from_config( - config, success_mock: MockType, mocker: MockFixture +@pytest.mark.parametrize( + "test_id,body,body_length_limit", + [ + # Basic wrapping - long line gets wrapped + ( + "wrapping", + "This is a very long line that exceeds 72 characters and should be automatically wrapped by the system to fit within the limit", + 72, + ), + # Line break preservation - multiple lines with \n + ( + "preserves_line_breaks", + "Line1 that is very long and exceeds the limit\nLine2 that is very long and exceeds the limit\nLine3 that is very long and exceeds the limit", + 72, + ), + # Disabled wrapping - limit = 0 + ( + "disabled", + "This is a very long line that exceeds 72 characters and should NOT be wrapped when body_length_limit is set to 0", + 0, + ), + # No body - empty string + ( + "no_body", + "", + 72, + ), + ], +) +def test_commit_command_body_length_limit( + test_id, + body, + body_length_limit, + config, + success_mock: MockType, + commit_mock, + mocker: MockFixture, + file_regression, ): - """Test that body_length_limit can be set via config.""" + """Parameterized test for body_length_limit feature with file regression.""" mocker.patch( "questionary.prompt", return_value={ @@ -493,102 +415,29 @@ def test_commit_command_with_body_length_limit_from_config( "subject": "add feature", "scope": "", "is_breaking_change": False, - "body": "This is a very long line that exceeds 50 characters and should be wrapped", + "body": body, "footer": "", }, ) - commit_mock = mocker.patch( - "commitizen.git.commit", return_value=cmd.Command("success", "", b"", b"", 0) - ) - - # Set body_length_limit in config - config.settings["body_length_limit"] = 50 - + config.settings["body_length_limit"] = body_length_limit commands.Commit(config, {})() success_mock.assert_called_once() - - # Get the actual commit message committed_message = commit_mock.call_args[0][0] - # Verify all body lines are within the limit - lines = committed_message.split("\n") - body_lines = lines[2:] - for line in body_lines: - if line.strip(): - assert len(line) <= 50, ( - f"Line exceeds 50 chars: '{line}' ({len(line)} chars)" - ) - - -@pytest.mark.usefixtures("staging_is_clean") -def test_commit_command_body_length_limit_cli_overrides_config( - config, success_mock: MockType, mocker: MockFixture -): - """Test that CLI argument overrides config setting.""" - mocker.patch( - "questionary.prompt", - return_value={ - "prefix": "feat", - "subject": "add feature", - "scope": "", - "is_breaking_change": False, - "body": "This is a line that is longer than 40 characters but shorter than 80 characters", - "footer": "", - }, + # File regression check - uses test_id to create separate files + file_regression.check( + committed_message, + extension=".txt", + basename=f"test_commit_command_body_length_limit_{test_id}", ) - commit_mock = mocker.patch( - "commitizen.git.commit", return_value=cmd.Command("success", "", b"", b"", 0) - ) - - # Set config to 40 (would wrap) - config.settings["body_length_limit"] = 40 - - # Override with CLI argument to 0 (should NOT wrap) - commands.Commit(config, {"body_length_limit": 0})() - - success_mock.assert_called_once() - - # Get the actual commit message - committed_message = commit_mock.call_args[0][0] - - # The line should NOT be wrapped (CLI override to 0 disables wrapping) - assert ( - "This is a line that is longer than 40 characters but shorter than 80 characters" - in committed_message - ) - - -@pytest.mark.usefixtures("staging_is_clean") -def test_commit_command_with_body_length_limit_no_body( - config, success_mock: MockType, mocker: MockFixture -): - """Test that commits without body work correctly with body_length_limit set.""" - mocker.patch( - "questionary.prompt", - return_value={ - "prefix": "feat", - "subject": "add feature", - "scope": "", - "is_breaking_change": False, - "body": "", # No body - "footer": "", - }, - ) - - commit_mock = mocker.patch( - "commitizen.git.commit", return_value=cmd.Command("success", "", b"", b"", 0) - ) - - # Execute commit with body_length_limit (should not crash) - commands.Commit(config, {"body_length_limit": 72})() - - success_mock.assert_called_once() - - # Get the actual commit message - committed_message = commit_mock.call_args[0][0] - - # Should just be the subject line - assert committed_message.strip() == "feat: add feature" + # Validate line lengths if limit is not 0 + if body_length_limit > 0: + lines = committed_message.split("\n") + body_lines = lines[2:] # Skip subject and blank line + for line in body_lines: + assert len(line) <= body_length_limit, ( + f"Line exceeds {body_length_limit} chars: '{line}' ({len(line)} chars)" + ) diff --git a/tests/commands/test_commit_command/test_commit_command_body_length_limit_disabled.txt b/tests/commands/test_commit_command/test_commit_command_body_length_limit_disabled.txt new file mode 100644 index 0000000000..dea2570273 --- /dev/null +++ b/tests/commands/test_commit_command/test_commit_command_body_length_limit_disabled.txt @@ -0,0 +1,3 @@ +feat: add feature + +This is a very long line that exceeds 72 characters and should NOT be wrapped when body_length_limit is set to 0 \ No newline at end of file diff --git a/tests/commands/test_commit_command/test_commit_command_body_length_limit_no_body.txt b/tests/commands/test_commit_command/test_commit_command_body_length_limit_no_body.txt new file mode 100644 index 0000000000..4b0faba13c --- /dev/null +++ b/tests/commands/test_commit_command/test_commit_command_body_length_limit_no_body.txt @@ -0,0 +1 @@ +feat: add feature \ No newline at end of file diff --git a/tests/commands/test_commit_command/test_commit_command_body_length_limit_preserves_line_breaks.txt b/tests/commands/test_commit_command/test_commit_command_body_length_limit_preserves_line_breaks.txt new file mode 100644 index 0000000000..9f36353a22 --- /dev/null +++ b/tests/commands/test_commit_command/test_commit_command_body_length_limit_preserves_line_breaks.txt @@ -0,0 +1,5 @@ +feat: add feature + +Line1 that is very long and exceeds the limit +Line2 that is very long and exceeds the limit +Line3 that is very long and exceeds the limit \ No newline at end of file diff --git a/tests/commands/test_commit_command/test_commit_command_body_length_limit_wrapping.txt b/tests/commands/test_commit_command/test_commit_command_body_length_limit_wrapping.txt new file mode 100644 index 0000000000..a9591df70c --- /dev/null +++ b/tests/commands/test_commit_command/test_commit_command_body_length_limit_wrapping.txt @@ -0,0 +1,4 @@ +feat: add feature + +This is a very long line that exceeds 72 characters and should be +automatically wrapped by the system to fit within the limit \ No newline at end of file From 071e450d4d46424c993389c1f33b6073b9ca2f09 Mon Sep 17 00:00:00 2001 From: Ying-Fang Date: Sat, 7 Feb 2026 04:24:17 -0600 Subject: [PATCH 4/5] refactor(commit): rewrite logic and test for tag: --body-length-limit in commit --- commitizen/commands/commit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commitizen/commands/commit.py b/commitizen/commands/commit.py index 65fbf5fa41..65b7d4672f 100644 --- a/commitizen/commands/commit.py +++ b/commitizen/commands/commit.py @@ -111,7 +111,7 @@ def _wrap_body(self, message: str) -> str: "body_length_limit", self.config.settings.get("body_length_limit", 0) ) # By the contract, body_length_limit is set to 0 for no limit - if body_length_limit <= 0: + if not body_length_limit or body_length_limit <= 0: return message lines = message.split("\n") From 005b4228caffabfbb690370fec55b35f452f4985 Mon Sep 17 00:00:00 2001 From: Ying-Fang Date: Sat, 7 Feb 2026 05:03:38 -0600 Subject: [PATCH 5/5] fix(commit): add default value for --body-length-tag in the cli.py and use tuple argument in pytest.mark.parametrize --- commitizen/cli.py | 1 + tests/commands/test_commit_command.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/commitizen/cli.py b/commitizen/cli.py index fac79af3af..3be4a53556 100644 --- a/commitizen/cli.py +++ b/commitizen/cli.py @@ -167,6 +167,7 @@ def __call__( { "name": ["--body-length-limit"], "type": int, + "default": 0, "help": "Set the length limit of the commit body. Commit message in body will be rewrapped to this length; 0 for no limit.", }, { diff --git a/tests/commands/test_commit_command.py b/tests/commands/test_commit_command.py index 483c1d2c28..8f66047d70 100644 --- a/tests/commands/test_commit_command.py +++ b/tests/commands/test_commit_command.py @@ -369,7 +369,7 @@ def test_commit_command_with_config_message_length_limit( @pytest.mark.usefixtures("staging_is_clean") @pytest.mark.parametrize( - "test_id,body,body_length_limit", + ("test_id", "body", "body_length_limit"), [ # Basic wrapping - long line gets wrapped (