Skip to content

Commit

Permalink
Merge pull request #8 from aj3sh/commit-hash
Browse files Browse the repository at this point in the history
feat: added support for hash, from-hash, and to-hash argument
  • Loading branch information
aj3sh authored Jan 23, 2024
2 parents 53d0601 + 11e20dd commit 9fa647c
Show file tree
Hide file tree
Showing 9 changed files with 499 additions and 63 deletions.
126 changes: 101 additions & 25 deletions src/commitlint/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@
import sys
from typing import List

from .commitlint import check_commit_message
from .messages import COMMIT_SUCCESSFUL
from .commitlint import check_commit_message, remove_comments
from .exceptions import CommitlintException
from .git_helpers import get_commit_message_of_hash, get_commit_messages_of_hash_range
from .messages import VALIDATION_SUCCESSFUL


def get_args() -> argparse.Namespace:
Expand All @@ -34,56 +36,130 @@ def get_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Check if a commit message follows the conventional commit format."
)
parser.add_argument(

# for commit message check
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument(
"commit_message", nargs="?", type=str, help="The commit message to be checked."
)
parser.add_argument(
group.add_argument(
"--file", type=str, help="Path to a file containing the commit message."
)
group.add_argument("--hash", type=str, help="Commit hash")
group.add_argument("--from-hash", type=str, help="From commit hash")
# --to-hash is optional
parser.add_argument("--to-hash", type=str, help="To commit hash", default="HEAD")

# parsing args
args = parser.parse_args()

if not args.file and not args.commit_message:
parser.error("Please provide either a commit message or a file.")

return args


def _show_errors(errors: List[str]) -> None:
def _show_errors(commit_message: str, errors: List[str]) -> None:
"""
Display a formatted error message for a list of errors.
Args:
errors (List[str]): A list of error messages to be displayed.
"""
error_count = len(errors)
commit_message = remove_comments(commit_message)

Returns:
None
sys.stderr.write(
f"⧗ Input:\n{commit_message}\n\n✖ Found {error_count} error(s).\n\n"
)
for index, error in enumerate(errors):
end_char = "" if index == error_count - 1 else "\n"
sys.stderr.write(f"- {error}\n{end_char}")


def _get_commit_message_from_file(filepath: str) -> str:
"""
sys.stderr.write(f"✖ Found {len(errors)} errors.\n\n")
for error in errors:
sys.stderr.write(f"- {error}\n\n")
Reads and returns the commit message from the specified file.
Args:
filepath (str): The path to the file containing the commit message.
def main() -> None:
Returns:
str: The commit message read from the file.
Raises:
FileNotFoundError: If the specified file does not exist.
IOError: If there is an issue reading the file.
"""
Main function for cli to check a commit message.
abs_filepath = os.path.abspath(filepath)
with open(abs_filepath, encoding="utf-8") as commit_message_file:
commit_message = commit_message_file.read().strip()
return commit_message


def _handle_commit_message(commit_message: str) -> None:
"""
args = get_args()
Handles a single commit message, checks its validity, and prints the result.
if args.file:
commit_message_filepath = os.path.abspath(args.file)
with open(commit_message_filepath, encoding="utf-8") as commit_message_file:
commit_message = commit_message_file.read().strip()
else:
commit_message = args.commit_message.strip()
Args:
commit_message (str): The commit message to be handled.
Raises:
SystemExit: If the commit message is invalid.
"""
success, errors = check_commit_message(commit_message)

if success:
sys.stdout.write(f"{COMMIT_SUCCESSFUL}\n")
sys.exit(0)
sys.stdout.write(f"{VALIDATION_SUCCESSFUL}\n")
else:
_show_errors(errors)
_show_errors(commit_message, errors)
sys.exit(1)


def _handle_multiple_commit_messages(commit_messages: List[str]) -> None:
"""
Handles multiple commit messages, checks their validity, and prints the result.
Args:
commit_messages (List[str]): List of commit messages to be handled.
Raises:
SystemExit: If any of the commit messages is invalid.
"""
has_error = False
for commit_message in commit_messages:
success, errors = check_commit_message(commit_message)
if not success:
has_error = True
_show_errors(commit_message, errors)
sys.stderr.write("\n")

if has_error:
sys.exit(1)
else:
sys.stdout.write(f"{VALIDATION_SUCCESSFUL}\n")


def main() -> None:
"""
Main function for cli to check a commit message.
"""
args = get_args()

try:
if args.file:
commit_message = _get_commit_message_from_file(args.file)
_handle_commit_message(commit_message)
elif args.hash:
commit_message = get_commit_message_of_hash(args.hash)
_handle_commit_message(commit_message)
elif args.from_hash:
commit_messages = get_commit_messages_of_hash_range(
args.from_hash, args.to_hash
)
_handle_multiple_commit_messages(commit_messages)
else:
commit_message = args.commit_message.strip()
_handle_commit_message(commit_message)
except CommitlintException as ex:
sys.stderr.write(f"{ex}\n")
sys.exit(1)


Expand Down
4 changes: 2 additions & 2 deletions src/commitlint/commitlint.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
import re
from typing import List, Tuple

from .constants import COMMIT_MAX_LENGTH
from .constants import COMMIT_HEADER_MAX_LENGTH
from .messages import HEADER_LENGTH_ERROR, INCORRECT_FORMAT_ERROR

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

# checking the length of header
header = commit_message.split("\n").pop()
if len(header) > COMMIT_MAX_LENGTH:
if len(header) > COMMIT_HEADER_MAX_LENGTH:
success = False
errors.append(HEADER_LENGTH_ERROR)

Expand Down
2 changes: 1 addition & 1 deletion src/commitlint/constants.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""This module defines constants used throughout the application."""

COMMIT_MAX_LENGTH = 72
COMMIT_HEADER_MAX_LENGTH = 72
17 changes: 17 additions & 0 deletions src/commitlint/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
"""Custom exceptions module for Commitlint."""


class CommitlintException(Exception):
"""Base exception for Commitlint."""


class GitException(CommitlintException):
"""Exceptions related to Git."""


class GitCommitNotFoundException(GitException):
"""Exception raised when a Git commit could not be retrieved."""


class GitInvalidCommitRangeException(GitException):
"""Exception raised when an invalid commit range was provided."""
89 changes: 89 additions & 0 deletions src/commitlint/git_helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
"""
This module contains the git related helper functions.
"""
import subprocess
from typing import List

from .exceptions import GitCommitNotFoundException, GitInvalidCommitRangeException


def get_commit_message_of_hash(commit_hash: str) -> str:
"""
Retrieve the commit message for a given Git commit hash.
Args:
commit_hash (str): The Git commit hash for which the commit message is to
be retrieved.
Returns:
str: The commit message associated with the specified commit hash.
Raises:
GitCommitNotFoundException: If the specified commit hash is not found
or if there is an error retrieving the commit message.
"""
try:
# Run 'git show --format=%B -s' command to get the commit message
commit_message = subprocess.check_output(
["git", "show", "--format=%B", "-s", commit_hash],
text=True,
stderr=subprocess.PIPE,
).strip()

return commit_message
except subprocess.CalledProcessError:
raise GitCommitNotFoundException(
f"Failed to retrieve commit message for hash {commit_hash}"
) from None


def get_commit_messages_of_hash_range(
from_hash: str, to_hash: str = "HEAD"
) -> List[str]:
"""
Retrieve an array of commit messages for a range of Git commit hashes.
Note:
This function will not support initial commit as from_hash.
Args:
from_hash (str): The starting Git commit hash.
to_hash (str, optional): The ending Git commit hash or branch
(default is "HEAD").
Returns:
List[str]: A list of commit messages for the specified commit range.
Raises:
GitCommitNotFoundException: If the commit hash of `from_hash` is not found
or if there is an error retrieving the commit message.
GitInvalidCommitRangeException: If the commit range of from_hash..to_hash is not
found or if there is an error retrieving the commit message.
"""
# as the commit range doesn't support initial commit hash,
# commit message of `from_hash` is taken separately
from_commit_message = get_commit_message_of_hash(from_hash)

try:
# Runs the below git command:
# git log --format=%B --reverse FROM_HASH..TO_HASH
# This outputs the commit messages excluding of FROM_HASH
delimiter = "========commit-delimiter========"
hash_range = f"{from_hash}..{to_hash}"

commit_messages_output = subprocess.check_output(
["git", "log", f"--format=%B{delimiter}", "--reverse", hash_range],
text=True,
stderr=subprocess.PIPE,
)
commit_messages = commit_messages_output.split(f"{delimiter}\n")
return [from_commit_message] + [
commit_message.strip()
for commit_message in commit_messages
if commit_message.strip()
]
except subprocess.CalledProcessError:
raise GitInvalidCommitRangeException(
f"Failed to retrieve commit messages for the range {from_hash} to {to_hash}"
) from None
8 changes: 5 additions & 3 deletions src/commitlint/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
This module provides constant messages used in the application for various scenarios.
"""

from .constants import COMMIT_MAX_LENGTH
from .constants import COMMIT_HEADER_MAX_LENGTH

COMMIT_SUCCESSFUL = "Commit validation: successful!"
VALIDATION_SUCCESSFUL = "Commit validation: successful!"

CORRECT_OUTPUT_FORMAT = (
"Correct commit format:\n"
Expand All @@ -18,4 +18,6 @@
"Commit message does not follow conventional commits format."
f"\n{CORRECT_OUTPUT_FORMAT}"
)
HEADER_LENGTH_ERROR = f"Header must not be longer than {COMMIT_MAX_LENGTH} characters."
HEADER_LENGTH_ERROR = (
f"Header must not be longer than {COMMIT_HEADER_MAX_LENGTH} characters."
)
Loading

0 comments on commit 9fa647c

Please sign in to comment.