Skip to content

Commit d092c97

Browse files
committed
feat: added strict regex for validation and ignored auto commits
1 parent 60d361a commit d092c97

File tree

4 files changed

+170
-42
lines changed

4 files changed

+170
-42
lines changed

src/commitlint/cli.py

Lines changed: 48 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,22 @@
1414
"""
1515
import argparse
1616
import os
17+
import sys
18+
from typing import List
1719

1820
from .commitlint import check_commit_message
21+
from .messages import COMMIT_SUCCESSFUL
1922

2023

21-
def main() -> None:
24+
def get_args() -> argparse.Namespace:
2225
"""
23-
Main function for cli to check a commit message.
26+
Parse CLI arguments for checking if a commit message.
27+
28+
Returns:
29+
argparse.Namespace: The parsed CLI arguments.
30+
31+
Raises:
32+
argparse.ArgumentError: If any argument error.
2433
"""
2534
parser = argparse.ArgumentParser(
2635
description="Check if a commit message follows the conventional commit format."
@@ -34,16 +43,48 @@ def main() -> None:
3443

3544
args = parser.parse_args()
3645

46+
if not args.file and not args.commit_message:
47+
parser.error("Please provide either a commit message or a file.")
48+
49+
return args
50+
51+
52+
def _show_errors(errors: List[str]) -> None:
53+
"""
54+
Display a formatted error message for a list of errors.
55+
56+
Args:
57+
errors (List[str]): A list of error messages to be displayed.
58+
59+
Returns:
60+
None
61+
"""
62+
sys.stderr.write(f"✖ Found {len(errors)} errors.\n\n")
63+
for error in errors:
64+
sys.stderr.write(f"- {error}\n\n")
65+
66+
67+
def main() -> None:
68+
"""
69+
Main function for cli to check a commit message.
70+
"""
71+
args = get_args()
72+
3773
if args.file:
3874
commit_message_filepath = os.path.abspath(args.file)
3975
with open(commit_message_filepath, encoding="utf-8") as commit_message_file:
40-
commit_message = commit_message_file.read().strip().split("\n\n")[0]
41-
elif args.commit_message:
42-
commit_message = args.commit_message.strip()
76+
commit_message = commit_message_file.read().strip()
4377
else:
44-
parser.error("Please provide either a commit message or a file.")
78+
commit_message = args.commit_message.strip()
79+
80+
success, errors = check_commit_message(commit_message)
81+
82+
if success:
83+
sys.stdout.write(f"{COMMIT_SUCCESSFUL}\n")
84+
sys.exit(0)
4585

46-
check_commit_message(commit_message)
86+
_show_errors(errors)
87+
sys.exit(1)
4788

4889

4990
if __name__ == "__main__":

src/commitlint/commitlint.py

Lines changed: 98 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,124 @@
1-
"""This module contains source for commitlint"""
1+
"""
2+
This module provides functionality for validating commit messages according
3+
to conventional commit standards.
4+
5+
Usage:
6+
------
7+
8+
```python
9+
from commitlint import check_commit_message
10+
11+
commit_message = "feat(module): add module documentation"
12+
success, errors = check_commit_message(commit_message)
13+
```
14+
"""
215
import re
3-
import sys
16+
from typing import List, Tuple
17+
18+
from .constants import COMMIT_MAX_LENGTH
19+
from .messages import HEADER_LENGTH_ERROR, INCORRECT_FORMAT_ERROR
20+
21+
CONVENTIONAL_COMMIT_PATTERN = (
22+
r"(?s)" # To explicitly make . match new line
23+
r"(?P<type>build|ci|docs|feat|fix|perf|refactor|style|test|chore|revert|bump)"
24+
r"(?P<scope>\(\S+\))?!?:"
25+
r"(?: (?P<description>[^\n\r]+))"
26+
r"((\n\n(?P<body>.*))|(\s*))?$"
27+
)
428

5-
COMMIT_MAX_LENGTH = 72
29+
IGNORED_PATTERN = (
30+
r"^((Merge pull request)|(Merge (.*?) into (.*?)|(Merge branch (.*?)))(?:\r?\n)*$)|"
31+
r"^(Merge tag (.*?))(?:\r?\n)*$|"
32+
r"^(R|r)evert (.*)|"
33+
r"^(Merged (.*?)(in|into) (.*)|Merged PR (.*): (.*))$|"
34+
r"^Merge remote-tracking branch(\s*)(.*)$|"
35+
r"^Automatic merge(.*)$|"
36+
r"^Auto-merged (.*?) into (.*)$"
37+
)
638

739

8-
def is_conventional_commit(commit_message: str) -> bool:
40+
def is_ignored(commit_message: str) -> bool:
941
"""
10-
Checks if a commit message follows the conventional commit format.
42+
Checks if a commit message should be ignored.
43+
44+
Some commit messages like merge, revert, auto merge, etc is ignored
45+
from linting.
1146
1247
Args:
13-
commit_message (str): The commit message to be checked.
48+
commit_message (str): The commit message to check.
1449
1550
Returns:
16-
bool: True if the commit message follows the conventional commit format,
17-
False otherwise.
51+
bool: True if the commit message should be ignored, False otherwise.
1852
"""
19-
pattern = re.compile(r"^(\w+)(\([^\)]+\))?: .+")
20-
return bool(pattern.match(commit_message))
53+
return bool(re.match(IGNORED_PATTERN, commit_message))
2154

2255

23-
def is_valid_length(commit_message: str, max_length: int) -> bool:
24-
"""
25-
Checks if a commit message has a valid length.
56+
def remove_comments(msg: str) -> str:
57+
"""Removes comments from the commit message.
58+
59+
For `git commit --verbose`, excluding the diff generated message,
60+
for example:
61+
62+
```bash
63+
...
64+
# ------------------------ >8 ------------------------
65+
# Do not modify or remove the line above.
66+
# Everything below it will be ignored.
67+
diff --git a/... b/...
68+
...
69+
```
2670
2771
Args:
28-
commit_message (str): The commit message to be checked.
29-
max_length (int): The maximum allowed length for the commit message.
72+
msg(str): The commit message to remove comments.
3073
3174
Returns:
32-
bool: True if the commit message length is valid, False otherwise.
75+
str: The commit message without comments.
3376
"""
34-
return len(commit_message) <= max_length
3577

78+
lines: List[str] = []
79+
for line in msg.split("\n"):
80+
if "# ------------------------ >8 ------------------------" in line:
81+
# ignoring all the verbose message below this line
82+
break
83+
if not line.startswith("#"):
84+
lines.append(line)
3685

37-
def check_commit_message(commit_message: str) -> None:
86+
return "\n".join(lines)
87+
88+
89+
def check_commit_message(commit_message: str) -> Tuple[bool, List[str]]:
3890
"""
39-
Check the validity of a commit message.
91+
Checks the validity of a commit message. Returns success and error list.
4092
4193
Args:
4294
commit_message (str): The commit message to be validated.
4395
44-
Raises:
45-
SystemExit: Exits the program with status code 1 if the commit message
46-
violates length or conventional commit format rules.
47-
4896
Returns:
49-
None: This function does not return any value; it either exits or
50-
continues based on the validity of the commit message.
97+
Tuple[bool, List[str]]: Returns success as a first element and list
98+
of errors on the second elements. If success is true, errors will be
99+
empty.
51100
"""
52-
if not is_valid_length(commit_message, max_length=COMMIT_MAX_LENGTH):
53-
sys.stderr.write(
54-
"Commit message is too long. "
55-
f"Max length is {COMMIT_MAX_LENGTH} characters.\n"
56-
)
57-
sys.exit(1)
58-
59-
if not is_conventional_commit(commit_message):
60-
sys.stderr.write("Commit message does not follow conventional commit format.\n")
61-
sys.exit(1)
101+
# default values
102+
success = True
103+
errors: List[str] = []
104+
105+
# removing unnecessary commit comments
106+
commit_message = remove_comments(commit_message)
107+
108+
# checking if commit message should be ignored
109+
if is_ignored(commit_message):
110+
return success, errors
111+
112+
# checking the length of header
113+
header = commit_message.split("\n").pop()
114+
if len(header) > COMMIT_MAX_LENGTH:
115+
success = False
116+
errors.append(HEADER_LENGTH_ERROR)
117+
118+
# matching commit message with the commit pattern
119+
pattern_match = re.match(CONVENTIONAL_COMMIT_PATTERN, commit_message)
120+
if pattern_match is None:
121+
success = False
122+
errors.append(INCORRECT_FORMAT_ERROR)
123+
124+
return success, errors

src/commitlint/constants.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
"""This module defines constants used throughout the application."""
2+
3+
COMMIT_MAX_LENGTH = 72

src/commitlint/messages.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
"""
2+
This module provides constant messages used in the application for various scenarios.
3+
"""
4+
5+
from .constants import COMMIT_MAX_LENGTH
6+
7+
COMMIT_SUCCESSFUL = "Commit validation: successful!"
8+
9+
CORRECT_OUTPUT_FORMAT = (
10+
"Correct commit format:\n"
11+
"---------------------------------------\n"
12+
"<type>(<optional scope>): <description>\n"
13+
"---------------------------------------\n"
14+
"For more details visit "
15+
"https://www.conventionalcommits.org/en/v1.0.0/"
16+
)
17+
INCORRECT_FORMAT_ERROR = (
18+
"Commit message does not follow conventional commits format."
19+
f"\n{CORRECT_OUTPUT_FORMAT}"
20+
)
21+
HEADER_LENGTH_ERROR = f"Header must not be longer than {COMMIT_MAX_LENGTH} characters."

0 commit comments

Comments
 (0)