diff --git a/commit_check/author.py b/commit_check/author.py index e3cfd208..f0a1eec4 100644 --- a/commit_check/author.py +++ b/commit_check/author.py @@ -1,34 +1,46 @@ """Check git author name and email""" import re from commit_check import YELLOW, RESET_COLOR, PASS, FAIL -from commit_check.util import get_commit_info, has_commits, print_error_header, print_error_message, print_suggestion +from commit_check.util import ( + get_commit_info, + has_commits, + _find_check, + _print_failure, +) + + +_AUTHOR_FORMAT_MAP = { + "author_name": "an", + "author_email": "ae", +} + + +def _get_author_value(check_type: str) -> str: + """Fetch the author value from git for the given check type.""" + format_str = _AUTHOR_FORMAT_MAP.get(check_type, "") + return str(get_commit_info(format_str)) def check_author(checks: list, check_type: str) -> int: + """Validate author name or email according to configured regex.""" if has_commits() is False: - return PASS # pragma: no cover - - for check in checks: - if check['check'] == check_type: - if check['regex'] == "": - print( - f"{YELLOW}Not found regex for {check_type}. skip checking.{RESET_COLOR}", - ) - return PASS - if check_type == "author_name": - format_str = "an" - if check_type == 'author_email': - format_str = "ae" - config_value = str(get_commit_info(format_str)) - result = re.match(check['regex'], config_value) - if result is None: - if not print_error_header.has_been_called: - print_error_header() - print_error_message( - check['check'], check['regex'], - check['error'], config_value, - ) - if check['suggest']: - print_suggestion(check['suggest']) - return FAIL - return PASS + return PASS # pragma: no cover + + check = _find_check(checks, check_type) + if not check: + return PASS + + # If regex is empty, skip without fetching author info + regex = check.get("regex", "") + if regex == "": + print(f"{YELLOW}Not found regex for {check_type}. skip checking.{RESET_COLOR}") + return PASS + + value = _get_author_value(check_type) + + if re.match(regex, value): + return PASS + + _print_failure(check, regex, value) + + return FAIL diff --git a/commit_check/branch.py b/commit_check/branch.py index b7446cc5..2436967e 100644 --- a/commit_check/branch.py +++ b/commit_check/branch.py @@ -1,30 +1,27 @@ """Check git branch naming convention.""" import re from commit_check import YELLOW, RESET_COLOR, PASS, FAIL -from commit_check.util import get_branch_name, git_merge_base, print_error_header, print_error_message, print_suggestion, has_commits +from commit_check.util import _find_check, _print_failure, get_branch_name, git_merge_base, has_commits def check_branch(checks: list) -> int: - for check in checks: - if check['check'] == 'branch': - if check['regex'] == "": - print( - f"{YELLOW}Not found regex for branch naming. skip checking.{RESET_COLOR}", - ) - return PASS - branch_name = get_branch_name() - result = re.match(check['regex'], branch_name) - if result is None: - if not print_error_header.has_been_called: - print_error_header() # pragma: no cover - print_error_message( - check['check'], check['regex'], - check['error'], branch_name, - ) - if check['suggest']: - print_suggestion(check['suggest']) - return FAIL - return PASS + check = _find_check(checks, 'branch') + if not check: + return PASS + + regex = check.get('regex', "") + if regex == "": + print( + f"{YELLOW}Not found regex for branch naming. skip checking.{RESET_COLOR}", + ) + return PASS + + branch_name = get_branch_name() + if re.match(regex, branch_name): + return PASS + + _print_failure(check, regex, branch_name) + return FAIL def check_merge_base(checks: list) -> int: @@ -36,24 +33,23 @@ def check_merge_base(checks: list) -> int: if has_commits() is False: return PASS # pragma: no cover - for check in checks: - if check['check'] == 'merge_base': - if check['regex'] == "": - print( - f"{YELLOW}Not found target branch for checking merge base. skip checking.{RESET_COLOR}", - ) - return PASS - target_branch = check['regex'] if "origin/" in check['regex'] else f"origin/{check['regex']}" - current_branch = get_branch_name() - result = git_merge_base(target_branch, current_branch) - if result != 0: - if not print_error_header.has_been_called: - print_error_header() # pragma: no cover - print_error_message( - check['check'], check['regex'], - check['error'], current_branch, - ) - if check['suggest']: - print_suggestion(check['suggest']) - return FAIL - return PASS + # locate merge_base rule, if any + check = _find_check(checks, 'merge_base') + if not check: + return PASS + + regex = check.get('regex', "") + if regex == "": + print( + f"{YELLOW}Not found target branch for checking merge base. skip checking.{RESET_COLOR}", + ) + return PASS + + target_branch = regex if "origin/" in regex else f"origin/{regex}" + current_branch = get_branch_name() + result = git_merge_base(target_branch, current_branch) + if result == 0: + return PASS + + _print_failure(check, regex, current_branch) + return FAIL diff --git a/commit_check/commit.py b/commit_check/commit.py index d9ef0ab2..14f5747a 100644 --- a/commit_check/commit.py +++ b/commit_check/commit.py @@ -2,7 +2,7 @@ import re from pathlib import PurePath from commit_check import YELLOW, RESET_COLOR, PASS, FAIL -from commit_check.util import cmd_output, get_commit_info, print_error_header, print_error_message, print_suggestion, has_commits +from commit_check.util import _find_check, _print_failure, cmd_output, get_commit_info, has_commits from commit_check.imperatives import IMPERATIVES @@ -10,6 +10,12 @@ def _load_imperatives() -> set: """Load imperative verbs from imperatives module.""" return IMPERATIVES +def _ensure_msg_file(commit_msg_file: str | None) -> str: + """Return a commit message file path, falling back to the default when empty.""" + if not commit_msg_file: + return get_default_commit_msg_file() + return commit_msg_file + def get_default_commit_msg_file() -> str: """Get the default commit message file.""" @@ -26,118 +32,89 @@ def read_commit_msg(commit_msg_file) -> str: # Commit message is composed by subject and body return str(get_commit_info("s") + "\n\n" + get_commit_info("b")) - def check_commit_msg(checks: list, commit_msg_file: str = "") -> int: """Check commit message against the provided checks.""" if has_commits() is False: - return PASS # pragma: no cover + return PASS # pragma: no cover - if commit_msg_file is None or commit_msg_file == "": - commit_msg_file = get_default_commit_msg_file() + check = _find_check(checks, 'message') + if not check: + return PASS # pragma: no cover - commit_msg = read_commit_msg(commit_msg_file) + regex = check.get('regex', "") + if regex == "": + print(f"{YELLOW}Not found regex for commit message. skip checking.{RESET_COLOR}") + return PASS - for check in checks: - if check['check'] == 'message': - if check['regex'] == "": - print( - f"{YELLOW}Not found regex for commit message. skip checking.{RESET_COLOR}", - ) - return PASS + path = _ensure_msg_file(commit_msg_file) + commit_msg = read_commit_msg(path) - result = re.match(check['regex'], commit_msg) - if result is None: - if not print_error_header.has_been_called: - print_error_header() # pragma: no cover - print_error_message( - check['check'], check['regex'], - check['error'], commit_msg, - ) - if check['suggest']: - print_suggestion(check['suggest']) - return FAIL + if re.match(regex, commit_msg): + return PASS - return PASS + _print_failure(check, regex, commit_msg) + return FAIL def check_commit_signoff(checks: list, commit_msg_file: str = "") -> int: if has_commits() is False: - return PASS # pragma: no cover + return PASS # pragma: no cover - if commit_msg_file is None or commit_msg_file == "": - commit_msg_file = get_default_commit_msg_file() + check = _find_check(checks, 'commit_signoff') + if not check: + return PASS # pragma: no cover - for check in checks: - if check['check'] == 'commit_signoff': - if check['regex'] == "": - print( - f"{YELLOW}Not found regex for commit signoff. skip checking.{RESET_COLOR}", - ) - return PASS + regex = check.get('regex', "") + if regex == "": + print(f"{YELLOW}Not found regex for commit signoff. skip checking.{RESET_COLOR}") + return PASS - commit_msg = read_commit_msg(commit_msg_file) + path = _ensure_msg_file(commit_msg_file) + commit_msg = read_commit_msg(path) - # Extract the subject line (first line of commit message) - subject = commit_msg.split('\n')[0].strip() + # Extract the subject line (first line of commit message) + subject = commit_msg.split('\n')[0].strip() - # Skip if merge commit - if subject.startswith('Merge'): - return PASS + # Skip if merge commit + if subject.startswith('Merge'): + return PASS - commit_hash = get_commit_info("H") - result = re.search(check['regex'], commit_msg) - if result is None: - if not print_error_header.has_been_called: - print_error_header() # pragma: no cover - print_error_message( - check['check'], check['regex'], - check['error'], commit_hash, - ) - if check['suggest']: - print_suggestion(check['suggest']) - return FAIL + commit_hash = get_commit_info("H") + if re.search(regex, commit_msg): + return PASS - return PASS + _print_failure(check, regex, commit_hash) + return FAIL def check_imperative(checks: list, commit_msg_file: str = "") -> int: """Check if commit message uses imperative mood.""" if has_commits() is False: - return PASS # pragma: no cover - - if commit_msg_file is None or commit_msg_file == "": - commit_msg_file = get_default_commit_msg_file() - - for check in checks: - if check['check'] == 'imperative': - commit_msg = read_commit_msg(commit_msg_file) - - # Extract the subject line (first line of commit message) - subject = commit_msg.split('\n')[0].strip() - - # Skip if empty or merge commit - if not subject or subject.startswith('Merge'): - return PASS - - # For conventional commits, extract description after the colon - if ':' in subject: - description = subject.split(':', 1)[1].strip() - else: - description = subject - - # Check if the description uses imperative mood - if not _is_imperative(description): - if not print_error_header.has_been_called: - print_error_header() # pragma: no cover - print_error_message( - check['check'], 'imperative mood pattern', - check['error'], subject, - ) - if check['suggest']: - print_suggestion(check['suggest']) - return FAIL - - return PASS + return PASS # pragma: no cover + + check = _find_check(checks, 'imperative') + if not check: + return PASS + + path = _ensure_msg_file(commit_msg_file) + commit_msg = read_commit_msg(path) + + # Extract the subject line (first line of commit message) + subject = commit_msg.split('\n')[0].strip() + + # Skip if empty or merge commit + if not subject or subject.startswith('Merge'): + return PASS + + # For conventional commits, extract description after the colon + description = subject.split(':', 1)[1].strip() if ':' in subject else subject + + # Check if the description uses imperative mood + if _is_imperative(description): + return PASS + + _print_failure(check, 'imperative mood pattern', subject) + return FAIL def _is_imperative(description: str) -> bool: diff --git a/commit_check/util.py b/commit_check/util.py index 6d3c33b7..ca62d63c 100644 --- a/commit_check/util.py +++ b/commit_check/util.py @@ -12,6 +12,27 @@ from commit_check import RED, GREEN, YELLOW, RESET_COLOR +def _find_check(checks: list, check_type: str) -> dict | None: + """Return the first check dict matching check_type, else None.""" + for check in checks: + if check.get('check') == check_type: + return check + return None + + +def _print_failure( + check: dict, + regex: str, + actual: str +) -> None: + """Print a standardized failure message.""" + if not print_error_header.has_been_called: + print_error_header() + print_error_message(check['check'], regex, check.get('error', ''), actual) + if check.get('suggest'): + print_suggestion(check.get('suggest')) + + def get_branch_name() -> str: """Identify current branch name. .. note:: @@ -159,7 +180,7 @@ def print_error_message(check_type: str, regex: str, error: str, reason: str): print(error) -def print_suggestion(suggest: str) -> None: +def print_suggestion(suggest: str | None) -> None: """Print suggestion to user :param suggest: what message to print out """ diff --git a/pyproject.toml b/pyproject.toml index 8f956392..b2e38487 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -67,3 +67,9 @@ omit = [ # don't include tests in coverage "tests/*", ] + +[tool.pytest.ini_options] +# Silence PytestUnknownMarkWarning for custom marks used in tests +markers = [ + "benchmark: performance-related tests (no-op marker in this project)", +] diff --git a/tests/author_test.py b/tests/author_test.py index fcc49391..30bad283 100644 --- a/tests/author_test.py +++ b/tests/author_test.py @@ -131,10 +131,10 @@ def test_check_author_with_result_none(self, mocker): return_value=None ) m_print_error_message = mocker.patch( - f"{LOCATION}.print_error_message" + "commit_check.util.print_error_message" ) m_print_suggestion = mocker.patch( - f"{LOCATION}.print_suggestion" + "commit_check.util.print_suggestion" ) retval = check_author(checks, "author_name") assert retval == FAIL @@ -246,10 +246,10 @@ def test_check_author_with_result_none(self, mocker): return_value=None ) m_print_error_message = mocker.patch( - f"{LOCATION}.print_error_message" + "commit_check.util.print_error_message" ) m_print_suggestion = mocker.patch( - f"{LOCATION}.print_suggestion" + "commit_check.util.print_suggestion" ) retval = check_author(checks, "author_email") assert retval == FAIL diff --git a/tests/branch_test.py b/tests/branch_test.py index 34e3a3b6..580cc1fe 100644 --- a/tests/branch_test.py +++ b/tests/branch_test.py @@ -107,10 +107,10 @@ def test_check_branch_with_result_none(self, mocker): return_value=None ) m_print_error_message = mocker.patch( - f"{LOCATION}.print_error_message" + "commit_check.util.print_error_message" ) m_print_suggestion = mocker.patch( - f"{LOCATION}.print_suggestion" + "commit_check.util.print_suggestion" ) retval = check_branch(checks) assert retval == FAIL @@ -160,8 +160,8 @@ def test_check_merge_base_fail_with_messages(self, mocker, capfd): "suggest": "Please rebase" }] mocker.patch(f"{LOCATION}.check_merge_base", return_value=1) - m_print_error = mocker.patch(f"{LOCATION}.print_error_message") - m_print_suggest = mocker.patch(f"{LOCATION}.print_suggestion") + m_print_error = mocker.patch("commit_check.util.print_error_message") + m_print_suggest = mocker.patch("commit_check.util.print_suggestion") retval = check_merge_base(checks) assert retval == FAIL diff --git a/tests/commit_test.py b/tests/commit_test.py index 2cabfb3f..2e9f98f9 100644 --- a/tests/commit_test.py +++ b/tests/commit_test.py @@ -5,7 +5,7 @@ # used by get_commit_info mock FAKE_BRANCH_NAME = "fake_commits_info" # The location of check_commit_msg() -LOCATION = "commit_check.commit" +LOCATION = "commit_check.util" # Commit message file MSG_FILE = '.git/COMMIT_EDITMSG'