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 ad339d0..c2b13f0 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 b9ac03a..f24da92 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 521f68f..1ec7a3f 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 ac1c63a..f431919 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 6faa070..ee9bb0c 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 c733968..666fc8b 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 c232efd..ef3c42f 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 f431919..7472383 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 0000000..0c3d091 --- /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 7472383..78249ec 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 ee9bb0c..f81af48 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 666fc8b..d0735fa 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 ef3c42f..4e277a6 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 c2b13f0..d215d90 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 f24da92..80072b6 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 1ec7a3f..8b78762 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 78249ec..05a08e3 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 d0735fa..9a1235d 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 80072b6..35a68e1 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 8f6f962..58c47aa 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 1fca51a..6a73596 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 pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy