Skip to content

Feature/ add a tag(--body-length-limit) for command commit#1849

Open
yjaw wants to merge 5 commits intocommitizen-tools:masterfrom
yjaw:feature/commit-line-limt
Open

Feature/ add a tag(--body-length-limit) for command commit#1849
yjaw wants to merge 5 commits intocommitizen-tools:masterfrom
yjaw:feature/commit-line-limt

Conversation

@yjaw
Copy link
Contributor

@yjaw yjaw commented Feb 5, 2026

Description

I added a tag (—body-length-limit) for command commit. This tag utilizes Python’s built-in library, textwrap, to rewrap the body. It also respects the user’s |(\n) signal. This tag will affect the footer as well.
The flow is as follows:

  1. Split the body into separate lines using the \n character.
  2. Start from the third line, where the body begins. Use textwrap to rewrap that line.
  3. Reconstruct the entire body message from those rewrapped lines.

Checklist

Was generative AI tooling used to co-author this PR?

  • Yes (please specify the tool below)

Generated-by: [Gemini] following the guidelines

Code Changes

  • Add test cases to all the changes you introduce
  • Run uv run poe all locally to ensure this change passes linter check and tests
  • Manually test the changes:
    • Verify the feature/bug fix works as expected in real-world scenarios
    • Test edge cases and error conditions
    • Ensure backward compatibility is maintained
    • Document any manual testing steps performed
  • Update the documentation for the changes

Documentation Changes

  • Run uv run poe doc locally to ensure the documentation pages renders correctly
  • Check and fix any broken links (internal or external)

Expected Behavior

  1. This option should modify the line length for your message if its length exceeds the limit you’ve set.
  2. Adding this tag or setting it to 0 should have no effect.
  3. If the message body is empty, it will be fine.
  4. This option will affect the text in both the body and footer sections.

Steps to Test This Pull Request

  1. When using the commit command, you can add the —body-length-limit [int] option to set a limit on the length of the commit message. If you don’t want to set a limit, you can set the option to 0.

Additional Context

close #1597

@codecov
Copy link

codecov bot commented Feb 5, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.00%. Comparing base (26e5d80) to head (005b422).
⚠️ Report is 21 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1849      +/-   ##
==========================================
+ Coverage   97.98%   98.00%   +0.01%     
==========================================
  Files          60       60              
  Lines        2686     2706      +20     
==========================================
+ Hits         2632     2652      +20     
  Misses         54       54              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

Copy link
Collaborator

@bearomorphism bearomorphism left a comment

Choose a reason for hiding this comment

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

Thanks!

)

# Execute with body_length_limit
commands.Commit(config, {"body_length_limit": 72})()
Copy link
Collaborator

Choose a reason for hiding this comment

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

The number 72 should be extracted.

assert len(line) <= 72, (
f"Line exceeds 72 chars: '{line}' ({len(line)} chars)"
)

Copy link
Collaborator

Choose a reason for hiding this comment

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

Add a file regression check here and remove assert line[0] == ... and assert line[1] == ...

Comment on lines 387 to 389
commit_mock = mocker.patch(
"commitizen.git.commit", return_value=cmd.Command("success", "", b"", b"", 0)
)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Please reuse commit_mock fixture. Same for other new tests.

assert lines[1] == ""
body_lines = lines[2:]
for line in body_lines:
if line.strip():
Copy link
Collaborator

Choose a reason for hiding this comment

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

This if can be removed?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I believe so. This line was intended to skip empty lines, which are no longer necessary.

# All lines should be <= 45 chars
for line in body_lines:
if line.strip():
assert len(line) == 45, (
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why not <=?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In this test case, every line is exactly 45 characters long, so I check if it truly wraps to 45-character lines. However, there’s a case where it wraps to a line shorter than 45 characters which is not the intended behavior but test still can pass.

  • I forgot to modify the comment.

Copy link
Collaborator

Choose a reason for hiding this comment

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

That is strange. Wrapping should not guarantee exactly N chars long.



@pytest.mark.usefixtures("staging_is_clean")
def test_commit_command_with_body_length_limit_wrapping(
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think all the new tests can be merged into one and use parameterize to shorten the tests

Basically we can do like following:

fixtures
parameterize
def test_...(...):
    mocker.patch(
        "questionary.prompt",
        return_value={
            "prefix": "feat",
            "subject": "add feature",
            "scope": "",
            "is_breaking_change": False,
            "body": body, # parameterized, can be a long text with / without line breaks or short strings or an empty string
            "footer": "",
        },
    )

    # commit with parameterized config

    # check each line in line[2:] does not exceed the line length limit if it is not 0

    # file regression check

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I understand! This will make the test more organized.

Comment on lines 113 to 115
if (
body_length_limit is None or body_length_limit <= 0
): # do nothing for no limit
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
if (
body_length_limit is None or body_length_limit <= 0
): # do nothing for no limit
if (
body_length_limit <= 0
): # do nothing for no limit

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Quick question: I understand that the value is guaranteed, but according to defensive programming, isn’t it better to handle the null value?

Copy link
Collaborator

Choose a reason for hiding this comment

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

I am not sure whether body_length_limit is guaranteed to be an int. If it is None under some code paths, then something might be wrong.

Copy link
Member

Choose a reason for hiding this comment

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

I probably would try not body_length_limit or body_length_limit < 0

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, I found that if I don’t handle None, it can’t commit due to a Null error.

Comment on lines 122 to 131
# 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}"
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
# 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}"
# First line is subject, second is blank line, rest is body
wrapped_body_lines = [textwrap.fill(line, width=body_length_limit) for line in lines[2:]]
return "\n".join(chain(lines[:2], wrapped_body_lines))

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Wow, this code is so clean! I Learned a lot.

Comment on lines 113 to 115
if (
body_length_limit is None or body_length_limit <= 0
): # do nothing for no limit
Copy link
Collaborator

Choose a reason for hiding this comment

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

I am not sure whether body_length_limit is guaranteed to be an int. If it is None under some code paths, then something might be wrong.

body_lines = body.split("\n")
wrapped_body_lines = []
for line in body_lines:
wrapped_body_lines.append(textwrap.fill(line, width=body_length_limit))
Copy link
Collaborator

Choose a reason for hiding this comment

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

we should use wrap instead of fill

Reference: https://docs.python.org/3/library/textwrap.html#textwrap.wrap

Copy link
Member

Choose a reason for hiding this comment

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

also we should prefer using list comphrehension

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Why is wrap used instead of fill in this context?

# All lines should be <= 45 chars
for line in body_lines:
if line.strip():
assert len(line) == 45, (
Copy link
Collaborator

Choose a reason for hiding this comment

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

That is strange. Wrapping should not guarantee exactly N chars long.

f"Length of commit message exceeds limit ({len(subject)}/{message_length_limit}), subject: '{subject}'"
)

def _rewrap_body(self, message: str) -> str:
Copy link
Collaborator

Choose a reason for hiding this comment

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

why _rewrap? I think _wrap_body is better

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds support for limiting/wrapping commit message body line length during cz commit, via a new --body-length-limit CLI option and a corresponding body_length_limit config/default setting.

Changes:

  • Add body_length_limit to default settings/config schema and expose it as --body-length-limit for cz commit.
  • Implement body rewrapping logic in the commit command using textwrap.
  • Add/adjust regression snapshots for cz commit --help and add commit-command tests around the new wrapping behavior.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
commitizen/commands/commit.py Adds body rewrapping logic (_rewrap_body) during interactive commit message creation.
commitizen/cli.py Adds --body-length-limit argument to the commit subcommand.
commitizen/defaults.py Introduces body_length_limit in Settings and DEFAULT_SETTINGS.
tests/test_conf.py Updates expected default config dictionaries to include body_length_limit.
tests/commands/test_commit_command.py Adds tests for body wrapping and config/CLI precedence.
tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_10_commit_.txt Updates CLI help snapshot to include --body-length-limit.
tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_11_commit_.txt Updates CLI help snapshot to include --body-length-limit.
tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_12_commit_.txt Updates CLI help snapshot to include --body-length-limit.
tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_13_commit_.txt Updates CLI help snapshot to include --body-length-limit.
tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_14_commit_.txt Updates CLI help snapshot to include --body-length-limit.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 118 to 121
lines = message.split("\n")
if len(message_parts) < 3:
return message

# First line is subject, second is blank line, rest is body
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.
# All lines should be <= 45 chars
for line in body_lines:
if line.strip():
assert len(line) == 45, (
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.

This test asserts every wrapped line has length exactly 45, but textwrap wrapping does not guarantee all lines are exactly the width (the last line of a wrapped paragraph is typically shorter). This assertion will be flaky/incorrect; it should validate <= 45 (and, if needed, separately assert that wrapping occurred).

Suggested change
assert len(line) == 45, (
assert len(line) <= 45, (

Copilot uses AI. Check for mistakes.
Comment on lines 377 to 384
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": "",
},
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.

All newly added body-length-limit tests set footer to an empty string, but the PR description states the option should affect the footer as well. Add at least one test case with a non-empty footer to ensure footer lines are wrapped/preserved correctly.

Copilot uses AI. Check for mistakes.
Comment on lines 113 to 115
if (
body_length_limit is None or body_length_limit <= 0
): # do nothing 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 probably would try not body_length_limit or body_length_limit < 0

): # do nothing for no limit
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

body_lines = body.split("\n")
wrapped_body_lines = []
for line in body_lines:
wrapped_body_lines.append(textwrap.fill(line, width=body_length_limit))
Copy link
Member

Choose a reason for hiding this comment

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

also we should prefer using list comphrehension

{
"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.",
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?

…d use tuple argument in pytest.mark.parametrize
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add option for body line length

3 participants