Skip to content

Commit

Permalink
feat: Validate multi-line messages
Browse files Browse the repository at this point in the history
The documented conventional commit specification states that on
multi-line commit message there must be a blank line between the title
and the body.

To avoid a breaking change this has been toggled using the strict
option, in the next major release this toggle will be removed.
  • Loading branch information
grahamhar committed Nov 5, 2023
1 parent 2a87832 commit 5d30aa0
Show file tree
Hide file tree
Showing 7 changed files with 112 additions and 11 deletions.
22 changes: 16 additions & 6 deletions conventional_pre_commit/format.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,13 @@ def r_delim():


def r_subject():
"""Regex str for subject line, body, footer."""
return r" .+"
"""Regex str for subject line."""
return r" .+$"


def r_body():
"""Regex str for the body"""
return r"(?P<multi>\n(?P<sep>^$\n)?.+)?"


def r_autosquash_prefixes():
Expand All @@ -56,18 +61,23 @@ def conventional_types(types=[]):
return types


def is_conventional(input, types=DEFAULT_TYPES, optional_scope=True):
def is_conventional(input, types=DEFAULT_TYPES, optional_scope=True, is_strict=False):
"""
Returns True if input matches Conventional Commits formatting
https://www.conventionalcommits.org
Optionally provide a list of additional custom types.
"""
types = conventional_types(types)
pattern = f"^({r_types(types)}){r_scope(optional_scope)}{r_delim()}{r_subject()}$"
regex = re.compile(pattern, re.DOTALL)
pattern = f"^({r_types(types)}){r_scope(optional_scope)}{r_delim()}{r_subject()}{r_body()}"
regex = re.compile(pattern, re.MULTILINE)

return bool(regex.match(input))
result = regex.match(input)
is_valid_subject = bool(result)
if is_valid_subject and is_strict and result.group("multi") and not result.group("sep"):
is_valid_subject = False

return is_valid_subject


def has_autosquash_prefix(input):
Expand Down
13 changes: 10 additions & 3 deletions conventional_pre_commit/hook.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ def main(argv=[]):
if format.has_autosquash_prefix(message):
return RESULT_SUCCESS

if format.is_conventional(message, args.types, args.optional_scope):
if format.is_conventional(message, args.types, args.optional_scope, args.strict):
return RESULT_SUCCESS
else:
print(
Expand All @@ -64,7 +64,7 @@ def main(argv=[]):
{Colors.LBLUE}https://www.conventionalcommits.org/{Colors.YELLOW}
Conventional Commits start with one of the below types, followed by a colon,
followed by the commit message:{Colors.RESTORE}
followed by the commit subject and an optional body seperated by a blank line:{Colors.RESTORE}
{" ".join(format.conventional_types(args.types))}
Expand All @@ -78,7 +78,14 @@ def main(argv=[]):
{Colors.YELLOW}Example commit with scope in parentheses after the type for more context:{Colors.RESTORE}
fix(account): remove infinite loop"""
fix(account): remove infinite loop
{Colors.YELLOW}Example commit with a body
fix: remove infinite loop
Additional information on the issue caused by the infinite loop
"""
)
return RESULT_FAIL

Expand Down
10 changes: 10 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,13 @@ def conventional_gbk_commit_path():
@pytest.fixture
def fixup_commit_path():
return get_message_path("fixup_commit")


@pytest.fixture
def conventional_commit_bad_multi_line_path():
return get_message_path("conventional_commit_bad_multi_line")


@pytest.fixture
def conventional_commit_multi_line_path():
return get_message_path("conventional_commit_multi_line")
2 changes: 2 additions & 0 deletions tests/messages/conventional_commit_bad_multi_line
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
fix: message
no blank line
3 changes: 3 additions & 0 deletions tests/messages/conventional_commit_multi_line
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
fix: message

A blank line is there
55 changes: 53 additions & 2 deletions tests/test_format.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,13 +162,64 @@ def test_is_conventional__with_scope():
assert format.is_conventional(input)


def test_is_conventional__body_multiline():
def test_is_conventional__body_multiline_not_strict():
input = """feat(scope): message
more message
"""

assert format.is_conventional(input)
assert format.is_conventional(input, is_strict=False)


def test_is_conventional__body_multiline_no_body_not_strict():
input = """feat(scope): message
"""
assert format.is_conventional(input, is_strict=False)


def test_is_conventional__body_multiline_body_bad_type_strict():
input = """wrong: message
more_message
"""

assert not format.is_conventional(input, is_strict=True)


def test_is_conventional__bad_body_multiline_not_strict():
input = """feat(scope): message
more message
"""

assert format.is_conventional(input, is_strict=False)


def test_is_conventional__bad_body_multiline_strict():
input = """feat(scope): message
more message
"""

assert not format.is_conventional(input, is_strict=True)


def test_is_conventional__body_multiline_strict():
input = """feat(scope): message
more message
"""

assert format.is_conventional(input, is_strict=True)


def test_is_conventional__bad_body_multiline_paragraphs_strict():
input = """feat(scope): message
more message
more body message
"""

assert not format.is_conventional(input, is_strict=True)


@pytest.mark.parametrize("char", ['"', "'", "`", "#", "&"])
Expand Down
18 changes: 18 additions & 0 deletions tests/test_hook.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,3 +116,21 @@ def test_main_success__fail_commit(cmd, fixup_commit_path):
result = subprocess.call((cmd, "--strict", fixup_commit_path))

assert result == RESULT_FAIL


def test_main_success__conventional_commit_multi_line(cmd, conventional_commit_multi_line_path):
result = subprocess.call((cmd, conventional_commit_multi_line_path))

assert result == RESULT_SUCCESS


def test_main_success__conventional_commit_bad_multi_line(cmd, conventional_commit_bad_multi_line_path):
result = subprocess.call((cmd, conventional_commit_bad_multi_line_path))

assert result == RESULT_SUCCESS


def test_main_success__conventional_commit_bad_multi_line_strict(cmd, conventional_commit_bad_multi_line_path):
result = subprocess.call((cmd, "--strict", conventional_commit_bad_multi_line_path))

assert result == RESULT_FAIL

0 comments on commit 5d30aa0

Please sign in to comment.