Skip to content

Commit 9fa647c

Browse files
authored
Merge pull request #8 from aj3sh/commit-hash
feat: added support for hash, from-hash, and to-hash argument
2 parents 53d0601 + 11e20dd commit 9fa647c

File tree

9 files changed

+499
-63
lines changed

9 files changed

+499
-63
lines changed

src/commitlint/cli.py

+101-25
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,10 @@
1717
import sys
1818
from typing import List
1919

20-
from .commitlint import check_commit_message
21-
from .messages import COMMIT_SUCCESSFUL
20+
from .commitlint import check_commit_message, remove_comments
21+
from .exceptions import CommitlintException
22+
from .git_helpers import get_commit_message_of_hash, get_commit_messages_of_hash_range
23+
from .messages import VALIDATION_SUCCESSFUL
2224

2325

2426
def get_args() -> argparse.Namespace:
@@ -34,56 +36,130 @@ def get_args() -> argparse.Namespace:
3436
parser = argparse.ArgumentParser(
3537
description="Check if a commit message follows the conventional commit format."
3638
)
37-
parser.add_argument(
39+
40+
# for commit message check
41+
group = parser.add_mutually_exclusive_group(required=True)
42+
group.add_argument(
3843
"commit_message", nargs="?", type=str, help="The commit message to be checked."
3944
)
40-
parser.add_argument(
45+
group.add_argument(
4146
"--file", type=str, help="Path to a file containing the commit message."
4247
)
48+
group.add_argument("--hash", type=str, help="Commit hash")
49+
group.add_argument("--from-hash", type=str, help="From commit hash")
50+
# --to-hash is optional
51+
parser.add_argument("--to-hash", type=str, help="To commit hash", default="HEAD")
4352

53+
# parsing args
4454
args = parser.parse_args()
4555

46-
if not args.file and not args.commit_message:
47-
parser.error("Please provide either a commit message or a file.")
48-
4956
return args
5057

5158

52-
def _show_errors(errors: List[str]) -> None:
59+
def _show_errors(commit_message: str, errors: List[str]) -> None:
5360
"""
5461
Display a formatted error message for a list of errors.
5562
5663
Args:
5764
errors (List[str]): A list of error messages to be displayed.
65+
"""
66+
error_count = len(errors)
67+
commit_message = remove_comments(commit_message)
5868

59-
Returns:
60-
None
69+
sys.stderr.write(
70+
f"⧗ Input:\n{commit_message}\n\n✖ Found {error_count} error(s).\n\n"
71+
)
72+
for index, error in enumerate(errors):
73+
end_char = "" if index == error_count - 1 else "\n"
74+
sys.stderr.write(f"- {error}\n{end_char}")
75+
76+
77+
def _get_commit_message_from_file(filepath: str) -> str:
6178
"""
62-
sys.stderr.write(f"✖ Found {len(errors)} errors.\n\n")
63-
for error in errors:
64-
sys.stderr.write(f"- {error}\n\n")
79+
Reads and returns the commit message from the specified file.
6580
81+
Args:
82+
filepath (str): The path to the file containing the commit message.
6683
67-
def main() -> None:
84+
Returns:
85+
str: The commit message read from the file.
86+
87+
Raises:
88+
FileNotFoundError: If the specified file does not exist.
89+
IOError: If there is an issue reading the file.
6890
"""
69-
Main function for cli to check a commit message.
91+
abs_filepath = os.path.abspath(filepath)
92+
with open(abs_filepath, encoding="utf-8") as commit_message_file:
93+
commit_message = commit_message_file.read().strip()
94+
return commit_message
95+
96+
97+
def _handle_commit_message(commit_message: str) -> None:
7098
"""
71-
args = get_args()
99+
Handles a single commit message, checks its validity, and prints the result.
72100
73-
if args.file:
74-
commit_message_filepath = os.path.abspath(args.file)
75-
with open(commit_message_filepath, encoding="utf-8") as commit_message_file:
76-
commit_message = commit_message_file.read().strip()
77-
else:
78-
commit_message = args.commit_message.strip()
101+
Args:
102+
commit_message (str): The commit message to be handled.
79103
104+
Raises:
105+
SystemExit: If the commit message is invalid.
106+
"""
80107
success, errors = check_commit_message(commit_message)
81108

82109
if success:
83-
sys.stdout.write(f"{COMMIT_SUCCESSFUL}\n")
84-
sys.exit(0)
110+
sys.stdout.write(f"{VALIDATION_SUCCESSFUL}\n")
85111
else:
86-
_show_errors(errors)
112+
_show_errors(commit_message, errors)
113+
sys.exit(1)
114+
115+
116+
def _handle_multiple_commit_messages(commit_messages: List[str]) -> None:
117+
"""
118+
Handles multiple commit messages, checks their validity, and prints the result.
119+
120+
Args:
121+
commit_messages (List[str]): List of commit messages to be handled.
122+
123+
Raises:
124+
SystemExit: If any of the commit messages is invalid.
125+
"""
126+
has_error = False
127+
for commit_message in commit_messages:
128+
success, errors = check_commit_message(commit_message)
129+
if not success:
130+
has_error = True
131+
_show_errors(commit_message, errors)
132+
sys.stderr.write("\n")
133+
134+
if has_error:
135+
sys.exit(1)
136+
else:
137+
sys.stdout.write(f"{VALIDATION_SUCCESSFUL}\n")
138+
139+
140+
def main() -> None:
141+
"""
142+
Main function for cli to check a commit message.
143+
"""
144+
args = get_args()
145+
146+
try:
147+
if args.file:
148+
commit_message = _get_commit_message_from_file(args.file)
149+
_handle_commit_message(commit_message)
150+
elif args.hash:
151+
commit_message = get_commit_message_of_hash(args.hash)
152+
_handle_commit_message(commit_message)
153+
elif args.from_hash:
154+
commit_messages = get_commit_messages_of_hash_range(
155+
args.from_hash, args.to_hash
156+
)
157+
_handle_multiple_commit_messages(commit_messages)
158+
else:
159+
commit_message = args.commit_message.strip()
160+
_handle_commit_message(commit_message)
161+
except CommitlintException as ex:
162+
sys.stderr.write(f"{ex}\n")
87163
sys.exit(1)
88164

89165

src/commitlint/commitlint.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
import re
1616
from typing import List, Tuple
1717

18-
from .constants import COMMIT_MAX_LENGTH
18+
from .constants import COMMIT_HEADER_MAX_LENGTH
1919
from .messages import HEADER_LENGTH_ERROR, INCORRECT_FORMAT_ERROR
2020

2121
CONVENTIONAL_COMMIT_PATTERN = (
@@ -111,7 +111,7 @@ def check_commit_message(commit_message: str) -> Tuple[bool, List[str]]:
111111

112112
# checking the length of header
113113
header = commit_message.split("\n").pop()
114-
if len(header) > COMMIT_MAX_LENGTH:
114+
if len(header) > COMMIT_HEADER_MAX_LENGTH:
115115
success = False
116116
errors.append(HEADER_LENGTH_ERROR)
117117

src/commitlint/constants.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
"""This module defines constants used throughout the application."""
22

3-
COMMIT_MAX_LENGTH = 72
3+
COMMIT_HEADER_MAX_LENGTH = 72

src/commitlint/exceptions.py

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
"""Custom exceptions module for Commitlint."""
2+
3+
4+
class CommitlintException(Exception):
5+
"""Base exception for Commitlint."""
6+
7+
8+
class GitException(CommitlintException):
9+
"""Exceptions related to Git."""
10+
11+
12+
class GitCommitNotFoundException(GitException):
13+
"""Exception raised when a Git commit could not be retrieved."""
14+
15+
16+
class GitInvalidCommitRangeException(GitException):
17+
"""Exception raised when an invalid commit range was provided."""

src/commitlint/git_helpers.py

+89
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
"""
2+
This module contains the git related helper functions.
3+
"""
4+
import subprocess
5+
from typing import List
6+
7+
from .exceptions import GitCommitNotFoundException, GitInvalidCommitRangeException
8+
9+
10+
def get_commit_message_of_hash(commit_hash: str) -> str:
11+
"""
12+
Retrieve the commit message for a given Git commit hash.
13+
14+
Args:
15+
commit_hash (str): The Git commit hash for which the commit message is to
16+
be retrieved.
17+
18+
Returns:
19+
str: The commit message associated with the specified commit hash.
20+
21+
Raises:
22+
GitCommitNotFoundException: If the specified commit hash is not found
23+
or if there is an error retrieving the commit message.
24+
"""
25+
try:
26+
# Run 'git show --format=%B -s' command to get the commit message
27+
commit_message = subprocess.check_output(
28+
["git", "show", "--format=%B", "-s", commit_hash],
29+
text=True,
30+
stderr=subprocess.PIPE,
31+
).strip()
32+
33+
return commit_message
34+
except subprocess.CalledProcessError:
35+
raise GitCommitNotFoundException(
36+
f"Failed to retrieve commit message for hash {commit_hash}"
37+
) from None
38+
39+
40+
def get_commit_messages_of_hash_range(
41+
from_hash: str, to_hash: str = "HEAD"
42+
) -> List[str]:
43+
"""
44+
Retrieve an array of commit messages for a range of Git commit hashes.
45+
46+
Note:
47+
This function will not support initial commit as from_hash.
48+
49+
Args:
50+
from_hash (str): The starting Git commit hash.
51+
to_hash (str, optional): The ending Git commit hash or branch
52+
(default is "HEAD").
53+
54+
Returns:
55+
List[str]: A list of commit messages for the specified commit range.
56+
57+
Raises:
58+
GitCommitNotFoundException: If the commit hash of `from_hash` is not found
59+
or if there is an error retrieving the commit message.
60+
61+
GitInvalidCommitRangeException: If the commit range of from_hash..to_hash is not
62+
found or if there is an error retrieving the commit message.
63+
"""
64+
# as the commit range doesn't support initial commit hash,
65+
# commit message of `from_hash` is taken separately
66+
from_commit_message = get_commit_message_of_hash(from_hash)
67+
68+
try:
69+
# Runs the below git command:
70+
# git log --format=%B --reverse FROM_HASH..TO_HASH
71+
# This outputs the commit messages excluding of FROM_HASH
72+
delimiter = "========commit-delimiter========"
73+
hash_range = f"{from_hash}..{to_hash}"
74+
75+
commit_messages_output = subprocess.check_output(
76+
["git", "log", f"--format=%B{delimiter}", "--reverse", hash_range],
77+
text=True,
78+
stderr=subprocess.PIPE,
79+
)
80+
commit_messages = commit_messages_output.split(f"{delimiter}\n")
81+
return [from_commit_message] + [
82+
commit_message.strip()
83+
for commit_message in commit_messages
84+
if commit_message.strip()
85+
]
86+
except subprocess.CalledProcessError:
87+
raise GitInvalidCommitRangeException(
88+
f"Failed to retrieve commit messages for the range {from_hash} to {to_hash}"
89+
) from None

src/commitlint/messages.py

+5-3
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22
This module provides constant messages used in the application for various scenarios.
33
"""
44

5-
from .constants import COMMIT_MAX_LENGTH
5+
from .constants import COMMIT_HEADER_MAX_LENGTH
66

7-
COMMIT_SUCCESSFUL = "Commit validation: successful!"
7+
VALIDATION_SUCCESSFUL = "Commit validation: successful!"
88

99
CORRECT_OUTPUT_FORMAT = (
1010
"Correct commit format:\n"
@@ -18,4 +18,6 @@
1818
"Commit message does not follow conventional commits format."
1919
f"\n{CORRECT_OUTPUT_FORMAT}"
2020
)
21-
HEADER_LENGTH_ERROR = f"Header must not be longer than {COMMIT_MAX_LENGTH} characters."
21+
HEADER_LENGTH_ERROR = (
22+
f"Header must not be longer than {COMMIT_HEADER_MAX_LENGTH} characters."
23+
)

0 commit comments

Comments
 (0)