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)
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