Skip to content
5 changes: 5 additions & 0 deletions commitizen/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Comment thread
Lee-W marked this conversation as resolved.
},
{
"name": ["--"],
"action": "store_true",
Expand Down
26 changes: 26 additions & 0 deletions commitizen/commands/commit.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import shutil
import subprocess
import tempfile
import textwrap
from itertools import chain
from pathlib import Path
from typing import TYPE_CHECKING, TypedDict

Expand Down Expand Up @@ -36,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
Expand Down Expand Up @@ -82,6 +85,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:
Expand All @@ -100,6 +104,28 @@ 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:
Comment thread
bearomorphism marked this conversation as resolved.
"""
Wrap the body of the commit message to the --body-length-limit length.
"""

body_length_limit = self.arguments.get(
"body_length_limit", self.config.settings["body_length_limit"]
)
# 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")
Comment thread
bearomorphism marked this conversation as resolved.
if len(lines) < 3:
return message

# First line is subject, second is blank line, rest are body lines
wrapped_body_lines = chain.from_iterable(
textwrap.wrap(line, width=body_length_limit) for line in lines[2:]
)
return "\n".join(chain(lines[:2], wrapped_body_lines))

def manual_edit(self, message: str) -> str:
editor = git.get_core_editor()
if editor is None:
Expand Down
2 changes: 2 additions & 0 deletions commitizen/defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"
Expand Down
67 changes: 67 additions & 0 deletions tests/commands/test_commit_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -368,3 +368,70 @@ 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(
("body", "body_length_limit"),
[
pytest.param(
"This is a very long line that exceeds 72 characters and should be automatically wrapped by the system to fit within the limit",
72,
id="wrapping",
),
pytest.param(
"Line1 is shorter than the limit but has newline\nLine2 is shorter than the limit but has newline\nLine3 is shorter than the limit but has newline",
100,
id="preserves_line_breaks",
),
pytest.param(
"This is a very long line that exceeds 72 characters and should NOT be wrapped when body_length_limit is set to 0",
0,
id="disabled",
),
pytest.param(
"",
72,
id="no_body",
),
],
)
def test_commit_command_body_length_limit(
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": "",
},
)

commands.Commit(config, {"body_length_limit": body_length_limit})()
success_mock.assert_called_once()
committed_message = commit_mock.call_args[0][0]
file_regression.check(committed_message, extension=".txt")

lines = committed_message.split("\n")
body_lines = lines[2:] # Skip subject and blank line

if body_length_limit > 0:
for line in body_lines:
assert len(line) <= body_length_limit, (
f"Line exceeds {body_length_limit} chars: '{line}' ({len(line)} chars)"
)
elif body_length_limit == 0:
assert len(body_lines) == 1, (
"Body should not be wrapped when body_length_limit is set to 0"
)
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
feat: add feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
feat: add feature

Line1 is shorter than the limit but has newline
Line2 is shorter than the limit but has newline
Line3 is shorter than the limit but has newline
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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).
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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).
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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).
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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).
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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).
2 changes: 2 additions & 0 deletions tests/test_conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@
"extras": {},
"breaking_change_exclamation_in_title": False,
"message_length_limit": 0,
"body_length_limit": 0,
}

_new_settings: dict[str, Any] = {
Expand Down Expand Up @@ -152,6 +153,7 @@
"extras": {},
"breaking_change_exclamation_in_title": False,
"message_length_limit": 0,
"body_length_limit": 0,
}


Expand Down