diff --git a/commitizen/cli.py b/commitizen/cli.py index 79988fb5c..3be4a5355 100644 --- a/commitizen/cli.py +++ b/commitizen/cli.py @@ -164,6 +164,12 @@ def __call__( "type": int, "help": "Set the length limit of the commit message; 0 for no limit.", }, + { + "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.", + }, { "name": ["--"], "action": "store_true", diff --git a/commitizen/commands/commit.py b/commitizen/commands/commit.py index 5776af420..65b7d4672 100644 --- a/commitizen/commands/commit.py +++ b/commitizen/commands/commit.py @@ -5,6 +5,8 @@ import shutil import subprocess import tempfile +import textwrap +from itertools import chain from typing import TYPE_CHECKING, TypedDict import questionary @@ -37,6 +39,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 +87,7 @@ def _get_message_by_prompt_commit_questions(self) -> str: message = self.cz.message(answers) self._validate_subject_length(message) + message = self._wrap_body(message) return message def _validate_subject_length(self, message: str) -> None: @@ -102,6 +106,24 @@ def _validate_subject_length(self, message: str) -> None: f"Length of commit message exceeds limit ({len(subject)}/{message_length_limit}), subject: '{subject}'" ) + 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 not body_length_limit or body_length_limit <= 0: + return message + + lines = message.split("\n") + if len(lines) < 3: + return message + + # First line is subject, second is blank line, rest is 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() if editor is None: diff --git a/commitizen/defaults.py b/commitizen/defaults.py index 4865ccc18..94bd16669 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_commit_command.py b/tests/commands/test_commit_command.py index c80a13823..8f66047d7 100644 --- a/tests/commands/test_commit_command.py +++ b/tests/commands/test_commit_command.py @@ -365,3 +365,79 @@ 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") +@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, +): + """Parameterized test for body_length_limit feature with file regression.""" + mocker.patch( + "questionary.prompt", + return_value={ + "prefix": "feat", + "subject": "add feature", + "scope": "", + "is_breaking_change": False, + "body": body, + "footer": "", + }, + ) + + config.settings["body_length_limit"] = body_length_limit + commands.Commit(config, {})() + + success_mock.assert_called_once() + committed_message = commit_mock.call_args[0][0] + + # 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}", + ) + + # 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 000000000..dea257027 --- /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 000000000..4b0faba13 --- /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 000000000..9f36353a2 --- /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 000000000..a9591df70 --- /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 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 bd256ccf8..7cc8366ee 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 bd256ccf8..7cc8366ee 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 bd256ccf8..7cc8366ee 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 cbd5780f6..df3df7c54 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 cbd5780f6..df3df7c54 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 f1ff76ff8..9d69b6304 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, }