Feature/ add a tag(--body-length-limit) for command commit#1849
Feature/ add a tag(--body-length-limit) for command commit#1849yjaw wants to merge 5 commits intocommitizen-tools:masterfrom
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. 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. |
| ) | ||
|
|
||
| # Execute with body_length_limit | ||
| commands.Commit(config, {"body_length_limit": 72})() |
There was a problem hiding this comment.
The number 72 should be extracted.
| assert len(line) <= 72, ( | ||
| f"Line exceeds 72 chars: '{line}' ({len(line)} chars)" | ||
| ) | ||
|
|
There was a problem hiding this comment.
Add a file regression check here and remove assert line[0] == ... and assert line[1] == ...
| commit_mock = mocker.patch( | ||
| "commitizen.git.commit", return_value=cmd.Command("success", "", b"", b"", 0) | ||
| ) |
There was a problem hiding this comment.
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(): |
There was a problem hiding this comment.
This if can be removed?
There was a problem hiding this comment.
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, ( |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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 checkThere was a problem hiding this comment.
I understand! This will make the test more organized.
commitizen/commands/commit.py
Outdated
| if ( | ||
| body_length_limit is None or body_length_limit <= 0 | ||
| ): # do nothing for no limit |
There was a problem hiding this comment.
| 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 |
There was a problem hiding this comment.
Quick question: I understand that the value is guaranteed, but according to defensive programming, isn’t it better to handle the null value?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
I probably would try not body_length_limit or body_length_limit < 0
There was a problem hiding this comment.
Yes, I found that if I don’t handle None, it can’t commit due to a Null error.
commitizen/commands/commit.py
Outdated
| # 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}" |
There was a problem hiding this comment.
| # 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)) |
There was a problem hiding this comment.
Wow, this code is so clean! I Learned a lot.
commitizen/commands/commit.py
Outdated
| if ( | ||
| body_length_limit is None or body_length_limit <= 0 | ||
| ): # do nothing for no limit |
There was a problem hiding this comment.
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.
commitizen/commands/commit.py
Outdated
| body_lines = body.split("\n") | ||
| wrapped_body_lines = [] | ||
| for line in body_lines: | ||
| wrapped_body_lines.append(textwrap.fill(line, width=body_length_limit)) |
There was a problem hiding this comment.
we should use wrap instead of fill
Reference: https://docs.python.org/3/library/textwrap.html#textwrap.wrap
There was a problem hiding this comment.
also we should prefer using list comphrehension
There was a problem hiding this comment.
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, ( |
There was a problem hiding this comment.
That is strange. Wrapping should not guarantee exactly N chars long.
commitizen/commands/commit.py
Outdated
| f"Length of commit message exceeds limit ({len(subject)}/{message_length_limit}), subject: '{subject}'" | ||
| ) | ||
|
|
||
| def _rewrap_body(self, message: str) -> str: |
There was a problem hiding this comment.
why _rewrap? I think _wrap_body is better
There was a problem hiding this comment.
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_limitto default settings/config schema and expose it as--body-length-limitforcz commit. - Implement body rewrapping logic in the commit command using
textwrap. - Add/adjust regression snapshots for
cz commit --helpand 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.
| lines = message.split("\n") | ||
| if len(message_parts) < 3: | ||
| return message | ||
|
|
||
| # First line is subject, second is blank line, rest is body |
There was a problem hiding this comment.
_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.
| # All lines should be <= 45 chars | ||
| for line in body_lines: | ||
| if line.strip(): | ||
| assert len(line) == 45, ( |
There was a problem hiding this comment.
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).
| assert len(line) == 45, ( | |
| assert len(line) <= 45, ( |
| 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": "", | ||
| }, |
There was a problem hiding this comment.
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.
commitizen/commands/commit.py
Outdated
| if ( | ||
| body_length_limit is None or body_length_limit <= 0 | ||
| ): # do nothing for no limit |
There was a problem hiding this comment.
I probably would try not body_length_limit or body_length_limit < 0
| ): # do nothing for no limit | ||
| return message | ||
|
|
||
| lines = message.split("\n") |
There was a problem hiding this comment.
looks like lines can be moved below
commitizen/commands/commit.py
Outdated
| body_lines = body.split("\n") | ||
| wrapped_body_lines = [] | ||
| for line in body_lines: | ||
| wrapped_body_lines.append(textwrap.fill(line, width=body_length_limit)) |
There was a problem hiding this comment.
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.", |
There was a problem hiding this comment.
I think we can set a default value here?
There was a problem hiding this comment.
Yes, I agree. What’s the difference between setting a default value here and using default_setting in defaults.py?
…limit in commit command
49bb71b to
071e450
Compare
…d use tuple argument in pytest.mark.parametrize
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:
\ncharacter.textwrapto rewrap that line.Checklist
Was generative AI tooling used to co-author this PR?
Generated-by: [Gemini] following the guidelines
Code Changes
uv run poe alllocally to ensure this change passes linter check and testsDocumentation Changes
uv run poe doclocally to ensure the documentation pages renders correctlyExpected Behavior
Steps to Test This Pull Request
Additional Context
close #1597