Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions commitizen/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can set a default value here?

Copy link
Contributor Author

@yjaw yjaw Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I agree. What’s the difference between setting a default value here and using default_setting in defaults.py?

},
{
"name": ["--"],
"action": "store_true",
Expand Down
22 changes: 22 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 typing import TYPE_CHECKING, TypedDict

import questionary
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand All @@ -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")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks like lines can be moved below

if len(lines) < 3:
return message

# First line is subject, second is blank line, rest is body
Comment on lines 117 to 121
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_rewrap_body uses message_parts but only defines lines = message.split("\n"), which will raise a NameError at runtime. Also, the current indexing logic only considers message_parts[2] (the third line) and reconstructs the message from just subject + blank + wrapped content, which will drop any additional body lines and the footer. Use the already-split list (lines) and wrap all lines after the subject/blank line while preserving existing blank lines and any footer content.

Copilot uses AI. Check for mistakes.
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:
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
76 changes: 76 additions & 0 deletions tests/commands/test_commit_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)"
)
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 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
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