Skip to content

Commit 2b9711c

Browse files
authored
feat: add impreative mode (#258)
* feat: add impreative mode * feat: introduce imperatives.py file * chore: rename to --imperative * chore: rename to --imperative * chore: fix docs formatting * feat: add check imperative hook
1 parent 41b53a9 commit 2b9711c

File tree

10 files changed

+606
-17
lines changed

10 files changed

+606
-17
lines changed

.commit-check.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,8 @@ checks:
3232
regex: main # it can be master, develop, devel etc based on your project.
3333
error: Current branch is not rebased onto target branch
3434
suggest: Please ensure your branch is rebased with the target branch
35+
36+
- check: imperative
37+
regex: '' # Not used for imperative mood check
38+
error: 'Commit message should use imperative mood (e.g., "Add feature" not "Added feature")'
39+
suggest: 'Use imperative mood in commit message like "Add", "Fix", "Update", "Remove"'

.pre-commit-config.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,5 @@ repos:
4242
- id: check-author-name # uncomment if you need.
4343
- id: check-author-email # uncomment if you need.
4444
# - id: check-commit-signoff # uncomment if you need.
45-
# - id: check-merge-base # requires download all git history
45+
# - id: check-merge-base # requires download all git history
46+
# - id: check-imperative # uncomment if you need.

.pre-commit-hooks.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,10 @@
4141
args: [--merge-base]
4242
pass_filenames: false
4343
language: python
44+
- id: check-imperative
45+
name: check imperative mood
46+
description: ensures commit message uses imperative mood
47+
entry: commit-check
48+
args: [--imperative]
49+
pass_filenames: true
50+
language: python

README.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ Running as pre-commit hook
7777
- id: check-author-email
7878
- id: check-commit-signoff
7979
- id: check-merge-base # requires download all git history
80+
- id: check-imperative
8081
8182
Running as CLI
8283
~~~~~~~~~~~~~~
@@ -109,7 +110,7 @@ To configure the hook, create a script file in the ``.git/hooks/`` directory.
109110
.. code-block:: bash
110111
111112
#!/bin/sh
112-
commit-check --message --branch --author-name --author-email --commit-signoff --merge-base
113+
commit-check --message --branch --author-name --author-email --commit-signoff --merge-base --imperative
113114
114115
Save the script file as ``pre-push`` and make it executable:
115116

commit_check/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,12 @@
5454
'error': 'Current branch is not rebased onto target branch',
5555
'suggest': 'Please ensure your branch is rebased with the target branch',
5656
},
57+
{
58+
'check': 'imperative',
59+
'regex': r'', # Not used for imperative mood check
60+
'error': 'Commit message should use imperative mood (e.g., "Add feature" not "Added feature")',
61+
'suggest': 'Use imperative mood in commit message like "Add", "Fix", "Update", "Remove"',
62+
},
5763
],
5864
}
5965

commit_check/commit.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@
33
from pathlib import PurePath
44
from commit_check import YELLOW, RESET_COLOR, PASS, FAIL
55
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
612

713

814
def get_default_commit_msg_file() -> str:
@@ -84,3 +90,92 @@ def check_commit_signoff(checks: list, commit_msg_file: str = "") -> int:
8490
return FAIL
8591

8692
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

Comments
 (0)