Skip to content

Commit ad10cec

Browse files
authored
feat: Validate multi-line messages (#68)
2 parents 2a87832 + bebe84a commit ad10cec

File tree

7 files changed

+112
-11
lines changed

7 files changed

+112
-11
lines changed

conventional_pre_commit/format.py

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,13 @@ def r_delim():
4040

4141

4242
def r_subject():
43-
"""Regex str for subject line, body, footer."""
44-
return r" .+"
43+
"""Regex str for subject line."""
44+
return r" .+$"
45+
46+
47+
def r_body():
48+
"""Regex str for the body"""
49+
return r"(?P<multi>\r?\n(?P<sep>^$\r?\n)?.+)?"
4550

4651

4752
def r_autosquash_prefixes():
@@ -56,18 +61,23 @@ def conventional_types(types=[]):
5661
return types
5762

5863

59-
def is_conventional(input, types=DEFAULT_TYPES, optional_scope=True):
64+
def is_conventional(input, types=DEFAULT_TYPES, optional_scope=True, is_strict=False):
6065
"""
6166
Returns True if input matches Conventional Commits formatting
6267
https://www.conventionalcommits.org
6368
6469
Optionally provide a list of additional custom types.
6570
"""
6671
types = conventional_types(types)
67-
pattern = f"^({r_types(types)}){r_scope(optional_scope)}{r_delim()}{r_subject()}$"
68-
regex = re.compile(pattern, re.DOTALL)
72+
pattern = f"^({r_types(types)}){r_scope(optional_scope)}{r_delim()}{r_subject()}{r_body()}"
73+
regex = re.compile(pattern, re.MULTILINE)
6974

70-
return bool(regex.match(input))
75+
result = regex.match(input)
76+
is_valid_subject = bool(result)
77+
if is_valid_subject and is_strict and result.group("multi") and not result.group("sep"):
78+
is_valid_subject = False
79+
80+
return is_valid_subject
7181

7282

7383
def has_autosquash_prefix(input):

conventional_pre_commit/hook.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ def main(argv=[]):
5454
if format.has_autosquash_prefix(message):
5555
return RESULT_SUCCESS
5656

57-
if format.is_conventional(message, args.types, args.optional_scope):
57+
if format.is_conventional(message, args.types, args.optional_scope, args.strict):
5858
return RESULT_SUCCESS
5959
else:
6060
print(
@@ -64,7 +64,7 @@ def main(argv=[]):
6464
{Colors.LBLUE}https://www.conventionalcommits.org/{Colors.YELLOW}
6565
6666
Conventional Commits start with one of the below types, followed by a colon,
67-
followed by the commit message:{Colors.RESTORE}
67+
followed by the commit subject and an optional body seperated by a blank line:{Colors.RESTORE}
6868
6969
{" ".join(format.conventional_types(args.types))}
7070
@@ -78,7 +78,14 @@ def main(argv=[]):
7878
7979
{Colors.YELLOW}Example commit with scope in parentheses after the type for more context:{Colors.RESTORE}
8080
81-
fix(account): remove infinite loop"""
81+
fix(account): remove infinite loop
82+
83+
{Colors.YELLOW}Example commit with a body
84+
85+
fix: remove infinite loop
86+
87+
Additional information on the issue caused by the infinite loop
88+
"""
8289
)
8390
return RESULT_FAIL
8491

tests/conftest.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,13 @@ def conventional_gbk_commit_path():
4242
@pytest.fixture
4343
def fixup_commit_path():
4444
return get_message_path("fixup_commit")
45+
46+
47+
@pytest.fixture
48+
def conventional_commit_bad_multi_line_path():
49+
return get_message_path("conventional_commit_bad_multi_line")
50+
51+
52+
@pytest.fixture
53+
def conventional_commit_multi_line_path():
54+
return get_message_path("conventional_commit_multi_line")
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
fix: message
2+
no blank line
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
fix: message
2+
3+
A blank line is there

tests/test_format.py

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -162,13 +162,64 @@ def test_is_conventional__with_scope():
162162
assert format.is_conventional(input)
163163

164164

165-
def test_is_conventional__body_multiline():
165+
def test_is_conventional__body_multiline_not_strict():
166166
input = """feat(scope): message
167167
168168
more message
169169
"""
170170

171-
assert format.is_conventional(input)
171+
assert format.is_conventional(input, is_strict=False)
172+
173+
174+
def test_is_conventional__body_multiline_no_body_not_strict():
175+
input = """feat(scope): message
176+
177+
"""
178+
assert format.is_conventional(input, is_strict=False)
179+
180+
181+
def test_is_conventional__body_multiline_body_bad_type_strict():
182+
input = """wrong: message
183+
184+
more_message
185+
"""
186+
187+
assert not format.is_conventional(input, is_strict=True)
188+
189+
190+
def test_is_conventional__bad_body_multiline_not_strict():
191+
input = """feat(scope): message
192+
more message
193+
"""
194+
195+
assert format.is_conventional(input, is_strict=False)
196+
197+
198+
def test_is_conventional__bad_body_multiline_strict():
199+
input = """feat(scope): message
200+
more message
201+
"""
202+
203+
assert not format.is_conventional(input, is_strict=True)
204+
205+
206+
def test_is_conventional__body_multiline_strict():
207+
input = """feat(scope): message
208+
209+
more message
210+
"""
211+
212+
assert format.is_conventional(input, is_strict=True)
213+
214+
215+
def test_is_conventional__bad_body_multiline_paragraphs_strict():
216+
input = """feat(scope): message
217+
more message
218+
219+
more body message
220+
"""
221+
222+
assert not format.is_conventional(input, is_strict=True)
172223

173224

174225
@pytest.mark.parametrize("char", ['"', "'", "`", "#", "&"])

tests/test_hook.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,3 +116,21 @@ def test_main_success__fail_commit(cmd, fixup_commit_path):
116116
result = subprocess.call((cmd, "--strict", fixup_commit_path))
117117

118118
assert result == RESULT_FAIL
119+
120+
121+
def test_main_success__conventional_commit_multi_line(cmd, conventional_commit_multi_line_path):
122+
result = subprocess.call((cmd, conventional_commit_multi_line_path))
123+
124+
assert result == RESULT_SUCCESS
125+
126+
127+
def test_main_success__conventional_commit_bad_multi_line(cmd, conventional_commit_bad_multi_line_path):
128+
result = subprocess.call((cmd, conventional_commit_bad_multi_line_path))
129+
130+
assert result == RESULT_SUCCESS
131+
132+
133+
def test_main_success__conventional_commit_bad_multi_line_strict(cmd, conventional_commit_bad_multi_line_path):
134+
result = subprocess.call((cmd, "--strict", conventional_commit_bad_multi_line_path))
135+
136+
assert result == RESULT_FAIL

0 commit comments

Comments
 (0)