diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 045335c2..8d071e54 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,11 +14,118 @@ Our development branch is `main`. When submitting pull requests, please adhere t By contributing any code or documentation to this repository (by raising pull requests or otherwise), you explicitly agree to the [License Agreement](https://github.com/commit-check/commit-check/blob/main/LICENSE). -We appreciate your contributions to make Commit Check even better! +## Architecture + +### Overview + +Commit-check validates Git commit metadata using a pipeline of configurable validators. The key flow is: + +``` +CLI args / Env vars / TOML file + │ + ▼ + ConfigMerger ← Merges all config sources (priority: CLI > Env > TOML > Defaults) + │ + ▼ + RuleBuilder ← Builds ValidationRule objects from merged config + rules catalog + │ + ▼ + ValidationEngine ← Iterates over rules, picks the right validator for each + │ + ▼ + BaseValidator subclasses ← Each validator performs one focused check + │ + ▼ + Exit code 0/1 +``` + +### Module responsibilities + +``` +commit_check/ +├── __init__.py # Package constants: DEFAULT_COMMIT_TYPES, DEFAULT_BRANCH_TYPES, DEFAULT_BOOLEAN_RULES +├── main.py # CLI entry point, argument parsing, StdinReader +├── config.py # TOML file loading (uses tomllib on Python 3.11+, tomli on older) +├── config_merger.py # ConfigMerger: merges CLI → Env → TOML → Defaults +├── rule_builder.py # RuleBuilder: creates ValidationRule objects from config + catalog +├── rules_catalog.py # Catalog of all rules (COMMIT_RULES, BRANCH_RULES) +├── engine.py # ValidationEngine, BaseValidator ABC, ValidationContext, ValidationResult +├── imperatives.py # ~258 English imperative verbs for subject validation +└── util.py # Git operations, output formatting (_print_failure) +``` -## Development +### Validator class hierarchy -### Debug commit-check pre-commit hook +``` +BaseValidator (ABC) +├── CommitMessageValidator # Full message: conventional commits format +├── SubjectValidator (ABC) +│ ├── SubjectCapitalizationValidator # First letter must be uppercase +│ ├── SubjectImperativeValidator # Subject must start with imperative verb +│ └── SubjectLengthValidator # Subject length min/max +├── AuthorValidator # Author name and email format +├── BranchValidator # Branch naming conventions +├── MergeBaseValidator # Merge base / rebase target +├── SignoffValidator # Signed-off-by trailer presence +├── BodyValidator # Commit body presence +└── CommitTypeValidator # Handles merge/revert/fixup/wip/empty commits +``` + +### Configuration priority cascade + +| Priority | Source | Example | +|----------|--------|---------| +| 1 (highest) | CLI arguments | `--subject-max-length=72` | +| 2 | Environment variables | `CCHK_SUBJECT_MAX_LENGTH=72` | +| 3 | TOML config files | `cchk.toml`, `.github/cchk.toml`, etc. | +| 4 (lowest) | Built-in defaults | defined in `commit_check/__init__.py` | + +## Development setup + +### Prerequisites + +- Python 3.9 or newer +- `nox` for running build sessions: `pip install nox` + +### Install in development mode + +```bash +git clone https://github.com/commit-check/commit-check.git +cd commit-check +pip install -e ".[test]" +``` + +### Run tests + +```bash +# Fastest: run pytest directly +pytest tests/ -v + +# With coverage report +nox -s coverage +``` + +### Lint and format + +```bash +# Run all pre-commit hooks (ruff, mypy, codespell, etc.) +nox -s lint + +# Or install hooks for automatic checks on every commit +pre-commit install +``` + +### Build documentation + +```bash +# One-time build +nox -s docs + +# Live preview with auto-reload +nox -s docs-live +``` + +### Test the pre-commit hook locally ```bash pre-commit try-repo ./../commit-check/ check-message --verbose --hook-stage commit-msg --commit-msg-filename .git/COMMIT_EDITMSG @@ -32,7 +139,7 @@ pip install -e ./../commit-check/ commit-check -m ``` -### Test commit-check pre-commit hook on GitHub +## Test commit-check pre-commit hook on GitHub ```yaml - repo: https://github.com/commit-check/commit-check @@ -42,3 +149,5 @@ commit-check -m - id: check-branch - id: check-author-email ``` + +We appreciate your contributions to make Commit Check even better! diff --git a/commit_check/rule_builder.py b/commit_check/rule_builder.py index 46c57322..88505172 100644 --- a/commit_check/rule_builder.py +++ b/commit_check/rule_builder.py @@ -110,11 +110,16 @@ def _build_conventional_commit_rule( allowed_types = self._get_allowed_commit_types() regex = self._build_conventional_commit_regex(allowed_types) + types_str = ", ".join(allowed_types) + suggest = ( + f"Use (): , where is one of: {types_str}" + ) + return ValidationRule( check=catalog_entry.check, regex=regex, error=catalog_entry.error, - suggest=catalog_entry.suggest, + suggest=suggest, allowed=allowed_types, ) diff --git a/commit_check/rules_catalog.py b/commit_check/rules_catalog.py index e291915a..1c806828 100644 --- a/commit_check/rules_catalog.py +++ b/commit_check/rules_catalog.py @@ -29,8 +29,8 @@ class RuleCatalogEntry: RuleCatalogEntry( check="subject_imperative", regex=None, - error="Commit message should use imperative mood (e.g., 'Add feature' not 'Added feature')", - suggest="Use imperative mood in the subject line", + error="Commit message should use imperative mood (e.g., 'fix bug' not 'fixed bug', 'add feature' not 'adding feature')", + suggest="Change the first verb to imperative form, e.g., 'fix' instead of 'fixed'/'fixes'/'fixing'", ), RuleCatalogEntry( check="subject_max_length", diff --git a/commit_check/util.py b/commit_check/util.py index 57b07826..b1a6cd05 100644 --- a/commit_check/util.py +++ b/commit_check/util.py @@ -36,7 +36,7 @@ def _print_failure(check: dict, regex: str, actual: str) -> None: """Print a standardized failure message.""" if not print_error_header.has_been_called: print_error_header() - print_error_message(check["check"], regex, check.get("error", ""), actual) + print_error_message(check["check"], check.get("error", ""), actual) if check.get("suggest"): print_suggestion(check.get("suggest")) @@ -241,10 +241,9 @@ def print_error_header(): print(" ") -def print_error_message(check_type: str, regex: str, error: str, reason: str): +def print_error_message(check_type: str, error: str, reason: str): """Print error message. :param check_type: - :param regex: :param error: :param reason: @@ -255,8 +254,8 @@ def print_error_message(check_type: str, regex: str, error: str, reason: str): end="", ) print("") - print(f"It doesn't match regex: {regex}") - print(error) + if error: + print(error) def print_suggestion(suggest: Optional[str]) -> None: diff --git a/tests/util_test.py b/tests/util_test.py index d9caf2fb..98e9624f 100644 --- a/tests/util_test.py +++ b/tests/util_test.py @@ -322,14 +322,12 @@ def test_print_error_header(self, capfd): @pytest.mark.benchmark def test_print_error_message(self, capfd, check_type, type_failed_msg): # Must print on stdout with given argument. - dummy_regex = "dummy regex" dummy_reason = "failure reason" dummy_error = "dummy error" - print_error_message(check_type, dummy_regex, dummy_error, dummy_reason) + print_error_message(check_type, dummy_error, dummy_reason) stdout, _ = capfd.readouterr() assert check_type in stdout assert type_failed_msg in stdout - assert f"It doesn't match regex: {dummy_regex}" in stdout assert dummy_error in stdout class TestPrintSuggestion: