|
3 | 3 | from pathlib import PurePath
|
4 | 4 | from commit_check import YELLOW, RESET_COLOR, PASS, FAIL
|
5 | 5 | from commit_check.util import cmd_output, get_commit_info, print_error_header, print_error_message, print_suggestion, has_commits
|
| 6 | +from commit_check.imperatives import IMPERATIVES |
| 7 | + |
| 8 | + |
| 9 | +def _load_imperatives() -> set: |
| 10 | + """Load imperative verbs from imperatives module.""" |
| 11 | + return IMPERATIVES |
6 | 12 |
|
7 | 13 |
|
8 | 14 | def get_default_commit_msg_file() -> str:
|
@@ -84,3 +90,92 @@ def check_commit_signoff(checks: list, commit_msg_file: str = "") -> int:
|
84 | 90 | return FAIL
|
85 | 91 |
|
86 | 92 | return PASS
|
| 93 | + |
| 94 | + |
| 95 | +def check_imperative(checks: list, commit_msg_file: str = "") -> int: |
| 96 | + """Check if commit message uses imperative mood.""" |
| 97 | + if has_commits() is False: |
| 98 | + return PASS # pragma: no cover |
| 99 | + |
| 100 | + if commit_msg_file is None or commit_msg_file == "": |
| 101 | + commit_msg_file = get_default_commit_msg_file() |
| 102 | + |
| 103 | + for check in checks: |
| 104 | + if check['check'] == 'imperative': |
| 105 | + commit_msg = read_commit_msg(commit_msg_file) |
| 106 | + |
| 107 | + # Extract the subject line (first line of commit message) |
| 108 | + subject = commit_msg.split('\n')[0].strip() |
| 109 | + |
| 110 | + # Skip if empty or merge commit |
| 111 | + if not subject or subject.startswith('Merge'): |
| 112 | + return PASS |
| 113 | + |
| 114 | + # For conventional commits, extract description after the colon |
| 115 | + if ':' in subject: |
| 116 | + description = subject.split(':', 1)[1].strip() |
| 117 | + else: |
| 118 | + description = subject |
| 119 | + |
| 120 | + # Check if the description uses imperative mood |
| 121 | + if not _is_imperative(description): |
| 122 | + if not print_error_header.has_been_called: |
| 123 | + print_error_header() # pragma: no cover |
| 124 | + print_error_message( |
| 125 | + check['check'], 'imperative mood pattern', |
| 126 | + check['error'], subject, |
| 127 | + ) |
| 128 | + if check['suggest']: |
| 129 | + print_suggestion(check['suggest']) |
| 130 | + return FAIL |
| 131 | + |
| 132 | + return PASS |
| 133 | + |
| 134 | + |
| 135 | +def _is_imperative(description: str) -> bool: |
| 136 | + """Check if a description uses imperative mood.""" |
| 137 | + if not description: |
| 138 | + return True |
| 139 | + |
| 140 | + # Get the first word of the description |
| 141 | + first_word = description.split()[0].lower() |
| 142 | + |
| 143 | + # Load imperative verbs from file |
| 144 | + imperatives = _load_imperatives() |
| 145 | + |
| 146 | + # Check for common past tense pattern (-ed ending) but be more specific |
| 147 | + if (first_word.endswith('ed') and len(first_word) > 3 and |
| 148 | + first_word not in {'red', 'bed', 'fed', 'led', 'wed', 'shed', 'fled'}): |
| 149 | + return False |
| 150 | + |
| 151 | + # Check for present continuous pattern (-ing ending) but be more specific |
| 152 | + if (first_word.endswith('ing') and len(first_word) > 4 and |
| 153 | + first_word not in {'ring', 'sing', 'king', 'wing', 'thing', 'string', 'bring'}): |
| 154 | + return False |
| 155 | + |
| 156 | + # Check for third person singular (-s ending) but be more specific |
| 157 | + # Only flag if it's clearly a verb in third person singular form |
| 158 | + if first_word.endswith('s') and len(first_word) > 3: |
| 159 | + # Common nouns ending in 's' that should be allowed |
| 160 | + 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'} |
| 161 | + |
| 162 | + # Words ending in 'ss' or 'us' are usually not third person singular verbs |
| 163 | + if first_word.endswith('ss') or first_word.endswith('us'): |
| 164 | + return True # Allow these |
| 165 | + |
| 166 | + # If it's a common noun, allow it |
| 167 | + if first_word in common_nouns_ending_s: |
| 168 | + return True |
| 169 | + |
| 170 | + # Otherwise, it's likely a third person singular verb |
| 171 | + return False |
| 172 | + |
| 173 | + # If we have imperatives loaded, check if the first word is imperative |
| 174 | + if imperatives: |
| 175 | + # Check if the first word is in our imperative list |
| 176 | + if first_word in imperatives: |
| 177 | + return True |
| 178 | + |
| 179 | + # If word is not in imperatives list, apply some heuristics |
| 180 | + # If it passes all the negative checks above, it's likely imperative |
| 181 | + return True |
0 commit comments