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
18 changes: 15 additions & 3 deletions src/git_commit_guard/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,12 @@ def _download_if_missing(resource):
nltk.download(resource.rsplit("/", maxsplit=1)[-1], quiet=True)


def _format_allowed_hint(allowed, kind):
if len(allowed) <= len(TYPES):
return f"(allowed: {', '.join(sorted(allowed))})"
return f"(see configured {kind})"


def _strip_comments(message):
return "\n".join(
line for line in message.split("\n") if not line.lstrip().startswith("#")
Expand Down Expand Up @@ -178,13 +184,16 @@ def check_subject( # noqa: PLR0913 Too many arguments in function definition (9
return None

if m.group("type") not in allowed_types:
result.error(f"unknown type: {m.group('type')}", check=Check.SUBJECT)
bad_type = m.group("type")
hint = _format_allowed_hint(allowed_types, "types")
result.error(f"unknown type: {bad_type} {hint}", check=Check.SUBJECT)

scope = m.group("scope")
if require_scope and scope is None:
result.error("scope is required", check=Check.SUBJECT)
if allowed_scopes and scope is not None and scope not in allowed_scopes:
result.error(f"unknown scope: {scope}", check=Check.SUBJECT)
hint = _format_allowed_hint(allowed_scopes, "scopes")
result.error(f"unknown scope: {scope} {hint}", check=Check.SUBJECT)

desc = m.group("desc")
if require_lowercase and desc[0].isupper():
Expand Down Expand Up @@ -249,7 +258,10 @@ def check_body(lines, result):

def check_signed_off(message, result):
if not SIGNED_OFF_RE.search(message):
result.error("missing 'Signed-off-by' trailer", check=Check.SIGNED_OFF)
result.error(
"missing 'Signed-off-by' trailer — use 'git commit -s'",
check=Check.SIGNED_OFF,
)


def check_subject_pattern(subject, pattern, result):
Expand Down
40 changes: 40 additions & 0 deletions tests/test_git_commit_guard.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,25 @@ def test_unknown_type(self):
check_subject("unknown: add thing", r)
assert not r.ok

def test_unknown_type_with_default_lists_all_allowed(self):
r = Result()
check_subject("unknown: add thing", r)
assert any(
"allowed: " in m and "feat" in m and "fix" in m for _, _, m in r.errors
)

def test_unknown_type_with_smaller_set_lists_them(self):
r = Result()
check_subject("foo: add thing", r, allowed_types=frozenset({"feat", "fix"}))
assert any("allowed: feat, fix" in m for _, _, m in r.errors)

def test_unknown_type_with_larger_than_default_points_at_config(self):
r = Result()
oversized = frozenset({f"t{i}" for i in range(20)})
check_subject("foo: add thing", r, allowed_types=oversized)
assert any("see configured types" in m for _, _, m in r.errors)
assert not any("allowed:" in m for _, _, m in r.errors)

def test_uppercase_description(self):
r = Result()
check_subject("fix: Add token", r)
Expand Down Expand Up @@ -184,6 +203,22 @@ def test_scope_not_in_allowlist_fails(self):
check_subject("fix(api): add token", r, allowed_scopes=frozenset(["auth"]))
assert not r.ok

def test_unknown_scope_with_small_set_lists_them(self):
r = Result()
check_subject(
"fix(api): add token",
r,
allowed_scopes=frozenset({"auth", "db"}),
)
assert any("allowed: auth, db" in m for _, _, m in r.errors)

def test_unknown_scope_with_larger_than_default_points_at_config(self):
r = Result()
oversized = frozenset({f"s{i}" for i in range(20)})
check_subject("fix(foo): add token", r, allowed_scopes=oversized)
assert any("see configured scopes" in m for _, _, m in r.errors)
assert not any("allowed:" in m for _, _, m in r.errors)

def test_no_scope_with_allowlist_passes(self):
r = Result()
check_subject("fix: add token", r, allowed_scopes=frozenset(["auth"]))
Expand Down Expand Up @@ -325,6 +360,11 @@ def test_missing(self):
check_signed_off("fix: add thing\n\nbody", r)
assert not r.ok

def test_missing_message_hints_at_git_commit_dash_s(self):
r = Result()
check_signed_off("fix: add thing\n\nbody", r)
assert any("git commit -s" in m for _, _, m in r.errors)

def test_malformed_no_email(self):
r = Result()
check_signed_off("fix: add thing\n\nSigned-off-by: John Doe", r)
Expand Down
Loading