Skip to content
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
125 changes: 15 additions & 110 deletions src/git_commit_guard/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from pathlib import Path

import nltk
from nltk.corpus import wordnet

TYPES = frozenset(
{
Expand All @@ -24,113 +25,7 @@
}
)

IMPERATIVE_VERBS = frozenset(
{
"add",
"allow",
"apply",
"avoid",
"bump",
"catch",
"change",
"check",
"clean",
"clear",
"configure",
"correct",
"create",
"define",
"delete",
"deprecate",
"disable",
"document",
"drop",
"enable",
"enforce",
"ensure",
"exclude",
"export",
"extend",
"extract",
"fix",
"format",
"guard",
"handle",
"ignore",
"implement",
"import",
"improve",
"include",
"increase",
"initialize",
"inline",
"install",
"introduce",
"invalidate",
"limit",
"log",
"lint",
"make",
"mark",
"merge",
"migrate",
"move",
"normalize",
"open",
"optimize",
"override",
"parse",
"pass",
"patch",
"pin",
"port",
"prevent",
"print",
"provide",
"publish",
"reduce",
"refactor",
"release",
"remove",
"rename",
"reorganize",
"replace",
"report",
"require",
"reset",
"resolve",
"restore",
"restrict",
"return",
"revert",
"run",
"separate",
"set",
"show",
"simplify",
"skip",
"sort",
"split",
"start",
"stop",
"store",
"support",
"suppress",
"switch",
"sync",
"track",
"trim",
"unify",
"update",
"upgrade",
"use",
"validate",
"vendor",
"verify",
"wait",
"wrap",
}
)
_NON_IMPERATIVE_SUFFIX_RE = re.compile(r"(?:ing|ed)$")

SUBJECT_RE = re.compile(
r"^(?P<type>\w+)(?:\((?P<scope>[^)]+)\))?!?:\s+(?P<desc>.+)$",
Expand Down Expand Up @@ -190,6 +85,7 @@ def ok(self):
def _ensure_nltk_data():
_download_if_missing("taggers/averaged_perceptron_tagger_eng")
_download_if_missing("tokenizers/punkt_tab")
_download_if_missing("corpora/wordnet")


def _download_if_missing(resource):
Expand Down Expand Up @@ -229,10 +125,19 @@ def check_imperative(desc, result):
if not tokens:
return
first = tokens[0]
if first in IMPERATIVE_VERBS:
if _NON_IMPERATIVE_SUFFIX_RE.search(first):
result.error(f"expected imperative verb, got '{first}' (non-imperative suffix)")
return
base = wordnet.morphy(first, wordnet.VERB)
if base is not None and base != first:
result.error(
f"expected imperative verb, got '{first}' (inflected form of '{base}')"
)
return
tagged = nltk.pos_tag(["i", *tokens])
if tagged[1][1] not in {"VB", "VBP"}:
tagged = nltk.pos_tag(["to", *tokens])
if tagged[1][1] != "VB":
if wordnet.morphy(first, wordnet.VERB) == first:
return
result.error(
f"expected imperative verb, got '{tagged[1][0]}' (POS={tagged[1][1]})",
)
Expand Down
32 changes: 29 additions & 3 deletions tests/test_git_commit_guard.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,17 +190,17 @@ def test_malformed_no_email(self):


class TestCheckImperative:
def test_whitelist_verb_passes(self):
def test_imperative_verb_passes(self):
r = Result()
check_imperative("add token refresh", r)
assert r.ok

def test_past_tense_fails(self):
def test_ed_suffix_fails(self):
r = Result()
check_imperative("added token refresh", r)
assert not r.ok

def test_gerund_fails(self):
def test_ing_suffix_fails(self):
r = Result()
check_imperative("adding token refresh", r)
assert not r.ok
Expand All @@ -210,6 +210,11 @@ def test_third_person_fails(self):
check_imperative("adds token refresh", r)
assert not r.ok

def test_third_person_es_suffix_fails(self):
r = Result()
check_imperative("fixes the bug", r)
assert not r.ok

def test_non_whitelist_imperative_passes(self):
r = Result()
check_imperative("refactor authentication module", r)
Expand All @@ -220,6 +225,27 @@ def test_write_imperative_passes(self):
check_imperative("write unit tests", r)
assert r.ok

def test_tagger_misclassified_verb_passes(self):
# 'disable' is tagged non-VB by the tagger but wordnet confirms it as a verb
r = Result()
check_imperative("disable feature flag", r)
assert r.ok

def test_refactor_passes(self):
r = Result()
check_imperative("refactor authentication module", r)
assert r.ok

def test_vendor_passes(self):
r = Result()
check_imperative("vendor third-party libs", r)
assert r.ok

def test_configure_passes(self):
r = Result()
check_imperative("configure logging pipeline", r)
assert r.ok

def test_empty_desc_passes(self):
r = Result()
check_imperative("", r)
Expand Down