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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ commit_check.egg-info
__pycache__
.mypy_cache
.vscode
*.swp
venv
.venv
UNKNOWN.egg-info
Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,11 @@ repos:
args:
- --subject-imperative=false
- --subject-max-length=100
- id: check-author-email
args:
- --no-banner
- --author-email
- --author-email-pattern=^.+@example\.com$
```

See the [Configuration documentation](https://commit-check.github.io/commit-check/configuration.html) for all available options.
Expand Down
6 changes: 6 additions & 0 deletions commit_check/config_merger.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ def get_default_config() -> dict[str, Any]:
"require_signed_off_by": DEFAULT_BOOLEAN_RULES["require_signed_off_by"],
"ignore_authors": [],
"ai_attribution": DEFAULT_AI_ATTRIBUTION,
"author_email_pattern": "^.+@.+$",
"author_name_pattern": "",
Comment thread
coderabbitai[bot] marked this conversation as resolved.
},
"branch": {
"conventional_branch": True,
Expand Down Expand Up @@ -123,6 +125,8 @@ class ConfigMerger:
"CCHK_REQUIRE_SIGNED_OFF_BY": ("commit", "require_signed_off_by", parse_bool),
"CCHK_IGNORE_AUTHORS": ("commit", "ignore_authors", parse_list),
"CCHK_AI_ATTRIBUTION": ("commit", "ai_attribution", str),
"CCHK_AUTHOR_EMAIL_PATTERN": ("commit", "author_email_pattern", str),
"CCHK_AUTHOR_NAME_PATTERN": ("commit", "author_name_pattern", str),
# Branch section
"CCHK_CONVENTIONAL_BRANCH": ("branch", "conventional_branch", parse_bool),
"CCHK_ALLOW_BRANCH_TYPES": ("branch", "allow_branch_types", parse_list),
Expand Down Expand Up @@ -151,6 +155,8 @@ class ConfigMerger:
"require_signed_off_by": ("commit", "require_signed_off_by"),
"ignore_authors": ("commit", "ignore_authors"),
"ai_attribution": ("commit", "ai_attribution"),
"author_email_pattern": ("commit", "author_email_pattern"),
"author_name_pattern": ("commit", "author_name_pattern"),
# Branch section
"conventional_branch": ("branch", "conventional_branch"),
"allow_branch_types": ("branch", "allow_branch_types"),
Expand Down
16 changes: 16 additions & 0 deletions commit_check/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,22 @@ def _get_parser() -> argparse.ArgumentParser:
"'forbid' rejects commits with known AI tool signatures.",
)

commit_group.add_argument(
"--author-email-pattern",
type=str,
default=None,
metavar="REGEX",
help="regex to check author email (requires --author-email)",
)

commit_group.add_argument(
"--author-name-pattern",
type=str,
default=None,
metavar="REGEX",
help="regex to check author name (requires --author-name)",
)

# Branch configuration options
branch_group = parser.add_argument_group(
"branch options", "Configuration options for --branch validation"
Expand Down
21 changes: 21 additions & 0 deletions commit_check/rule_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,12 @@ def _build_single_rule(
return self._build_author_list_rule(catalog_entry, "ignore_authors")
elif check == "ai_attribution":
return self._build_ai_attribution_rule(catalog_entry)
elif check == "author_email":
return self._build_author_pattern_rule(
catalog_entry, "author_email_pattern"
)
elif check == "author_name":
return self._build_author_pattern_rule(catalog_entry, "author_name_pattern")
elif check == "merge_base":
return self._build_merge_base_rule(catalog_entry)
else:
Expand Down Expand Up @@ -236,6 +242,21 @@ def _build_author_list_rule(
return ValidationRule(check=catalog_entry.check, ignored=author_list)
return None

def _build_author_pattern_rule(
self, catalog_entry: RuleCatalogEntry, config_key: str
) -> ValidationRule | None:
"""Build author name or email validation rule."""
regex = catalog_entry.regex
if self.commit_config.get(config_key, ""):
regex = self.commit_config.get(config_key, "").strip()

return ValidationRule(
check=catalog_entry.check,
regex=regex,
error=catalog_entry.error,
suggest=catalog_entry.suggest,
)

Comment thread
coderabbitai[bot] marked this conversation as resolved.
def _build_merge_base_rule(
self, catalog_entry: RuleCatalogEntry
) -> ValidationRule | None:
Expand Down
18 changes: 18 additions & 0 deletions docs/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,12 @@ Configuration can also be set via environment variables with the ``CCHK_`` prefi
* - ``ignore_authors = ["bot"]``
- ``CCHK_IGNORE_AUTHORS=bot,user``
- ``--ignore-authors=bot,user``
* - ``author_email_pattern=^.+@example\.com$``
- ``CCHK_AUTHOR_EMAIL_PATTERN=^.+@example\.com$``
- ``--author-email-pattern=^.+@example\.com$``
* - ``author_name_pattern=^.+ .+$``
- ``CCHK_AUTHOR_NAME_PATTERN=^.+ .+$``
- ``--author-name-pattern=^.+ .+$``
* - ``conventional_branch = true``
- ``CCHK_CONVENTIONAL_BRANCH=true``
- ``--conventional-branch=true``
Expand Down Expand Up @@ -397,6 +403,18 @@ Options Table Description
- list[str]
- [] (none ignored)
- List of commit authors **or co-authors** (``Co-authored-by:`` lines) to bypass all commit checks. Useful for bots (e.g., ``"dependabot[bot]"``, ``"coderabbitai[bot]"``).
* - commit
- author_email_pattern
- str
- ^.+@.+$
- Custom regex for the author email check. When empty, the built-in default pattern is used.
This option only takes effect when the author_email check is enabled (``-e`` / ``--author-email``).
* - commit
- author_name_pattern
- str
- "" (built-in default)
- Custom regex for the author name check. When empty, the built-in default pattern is used (it is not disabled).
This option only takes effect when the author_name check is enabled (``-n`` / ``--author-name``).
* - commit
- require_signed_off_by
- bool
Expand Down
7 changes: 7 additions & 0 deletions tests/config_merger_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,13 @@ def test_parse_branch_env_vars(self, monkeypatch):
assert config["branch"]["conventional_branch"] is False
assert config["branch"]["allow_branch_types"] == ["feature", "bugfix"]

def test_parse_author_pattern_env_vars(self, monkeypatch):
monkeypatch.setenv("CCHK_AUTHOR_NAME_PATTERN", r"^[A-Z][a-z]+ [A-Z][a-z]+$")
monkeypatch.setenv("CCHK_AUTHOR_EMAIL_PATTERN", r"^.+@company\.com$")
config = ConfigMerger.parse_env_vars()
assert config["commit"]["author_name_pattern"] == r"^[A-Z][a-z]+ [A-Z][a-z]+$"
assert config["commit"]["author_email_pattern"] == r"^.+@company\.com$"

def test_invalid_env_var_is_skipped(self, monkeypatch, capsys):
monkeypatch.setenv("CCHK_SUBJECT_MAX_LENGTH", "invalid")
config = ConfigMerger.parse_env_vars()
Expand Down
51 changes: 51 additions & 0 deletions tests/engine_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -556,6 +556,57 @@ def test_get_author_value_with_email_format(self):
assert author_value == "test@example.com"


class TestAuthorPatternConfig:
"""Tests for configurable author_name_pattern / author_email_pattern.

Rules are built through RuleBuilder so the actual config resolution
(custom pattern override + fallback to the built-in catalog regex) is
exercised, not just an inline regex.
"""

@staticmethod
def _author_rule(commit_config, check):
builder = RuleBuilder({"commit": commit_config})
rules = builder.build_all_rules()
return next(r for r in rules if r.check == check)

@staticmethod
def _validate(rule, author_value):
validator = AuthorValidator(rule)
with patch("commit_check.util._print_failure"):
return validator.validate(ValidationContext(stdin_text=author_value))

@pytest.mark.benchmark
def test_custom_name_pattern_pass_and_fail(self):
"""A custom author_name_pattern accepts matches and rejects non-matches."""
rule = self._author_rule(
{"author_name_pattern": r"^[A-Z][a-z]+ [A-Z][a-z]+$"}, "author_name"
)
assert self._validate(rule, "Jane Doe") == ValidationResult.PASS
assert self._validate(rule, "jane") == ValidationResult.FAIL

@pytest.mark.benchmark
def test_custom_email_pattern_enforces_domain(self):
"""A custom author_email_pattern can enforce a company domain."""
rule = self._author_rule(
{"author_email_pattern": r"^.+@company\.com$"}, "author_email"
)
assert self._validate(rule, "bob@company.com") == ValidationResult.PASS
assert self._validate(rule, "bob@gmail.com") == ValidationResult.FAIL

@pytest.mark.benchmark
def test_default_name_pattern_uses_builtin_regex(self):
"""With no custom pattern, the built-in catalog regex still applies.

Regression guard: an empty/omitted author_name_pattern must not disable
the check — it should fall back to the shipped default so an invalid
name is still rejected.
"""
rule = self._author_rule({}, "author_name")
assert self._validate(rule, "Jane Doe") == ValidationResult.PASS
assert self._validate(rule, "12345 !!!") == ValidationResult.FAIL


class TestCommitTypeValidator:
@pytest.mark.benchmark
def test_commit_type_validator_merge_commits(self):
Expand Down
Loading