From b356f9af6ff602047d1446e4f497d47a6e84b9be Mon Sep 17 00:00:00 2001 From: shenxianpeng Date: Sat, 12 Jul 2025 07:29:52 +0000 Subject: [PATCH 1/6] feat: add impreative mode --- .commit-check.yml | 5 + README.rst | 3 +- commit_check/__init__.py | 6 ++ commit_check/commit.py | 102 ++++++++++++++++++ commit_check/main.py | 10 ++ tests/commit_test.py | 223 ++++++++++++++++++++++++++++++++++++++- tests/main_test.py | 34 +++--- 7 files changed, 367 insertions(+), 16 deletions(-) diff --git a/.commit-check.yml b/.commit-check.yml index ad339d06..c2b13f02 100644 --- a/.commit-check.yml +++ b/.commit-check.yml @@ -32,3 +32,8 @@ checks: regex: main # it can be master, develop, devel etc based on your project. error: Current branch is not rebased onto target branch suggest: Please ensure your branch is rebased with the target branch + + - check: imperative_mood + regex: '' # Not used for imperative mood check + error: 'Commit message should use imperative mood (e.g., "Add feature" not "Added feature")' + suggest: 'Use imperative mood in commit message like "Add", "Fix", "Update", "Remove"' diff --git a/README.rst b/README.rst index b9ac03a2..f24da921 100644 --- a/README.rst +++ b/README.rst @@ -77,6 +77,7 @@ Running as pre-commit hook - id: check-author-email - id: check-commit-signoff - id: check-merge-base # requires download all git history + - id: check-imperative-mood Running as CLI ~~~~~~~~~~~~~~ @@ -109,7 +110,7 @@ To configure the hook, create a script file in the ``.git/hooks/`` directory. .. code-block:: bash #!/bin/sh - commit-check --message --branch --author-name --author-email --commit-signoff --merge-base + commit-check --message --branch --author-name --author-email --commit-signoff --merge-base --imperative-mood Save the script file as ``pre-push`` and make it executable: diff --git a/commit_check/__init__.py b/commit_check/__init__.py index 521f68fe..1ec7a3f0 100644 --- a/commit_check/__init__.py +++ b/commit_check/__init__.py @@ -54,6 +54,12 @@ 'error': 'Current branch is not rebased onto target branch', 'suggest': 'Please ensure your branch is rebased with the target branch', }, + { + 'check': 'imperative_mood', + 'regex': r'', # Not used for imperative mood check + 'error': 'Commit message should use imperative mood (e.g., "Add feature" not "Added feature")', + 'suggest': 'Use imperative mood in commit message like "Add", "Fix", "Update", "Remove"', + }, ], } diff --git a/commit_check/commit.py b/commit_check/commit.py index ac1c63a0..f4319199 100644 --- a/commit_check/commit.py +++ b/commit_check/commit.py @@ -84,3 +84,105 @@ def check_commit_signoff(checks: list, commit_msg_file: str = "") -> int: return FAIL return PASS + + +def check_imperative_mood(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_mood': + 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_mood(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 + + +def _is_imperative_mood(description: str) -> bool: + """Check if a description uses imperative mood.""" + if not description: + return True + + # Get the first word of the description + first_word = description.split()[0].lower() + + # Common non-imperative words (explicit list of bad forms) + non_imperative_words = { + # Past tense forms + 'added', 'updated', 'changed', 'modified', 'created', 'deleted', 'removed', + 'fixed', 'improved', 'refactored', 'optimized', 'enhanced', 'implemented', + 'configured', 'installed', 'uninstalled', 'upgraded', 'downgraded', + 'merged', 'rebased', 'committed', 'pushed', 'pulled', 'cloned', + 'tested', 'deployed', 'released', 'published', 'documented', + 'formatted', 'linted', 'cleaned', 'organized', 'restructured', + + # Present continuous forms + 'adding', 'updating', 'changing', 'modifying', 'creating', 'deleting', 'removing', + 'fixing', 'improving', 'refactoring', 'optimizing', 'enhancing', 'implementing', + 'configuring', 'installing', 'uninstalling', 'upgrading', 'downgrading', + 'merging', 'rebasing', 'committing', 'pushing', 'pulling', 'cloning', + 'testing', 'deploying', 'releasing', 'publishing', 'documenting', + 'formatting', 'linting', 'cleaning', 'organizing', 'restructuring', + + # Third person singular forms + 'adds', 'updates', 'changes', 'modifies', 'creates', 'deletes', 'removes', + 'fixes', 'improves', 'refactors', 'optimizes', 'enhances', 'implements', + 'configures', 'installs', 'uninstalls', 'upgrades', 'downgrades', + 'merges', 'rebases', 'commits', 'pushes', 'pulls', 'clones', + 'tests', 'deploys', 'releases', 'publishes', 'documents', + 'formats', 'lints', 'cleans', 'organizes', 'restructures', + } + + # Check if the first word is in our non-imperative list + if first_word in non_imperative_words: + return False + + # Check for common past tense pattern (-ed ending) but be more specific + if (first_word.endswith('ed') and len(first_word) > 3 and + first_word not in {'red', 'bed', 'fed', 'led', 'wed', 'shed', 'fled'}): + return False + + # Check for present continuous pattern (-ing ending) but be more specific + if (first_word.endswith('ing') and len(first_word) > 4 and + first_word not in {'ring', 'sing', 'king', 'wing', 'thing', 'string', 'bring'}): + return False + + # Check for third person singular (-s ending) but be more specific + # Only flag if it's clearly a verb in third person singular form + if (first_word.endswith('s') and len(first_word) > 3 and + not first_word.endswith('ss') and not first_word.endswith('us') and + not first_word.endswith('es') and first_word not in {'process', 'access', 'address', 'express', 'suppress', 'compress', 'assess'}): + # Additional check: if it's a common noun ending in 's', allow it + common_nouns_ending_s = {'process', 'access', 'address', 'progress', 'express', 'stress', 'success', 'class', 'pass', 'mass', 'loss', 'cross', 'gross', 'boss', 'toss', 'less', 'mess', 'dress', 'press', 'bless', 'guess', 'chess', 'glass', 'grass', 'brass'} + if first_word not in common_nouns_ending_s: + return False + + return True diff --git a/commit_check/main.py b/commit_check/main.py index 6faa0707..ee9bb0c8 100644 --- a/commit_check/main.py +++ b/commit_check/main.py @@ -92,6 +92,14 @@ def get_parser() -> argparse.ArgumentParser: required=False, ) + parser.add_argument( + '-im', + '--imperative-mood', + help='check commit message uses imperative mood', + action="store_true", + required=False, + ) + return parser @@ -122,6 +130,8 @@ def main() -> int: check_results.append(commit.check_commit_signoff(checks)) if args.merge_base: check_results.append(branch.check_merge_base(checks)) + if args.imperative_mood: + check_results.append(commit.check_imperative_mood(checks, args.commit_msg_file)) return PASS if all(val == PASS for val in check_results) else FAIL diff --git a/tests/commit_test.py b/tests/commit_test.py index c7339688..666fc8be 100644 --- a/tests/commit_test.py +++ b/tests/commit_test.py @@ -1,6 +1,6 @@ import pytest from commit_check import PASS, FAIL -from commit_check.commit import check_commit_msg, get_default_commit_msg_file, read_commit_msg, check_commit_signoff +from commit_check.commit import check_commit_msg, get_default_commit_msg_file, read_commit_msg, check_commit_signoff, check_imperative_mood # used by get_commit_info mock FAKE_BRANCH_NAME = "fake_commits_info" @@ -177,3 +177,224 @@ def test_check_commit_signoff_with_empty_checks(mocker): retval = check_commit_signoff(checks) assert retval == PASS assert m_re_match.call_count == 0 + + +@pytest.mark.benchmark +def test_check_imperative_mood_pass(mocker): + """Test imperative mood check passes for valid imperative mood.""" + checks = [{ + "check": "imperative_mood", + "regex": "", + "error": "Commit message should use imperative mood", + "suggest": "Use imperative mood" + }] + + mocker.patch( + "commit_check.commit.read_commit_msg", + return_value="feat: Add new feature\n\nThis adds a new feature to the application." + ) + + retval = check_imperative_mood(checks, MSG_FILE) + assert retval == PASS + + +@pytest.mark.benchmark +def test_check_imperative_mood_fail_past_tense(mocker): + """Test imperative mood check fails for past tense.""" + checks = [{ + "check": "imperative_mood", + "regex": "", + "error": "Commit message should use imperative mood", + "suggest": "Use imperative mood" + }] + + mocker.patch( + "commit_check.commit.read_commit_msg", + return_value="feat: Added new feature" + ) + + m_print_error_message = mocker.patch( + f"{LOCATION}.print_error_message" + ) + m_print_suggestion = mocker.patch( + f"{LOCATION}.print_suggestion" + ) + + retval = check_imperative_mood(checks, MSG_FILE) + assert retval == FAIL + assert m_print_error_message.call_count == 1 + assert m_print_suggestion.call_count == 1 + + +@pytest.mark.benchmark +def test_check_imperative_mood_fail_present_continuous(mocker): + """Test imperative mood check fails for present continuous.""" + checks = [{ + "check": "imperative_mood", + "regex": "", + "error": "Commit message should use imperative mood", + "suggest": "Use imperative mood" + }] + + mocker.patch( + "commit_check.commit.read_commit_msg", + return_value="feat: Adding new feature" + ) + + m_print_error_message = mocker.patch( + f"{LOCATION}.print_error_message" + ) + m_print_suggestion = mocker.patch( + f"{LOCATION}.print_suggestion" + ) + + retval = check_imperative_mood(checks, MSG_FILE) + assert retval == FAIL + assert m_print_error_message.call_count == 1 + assert m_print_suggestion.call_count == 1 + + +@pytest.mark.benchmark +def test_check_imperative_mood_skip_merge_commit(mocker): + """Test imperative mood check skips merge commits.""" + checks = [{ + "check": "imperative_mood", + "regex": "", + "error": "Commit message should use imperative mood", + "suggest": "Use imperative mood" + }] + + mocker.patch( + "commit_check.commit.read_commit_msg", + return_value="Merge branch 'feature/test' into main" + ) + + retval = check_imperative_mood(checks, MSG_FILE) + assert retval == PASS + + +@pytest.mark.benchmark +def test_check_imperative_mood_different_check_type(mocker): + """Test imperative mood check skips different check types.""" + checks = [{ + "check": "message", + "regex": "dummy_regex" + }] + + m_read_commit_msg = mocker.patch( + "commit_check.commit.read_commit_msg", + return_value="feat: Added new feature" + ) + + retval = check_imperative_mood(checks, MSG_FILE) + assert retval == PASS + assert m_read_commit_msg.call_count == 0 + + +@pytest.mark.benchmark +def test_check_imperative_mood_no_commits(mocker): + """Test imperative mood check passes when there are no commits.""" + checks = [{ + "check": "imperative_mood", + "regex": "", + "error": "Commit message should use imperative mood", + "suggest": "Use imperative mood" + }] + + mocker.patch("commit_check.commit.has_commits", return_value=False) + + retval = check_imperative_mood(checks, MSG_FILE) + assert retval == PASS + + +@pytest.mark.benchmark +def test_check_imperative_mood_empty_checks(mocker): + """Test imperative mood check with empty checks list.""" + checks = [] + + m_read_commit_msg = mocker.patch( + "commit_check.commit.read_commit_msg", + return_value="feat: Added new feature" + ) + + retval = check_imperative_mood(checks, MSG_FILE) + assert retval == PASS + assert m_read_commit_msg.call_count == 0 + + +@pytest.mark.benchmark +def test_is_imperative_mood_valid_cases(): + """Test _is_imperative_mood function with valid imperative mood cases.""" + from commit_check.commit import _is_imperative_mood + + valid_cases = [ + "Add new feature", + "Fix bug in authentication", + "Update documentation", + "Remove deprecated code", + "Refactor user service", + "Optimize database queries", + "Create new component", + "Delete unused files", + "Improve error handling", + "Enhance user experience", + "Implement new API", + "Configure CI/CD pipeline", + "Setup testing framework", + "Handle edge cases", + "Process user input", + "Validate form data", + "Transform data format", + "Initialize application", + "Load configuration", + "Save user preferences", + "", # Empty description should pass + ] + + for case in valid_cases: + assert _is_imperative_mood(case), f"'{case}' should be imperative mood" + + +@pytest.mark.benchmark +def test_is_imperative_mood_invalid_cases(): + """Test _is_imperative_mood function with invalid imperative mood cases.""" + from commit_check.commit import _is_imperative_mood + + invalid_cases = [ + "Added new feature", + "Fixed bug in authentication", + "Updated documentation", + "Removed deprecated code", + "Refactored user service", + "Optimized database queries", + "Created new component", + "Deleted unused files", + "Improved error handling", + "Enhanced user experience", + "Implemented new API", + "Adding new feature", + "Fixing bug in authentication", + "Updating documentation", + "Removing deprecated code", + "Refactoring user service", + "Optimizing database queries", + "Creating new component", + "Deleting unused files", + "Improving error handling", + "Enhancing user experience", + "Implementing new API", + "Adds new feature", + "Fixes bug in authentication", + "Updates documentation", + "Removes deprecated code", + "Refactors user service", + "Optimizes database queries", + "Creates new component", + "Deletes unused files", + "Improves error handling", + "Enhances user experience", + "Implements new API", + ] + + for case in invalid_cases: + assert not _is_imperative_mood(case), f"'{case}' should not be imperative mood" diff --git a/tests/main_test.py b/tests/main_test.py index c232efd8..ef3c42f5 100644 --- a/tests/main_test.py +++ b/tests/main_test.py @@ -8,20 +8,22 @@ class TestMain: @pytest.mark.benchmark - @pytest.mark.parametrize("argv, check_commit_call_count, check_branch_call_count, check_author_call_count, check_commit_signoff_call_count, check_merge_base_call_count", [ - ([CMD, "--message"], 1, 0, 0, 0, 0), - ([CMD, "--branch"], 0, 1, 0, 0, 0), - ([CMD, "--author-name"], 0, 0, 1, 0, 0), - ([CMD, "--author-email"], 0, 0, 1, 0, 0), - ([CMD, "--commit-signoff"], 0, 0, 0, 1, 0), - ([CMD, "--merge-base"], 0, 0, 0, 0, 1), - ([CMD, "--message", "--author-email"], 1, 0, 1, 0, 0), - ([CMD, "--branch", "--message"], 1, 1, 0, 0, 0), - ([CMD, "--author-name", "--author-email"], 0, 0, 2, 0, 0), - ([CMD, "--message", "--branch", "--author-email"], 1, 1, 1, 0, 0), - ([CMD, "--branch", "--message", "--author-name", "--author-email"], 1, 1, 2, 0, 0), - ([CMD, "--message", "--branch", "--author-name", "--author-email", "--commit-signoff", "--merge-base"], 1, 1, 2, 1, 1), - ([CMD, "--dry-run"], 0, 0, 0, 0, 0), + @pytest.mark.parametrize("argv, check_commit_call_count, check_branch_call_count, check_author_call_count, check_commit_signoff_call_count, check_merge_base_call_count, check_imperative_mood_call_count", [ + ([CMD, "--message"], 1, 0, 0, 0, 0, 0), + ([CMD, "--branch"], 0, 1, 0, 0, 0, 0), + ([CMD, "--author-name"], 0, 0, 1, 0, 0, 0), + ([CMD, "--author-email"], 0, 0, 1, 0, 0, 0), + ([CMD, "--commit-signoff"], 0, 0, 0, 1, 0, 0), + ([CMD, "--merge-base"], 0, 0, 0, 0, 1, 0), + ([CMD, "--imperative-mood"], 0, 0, 0, 0, 0, 1), + ([CMD, "--message", "--author-email"], 1, 0, 1, 0, 0, 0), + ([CMD, "--branch", "--message"], 1, 1, 0, 0, 0, 0), + ([CMD, "--author-name", "--author-email"], 0, 0, 2, 0, 0, 0), + ([CMD, "--message", "--branch", "--author-email"], 1, 1, 1, 0, 0, 0), + ([CMD, "--branch", "--message", "--author-name", "--author-email"], 1, 1, 2, 0, 0, 0), + ([CMD, "--message", "--branch", "--author-name", "--author-email", "--commit-signoff", "--merge-base"], 1, 1, 2, 1, 1, 0), + ([CMD, "--message", "--imperative-mood"], 1, 0, 0, 0, 0, 1), + ([CMD, "--dry-run"], 0, 0, 0, 0, 0, 0), ]) def test_main( self, @@ -32,6 +34,7 @@ def test_main( check_author_call_count, check_commit_signoff_call_count, check_merge_base_call_count, + check_imperative_mood_call_count, ): mocker.patch( "commit_check.main.validate_config", @@ -46,6 +49,7 @@ def test_main( m_check_author = mocker.patch("commit_check.author.check_author") m_check_commit_signoff = mocker.patch("commit_check.commit.check_commit_signoff") m_check_merge_base = mocker.patch("commit_check.branch.check_merge_base") + m_check_imperative_mood = mocker.patch("commit_check.commit.check_imperative_mood") sys.argv = argv main() assert m_check_commit.call_count == check_commit_call_count @@ -53,6 +57,7 @@ def test_main( assert m_check_author.call_count == check_author_call_count assert m_check_commit_signoff.call_count == check_commit_signoff_call_count assert m_check_merge_base.call_count == check_merge_base_call_count + assert m_check_imperative_mood.call_count == check_imperative_mood_call_count @pytest.mark.benchmark def test_main_help(self, mocker, capfd): @@ -168,6 +173,7 @@ def test_main_multiple_checks( mocker.patch( "commit_check.branch.check_merge_base", return_value=merge_base_result ) + mocker.patch("commit_check.commit.check_imperative_mood", return_value=PASS) # this is messy. why isn't this a private implementation detail with a # public check_author_name and check_author email? From fd82d87f3b0dd30743c2e687290fefff5d1568fa Mon Sep 17 00:00:00 2001 From: shenxianpeng Date: Sat, 12 Jul 2025 23:31:22 +0300 Subject: [PATCH 2/6] feat: introduce imperatives.py file --- commit_check/commit.py | 65 +++++----- commit_check/imperatives.py | 237 ++++++++++++++++++++++++++++++++++++ 2 files changed, 266 insertions(+), 36 deletions(-) create mode 100644 commit_check/imperatives.py diff --git a/commit_check/commit.py b/commit_check/commit.py index f4319199..7472383f 100644 --- a/commit_check/commit.py +++ b/commit_check/commit.py @@ -3,6 +3,12 @@ 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.imperatives import IMPERATIVES + + +def _load_imperatives() -> set: + """Load imperative verbs from imperatives module.""" + return IMPERATIVES def get_default_commit_msg_file() -> str: @@ -134,36 +140,8 @@ def _is_imperative_mood(description: str) -> bool: # Get the first word of the description first_word = description.split()[0].lower() - # Common non-imperative words (explicit list of bad forms) - non_imperative_words = { - # Past tense forms - 'added', 'updated', 'changed', 'modified', 'created', 'deleted', 'removed', - 'fixed', 'improved', 'refactored', 'optimized', 'enhanced', 'implemented', - 'configured', 'installed', 'uninstalled', 'upgraded', 'downgraded', - 'merged', 'rebased', 'committed', 'pushed', 'pulled', 'cloned', - 'tested', 'deployed', 'released', 'published', 'documented', - 'formatted', 'linted', 'cleaned', 'organized', 'restructured', - - # Present continuous forms - 'adding', 'updating', 'changing', 'modifying', 'creating', 'deleting', 'removing', - 'fixing', 'improving', 'refactoring', 'optimizing', 'enhancing', 'implementing', - 'configuring', 'installing', 'uninstalling', 'upgrading', 'downgrading', - 'merging', 'rebasing', 'committing', 'pushing', 'pulling', 'cloning', - 'testing', 'deploying', 'releasing', 'publishing', 'documenting', - 'formatting', 'linting', 'cleaning', 'organizing', 'restructuring', - - # Third person singular forms - 'adds', 'updates', 'changes', 'modifies', 'creates', 'deletes', 'removes', - 'fixes', 'improves', 'refactors', 'optimizes', 'enhances', 'implements', - 'configures', 'installs', 'uninstalls', 'upgrades', 'downgrades', - 'merges', 'rebases', 'commits', 'pushes', 'pulls', 'clones', - 'tests', 'deploys', 'releases', 'publishes', 'documents', - 'formats', 'lints', 'cleans', 'organizes', 'restructures', - } - - # Check if the first word is in our non-imperative list - if first_word in non_imperative_words: - return False + # Load imperative verbs from file + imperatives = _load_imperatives() # Check for common past tense pattern (-ed ending) but be more specific if (first_word.endswith('ed') and len(first_word) > 3 and @@ -177,12 +155,27 @@ def _is_imperative_mood(description: str) -> bool: # Check for third person singular (-s ending) but be more specific # Only flag if it's clearly a verb in third person singular form - if (first_word.endswith('s') and len(first_word) > 3 and - not first_word.endswith('ss') and not first_word.endswith('us') and - not first_word.endswith('es') and first_word not in {'process', 'access', 'address', 'express', 'suppress', 'compress', 'assess'}): - # Additional check: if it's a common noun ending in 's', allow it + if first_word.endswith('s') and len(first_word) > 3: + # Common nouns ending in 's' that should be allowed common_nouns_ending_s = {'process', 'access', 'address', 'progress', 'express', 'stress', 'success', 'class', 'pass', 'mass', 'loss', 'cross', 'gross', 'boss', 'toss', 'less', 'mess', 'dress', 'press', 'bless', 'guess', 'chess', 'glass', 'grass', 'brass'} - if first_word not in common_nouns_ending_s: - return False + # Words ending in 'ss' or 'us' are usually not third person singular verbs + if first_word.endswith('ss') or first_word.endswith('us'): + return True # Allow these + + # If it's a common noun, allow it + if first_word in common_nouns_ending_s: + return True + + # Otherwise, it's likely a third person singular verb + return False + + # If we have imperatives loaded, check if the first word is imperative + if imperatives: + # Check if the first word is in our imperative list + if first_word in imperatives: + return True + + # If word is not in imperatives list, apply some heuristics + # If it passes all the negative checks above, it's likely imperative return True diff --git a/commit_check/imperatives.py b/commit_check/imperatives.py new file mode 100644 index 00000000..0c3d0918 --- /dev/null +++ b/commit_check/imperatives.py @@ -0,0 +1,237 @@ +# https://github.com/crate-ci/imperative/blob/master/assets/imperatives.txt +# Imperative forms of verbs +# +# This file contains the imperative form of frequently encountered +# docstring verbs. Some of these may be more commonly encountered as +# nouns, but blacklisting them for this may cause false positives. + +IMPERATIVES = { + 'accept', + 'access', + 'add', + 'adjust', + 'aggregate', + 'allow', + 'append', + 'apply', + 'archive', + 'assert', + 'assign', + 'attempt', + 'authenticate', + 'authorize', + 'break', + 'build', + 'cache', + 'calculate', + 'call', + 'cancel', + 'capture', + 'change', + 'check', + 'clean', + 'clear', + 'close', + 'collect', + 'combine', + 'commit', + 'compare', + 'compute', + 'configure', + 'confirm', + 'connect', + 'construct', + 'control', + 'convert', + 'copy', + 'count', + 'create', + 'customize', + 'declare', + 'decode', + 'decorate', + 'define', + 'delegate', + 'delete', + 'deprecate', + 'derive', + 'describe', + 'detect', + 'determine', + 'display', + 'download', + 'drop', + 'dump', + 'emit', + 'empty', + 'enable', + 'encapsulate', + 'encode', + 'end', + 'ensure', + 'enumerate', + 'establish', + 'evaluate', + 'examine', + 'execute', + 'exit', + 'expand', + 'expect', + 'export', + 'extend', + 'extract', + 'feed', + 'fetch', + 'fill', + 'filter', + 'finalize', + 'find', + 'fire', + 'fix', + 'flag', + 'force', + 'format', + 'forward', + 'generate', + 'get', + 'give', + 'go', + 'group', + 'handle', + 'help', + 'hold', + 'identify', + 'implement', + 'import', + 'indicate', + 'init', + 'initialise', + 'initialize', + 'initiate', + 'input', + 'insert', + 'instantiate', + 'intercept', + 'invoke', + 'iterate', + 'join', + 'keep', + 'launch', + 'list', + 'listen', + 'load', + 'log', + 'look', + 'make', + 'manage', + 'manipulate', + 'map', + 'mark', + 'match', + 'merge', + 'mock', + 'modify', + 'monitor', + 'move', + 'normalize', + 'note', + 'obtain', + 'open', + 'output', + 'override', + 'overwrite', + 'package', + 'pad', + 'parse', + 'partial', + 'pass', + 'perform', + 'persist', + 'pick', + 'plot', + 'poll', + 'populate', + 'post', + 'prepare', + 'print', + 'process', + 'produce', + 'provide', + 'publish', + 'pull', + 'put', + 'query', + 'raise', + 'read', + 'record', + 'refer', + 'refresh', + 'register', + 'reload', + 'remove', + 'rename', + 'render', + 'replace', + 'reply', + 'report', + 'represent', + 'request', + 'require', + 'reset', + 'resolve', + 'retrieve', + 'return', + 'roll', + 'rollback', + 'round', + 'run', + 'sample', + 'save', + 'scan', + 'search', + 'select', + 'send', + 'serialise', + 'serialize', + 'serve', + 'set', + 'show', + 'simulate', + 'source', + 'specify', + 'split', + 'start', + 'step', + 'stop', + 'store', + 'strip', + 'submit', + 'subscribe', + 'sum', + 'swap', + 'sync', + 'synchronise', + 'synchronize', + 'take', + 'tear', + 'test', + 'time', + 'transform', + 'translate', + 'transmit', + 'truncate', + 'try', + 'turn', + 'tweak', + 'update', + 'upload', + 'use', + 'validate', + 'verify', + 'view', + 'wait', + 'walk', + 'wrap', + 'write', + 'yield', +} From fce69c1a6ac0848306f5e13311040bddd4002854 Mon Sep 17 00:00:00 2001 From: shenxianpeng Date: Sun, 13 Jul 2025 00:03:48 +0300 Subject: [PATCH 3/6] chore: rename to --imperative --- commit_check/commit.py | 2 +- commit_check/main.py | 8 ++++---- tests/commit_test.py | 30 +++++++++++++++--------------- tests/main_test.py | 14 +++++++------- 4 files changed, 27 insertions(+), 27 deletions(-) diff --git a/commit_check/commit.py b/commit_check/commit.py index 7472383f..78249ec4 100644 --- a/commit_check/commit.py +++ b/commit_check/commit.py @@ -92,7 +92,7 @@ def check_commit_signoff(checks: list, commit_msg_file: str = "") -> int: return PASS -def check_imperative_mood(checks: list, commit_msg_file: str = "") -> int: +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 diff --git a/commit_check/main.py b/commit_check/main.py index ee9bb0c8..f81af486 100644 --- a/commit_check/main.py +++ b/commit_check/main.py @@ -93,8 +93,8 @@ def get_parser() -> argparse.ArgumentParser: ) parser.add_argument( - '-im', - '--imperative-mood', + '-i', + '--imperative', help='check commit message uses imperative mood', action="store_true", required=False, @@ -130,8 +130,8 @@ def main() -> int: check_results.append(commit.check_commit_signoff(checks)) if args.merge_base: check_results.append(branch.check_merge_base(checks)) - if args.imperative_mood: - check_results.append(commit.check_imperative_mood(checks, args.commit_msg_file)) + if args.imperative: + check_results.append(commit.check_imperative(checks, args.commit_msg_file)) return PASS if all(val == PASS for val in check_results) else FAIL diff --git a/tests/commit_test.py b/tests/commit_test.py index 666fc8be..d0735fa2 100644 --- a/tests/commit_test.py +++ b/tests/commit_test.py @@ -1,6 +1,6 @@ import pytest from commit_check import PASS, FAIL -from commit_check.commit import check_commit_msg, get_default_commit_msg_file, read_commit_msg, check_commit_signoff, check_imperative_mood +from commit_check.commit import check_commit_msg, get_default_commit_msg_file, read_commit_msg, check_commit_signoff, check_imperative # used by get_commit_info mock FAKE_BRANCH_NAME = "fake_commits_info" @@ -180,7 +180,7 @@ def test_check_commit_signoff_with_empty_checks(mocker): @pytest.mark.benchmark -def test_check_imperative_mood_pass(mocker): +def test_check_imperative_pass(mocker): """Test imperative mood check passes for valid imperative mood.""" checks = [{ "check": "imperative_mood", @@ -194,12 +194,12 @@ def test_check_imperative_mood_pass(mocker): return_value="feat: Add new feature\n\nThis adds a new feature to the application." ) - retval = check_imperative_mood(checks, MSG_FILE) + retval = check_imperative(checks, MSG_FILE) assert retval == PASS @pytest.mark.benchmark -def test_check_imperative_mood_fail_past_tense(mocker): +def test_check_imperative_fail_past_tense(mocker): """Test imperative mood check fails for past tense.""" checks = [{ "check": "imperative_mood", @@ -220,14 +220,14 @@ def test_check_imperative_mood_fail_past_tense(mocker): f"{LOCATION}.print_suggestion" ) - retval = check_imperative_mood(checks, MSG_FILE) + retval = check_imperative(checks, MSG_FILE) assert retval == FAIL assert m_print_error_message.call_count == 1 assert m_print_suggestion.call_count == 1 @pytest.mark.benchmark -def test_check_imperative_mood_fail_present_continuous(mocker): +def test_check_imperative_fail_present_continuous(mocker): """Test imperative mood check fails for present continuous.""" checks = [{ "check": "imperative_mood", @@ -248,14 +248,14 @@ def test_check_imperative_mood_fail_present_continuous(mocker): f"{LOCATION}.print_suggestion" ) - retval = check_imperative_mood(checks, MSG_FILE) + retval = check_imperative(checks, MSG_FILE) assert retval == FAIL assert m_print_error_message.call_count == 1 assert m_print_suggestion.call_count == 1 @pytest.mark.benchmark -def test_check_imperative_mood_skip_merge_commit(mocker): +def test_check_imperative_skip_merge_commit(mocker): """Test imperative mood check skips merge commits.""" checks = [{ "check": "imperative_mood", @@ -269,12 +269,12 @@ def test_check_imperative_mood_skip_merge_commit(mocker): return_value="Merge branch 'feature/test' into main" ) - retval = check_imperative_mood(checks, MSG_FILE) + retval = check_imperative(checks, MSG_FILE) assert retval == PASS @pytest.mark.benchmark -def test_check_imperative_mood_different_check_type(mocker): +def test_check_imperative_different_check_type(mocker): """Test imperative mood check skips different check types.""" checks = [{ "check": "message", @@ -286,13 +286,13 @@ def test_check_imperative_mood_different_check_type(mocker): return_value="feat: Added new feature" ) - retval = check_imperative_mood(checks, MSG_FILE) + retval = check_imperative(checks, MSG_FILE) assert retval == PASS assert m_read_commit_msg.call_count == 0 @pytest.mark.benchmark -def test_check_imperative_mood_no_commits(mocker): +def test_check_imperative_no_commits(mocker): """Test imperative mood check passes when there are no commits.""" checks = [{ "check": "imperative_mood", @@ -303,12 +303,12 @@ def test_check_imperative_mood_no_commits(mocker): mocker.patch("commit_check.commit.has_commits", return_value=False) - retval = check_imperative_mood(checks, MSG_FILE) + retval = check_imperative(checks, MSG_FILE) assert retval == PASS @pytest.mark.benchmark -def test_check_imperative_mood_empty_checks(mocker): +def test_check_imperative_empty_checks(mocker): """Test imperative mood check with empty checks list.""" checks = [] @@ -317,7 +317,7 @@ def test_check_imperative_mood_empty_checks(mocker): return_value="feat: Added new feature" ) - retval = check_imperative_mood(checks, MSG_FILE) + retval = check_imperative(checks, MSG_FILE) assert retval == PASS assert m_read_commit_msg.call_count == 0 diff --git a/tests/main_test.py b/tests/main_test.py index ef3c42f5..4e277a6a 100644 --- a/tests/main_test.py +++ b/tests/main_test.py @@ -8,21 +8,21 @@ class TestMain: @pytest.mark.benchmark - @pytest.mark.parametrize("argv, check_commit_call_count, check_branch_call_count, check_author_call_count, check_commit_signoff_call_count, check_merge_base_call_count, check_imperative_mood_call_count", [ + @pytest.mark.parametrize("argv, check_commit_call_count, check_branch_call_count, check_author_call_count, check_commit_signoff_call_count, check_merge_base_call_count, check_imperative_call_count", [ ([CMD, "--message"], 1, 0, 0, 0, 0, 0), ([CMD, "--branch"], 0, 1, 0, 0, 0, 0), ([CMD, "--author-name"], 0, 0, 1, 0, 0, 0), ([CMD, "--author-email"], 0, 0, 1, 0, 0, 0), ([CMD, "--commit-signoff"], 0, 0, 0, 1, 0, 0), ([CMD, "--merge-base"], 0, 0, 0, 0, 1, 0), - ([CMD, "--imperative-mood"], 0, 0, 0, 0, 0, 1), + ([CMD, "--imperative"], 0, 0, 0, 0, 0, 1), ([CMD, "--message", "--author-email"], 1, 0, 1, 0, 0, 0), ([CMD, "--branch", "--message"], 1, 1, 0, 0, 0, 0), ([CMD, "--author-name", "--author-email"], 0, 0, 2, 0, 0, 0), ([CMD, "--message", "--branch", "--author-email"], 1, 1, 1, 0, 0, 0), ([CMD, "--branch", "--message", "--author-name", "--author-email"], 1, 1, 2, 0, 0, 0), ([CMD, "--message", "--branch", "--author-name", "--author-email", "--commit-signoff", "--merge-base"], 1, 1, 2, 1, 1, 0), - ([CMD, "--message", "--imperative-mood"], 1, 0, 0, 0, 0, 1), + ([CMD, "--message", "--imperative"], 1, 0, 0, 0, 0, 1), ([CMD, "--dry-run"], 0, 0, 0, 0, 0, 0), ]) def test_main( @@ -34,7 +34,7 @@ def test_main( check_author_call_count, check_commit_signoff_call_count, check_merge_base_call_count, - check_imperative_mood_call_count, + check_imperative_call_count, ): mocker.patch( "commit_check.main.validate_config", @@ -49,7 +49,7 @@ def test_main( m_check_author = mocker.patch("commit_check.author.check_author") m_check_commit_signoff = mocker.patch("commit_check.commit.check_commit_signoff") m_check_merge_base = mocker.patch("commit_check.branch.check_merge_base") - m_check_imperative_mood = mocker.patch("commit_check.commit.check_imperative_mood") + m_check_imperative = mocker.patch("commit_check.commit.check_imperative") sys.argv = argv main() assert m_check_commit.call_count == check_commit_call_count @@ -57,7 +57,7 @@ def test_main( assert m_check_author.call_count == check_author_call_count assert m_check_commit_signoff.call_count == check_commit_signoff_call_count assert m_check_merge_base.call_count == check_merge_base_call_count - assert m_check_imperative_mood.call_count == check_imperative_mood_call_count + assert m_check_imperative.call_count == check_imperative_call_count @pytest.mark.benchmark def test_main_help(self, mocker, capfd): @@ -173,7 +173,7 @@ def test_main_multiple_checks( mocker.patch( "commit_check.branch.check_merge_base", return_value=merge_base_result ) - mocker.patch("commit_check.commit.check_imperative_mood", return_value=PASS) + mocker.patch("commit_check.commit.check_imperative", return_value=PASS) # this is messy. why isn't this a private implementation detail with a # public check_author_name and check_author email? From fdb6de989f3b36a5df0e9407c314abcea9d5c221 Mon Sep 17 00:00:00 2001 From: shenxianpeng Date: Sun, 13 Jul 2025 00:09:27 +0300 Subject: [PATCH 4/6] chore: rename to --imperative --- .commit-check.yml | 2 +- README.rst | 21 +++++++++++++++++++-- commit_check/__init__.py | 2 +- commit_check/commit.py | 6 +++--- tests/commit_test.py | 26 +++++++++++++------------- 5 files changed, 37 insertions(+), 20 deletions(-) diff --git a/.commit-check.yml b/.commit-check.yml index c2b13f02..d215d90d 100644 --- a/.commit-check.yml +++ b/.commit-check.yml @@ -33,7 +33,7 @@ checks: error: Current branch is not rebased onto target branch suggest: Please ensure your branch is rebased with the target branch - - check: imperative_mood + - check: imperative regex: '' # Not used for imperative mood check error: 'Commit message should use imperative mood (e.g., "Add feature" not "Added feature")' suggest: 'Use imperative mood in commit message like "Add", "Fix", "Update", "Remove"' diff --git a/README.rst b/README.rst index f24da921..80072b6b 100644 --- a/README.rst +++ b/README.rst @@ -77,7 +77,7 @@ Running as pre-commit hook - id: check-author-email - id: check-commit-signoff - id: check-merge-base # requires download all git history - - id: check-imperative-mood + - id: check-imperative Running as CLI ~~~~~~~~~~~~~~ @@ -109,8 +109,25 @@ To configure the hook, create a script file in the ``.git/hooks/`` directory. .. code-block:: bash + + + + + + + + + + + + + + + + + #!/bin/sh - commit-check --message --branch --author-name --author-email --commit-signoff --merge-base --imperative-mood + commit-check --message --branch --author-name --author-email --commit-signoff --merge-base --imperative Save the script file as ``pre-push`` and make it executable: diff --git a/commit_check/__init__.py b/commit_check/__init__.py index 1ec7a3f0..8b787628 100644 --- a/commit_check/__init__.py +++ b/commit_check/__init__.py @@ -55,7 +55,7 @@ 'suggest': 'Please ensure your branch is rebased with the target branch', }, { - 'check': 'imperative_mood', + 'check': 'imperative', 'regex': r'', # Not used for imperative mood check 'error': 'Commit message should use imperative mood (e.g., "Add feature" not "Added feature")', 'suggest': 'Use imperative mood in commit message like "Add", "Fix", "Update", "Remove"', diff --git a/commit_check/commit.py b/commit_check/commit.py index 78249ec4..05a08e35 100644 --- a/commit_check/commit.py +++ b/commit_check/commit.py @@ -101,7 +101,7 @@ def check_imperative(checks: list, commit_msg_file: str = "") -> int: commit_msg_file = get_default_commit_msg_file() for check in checks: - if check['check'] == 'imperative_mood': + if check['check'] == 'imperative': commit_msg = read_commit_msg(commit_msg_file) # Extract the subject line (first line of commit message) @@ -118,7 +118,7 @@ def check_imperative(checks: list, commit_msg_file: str = "") -> int: description = subject # Check if the description uses imperative mood - if not _is_imperative_mood(description): + if not _is_imperative(description): if not print_error_header.has_been_called: print_error_header() # pragma: no cover print_error_message( @@ -132,7 +132,7 @@ def check_imperative(checks: list, commit_msg_file: str = "") -> int: return PASS -def _is_imperative_mood(description: str) -> bool: +def _is_imperative(description: str) -> bool: """Check if a description uses imperative mood.""" if not description: return True diff --git a/tests/commit_test.py b/tests/commit_test.py index d0735fa2..9a1235d7 100644 --- a/tests/commit_test.py +++ b/tests/commit_test.py @@ -183,7 +183,7 @@ def test_check_commit_signoff_with_empty_checks(mocker): def test_check_imperative_pass(mocker): """Test imperative mood check passes for valid imperative mood.""" checks = [{ - "check": "imperative_mood", + "check": "imperative", "regex": "", "error": "Commit message should use imperative mood", "suggest": "Use imperative mood" @@ -202,7 +202,7 @@ def test_check_imperative_pass(mocker): def test_check_imperative_fail_past_tense(mocker): """Test imperative mood check fails for past tense.""" checks = [{ - "check": "imperative_mood", + "check": "imperative", "regex": "", "error": "Commit message should use imperative mood", "suggest": "Use imperative mood" @@ -230,7 +230,7 @@ def test_check_imperative_fail_past_tense(mocker): def test_check_imperative_fail_present_continuous(mocker): """Test imperative mood check fails for present continuous.""" checks = [{ - "check": "imperative_mood", + "check": "imperative", "regex": "", "error": "Commit message should use imperative mood", "suggest": "Use imperative mood" @@ -258,7 +258,7 @@ def test_check_imperative_fail_present_continuous(mocker): def test_check_imperative_skip_merge_commit(mocker): """Test imperative mood check skips merge commits.""" checks = [{ - "check": "imperative_mood", + "check": "imperative", "regex": "", "error": "Commit message should use imperative mood", "suggest": "Use imperative mood" @@ -295,7 +295,7 @@ def test_check_imperative_different_check_type(mocker): def test_check_imperative_no_commits(mocker): """Test imperative mood check passes when there are no commits.""" checks = [{ - "check": "imperative_mood", + "check": "imperative", "regex": "", "error": "Commit message should use imperative mood", "suggest": "Use imperative mood" @@ -323,9 +323,9 @@ def test_check_imperative_empty_checks(mocker): @pytest.mark.benchmark -def test_is_imperative_mood_valid_cases(): - """Test _is_imperative_mood function with valid imperative mood cases.""" - from commit_check.commit import _is_imperative_mood +def test_is_imperative_valid_cases(): + """Test _is_imperative function with valid imperative mood cases.""" + from commit_check.commit import _is_imperative valid_cases = [ "Add new feature", @@ -352,13 +352,13 @@ def test_is_imperative_mood_valid_cases(): ] for case in valid_cases: - assert _is_imperative_mood(case), f"'{case}' should be imperative mood" + assert _is_imperative(case), f"'{case}' should be imperative mood" @pytest.mark.benchmark -def test_is_imperative_mood_invalid_cases(): - """Test _is_imperative_mood function with invalid imperative mood cases.""" - from commit_check.commit import _is_imperative_mood +def test_is_imperative_invalid_cases(): + """Test _is_imperative function with invalid imperative mood cases.""" + from commit_check.commit import _is_imperative invalid_cases = [ "Added new feature", @@ -397,4 +397,4 @@ def test_is_imperative_mood_invalid_cases(): ] for case in invalid_cases: - assert not _is_imperative_mood(case), f"'{case}' should not be imperative mood" + assert not _is_imperative(case), f"'{case}' should not be imperative mood" From 5b3151e69e11ceaa1f79da34af5e89e7a05235e4 Mon Sep 17 00:00:00 2001 From: shenxianpeng Date: Sun, 13 Jul 2025 00:11:48 +0300 Subject: [PATCH 5/6] chore: fix docs formatting --- README.rst | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/README.rst b/README.rst index 80072b6b..35a68e15 100644 --- a/README.rst +++ b/README.rst @@ -109,23 +109,6 @@ To configure the hook, create a script file in the ``.git/hooks/`` directory. .. code-block:: bash - - - - - - - - - - - - - - - - - #!/bin/sh commit-check --message --branch --author-name --author-email --commit-signoff --merge-base --imperative From c005ce13cca2fcfb50b3f6c2210bb2e37ad2c566 Mon Sep 17 00:00:00 2001 From: shenxianpeng Date: Sun, 13 Jul 2025 00:24:51 +0300 Subject: [PATCH 6/6] feat: add check imperative hook --- .pre-commit-config.yaml | 3 ++- .pre-commit-hooks.yaml | 7 +++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8f6f9629..58c47aa8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -42,4 +42,5 @@ repos: - id: check-author-name # uncomment if you need. - id: check-author-email # uncomment if you need. # - id: check-commit-signoff # uncomment if you need. - # - id: check-merge-base # requires download all git history + # - id: check-merge-base # requires download all git history + # - id: check-imperative # uncomment if you need. diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml index 1fca51a2..6a735963 100644 --- a/.pre-commit-hooks.yaml +++ b/.pre-commit-hooks.yaml @@ -41,3 +41,10 @@ args: [--merge-base] pass_filenames: false language: python +- id: check-imperative + name: check imperative mood + description: ensures commit message uses imperative mood + entry: commit-check + args: [--imperative] + pass_filenames: true + language: python