Skip to content

feat: add imperative mode #258

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Jul 12, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .commit-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
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"'
3 changes: 2 additions & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
7 changes: 7 additions & 0 deletions .pre-commit-hooks.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
3 changes: 2 additions & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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

Running as CLI
~~~~~~~~~~~~~~
Expand Down Expand Up @@ -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

Save the script file as ``pre-push`` and make it executable:

Expand Down
6 changes: 6 additions & 0 deletions commit_check/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
'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"',
},
],
}

Expand Down
95 changes: 95 additions & 0 deletions commit_check/commit.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -84,3 +90,92 @@
return FAIL

return PASS


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

if commit_msg_file is None or commit_msg_file == "":
commit_msg_file = get_default_commit_msg_file()

Check warning on line 101 in commit_check/commit.py

View check run for this annotation

Codecov / codecov/patch

commit_check/commit.py#L101

Added line #L101 was not covered by tests

for check in checks:
if check['check'] == 'imperative':
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 warning on line 118 in commit_check/commit.py

View check run for this annotation

Codecov / codecov/patch

commit_check/commit.py#L118

Added line #L118 was not covered by tests

# Check if the description uses imperative mood
if not _is_imperative(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(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()

# 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
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:
# 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'}

# 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

Check warning on line 168 in commit_check/commit.py

View check run for this annotation

Codecov / codecov/patch

commit_check/commit.py#L168

Added line #L168 was not covered by tests

# 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
Loading
Loading