Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit f66afbc

Browse files
feat: add custom validation
1 parent d6547c1 commit f66afbc

File tree

5 files changed

+236
-29
lines changed

5 files changed

+236
-29
lines changed

‎commitizen/commands/check.py

Lines changed: 14 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
from __future__ import annotations
22

33
import os
4-
import re
54
import sys
65
from typing import Any
76

@@ -65,30 +64,30 @@ def __call__(self):
6564
"""Validate if commit messages follows the conventional pattern.
6665
6766
Raises:
68-
InvalidCommitMessageError: if the commit provided not follows the conventional pattern
67+
InvalidCommitMessageError: if the commit provided does not follow the conventional pattern
6968
"""
7069
commits = self._get_commits()
7170
if not commits:
7271
raise NoCommitsFoundError(f"No commit found with range: '{self.rev_range}'")
7372

7473
pattern = self.cz.schema_pattern()
7574
ill_formated_commits = [
76-
commit
75+
(commit, check[1])
7776
for commit in commits
78-
if not self.validate_commit_message(commit.message, pattern)
77+
if not (
78+
check := self.cz.validate_commit_message(
79+
commit.message,
80+
pattern,
81+
allow_abort=self.allow_abort,
82+
allowed_prefixes=self.allowed_prefixes,
83+
max_msg_length=self.max_msg_length,
84+
)
85+
)[0]
7986
]
80-
displayed_msgs_content = "\n".join(
81-
[
82-
f'commit "{commit.rev}": "{commit.message}"'
83-
for commit in ill_formated_commits
84-
]
85-
)
86-
if displayed_msgs_content:
87+
88+
if ill_formated_commits:
8789
raise InvalidCommitMessageError(
88-
"commit validation: failed!\n"
89-
"please enter a commit message in the commitizen format.\n"
90-
f"{displayed_msgs_content}\n"
91-
f"pattern: {pattern}"
90+
self.cz.format_exception_message(ill_formated_commits)
9291
)
9392
out.success("Commit validation: successful!")
9493

@@ -139,15 +138,3 @@ def _filter_comments(msg: str) -> str:
139138
if not line.startswith("#"):
140139
lines.append(line)
141140
return "\n".join(lines)
142-
143-
def validate_commit_message(self, commit_msg: str, pattern: str) -> bool:
144-
if not commit_msg:
145-
return self.allow_abort
146-
147-
if any(map(commit_msg.startswith, self.allowed_prefixes)):
148-
return True
149-
if self.max_msg_length:
150-
msg_len = len(commit_msg.partition("\n")[0].strip())
151-
if msg_len > self.max_msg_length:
152-
return False
153-
return bool(re.match(pattern, commit_msg))

‎commitizen/cz/base.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
import re
34
from abc import ABCMeta, abstractmethod
45
from typing import Any, Callable, Iterable, Protocol
56

@@ -95,6 +96,46 @@ def schema_pattern(self) -> str | None:
9596
"""Regex matching the schema used for message validation."""
9697
raise NotImplementedError("Not Implemented yet")
9798

99+
def validate_commit_message(
100+
self,
101+
commit_msg: str,
102+
pattern: str | None,
103+
allow_abort: bool,
104+
allowed_prefixes: list[str],
105+
max_msg_length: int,
106+
) -> tuple[bool, list]:
107+
"""Validate commit message against the pattern."""
108+
if not commit_msg:
109+
return allow_abort, []
110+
111+
if pattern is None:
112+
return True, []
113+
114+
if any(map(commit_msg.startswith, allowed_prefixes)):
115+
return True, []
116+
if max_msg_length:
117+
msg_len = len(commit_msg.partition("\n")[0].strip())
118+
if msg_len > max_msg_length:
119+
return False, []
120+
return bool(re.match(pattern, commit_msg)), []
121+
122+
def format_exception_message(
123+
self, ill_formated_commits: list[tuple[git.GitCommit, list]]
124+
) -> str:
125+
"""Format commit errors."""
126+
displayed_msgs_content = "\n".join(
127+
[
128+
f'commit "{commit.rev}": "{commit.message}"'
129+
for commit, _ in ill_formated_commits
130+
]
131+
)
132+
return (
133+
"commit validation: failed!\n"
134+
"please enter a commit message in the commitizen format.\n"
135+
f"{displayed_msgs_content}\n"
136+
f"pattern: {self.schema_pattern}"
137+
)
138+
98139
def info(self) -> str | None:
99140
"""Information about the standardized commit message."""
100141
raise NotImplementedError("Not Implemented yet")

‎docs/customization.md

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
Customizing commitizen is not hard at all.
1+
from commitizen import BaseCommitizenCustomizing commitizen is not hard at all.
22
We have two different ways to do so.
33

44
## 1. Customize in configuration file
@@ -308,6 +308,72 @@ cz -n cz_strange bump
308308

309309
[convcomms]: https://github.com/commitizen-tools/commitizen/blob/master/commitizen/cz/conventional_commits/conventional_commits.py
310310

311+
### Custom commit validation and error message
312+
313+
The commit message validation can be customized by overriding the `validate_commit_message` and `format_error_message`
314+
methods from `BaseCommitizen`. This allows for a more detailed feedback to the user where the error originates from.
315+
316+
```python
317+
import re
318+
319+
from commitizen.cz.base import BaseCommitizen
320+
from commitizen import git
321+
322+
323+
class CustomValidationCz(BaseCommitizen):
324+
def validate_commit_message(
325+
self,
326+
commit_msg: str,
327+
pattern: str | None,
328+
allow_abort: bool,
329+
allowed_prefixes: list[str],
330+
max_msg_length: int,
331+
) -> tuple[bool, list]:
332+
"""Validate commit message against the pattern."""
333+
if not commit_msg:
334+
return allow_abort, [] if allow_abort else [f"commit message is empty"]
335+
336+
if pattern is None:
337+
return True, []
338+
339+
if any(map(commit_msg.startswith, allowed_prefixes)):
340+
return True, []
341+
if max_msg_length:
342+
msg_len = len(commit_msg.partition("\n")[0].strip())
343+
if msg_len > max_msg_length:
344+
return False, [
345+
f"commit message is too long. Max length is {max_msg_length}"
346+
]
347+
pattern_match = re.match(pattern, commit_msg)
348+
if pattern_match:
349+
return True, []
350+
else:
351+
# Perform additional validation of the commit message format
352+
# and add custom error messages as needed
353+
return False, ["commit message does not match the pattern"]
354+
355+
def format_exception_message(
356+
self, ill_formated_commits: list[tuple[git.GitCommit, list]]
357+
) -> str:
358+
"""Format commit errors."""
359+
displayed_msgs_content = "\n".join(
360+
[
361+
(
362+
f'commit "{commit.rev}": "{commit.message}"'
363+
f"errors:\n"
364+
"\n".join((f"- {error}" for error in errors))
365+
)
366+
for commit, errors in ill_formated_commits
367+
]
368+
)
369+
return (
370+
"commit validation: failed!\n"
371+
"please enter a commit message in the commitizen format.\n"
372+
f"{displayed_msgs_content}\n"
373+
f"pattern: {self.schema_pattern}"
374+
)
375+
```
376+
311377
### Custom changelog generator
312378

313379
The changelog generator should just work in a very basic manner without touching anything.

‎tests/commands/test_check_command.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -452,3 +452,44 @@ def test_check_command_with_message_length_limit_exceeded(config, mocker: MockFi
452452
with pytest.raises(InvalidCommitMessageError):
453453
check_cmd()
454454
error_mock.assert_called_once()
455+
456+
457+
@pytest.mark.usefixtures("use_cz_custom_validator")
458+
def test_check_command_with_custom_validator_succeed(mocker: MockFixture, capsys):
459+
testargs = [
460+
"cz",
461+
"--name",
462+
"cz_custom_validator",
463+
"check",
464+
"--commit-msg-file",
465+
"some_file",
466+
]
467+
mocker.patch.object(sys, "argv", testargs)
468+
mocker.patch(
469+
"commitizen.commands.check.open",
470+
mocker.mock_open(read_data="ABC-123: add commitizen pre-commit hook"),
471+
)
472+
cli.main()
473+
out, _ = capsys.readouterr()
474+
assert "Commit validation: successful!" in out
475+
476+
477+
@pytest.mark.usefixtures("use_cz_custom_validator")
478+
def test_check_command_with_custom_validator_failed(mocker: MockFixture):
479+
testargs = [
480+
"cz",
481+
"--name",
482+
"cz_custom_validator",
483+
"check",
484+
"--commit-msg-file",
485+
"some_file",
486+
]
487+
mocker.patch.object(sys, "argv", testargs)
488+
mocker.patch(
489+
"commitizen.commands.check.open",
490+
mocker.mock_open(read_data="ABC-123 add commitizen pre-commit hook"),
491+
)
492+
with pytest.raises(InvalidCommitMessageError) as excinfo:
493+
cli.main()
494+
assert "commit validation: failed!" in str(excinfo.value)
495+
assert "commit message does not match pattern" in str(excinfo.value)

‎tests/conftest.py

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import pytest
1010
from pytest_mock import MockerFixture
1111

12-
from commitizen import cmd, defaults
12+
from commitizen import cmd, defaults, git
1313
from commitizen.changelog_formats import (
1414
ChangelogFormat,
1515
get_changelog_format,
@@ -231,6 +231,78 @@ def mock_plugin(mocker: MockerFixture, config: BaseConfig) -> BaseCommitizen:
231231
return mock
232232

233233

234+
class ValidationCz(BaseCommitizen):
235+
def questions(self):
236+
return [
237+
{"type": "input", "name": "commit", "message": "Initial commit:\n"},
238+
{"type": "input", "name": "issue_nb", "message": "ABC-123"},
239+
]
240+
241+
def message(self, answers: dict):
242+
return f"{answers['issue_nb']}: {answers['commit']}"
243+
244+
def schema(self):
245+
return "<issue_nb>: <commit>"
246+
247+
def schema_pattern(self):
248+
return r"^(?P<issue_nb>[A-Z]{3}-\d+): (?P<commit>.*)$"
249+
250+
def validate_commit_message(
251+
self,
252+
commit_msg: str,
253+
pattern: str | None,
254+
allow_abort: bool,
255+
allowed_prefixes: list[str],
256+
max_msg_length: int,
257+
) -> tuple[bool, list]:
258+
"""Validate commit message against the pattern."""
259+
if not commit_msg:
260+
return allow_abort, [] if allow_abort else ["commit message is empty"]
261+
262+
if pattern is None:
263+
return True, []
264+
265+
if any(map(commit_msg.startswith, allowed_prefixes)):
266+
return True, []
267+
if max_msg_length:
268+
msg_len = len(commit_msg.partition("\n")[0].strip())
269+
if msg_len > max_msg_length:
270+
return False, [
271+
f"commit message is too long. Max length is {max_msg_length}"
272+
]
273+
pattern_match = bool(re.match(pattern, commit_msg))
274+
if not pattern_match:
275+
return False, [f"commit message does not match pattern {pattern}"]
276+
return True, []
277+
278+
def format_exception_message(
279+
self, ill_formated_commits: list[tuple[git.GitCommit, list]]
280+
) -> str:
281+
"""Format commit errors."""
282+
displayed_msgs_content = "\n".join(
283+
[
284+
(
285+
f'commit "{commit.rev}": "{commit.message}"\n'
286+
f"errors:\n"
287+
"\n".join(f"- {error}" for error in errors)
288+
)
289+
for (commit, errors) in ill_formated_commits
290+
]
291+
)
292+
return (
293+
"commit validation: failed!\n"
294+
"please enter a commit message in the commitizen format.\n"
295+
f"{displayed_msgs_content}\n"
296+
f"pattern: {self.schema_pattern}"
297+
)
298+
299+
300+
@pytest.fixture
301+
def use_cz_custom_validator(mocker):
302+
new_cz = {**registry, "cz_custom_validator": ValidationCz}
303+
mocker.patch.dict("commitizen.cz.registry", new_cz)
304+
305+
234306
SUPPORTED_FORMATS = ("markdown", "textile", "asciidoc", "restructuredtext")
235307

236308

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /