diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 487ea65b..fffc07db 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -107,7 +107,7 @@ jobs: uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: "commit-check_docs" - path: ${{ github.workspace }}/_build/html + path: ${{ github.workspace }}/site - name: Upload docs to github pages # only publish doc changes from main branch @@ -115,4 +115,7 @@ jobs: uses: peaceiris/actions-gh-pages@84c30a85c19949d7eee79c4ff27748b70285e453 # v4.1.0 with: github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: ./_build/html + publish_dir: ./site + # Pinned explicitly so the custom domain cannot be lost when the + # publish branch is replaced. + cname: docs.commit-check.com diff --git a/.gitignore b/.gitignore index 3dfd5ade..546ac132 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,7 @@ _build/ docs/_build docs/cli_args.rst docs/__pycache__ + +# MkDocs +site/ +docs/cli.md diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index efb135f6..50a3a9c8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -12,6 +12,11 @@ repos: rev: v6.0.0 hooks: - id: check-yaml + # mkdocs.yml carries the `!!python/name:` tags that Material's emoji + # extension requires, which yaml.safe_load cannot construct. MkDocs + # validates the file itself on every build (`mkdocs build --strict`), + # so it is checked more thoroughly than this hook would manage. + exclude: ^mkdocs\.yml$ - id: check-toml - id: end-of-file-fixer - id: trailing-whitespace diff --git a/commit_check/rules_catalog.py b/commit_check/rules_catalog.py index 71d4eb05..a912cc1c 100644 --- a/commit_check/rules_catalog.py +++ b/commit_check/rules_catalog.py @@ -21,7 +21,7 @@ from dataclasses import dataclass #: Base URL of the rules reference documentation. -RULES_DOCS_URL = "https://docs.commit-check.com/rules.html" +RULES_DOCS_URL = "https://docs.commit-check.com/rules/" @dataclass(frozen=True) diff --git a/docs/README.rst b/docs/README.rst deleted file mode 100644 index d2964103..00000000 --- a/docs/README.rst +++ /dev/null @@ -1,21 +0,0 @@ -:orphan: - -How to build the docs -===================== - -From the root directory of the repository, do the following to steps - -1. Install docs' dependencies - - .. code-block:: text - - pip install nox - -2. Build the docs - - .. code-block:: text - - nox -s docs - - Browse the files in /_build/html with your internet browser to see the rendered - output. diff --git a/docs/_static/extra_css.css b/docs/_static/extra_css.css deleted file mode 100644 index 34f05968..00000000 --- a/docs/_static/extra_css.css +++ /dev/null @@ -1,53 +0,0 @@ -tbody .stub, -thead { - background-color: var(--md-accent-bg-color--light); - color: var(--md-default-bg-color); -} - -.md-header, -.md-tabs, -.md-nav--primary .md-nav__title[for="__drawer"] { - background-color: #2c9ccd; -} - -/* Fix table header visibility for both light and dark modes */ -.md-content table th { - color: var(--md-typeset-color) !important; -} - -/* Custom color scheme to match logo */ -:root { - --md-primary-fg-color: #2c9ccd; - --md-primary-fg-color--light: #5bb3d9; - --md-primary-fg-color--dark: #1e85a8; -} - -/* Navigation and links */ -.md-nav__link--active, -.md-nav__link:hover { - color: #2c9ccd; -} - -/* Buttons and accent elements */ -.md-button--primary { - background-color: #2c9ccd; - border-color: #2c9ccd; -} - -.md-button--primary:hover { - background-color: #1e85a8; - border-color: #1e85a8; -} - -/* Code blocks and syntax highlighting accents */ -.md-typeset .codehilite .hll, -.md-typeset .highlight .hll { - background-color: rgba(44, 156, 205, 0.1); -} - -/* Admonition titles with your brand color */ -.md-typeset .admonition.note > .admonition-title, -.md-typeset .admonition.tip > .admonition-title { - background-color: rgba(44, 156, 205, 0.1); - border-color: #2c9ccd; -} diff --git a/docs/_static/logo.jpg b/docs/_static/logo.jpg deleted file mode 100644 index ed4cd723..00000000 Binary files a/docs/_static/logo.jpg and /dev/null differ diff --git a/docs/assets/extra.css b/docs/assets/extra.css new file mode 100644 index 00000000..5aab2bf1 --- /dev/null +++ b/docs/assets/extra.css @@ -0,0 +1,91 @@ +/* Brand palette ----------------------------------------------------------- + Material's "custom" primary/accent read these variables. */ +:root { + --cc-brand: #2c9ccd; + --cc-brand-dark: #1e85a8; + --cc-brand-light: #5bb3d9; + /* The brand blue only reaches 4.2:1 on white; links need 4.5:1. */ + --cc-link: #176b89; + + --md-primary-fg-color: var(--cc-brand); + --md-primary-fg-color--light: var(--cc-brand-light); + --md-primary-fg-color--dark: var(--cc-brand-dark); + --md-accent-fg-color: var(--cc-brand-dark); +} + +[data-md-color-scheme="slate"] { + --md-primary-fg-color: var(--cc-brand); + --md-accent-fg-color: var(--cc-brand-light); + --md-typeset-a-color: var(--cc-brand-light); +} + +[data-md-color-scheme="default"] { + --md-typeset-a-color: var(--cc-link); +} + +/* Landing page ------------------------------------------------------------- + The hero is the first thing a visitor sees, so it carries the value + proposition rather than a badge wall. */ +.cc-hero { + text-align: center; + padding: 3.5rem 1rem 2.5rem; +} + +.cc-hero h1 { + font-size: 2.9rem; + line-height: 1.15; + font-weight: 800; + letter-spacing: -0.02em; + margin: 0 0 0.6rem; +} + +.cc-hero p { + font-size: 1.05rem; + max-width: 40rem; + margin: 0 auto 1.8rem; + color: var(--md-default-fg-color--light); +} + +.cc-hero .md-button { + margin: 0.3rem; +} + +/* Hide the redundant H1 that MkDocs renders above a custom hero. */ +.cc-hero + hr { + display: none; +} + +/* Feature grid: uses Material's card grid, tightened up a little. */ +.md-typeset .grid.cards > ul > li { + border-radius: 0.4rem; + transition: border-color 125ms, box-shadow 125ms; +} + +.md-typeset .grid.cards > ul > li:hover { + border-color: var(--cc-brand); + box-shadow: 0 0 0 1px var(--cc-brand); +} + +.md-typeset .grid.cards > ul > li > hr { + margin: 0.6rem 0; +} + +/* Tables ------------------------------------------------------------------- + Cells align to the top so short values line up with a wrapped message. */ +.md-typeset table:not([class]) td, +.md-typeset table:not([class]) th { + vertical-align: top; +} + +/* In the rule index tables only, keep codes, names and CLI flags on one line + so the tables stay scannable. Other tables hold long regexes and lists that + must stay wrappable. */ +.md-typeset table.rules-index td code { + white-space: nowrap; +} + +/* Sidebar section headings ("Getting started", "Reference", ...) */ +.md-nav__item--section > .md-nav__link { + font-weight: 700; + color: var(--md-default-fg-color); +} diff --git a/docs/assets/favicon.svg b/docs/assets/favicon.svg new file mode 100644 index 00000000..896e52aa --- /dev/null +++ b/docs/assets/favicon.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/docs/assets/logo.svg b/docs/assets/logo.svg new file mode 100644 index 00000000..a4d11323 --- /dev/null +++ b/docs/assets/logo.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/docs/changelog.md b/docs/changelog.md new file mode 100644 index 00000000..f05bc244 --- /dev/null +++ b/docs/changelog.md @@ -0,0 +1,188 @@ +# Changelog + +All **notable changes** to this project will be documented in this file. + +Full changelog available at [GitHub releases](https://github.com/commit-check/commit-check/releases). + +## v2.11.0 (2026-07-06) + +### New Features + +* **AI attribution governance** — Added support for forbidding known AI tool + signatures (e.g., `Co-authored-by: Copilot`) in commit messages. New + `[commit]` config option `ai_attribution` (default `"ignore"`) rejects + commits carrying known AI tool signatures when set to `"forbid"`. See PR [#456](https://github.com/commit-check/commit-check/pull/456). + +### Bug Fixes + +* Fixed `MergeBaseValidator` branch detection — replaced `git branch -a` + regex matching with `git rev-parse --verify` to avoid false positives + (e.g., pattern `main` matching `main-staging`). See PR [#451](https://github.com/commit-check/commit-check/pull/451). + +### Chores + +* Added OpenSSF Scorecard workflow, badge, and pinned dependency SHAs for CI +* Migrated PyPI publishing to `pypa/gh-action-pypi-publish` +* Removed OpenSSF Scorecard badge after evaluation (moved to Scorecard dashboard) + +## v2.10.1 (2026-06-30) + +### Bug Fixes + +* **WIP detection case-insensitivity** — `WIP` (`[WIP]`, `WIP:`, `wip:`, + etc.) is now recognized regardless of case across all common patterns. + See PR [#448](https://github.com/commit-check/commit-check/pull/448). +* **Conventional commit special characters** — Allowed special characters + (parentheses, brackets, etc.) in the description part of conventional commit + messages. See PR [#447](https://github.com/commit-check/commit-check/pull/447). + +### Refactors + +* Extracted `_get_commit_message` to `BaseValidator` to remove code + duplication across validators. See PR [#445](https://github.com/commit-check/commit-check/pull/445). +* Removed legacy YAML config parsing code from `util.py`. + See PR [#444](https://github.com/commit-check/commit-check/pull/444). + +## v2.10.0 (2026-06-26) + +### New Features + +* **Dependabot / Renovate as default branch type** — `dependabot/` and + `renovate/` branch prefixes are now included in `DEFAULT_BRANCH_TYPES`, + so dependency update branches are automatically recognized. + See PR [#442](https://github.com/commit-check/commit-check/pull/442). + +## v2.9.0 (2026-06-22) + +### New Features + +* **AI agent branch prefixes (Conventional Branch v1.1.0)** — Added + `ai/`, `claude/`, `codex/`, `copilot/`, and `cursor/` to + `DEFAULT_BRANCH_TYPES` so branches created by AI coding agents are + recognized as valid. See PR [#438](https://github.com/commit-check/commit-check/pull/438). + +## v2.8.1 (2026-06-22) + +### Chores + +* Fixed 27 SonarQube code-quality issues across source and test files, + including path traversal vulnerability fix, cognitive complexity + reduction, and duplicate branch consolidation. See PR [#436](https://github.com/commit-check/commit-check/pull/436). +* Added SchemaStore IDE autocompletion support for `cchk.toml`. + See PR [#433](https://github.com/commit-check/commit-check/pull/433). + +## v2.8.0 (2026-06-13) + +### New Features + +* **Custom commit message pattern** — New `message_pattern` option in the + `[commit]` config section allows replacing the built-in Conventional Commits + regex with a user-defined regex pattern. Also supported via the + `CCHK_MESSAGE_PATTERN` environment variable. See PR [#427](https://github.com/commit-check/commit-check/pull/427). + +### Breaking Changes + +* **Dropped Python 3.9 support** — Minimum required Python version is now + 3.10. Type annotations have been modernized (PEP 604/585) and the + `py.typed` marker added for downstream type checkers. + See PR [#424](https://github.com/commit-check/commit-check/pull/424). + +## v2.7.1 (2026-06-08) + +### Chores + +* Added `auto` to the list of imperative verbs. See PR [#417](https://github.com/commit-check/commit-check/pull/417). +* Added commit-check vs GitHub Rulesets comparison table to the README. + See PR [#419](https://github.com/commit-check/commit-check/pull/419). + +## v2.7.0 (2026-05-16) + +### New Features + +* **Force push detection and blocking** — Added `--no-force-push` CLI flag and + `check-no-force-push` pre-push hook that inspect pushed ref ancestry via + `git merge-base --is-ancestor` to detect and block `git push --force` and + `git push -f`. A new `[push]` TOML config section with + `allow_force_push` (default `true`) controls the behavior. Environment + variable `CCHK_ALLOW_FORCE_PUSH` is also supported. + +* **`validate_push()` API** — New `commit_check.api.validate_push()` + function for programmatic push safety checks, matching the `--no-force-push` + CLI behavior without spawning a subprocess. + +* **Standalone mode** — When `--no-force-push` is run outside a pre-push hook + (no stdin), it checks whether pushing `HEAD` to its configured upstream + would require force, using `git ls-remote` and optional `git fetch` to + resolve the remote commit. + +* **Expanded imperative verbs** — Added 156 new imperative verbs across 10 + categories (auth/security, data ops, lifecycle, I/O, debugging, UI/UX, + engineering, general), growing the total from 234 to 390. + See PR [#414](https://github.com/commit-check/commit-check/pull/414). + +## v2.6.0 (2026-04-20) + +### New Features + +* **Lower-noise CLI failure output** — Added `--no-banner` to suppress the ASCII art header while preserving detailed errors and suggestions. +* **Compact failure mode** — Added `--compact` to print one `[FAIL]` line per failing check for CI logs and automation-friendly terminal output. This mode also suppresses the banner. + +### Bug Fixes + +* Fixed `print_error_header` state handling so repeated validations stay consistent when `--compact` is used. + +## v2.5.0 (2026-04-03) + +### New Features + +* **Co-author bypass in `ignore_authors`** — `_should_skip_commit_validation()` now parses `Co-authored-by:` trailers in the commit message body. If any co-author name matches `ignore_authors`, all commit checks are skipped. Useful for AI bots that co-author commits (e.g., `coderabbitai[bot]`). +* **Organization-level config inheritance via `inherit_from`** — New top-level TOML key that loads a parent config from a GitHub shorthand (`github:owner/repo:path`), a local file path, or an HTTPS URL, then deep-merges it with local settings. HTTP (non-TLS) URLs are rejected to prevent MITM attacks. +* **Git config author validation** — `AuthorValidator` now checks `git config user.name` / `user.email` first (the identity used for the *next* commit), falling back to `git log` if unset. Previously, a misconfigured identity would pass if the last commit had a valid author. + +### Bug Fixes + +* Fixed incorrect mock target in `test_main_with_message_empty_string_no_stdin_with_git`: was patching `commit_check.util.get_commit_info` (ineffective) instead of `commit_check.engine.get_commit_info`. + +## v2.0.0 (2025-10-01) + +!!! warning + + This major release introduces significant architectural changes and breaking updates to commit-check. Please review carefully before upgrading. + +### What's New + +* **TOML Configuration** — Replaces the old `.commit-check.yml` with `cchk.toml` or `commit-check.toml` for clearer syntax. +* **Simplified CLI & Hooks** — Legacy pre-commit hooks and command-line options have been removed for a cleaner, more consistent interface. +* **New Validation Engine** — The validation system has been completely redesigned around a new ValidationEngine to improve maintainability and flexibility. + +#### Breaking Changes + +Configuration Format: + +* `.commit-check.yml` has been replaced with `cchk.toml` or `commit-check.toml`. +* All YAML configurations must be migrated to TOML from this version onward. +* See the [Migration Guide](migration.md) for step-by-step instructions. + +Removed Pre-commit Hooks and CLI Options: + +* Several legacy hooks and command-line flags have been removed in favor of a simplified interface. +* Removed hooks: `check-commit-signoff`, `check-merge-base`, `check-imperative`. +* Removed CLI options: `--signoff`, `--merge-base`, `--imperative`. + +Module Removal: + +* The following legacy modules have been removed: `author.py`, `branch.py`, `commit.py`, `error.py`. + +Architecture Redesign: + +* The validation system has been completely restructured around the new `ValidationEngine`, breaking compatibility with any code or integrations relying on the old module structure. + +See PR [#280](https://github.com/commit-check/commit-check/pull/280) + +## v0.10.2 (2025-08-26) + +Last release before the big v2.0 changes. + +## v0.1.0 (2022-11-02) + +Initial release of commit-check. diff --git a/docs/changelog.rst b/docs/changelog.rst deleted file mode 100644 index 89d87726..00000000 --- a/docs/changelog.rst +++ /dev/null @@ -1,227 +0,0 @@ -Changelog -========= - -All **notable changes** to this project will be documented in this file. - -Full changelog available at `GitHub releases `_. - -v2.11.0 (2026-07-06) -------------------- - -New Features -~~~~~~~~~~~~ - -* **AI attribution governance** — Added support for forbidding known AI tool - signatures (e.g., ``Co-authored-by: Copilot``) in commit messages. New - ``[commit]`` config option ``forbid_ai_attribution`` (boolean, default - ``false``) rejects commits co-authored by AI coding agents. See PR :pr:`456`. - -Bug Fixes -~~~~~~~~~ - -* Fixed ``MergeBaseValidator`` branch detection — replaced ``git branch -a`` - regex matching with ``git rev-parse --verify`` to avoid false positives - (e.g., pattern ``main`` matching ``main-staging``). See PR :pr:`451`. - -Chores -~~~~~~ - -* Added OpenSSF Scorecard workflow, badge, and pinned dependency SHAs for CI -* Migrated PyPI publishing to ``pypa/gh-action-pypi-publish`` -* Removed OpenSSF Scorecard badge after evaluation (moved to Scorecard dashboard) - - -v2.10.1 (2026-06-30) -------------------- - -Bug Fixes -~~~~~~~~~ - -* **WIP detection case-insensitivity** — ``WIP`` (``[WIP]``, ``WIP:``, ``wip:``, - etc.) is now recognized regardless of case across all common patterns. - See PR :pr:`448`. -* **Conventional commit special characters** — Allowed special characters - (parentheses, brackets, etc.) in the description part of conventional commit - messages. See PR :pr:`447`. - -Refactors -~~~~~~~~~ - -* Extracted ``_get_commit_message`` to ``BaseValidator`` to remove code - duplication across validators. See PR :pr:`445`. -* Removed legacy YAML config parsing code from ``util.py``. - See PR :pr:`444`. - - -v2.10.0 (2026-06-26) -------------------- - -New Features -~~~~~~~~~~~~ - -* **Dependabot / Renovate as default branch type** — ``dependabot/`` and - ``renovate/`` branch prefixes are now included in ``DEFAULT_BRANCH_TYPES``, - so dependency update branches are automatically recognized. - See PR :pr:`442`. - - -v2.9.0 (2026-06-22) -------------------- - -New Features -~~~~~~~~~~~~ - -* **AI agent branch prefixes (Conventional Branch v1.1.0)** — Added - ``ai/``, ``claude/``, ``codex/``, ``copilot/``, and ``cursor/`` to - ``DEFAULT_BRANCH_TYPES`` so branches created by AI coding agents are - recognized as valid. See PR :pr:`438`. - - -v2.8.1 (2026-06-22) -------------------- - -Chores -~~~~~~ - -* Fixed 27 SonarQube code-quality issues across source and test files, - including path traversal vulnerability fix, cognitive complexity - reduction, and duplicate branch consolidation. See PR :pr:`436`. -* Added SchemaStore IDE autocompletion support for ``cchk.toml``. - See PR :pr:`433`. - - -v2.8.0 (2026-06-13) -------------------- - -New Features -~~~~~~~~~~~~ - -* **Custom commit message pattern** — New ``message_pattern`` option in the - ``[commit]`` config section allows replacing the built-in Conventional Commits - regex with a user-defined regex pattern. Also supported via the - ``CCHK_MESSAGE_PATTERN`` environment variable. See PR :pr:`427`. - -Breaking Changes -~~~~~~~~~~~~~~~~ - -* **Dropped Python 3.9 support** — Minimum required Python version is now - 3.10. Type annotations have been modernized (PEP 604/585) and the - ``py.typed`` marker added for downstream type checkers. - See PR :pr:`424`. - - -v2.7.1 (2026-06-08) -------------------- - -Chores -~~~~~~ - -* Added ``auto`` to the list of imperative verbs. See PR :pr:`417`. -* Added commit-check vs GitHub Rulesets comparison table to the README. - See PR :pr:`419`. - - -v2.7.0 (2026-05-16) -------------------- - -New Features -~~~~~~~~~~~~ - -* **Force push detection and blocking** — Added ``--no-force-push`` CLI flag and - ``check-no-force-push`` pre-push hook that inspect pushed ref ancestry via - ``git merge-base --is-ancestor`` to detect and block ``git push --force`` and - ``git push -f``. A new ``[push]`` TOML config section with - ``allow_force_push`` (default ``true``) controls the behavior. Environment - variable ``CCHK_ALLOW_FORCE_PUSH`` is also supported. - -* **``validate_push()`` API** — New ``commit_check.api.validate_push()`` - function for programmatic push safety checks, matching the ``--no-force-push`` - CLI behavior without spawning a subprocess. - -* **Standalone mode** — When ``--no-force-push`` is run outside a pre-push hook - (no stdin), it checks whether pushing ``HEAD`` to its configured upstream - would require force, using ``git ls-remote`` and optional ``git fetch`` to - resolve the remote commit. - -* **Expanded imperative verbs** — Added 156 new imperative verbs across 10 - categories (auth/security, data ops, lifecycle, I/O, debugging, UI/UX, - engineering, general), growing the total from 234 to 390. - See PR :pr:`414`. - - -v2.6.0 (2026-04-20) -------------------- - -New Features -~~~~~~~~~~~~ - -* **Lower-noise CLI failure output** — Added ``--no-banner`` to suppress the ASCII art header while preserving detailed errors and suggestions. -* **Compact failure mode** — Added ``--compact`` to print one ``[FAIL]`` line per failing check for CI logs and automation-friendly terminal output. This mode also suppresses the banner. - -Bug Fixes -~~~~~~~~~ - -* Fixed ``print_error_header`` state handling so repeated validations stay consistent when ``--compact`` is used. - -v2.5.0 (2026-04-03) -------------------- - -New Features -~~~~~~~~~~~~ - -* **Co-author bypass in ``ignore_authors``** — ``_should_skip_commit_validation()`` now parses ``Co-authored-by:`` trailers in the commit message body. If any co-author name matches ``ignore_authors``, all commit checks are skipped. Useful for AI bots that co-author commits (e.g., ``coderabbitai[bot]``). -* **Organization-level config inheritance via ``inherit_from``** — New top-level TOML key that loads a parent config from a GitHub shorthand (``github:owner/repo:path``), a local file path, or an HTTPS URL, then deep-merges it with local settings. HTTP (non-TLS) URLs are rejected to prevent MITM attacks. -* **Git config author validation** — ``AuthorValidator`` now checks ``git config user.name`` / ``user.email`` first (the identity used for the *next* commit), falling back to ``git log`` if unset. Previously, a misconfigured identity would pass if the last commit had a valid author. - -Bug Fixes -~~~~~~~~~ - -* Fixed incorrect mock target in ``test_main_with_message_empty_string_no_stdin_with_git``: was patching ``commit_check.util.get_commit_info`` (ineffective) instead of ``commit_check.engine.get_commit_info``. - -v2.0.0 (2025-10-01) -------------------- - -.. Attention:: - This major release introduces significant architectural changes and breaking updates to commit-check. Please review carefully before upgrading. - -What's New -~~~~~~~~~~ - -* **TOML Configuration** — Replaces the old ``.commit-check.yml`` with ``cchk.toml`` or ``commit-check.toml`` for clearer syntax. -* **Simplified CLI & Hooks** — Legacy pre-commit hooks and command-line options have been removed for a cleaner, more consistent interface. -* **New Validation Engine** — The validation system has been completely redesigned around a new ValidationEngine to improve maintainability and flexibility. - -Breaking Changes -^^^^^^^^^^^^^^^^ - -Configuration Format: - -* ``.commit-check.yml`` has been replaced with ``cchk.toml`` or ``commit-check.toml``. -* All YAML configurations must be migrated to TOML from this version onward. -* See the `Migration Guide `_ for step-by-step instructions. - -Removed Pre-commit Hooks and CLI Options: - -* Several legacy hooks and command-line flags have been removed in favor of a simplified interface. -* Removed hooks: ``check-commit-signoff``, ``check-merge-base``, ``check-imperative``. -* Removed CLI options: ``--signoff``, ``--merge-base``, ``--imperative``. - -Module Removal: - -* The following legacy modules have been removed: ``author.py``, ``branch.py``, ``commit.py``, ``error.py``. - -Architecture Redesign: - -* The validation system has been completely restructured around the new ``ValidationEngine``, breaking compatibility with any code or integrations relying on the old module structure. - -See PR :pr:`280` - -v0.10.2 (2025-08-26) --------------------- - -Last release before the big v2.0 changes. - -v0.1.0 (2022-11-02) --------------------- - -Initial release of commit-check. diff --git a/docs/conf.py b/docs/conf.py deleted file mode 100644 index 898349b0..00000000 --- a/docs/conf.py +++ /dev/null @@ -1,153 +0,0 @@ -# pylint: disable=all -# Configuration file for the Sphinx documentation builder. -# -# For the full list of built-in configuration values, see the documentation: -# https://www.sphinx-doc.org/en/master/usage/configuration.html -import re -import datetime -from pathlib import Path -import subprocess -from sphinx.application import Sphinx - -# -- Project information ----------------------------------------------------- -# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information -project = "commit-check" -copyright = f"{datetime.date.today().year}, shenxianpeng" -author = "shenxianpeng" - -# -- General configuration --------------------------------------------------- -# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration -extensions = [ - "myst_parser", - "sphinx_immaterial", - "sphinx.ext.autodoc", - "sphinx.ext.intersphinx", - "sphinx.ext.viewcode", - "sphinx_issues", -] - -source_suffix = { - ".rst": "restructuredtext", - ".md": "markdown", -} - -# Treat bare URLs as external links so MyST does not warn about TOC -# anchor references (e.g. `(#overview)`) being unresolved cross-references. -myst_all_links_external = True - -autodoc_member_order = "bysource" - -templates_path = ["_templates"] -exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] - -default_role = "any" - -# -- Options for sphinx_issues -------------------------------------------------------- -issues_default_group_project = "commit-check/commit-check" - -# -- Options for HTML output ------------------------------------------------- -# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output - -html_theme = "sphinx_immaterial" -html_static_path = ["_static"] -# html_logo = "_static/logo.jpg" can not display well in blue background -# html_favicon = "_static/favicon.ico" -html_css_files = ["extra_css.css"] -html_title = "Commit Check" - -html_theme_options = { - "repo_url": "https://github.com/commit-check/commit-check", - "repo_name": "commit-check", - "icon": { - "logo": "material/git", - }, - "palette": [ - { - "media": "(prefers-color-scheme: light)", - "scheme": "default", - "primary": "blue", - "accent": "light-blue", - "toggle": { - "icon": "material/lightbulb-outline", - "name": "Switch to dark mode", - }, - }, - { - "media": "(prefers-color-scheme: dark)", - "scheme": "slate", - "primary": "blue", - "accent": "light-blue", - "toggle": { - "icon": "material/lightbulb", - "name": "Switch to light mode", - }, - }, - ], - "features": [ - "navigation.top", - "navigation.tabs", - "navigation.tabs.sticky", - "toc.sticky", - "toc.follow", - "search.share", - ], -} - -object_description_options = [ - ("py:parameter", {"include_in_toc": False}), -] - -sphinx_immaterial_custom_admonitions = [ - { - "name": "seealso", - "color": (215, 59, 205), - "icon": "octicons/eye-24", - "override": True, - }, - { - "name": "note", - "icon": "material/file-document-edit-outline", - "override": True, - }, -] -for name in ("hint", "tip", "important"): - sphinx_immaterial_custom_admonitions.append( - {"name": name, "icon": "material/school", "override": True} - ) - - -def setup(app: Sphinx): - """Generate a doc from the executable script's ``--help`` output.""" - - result = subprocess.run( - ["commit-check", "--help"], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - encoding="utf-8", - ) - doc = "commit-check --help\n==============================\n\n" - CLI_OPT_NAME = re.compile(r"^\s*(\-\w)(?:\s+[A-Z_\[\]]*)?(?:,\s+(\-\-[a-z\-]+))?") - in_options_section = False - - for line in result.stdout.splitlines(): - # Start processing options when we see the "options:" line - if line.strip() == "options:": - in_options_section = True - doc += line + "\n" - continue - - # Only process option patterns in the options section - if in_options_section: - match = CLI_OPT_NAME.search(line) - if match is not None: - short_opt = match.group(1) - long_opt = match.group(2) - if short_opt and long_opt: - doc += "\n.. std:option:: " + short_opt + ", " + long_opt + "\n\n" - elif short_opt: - doc += "\n.. std:option:: " + short_opt + "\n\n" - - doc += line + "\n" - cli_doc = Path(app.srcdir, "cli_args.rst") - cli_doc.unlink(missing_ok=True) - cli_doc.write_text(doc) diff --git a/docs/configuration.md b/docs/configuration.md new file mode 100644 index 00000000..736511ef --- /dev/null +++ b/docs/configuration.md @@ -0,0 +1,305 @@ +# Configuration + +`commit-check` can be configured in three ways with the following priority (highest to lowest): + +1. **Command-line arguments** (`--subject-imperative=true`) +2. **Environment variables** (`CCHK_SUBJECT_IMPERATIVE=true`) +3. **Configuration files** (`cchk.toml` or `commit-check.toml`) +4. **Built-in defaults** + +This flexibility allows you to: + +* Use configuration files for project-wide settings +* Override with environment variables in CI/CD pipelines +* Override specific settings via CLI for one-off checks +* Use without any configuration files (relies on defaults) + +## Configuration Files + +`commit-check` configuration files support the TOML format. See `cchk.toml` for an example configuration. + +!!! tip "Default Behavior" + + * When no configuration file exists, commit-check uses sensible defaults with minimal restrictions. + * Enforced by default: the Conventional Commits format ([CC001](rules.md#cc001)), the Conventional Branch format ([CC201](rules.md#cc201)), the subject length limits of 5–80 characters ([CC004](rules.md#cc004), [CC005](rules.md#cc005)), and the author name and email patterns ([CC101](rules.md#cc101), [CC102](rules.md#cc102)). + * **Off** by default: subject capitalization, imperative mood, body and signoff requirements, rebase requirements, and every `allow_*` restriction. + + See [rules](rules.md) for the default state of every rule. + +commit-check can be configured via a `cchk.toml` or `commit-check.toml` file. + +The file should be placed in the root of your repository or in the `.github` folder. + +## Configuration File Locations + +commit-check searches for configuration files in the following order (first found is used): + +1. `cchk.toml` (root directory) +2. `commit-check.toml` (root directory) +3. `.github/cchk.toml` +4. `.github/commit-check.toml` + +!!! tip "GitHub Best Practice" + + Placing configuration files in the `.github` folder helps keep your repository root clean and follows GitHub conventions used by tools like Dependabot and Renovate. + +!!! tip "IDE Autocompletion" + + commit-check's TOML schema is published on [SchemaStore](https://www.schemastore.org/), + so editors like VS Code (via [Even Better TOML](https://marketplace.visualstudio.com/items?itemName=tamasfe.even-better-toml)), + PyCharm, and IntelliJ provide autocompletion, validation, and documentation + tooltips for `cchk.toml` out of the box — no manual schema path configuration needed. + +## Organization-Level Configuration (inherit_from) + +For organizations that want to share a common base configuration across many repositories, commit-check supports an `inherit_from` directive at the top level of your TOML config file. + +**How it works:** + +1. The `inherit_from` value can be a `github:` shorthand, a local file path, or an HTTPS URL. +2. The parent (inherited) configuration is loaded first. +3. Local settings in the current config file **override** the parent values. +4. The `inherit_from` key itself is not passed to the validation engine. + +**Example — inherit from a GitHub repository (recommended):** + +```toml +# .github/cchk.toml +inherit_from = "github:my-org/.github:cchk.toml" + +[commit] +subject_max_length = 72 # Overrides parent value +``` + +**GitHub shorthand format:** + +* `github:owner/repo:path/to/cchk.toml` — uses `HEAD` (default branch) +* `github:owner/repo@main:path/to/cchk.toml` — pins to the `main` branch + +**Example — inherit from a local file:** + +```toml +# repo/.github/cchk.toml +inherit_from = "../../shared/org-cchk.toml" + +[commit] +allow_wip_commits = true # Override for this project only +``` + +**Example — inherit from an HTTPS URL:** + +```toml +# .github/cchk.toml +inherit_from = "https://example.com/shared/cchk.toml" +``` + +!!! note + + If the `inherit_from` target is unreachable or the format is unrecognized, commit-check silently ignores the inheritance and uses only the local configuration. HTTP (non-TLS) URLs are rejected for security. + +## Example Configuration + +```toml +[commit] +# https://www.conventionalcommits.org +conventional_commits = true +# message_pattern = "" # Optional - custom regex (overrides conventional_commits) +subject_capitalized = false +subject_imperative = false +subject_max_length = 80 # Default - set to your own limit +subject_min_length = 5 # Default - set to your own minimum +allow_commit_types = ["feat", "fix", "docs", "style", "refactor", "test", "chore"] +allow_merge_commits = true +allow_revert_commits = true +allow_empty_commits = false +allow_fixup_commits = true +allow_wip_commits = false +require_body = false +# ignore_authors = [] # Optional - bypass checks for these commit/co-authors +require_signed_off_by = false +ai_attribution = "forbid" # "ignore" (default) or "forbid" — rejects AI tool trailers + +[push] +# Block force pushes when used as a pre-push hook or with --no-force-push +allow_force_push = true # Set to false to block force pushes + +[branch] +# https://conventionalbranch.org +conventional_branch = true +# Optional: defaults are a superset of the Conventional Branch spec — the +# spec types plus Conventional Commit types, AI agent prefixes and bot +# prefixes (see the Options table below for the full list). Omit this +# option to use the defaults, or set your own list for a strict subset. +allow_branch_types = [ + "feature", + "bugfix", + "hotfix", + "release", + "chore", + "feat", + "fix", + "build", + "ci", + "docs", + "perf", + "refactor", + "style", + "test", +] +# allow_branch_names = [] # Optional - additional standalone branch names (e.g., ["develop", "staging"]) +# require_rebase_target = "main" # Optional - no rebase requirement by default +# ignore_authors = [] # Optional - no authors ignored by default +``` + +## Command-Line Arguments + +All configuration options can be specified via command-line arguments, which take precedence over environment variables and configuration files. + +**Syntax:** + +* Boolean options: `--option-name=true` or `--option-name=false` +* Integer options: `--option-name=80` +* List options: `--option-name=value1,value2,value3` (comma-separated) +* String options: `--option-name=value` + +**Examples:** + +```bash +# Disable imperative mood check +commit-check --message --subject-imperative=false + +# Set custom subject length limit +commit-check --message --subject-max-length=72 + +# Restrict allowed commit types +commit-check --message --allow-commit-types=feat,fix,docs + +# Combine multiple options +commit-check --message --subject-imperative=true --subject-max-length=50 --allow-commit-types=feat,fix + +# Branch configuration via CLI +commit-check --branch --allow-branch-types=feature,bugfix,hotfix +``` + +**Pre-commit Hook Usage:** + +The primary use case for CLI arguments is configuring commit-check in `.pre-commit-config.yaml` without requiring a TOML file: + +```yaml +repos: + - repo: https://github.com/commit-check/commit-check + rev: v2.5.0 + hooks: + - id: check-message + args: + - --subject-imperative=false + - --subject-max-length=100 + - --allow-merge-commits=false +``` + +## Environment Variables + +Configuration can also be set via environment variables with the `CCHK_` prefix. This is useful for CI/CD pipelines and temporary overrides. + +**Naming Convention:** + +* Convert option name to uppercase +* Replace hyphens with underscores +* Add `CCHK_` prefix + +**Examples:** + +```bash +# Set boolean options +export CCHK_SUBJECT_IMPERATIVE=true +export CCHK_SUBJECT_CAPITALIZED=false + +# Set integer options +export CCHK_SUBJECT_MAX_LENGTH=72 +export CCHK_SUBJECT_MIN_LENGTH=10 + +# Set list options (comma-separated) +export CCHK_ALLOW_COMMIT_TYPES=feat,fix,docs,chore +export CCHK_ALLOW_BRANCH_TYPES=feature,bugfix,hotfix + +# Set string options +export CCHK_REQUIRE_REBASE_TARGET=main + +# Use in CI/CD +CCHK_SUBJECT_MAX_LENGTH=100 commit-check --message +``` + +**Complete Mapping:** + +| TOML Config | Environment Variable | CLI Argument | +|---|---|---| +| `conventional_commits = true` | `CCHK_CONVENTIONAL_COMMITS=true` | `--conventional-commits=true` | +| `message_pattern = "^PROJ-\\d+: .+"` | `CCHK_MESSAGE_PATTERN=^PROJ-\\d+: .+` | N/A (config file only) | +| `subject_capitalized = false` | `CCHK_SUBJECT_CAPITALIZED=false` | `--subject-capitalized=false` | +| `subject_imperative = true` | `CCHK_SUBJECT_IMPERATIVE=true` | `--subject-imperative=true` | +| `subject_max_length = 80` | `CCHK_SUBJECT_MAX_LENGTH=80` | `--subject-max-length=80` | +| `subject_min_length = 5` | `CCHK_SUBJECT_MIN_LENGTH=5` | `--subject-min-length=5` | +| `allow_commit_types = ["feat", "fix"]` | `CCHK_ALLOW_COMMIT_TYPES=feat,fix` | `--allow-commit-types=feat,fix` | +| `allow_merge_commits = true` | `CCHK_ALLOW_MERGE_COMMITS=true` | `--allow-merge-commits=true` | +| `allow_revert_commits = true` | `CCHK_ALLOW_REVERT_COMMITS=true` | `--allow-revert-commits=true` | +| `allow_empty_commits = false` | `CCHK_ALLOW_EMPTY_COMMITS=false` | `--allow-empty-commits=false` | +| `allow_fixup_commits = true` | `CCHK_ALLOW_FIXUP_COMMITS=true` | `--allow-fixup-commits=true` | +| `allow_wip_commits = false` | `CCHK_ALLOW_WIP_COMMITS=false` | `--allow-wip-commits=false` | +| `require_body = false` | `CCHK_REQUIRE_BODY=false` | `--require-body=false` | +| `require_signed_off_by = false` | `CCHK_REQUIRE_SIGNED_OFF_BY=false` | `--require-signed-off-by=false` | +| `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` | +| `allow_branch_types = ["feature"]` | `CCHK_ALLOW_BRANCH_TYPES=feature,bugfix` | `--allow-branch-types=feature,bugfix` | +| `allow_branch_names = ["develop"]` | `CCHK_ALLOW_BRANCH_NAMES=develop,staging` | `--allow-branch-names=develop,staging` | +| `require_rebase_target = "main"` | `CCHK_REQUIRE_REBASE_TARGET=main` | `--require-rebase-target=main` | +| `allow_force_push = true` | `CCHK_ALLOW_FORCE_PUSH=true` | `--no-force-push` (sets `allow_force_push` to `false`) | +| `ai_attribution = "forbid"` | `CCHK_AI_ATTRIBUTION=forbid` | `--ai-attribution=forbid` | +| `ignore_authors = ["bot"]` (in branch section) | `CCHK_BRANCH_IGNORE_AUTHORS=bot,user` | `--branch-ignore-authors=bot,user` | + +## Configuration Priority Example + +When the same option is specified in multiple places, the priority determines which value is used: + +```bash +# In cchk.toml: +# subject_max_length = 100 + +# Set via environment: +export CCHK_SUBJECT_MAX_LENGTH=80 + +# Override via CLI: +commit-check --message --subject-max-length=50 + +# Result: subject_max_length = 50 (CLI wins) +``` + +## Options Table Description + +| Section | Option | Type | Default | Description | +|---|---|---|---|---| +| commit | conventional_commits | bool | true | Enforce Conventional Commits specification. | +| commit | message_pattern | str | "" (disabled) | Custom regex pattern for commit message validation. When set, this pattern replaces the auto-generated Conventional Commits regex entirely, making it possible to enforce custom formats such as JIRA smart commits (e.g., `"^PROJ-\\d+: .+"`). When `message_pattern` is set (non-empty) it takes precedence over `conventional_commits`. | +| commit | subject_capitalized | bool | false | Subject must start with a capital letter. | +| commit | subject_imperative | bool | false | Subject must be in imperative mood. Forms of verbs can be found at [imperatives.py](https://github.com/commit-check/commit-check/blob/main/commit_check/imperatives.py) | +| commit | subject_max_length | int | 80 | Maximum length of the subject line. | +| commit | subject_min_length | int | 5 | Minimum length of the subject line. | +| commit | allow_commit_types | list[str] | ["feat", "fix", "docs", "style", "refactor", "test", "chore", "perf", "build", "ci"] | Allowed commit types when conventional_commits is true. | +| commit | allow_merge_commits | bool | true | Allow merge commits. | +| commit | allow_revert_commits | bool | true | Allow revert commits. | +| commit | allow_empty_commits | bool | true | Allow empty commits. | +| commit | allow_fixup_commits | bool | true | Allow fixup commits (e.g., "fixup! "). | +| commit | allow_wip_commits | bool | true | Allow work-in-progress commits (e.g., "WIP: "). | +| commit | require_body | bool | false | Require a body in the commit message. | +| commit | ignore_authors | 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 | false | Require "Signed-off-by" line in the commit message footer. | +| commit | ai_attribution | str | "ignore" | AI attribution policy. `"forbid"` rejects any commit containing known AI tool signatures (Claude Code, Copilot, Codex, Gemini, Cursor, Devin, Aider, Windsurf, Tabby, and generic AI model patterns). `"ignore"` disables the check. This feature is a response to the industry-wide discussion on AI disclosure in open source (Linux kernel `Assisted-by:` trailer, CPython, VS Code, Apache, Fedora policies). | +| branch | conventional_branch | bool | true | Enforce Conventional Branch specification. | +| branch | allow_branch_types | list[str] | ["feature", "bugfix", "hotfix", "release", "chore", "feat", "fix", "build", "ci", "docs", "perf", "refactor", "style", "test", "ai", "claude", "codex", "copilot", "cursor", "dependabot", "renovate"] | Allowed branch types when `conventional_branch` is true. The default is a superset of the [Conventional Branch spec](https://conventionalbranch.org/): the spec types (`feature`, `bugfix`, `hotfix`, `release`, `chore`) plus the Conventional Commit types (`build`, `ci`, `docs`, `perf`, `refactor`, `style`, `test`), AI agent prefixes (`ai`, `claude`, `codex`, `copilot`, `cursor`) and bot prefixes (`dependabot`, `renovate`). For strict spec-only validation, set this option explicitly (e.g. `["feature", "bugfix", "hotfix", "release", "chore"]`). | +| branch | allow_branch_names | list[str] | [] (empty list) | Additional standalone branch names allowed when conventional_branch is true (e.g., ["develop", "staging"]). By default, master, main, HEAD, and PR-* are always allowed. | +| branch | require_rebase_target | str | "" (no requirement) | Target branch for rebase requirement. If not set, no rebase validation is performed. | +| push | allow_force_push | bool | true | Allow force pushes. Set to `false` to block force pushes when used as a pre-push hook or with `--no-force-push`. | +| branch | ignore_authors | list[str] | [] (none ignored) | List of authors to ignore (i.e., always allow). | diff --git a/docs/configuration.rst b/docs/configuration.rst deleted file mode 100644 index 0c658df9..00000000 --- a/docs/configuration.rst +++ /dev/null @@ -1,476 +0,0 @@ -Configuration -============= - -``commit-check`` can be configured in three ways with the following priority (highest to lowest): - -1. **Command-line arguments** (``--subject-imperative=true``) -2. **Environment variables** (``CCHK_SUBJECT_IMPERATIVE=true``) -3. **Configuration files** (``cchk.toml`` or ``commit-check.toml``) -4. **Built-in defaults** - -This flexibility allows you to: - -* Use configuration files for project-wide settings -* Override with environment variables in CI/CD pipelines -* Override specific settings via CLI for one-off checks -* Use without any configuration files (relies on defaults) - -Configuration Files -------------------- - -``commit-check`` configuration files support the TOML format. See ``cchk.toml`` for an example configuration. - -.. tip:: - **Default Behavior** - - * When no configuration file exists, commit-check uses sensible defaults with minimal restrictions. - * Only conventional commits format, subject capitalization, and imperative mood are enforced by default. - * No length limits, author restrictions, or rebase requirements are applied. - -commit-check can be configured via a ``cchk.toml`` or ``commit-check.toml`` file. - -The file should be placed in the root of your repository or in the ``.github`` folder. - -Configuration File Locations ------------------------------ - -commit-check searches for configuration files in the following order (first found is used): - -1. ``cchk.toml`` (root directory) -2. ``commit-check.toml`` (root directory) -3. ``.github/cchk.toml`` -4. ``.github/commit-check.toml`` - -.. tip:: - **GitHub Best Practice** - - Placing configuration files in the ``.github`` folder helps keep your repository root clean and follows GitHub conventions used by tools like Dependabot and Renovate. - -.. tip:: - **IDE Autocompletion** - - commit-check's TOML schema is published on `SchemaStore `_, - so editors like VS Code (via `Even Better TOML `_), - PyCharm, and IntelliJ provide autocompletion, validation, and documentation - tooltips for ``cchk.toml`` out of the box — no manual schema path configuration needed. - -Organization-Level Configuration (inherit_from) -------------------------------------------------- - -For organizations that want to share a common base configuration across many repositories, commit-check supports an ``inherit_from`` directive at the top level of your TOML config file. - -**How it works:** - -1. The ``inherit_from`` value can be a ``github:`` shorthand, a local file path, or an HTTPS URL. -2. The parent (inherited) configuration is loaded first. -3. Local settings in the current config file **override** the parent values. -4. The ``inherit_from`` key itself is not passed to the validation engine. - -**Example — inherit from a GitHub repository (recommended):** - -.. code-block:: toml - - # .github/cchk.toml - inherit_from = "github:my-org/.github:cchk.toml" - - [commit] - subject_max_length = 72 # Overrides parent value - -**GitHub shorthand format:** - -* ``github:owner/repo:path/to/cchk.toml`` — uses ``HEAD`` (default branch) -* ``github:owner/repo@main:path/to/cchk.toml`` — pins to the ``main`` branch - -**Example — inherit from a local file:** - -.. code-block:: toml - - # repo/.github/cchk.toml - inherit_from = "../../shared/org-cchk.toml" - - [commit] - allow_wip_commits = true # Override for this project only - -**Example — inherit from an HTTPS URL:** - -.. code-block:: toml - - # .github/cchk.toml - inherit_from = "https://example.com/shared/cchk.toml" - -.. note:: - If the ``inherit_from`` target is unreachable or the format is unrecognized, commit-check silently ignores the inheritance and uses only the local configuration. HTTP (non-TLS) URLs are rejected for security. - -Example Configuration ---------------------- - -.. code-block:: toml - :class: copy - - [commit] - # https://www.conventionalcommits.org - conventional_commits = true - # message_pattern = "" # Optional - custom regex (overrides conventional_commits) - subject_capitalized = false - subject_imperative = false - # subject_max_length = 50 # Optional - no limit by default - # subject_min_length = 5 # Optional - no limit by default - allow_commit_types = ["feat", "fix", "docs", "style", "refactor", "test", "chore"] - allow_merge_commits = true - allow_revert_commits = true - allow_empty_commits = false - allow_fixup_commits = true - allow_wip_commits = false - require_body = false - # ignore_authors = [] # Optional - bypass checks for these commit/co-authors - require_signed_off_by = false - # required_signoff_name = "Your Name" # Optional - # required_signoff_email = "your.email@example.com" # Optional - ai_attribution = "forbid" # "ignore" (default) or "forbid" — rejects AI tool trailers - - [push] - # Block force pushes when used as a pre-push hook or with --no-force-push - allow_force_push = true # Set to false to block force pushes - - [branch] - # https://conventionalbranch.org - conventional_branch = true - # Optional: defaults are a superset of the Conventional Branch spec — the - # spec types plus Conventional Commit types, AI agent prefixes and bot - # prefixes (see the Options table below for the full list). Omit this - # option to use the defaults, or set your own list for a strict subset. - allow_branch_types = [ - "feature", - "bugfix", - "hotfix", - "release", - "chore", - "feat", - "fix", - "build", - "ci", - "docs", - "perf", - "refactor", - "style", - "test", - ] - # allow_branch_names = [] # Optional - additional standalone branch names (e.g., ["develop", "staging"]) - # require_rebase_target = "main" # Optional - no rebase requirement by default - # ignore_authors = [] # Optional - no authors ignored by default - - -Command-Line Arguments ----------------------- - -All configuration options can be specified via command-line arguments, which take precedence over environment variables and configuration files. - -**Syntax:** - -* Boolean options: ``--option-name=true`` or ``--option-name=false`` -* Integer options: ``--option-name=80`` -* List options: ``--option-name=value1,value2,value3`` (comma-separated) -* String options: ``--option-name=value`` - -**Examples:** - -.. code-block:: bash - - # Disable imperative mood check - commit-check --message --subject-imperative=false - - # Set custom subject length limit - commit-check --message --subject-max-length=72 - - # Restrict allowed commit types - commit-check --message --allow-commit-types=feat,fix,docs - - # Combine multiple options - commit-check --message --subject-imperative=true --subject-max-length=50 --allow-commit-types=feat,fix - - # Branch configuration via CLI - commit-check --branch --allow-branch-types=feature,bugfix,hotfix - -**Pre-commit Hook Usage:** - -The primary use case for CLI arguments is configuring commit-check in ``.pre-commit-config.yaml`` without requiring a TOML file: - -.. code-block:: yaml - - repos: - - repo: https://github.com/commit-check/commit-check - rev: v2.5.0 - hooks: - - id: check-message - args: - - --subject-imperative=false - - --subject-max-length=100 - - --allow-merge-commits=false - -Environment Variables ---------------------- - -Configuration can also be set via environment variables with the ``CCHK_`` prefix. This is useful for CI/CD pipelines and temporary overrides. - -**Naming Convention:** - -* Convert option name to uppercase -* Replace hyphens with underscores -* Add ``CCHK_`` prefix - -**Examples:** - -.. code-block:: bash - - # Set boolean options - export CCHK_SUBJECT_IMPERATIVE=true - export CCHK_SUBJECT_CAPITALIZED=false - - # Set integer options - export CCHK_SUBJECT_MAX_LENGTH=72 - export CCHK_SUBJECT_MIN_LENGTH=10 - - # Set list options (comma-separated) - export CCHK_ALLOW_COMMIT_TYPES=feat,fix,docs,chore - export CCHK_ALLOW_BRANCH_TYPES=feature,bugfix,hotfix - - # Set string options - export CCHK_REQUIRE_REBASE_TARGET=main - - # Use in CI/CD - CCHK_SUBJECT_MAX_LENGTH=100 commit-check --message - -**Complete Mapping:** - -.. list-table:: - :header-rows: 1 - - * - TOML Config - - Environment Variable - - CLI Argument - * - ``conventional_commits = true`` - - ``CCHK_CONVENTIONAL_COMMITS=true`` - - ``--conventional-commits=true`` - * - ``message_pattern = "^PROJ-\\d+: .+"`` - - ``CCHK_MESSAGE_PATTERN=^PROJ-\\d+: .+`` - - N/A (config file only) - * - ``subject_capitalized = false`` - - ``CCHK_SUBJECT_CAPITALIZED=false`` - - ``--subject-capitalized=false`` - * - ``subject_imperative = true`` - - ``CCHK_SUBJECT_IMPERATIVE=true`` - - ``--subject-imperative=true`` - * - ``subject_max_length = 80`` - - ``CCHK_SUBJECT_MAX_LENGTH=80`` - - ``--subject-max-length=80`` - * - ``subject_min_length = 5`` - - ``CCHK_SUBJECT_MIN_LENGTH=5`` - - ``--subject-min-length=5`` - * - ``allow_commit_types = ["feat", "fix"]`` - - ``CCHK_ALLOW_COMMIT_TYPES=feat,fix`` - - ``--allow-commit-types=feat,fix`` - * - ``allow_merge_commits = true`` - - ``CCHK_ALLOW_MERGE_COMMITS=true`` - - ``--allow-merge-commits=true`` - * - ``allow_revert_commits = true`` - - ``CCHK_ALLOW_REVERT_COMMITS=true`` - - ``--allow-revert-commits=true`` - * - ``allow_empty_commits = false`` - - ``CCHK_ALLOW_EMPTY_COMMITS=false`` - - ``--allow-empty-commits=false`` - * - ``allow_fixup_commits = true`` - - ``CCHK_ALLOW_FIXUP_COMMITS=true`` - - ``--allow-fixup-commits=true`` - * - ``allow_wip_commits = false`` - - ``CCHK_ALLOW_WIP_COMMITS=false`` - - ``--allow-wip-commits=false`` - * - ``require_body = false`` - - ``CCHK_REQUIRE_BODY=false`` - - ``--require-body=false`` - * - ``require_signed_off_by = false`` - - ``CCHK_REQUIRE_SIGNED_OFF_BY=false`` - - ``--require-signed-off-by=false`` - * - ``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`` - * - ``allow_branch_types = ["feature"]`` - - ``CCHK_ALLOW_BRANCH_TYPES=feature,bugfix`` - - ``--allow-branch-types=feature,bugfix`` - * - ``allow_branch_names = ["develop"]`` - - ``CCHK_ALLOW_BRANCH_NAMES=develop,staging`` - - ``--allow-branch-names=develop,staging`` - * - ``require_rebase_target = "main"`` - - ``CCHK_REQUIRE_REBASE_TARGET=main`` - - ``--require-rebase-target=main`` - * - ``allow_force_push = true`` - - ``CCHK_ALLOW_FORCE_PUSH=false`` - - ``--no-force-push`` (enable via ``--no-force-push`` flag) - * - ``ai_attribution = "forbid"`` - - ``CCHK_AI_ATTRIBUTION=forbid`` - - ``--ai-attribution=forbid`` - * - ``ignore_authors = ["bot"]`` (in branch section) - - ``CCHK_BRANCH_IGNORE_AUTHORS=bot,user`` - - ``--branch-ignore-authors=bot,user`` - - -Configuration Priority Example -------------------------------- - -When the same option is specified in multiple places, the priority determines which value is used: - -.. code-block:: bash - - # In cchk.toml: - # subject_max_length = 100 - - # Set via environment: - export CCHK_SUBJECT_MAX_LENGTH=80 - - # Override via CLI: - commit-check --message --subject-max-length=50 - - # Result: subject_max_length = 50 (CLI wins) - - -Options Table Description -------------------------- - -.. list-table:: - :header-rows: 1 - - * - Section - - Option - - Type - - Default - - Description - * - commit - - conventional_commits - - bool - - true - - Enforce Conventional Commits specification. - * - commit - - message_pattern - - str - - "" (disabled) - - Custom regex pattern for commit message validation. When set, this pattern replaces the auto-generated Conventional Commits regex entirely, making it possible to enforce custom formats such as JIRA smart commits (e.g., ``"^PROJ-\\d+: .+"``). When ``message_pattern`` is set (non-empty) it takes precedence over ``conventional_commits``. - * - commit - - subject_capitalized - - bool - - true - - Subject must start with a capital letter. - * - commit - - subject_imperative - - bool - - true - - Subject must be in imperative mood. Forms of verbs can be found at `imperatives.py `_ - * - commit - - subject_max_length - - int - - None (no limit) - - Maximum length of the subject line. - * - commit - - subject_min_length - - int - - None (no limit) - - Minimum length of the subject line. - * - commit - - allow_commit_types - - list[str] - - ["feat", "fix", "docs", "style", "refactor", "test", "chore"] - - Allowed commit types when conventional_commits is true. - * - commit - - allow_merge_commits - - bool - - true - - Allow merge commits. - * - commit - - allow_revert_commits - - bool - - true - - Allow revert commits. - * - commit - - allow_empty_commits - - bool - - false - - Allow empty commits. - * - commit - - allow_fixup_commits - - bool - - true - - Allow fixup commits (e.g., "fixup! "). - * - commit - - allow_wip_commits - - bool - - false - - Allow work-in-progress commits (e.g., "WIP: "). - * - commit - - require_body - - bool - - false - - Require a body in the commit message. - * - commit - - ignore_authors - - 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 - - false - - Require "Signed-off-by" line in the commit message footer. - * - commit - - ai_attribution - - str - - "ignore" - - AI attribution policy. ``"forbid"`` rejects any commit containing known AI tool signatures (Claude Code, Copilot, Codex, Gemini, Cursor, Devin, Aider, Windsurf, Tabby, and generic AI model patterns). ``"ignore"`` disables the check. This feature is a response to the industry-wide discussion on AI disclosure in open source (Linux kernel ``Assisted-by:`` trailer, CPython, VS Code, Apache, Fedora policies). - * - branch - - conventional_branch - - bool - - true - - Enforce Conventional Branch specification. - * - branch - - allow_branch_types - - list[str] - - ["feature", "bugfix", "hotfix", "release", "chore", "feat", "fix", "build", "ci", "docs", "perf", "refactor", "style", "test", "ai", "claude", "codex", "copilot", "cursor", "dependabot", "renovate"] - - Allowed branch types when ``conventional_branch`` is true. The default is a superset of the `Conventional Branch spec `_: the spec types (``feature``, ``bugfix``, ``hotfix``, ``release``, ``chore``) plus the Conventional Commit types (``build``, ``ci``, ``docs``, ``perf``, ``refactor``, ``style``, ``test``), AI agent prefixes (``ai``, ``claude``, ``codex``, ``copilot``, ``cursor``) and bot prefixes (``dependabot``, ``renovate``). For strict spec-only validation, set this option explicitly (e.g. ``["feature", "bugfix", "hotfix", "release", "chore"]``). - * - branch - - allow_branch_names - - list[str] - - [] (empty list) - - Additional standalone branch names allowed when conventional_branch is true (e.g., ["develop", "staging"]). By default, master, main, HEAD, and PR-* are always allowed. - * - branch - - require_rebase_target - - str - - None (no requirement) - - Target branch for rebase requirement. If not set, no rebase validation is performed. - * - push - - allow_force_push - - bool - - true - - Allow force pushes. Set to ``false`` to block force pushes when used as a pre-push hook or with ``--no-force-push``. - * - branch - - ignore_authors - - list[str] - - [] (none ignored) - - List of authors to ignore (i.e., always allow). diff --git a/docs/example.md b/docs/example.md new file mode 100644 index 00000000..df5fcaf3 --- /dev/null +++ b/docs/example.md @@ -0,0 +1,376 @@ +# Usage Examples + +This guide demonstrates how to use commit-check to validate commit messages, branch names, and author information. + +There are several ways to use commit-check: as a pre-commit hook, via STDIN, or directly with files. + +## Running as GitHub Action + +Please see [commit-check/commit-check-action](https://github.com/commit-check/commit-check-action) + +## Running as pre-commit hook + +1. **Install pre-commit:** + +!!! tip + + Make sure `pre-commit` is [installed](https://pre-commit.com/#install). + +```bash +pip install pre-commit +``` + +2. **Create .pre-commit-config.yaml:** + +```yaml +- repo: https://github.com/commit-check/commit-check + rev: the tag or revision + hooks: + - id: check-message + stages: [commit-msg] + - id: check-branch + - id: check-author-name + - id: check-author-email +``` + +3. **Install the hooks:** + +```bash +pre-commit install --hook-type pre-commit --hook-type commit-msg +``` + +4. **Test the integration:** + +```bash +# This will trigger validation automatically +git commit -m "feat: add new user authentication system" +``` + +### Pre-commit Validation Examples + +**✅ Successful Validation:** + +```text +$ git commit -m "feat: add user authentication system" + +check commit message.....................................................Passed +check committer name.....................................................Passed +check committer email....................................................Passed +[main abc1234] feat: add user authentication system +``` + +**❌ Failed Validation:** + +```text +$ git commit -m "bad commit message" + +check commit message.....................................................Failed +- hook id: check-message +- exit code: 1 + +Commit rejected by Commit-Check. + + (c).-.(c) (c).-.(c) (c).-.(c) (c).-.(c) (c).-.(c) + / ._. \ / ._. \ / ._. \ / ._. \ / ._. \ + __\( C )/__ __\( H )/__ __\( E )/__ __\( C )/__ __\( K )/__ +(_.-/'-'\-._)(_.-/'-'\-._)(_.-/'-'\-._)(_.-/'-'\-._)(_.-/'-'\-._) + || E || || R || || R || || O || || R || + _.' '-' '._ _.' '-' '._ _.' '-' '._ _.' '-' '._ _.' '-' '._ +(.-./`-´\.-.)(.-./`-´\.-.)(.-./`-´\.-.)(.-./`-´\.-.)(.-./`-´\.-.) + `-´ `-´ `-´ `-´ `-´ `-´ `-´ `-´ `-´ `-´ + +Commit rejected. + +Type message check failed ==> bad commit message +It doesn't match regex: ^(feat|fix|docs|style|refactor|test|chore)(\(.+\))?: .+ +The commit message should follow Conventional Commits. See https://www.conventionalcommits.org +Suggest: Use (): with allowed types +``` + +## Running as CLI + +Commit-check provides several command-line options for different validation scenarios. via options or STDIN + +!!! tip + + Validate commit messages by piping them through STDIN. This is useful for + testing or scripting. + +Available Commands see [commit-check --help](cli.md) + +### Message Validation Examples + +```bash +# Validate message from STDIN +echo "feat: new feature" | commit-check -m + +# Validate message from file +commit-check -m commit_message.txt + +# Validate current git commit message (from git log) +commit-check -m +``` + +**Reading from file:** + +```bash +# Create a commit message file +cat > commit_message.txt << EOF +fix(auth): resolve login timeout issue + +Users were experiencing timeouts during login. +Increased session timeout and improved error handling. + +Fixes #123 +EOF + +# Validate from file +commit-check -m commit_message.txt + +# Or pipe file content +cat commit_message.txt | commit-check -m +``` + +### Branch Validation Examples + +```bash +# Check current branch name +commit-check --branch + +# Example valid branch names: +# - feature/user-auth +# - fix/login-bug +# - hotfix/security-patch +# - release/v1.2.0 +``` + +### Push Validation Examples + +```bash +# Check whether pushing HEAD to its configured upstream would require force +commit-check --no-force-push +``` + +```yaml +# Configure the dedicated pre-push hook +- repo: https://github.com/commit-check/commit-check + rev: the tag or revision + hooks: + - id: check-no-force-push + stages: [pre-push] +``` + +`git push | commit-check --no-force-push` is not a prevention mechanism. The +push has already started, and normal `git push` output does not include the +pre-push ref lines that Git provides to hooks. + +### Author Validation Examples + +```bash +# Check author name +commit-check --author-name + +# Check author email +commit-check --author-email + +# Check both author name and email +commit-check --author-name --author-email +``` + +### Configuration Examples + +```bash +# Use custom configuration file +echo "feat: test" | commit-check --config my-config.toml -m + +# Use configuration from different directory +commit-check --config /path/to/config/cchk.toml -m +``` + +### Valid Commit Message Examples + +```bash +# Basic feature +echo "feat: add user registration" | commit-check -m + +# Feature with scope +echo "feat(auth): implement OAuth2 login" | commit-check -m + +# Bug fix +echo "fix: resolve memory leak in parser" | commit-check -m + +# Documentation update +echo "docs: add installation guide" | commit-check -m + +# Breaking change +echo "feat!: redesign API endpoints" | commit-check -m + +# Merge commit (automatically allowed) +echo "Merge pull request #123 from feature/new-api" | commit-check -m +``` + +### Invalid Commit Message Examples + +```bash +# No type prefix +echo "added new feature" | commit-check -m + +# Capitalized (if configured to disallow) +echo "feat: Add new feature" | commit-check -m + +# Too short +echo "fix" | commit-check -m + +# Non-imperative mood +echo "feat: added login functionality" | commit-check -m + +# Unknown type +echo "unknown: some changes" | commit-check -m +``` + +### Error Output Examples + +**Commit Message Validation Failure:** + +```text +Commit rejected by Commit-Check. + + (c).-.(c) (c).-.(c) (c).-.(c) (c).-.(c) (c).-.(c) + / ._. \ / ._. \ / ._. \ / ._. \ / ._. \ + __\( C )/__ __\( H )/__ __\( E )/__ __\( C )/__ __\( K )/__ +(_.-/'-'\-._)(_.-/'-'\-._)(_.-/'-'\-._)(_.-/'-'\-._)(_.-/'-'\-._) + || E || || R || || R || || O || || R || + _.' '-' '._ _.' '-' '._ _.' '-' '._ _.' '-' '._ _.' '-' '._ +(.-./`-´\.-.)(.-./`-´\.-.)(.-./`-´\.-.)(.-./`-´\.-.)(.-./`-´\.-.) + `-´ `-´ `-´ `-´ `-´ `-´ `-´ `-´ `-´ `-´ + +Commit rejected. + +Type message check failed ==> test commit message check +It doesn't match regex: ^(chore|ci|docs|feat|fix|refactor|style|test){1}(\([\w\-\.]+\))?(!)?: ([\w ])+([\s\S]*)|(Merge).*|(fixup!.*) +The commit message should follow Conventional Commits. See https://www.conventionalcommits.org +Suggest: Use (): with allowed types +``` + +**Branch Name Validation Failure:** + +```text +Commit rejected by Commit-Check. + + (c).-.(c) (c).-.(c) (c).-.(c) (c).-.(c) (c).-.(c) + / ._. \ / ._. \ / ._. \ / ._. \ / ._. \ + __\( C )/__ __\( H )/__ __\( E )/__ __\( C )/__ __\( K )/__ +(_.-/'-'\-._)(_.-/'-'\-._)(_.-/'-'\-._)(_.-/'-'\-._)(_.-/'-'\-._) + || E || || R || || R || || O || || R || + _.' '-' '._ _.' '-' '._ _.' '-' '._ _.' '-' '._ _.' '-' '._ +(.-./`-´\.-.)(.-./`-´\.-.)(.-./`-´\.-.)(.-./`-´\.-.)(.-./`-´\.-.) + `-´ `-´ `-´ `-´ `-´ `-´ `-´ `-´ `-´ `-´ + +Commit rejected. + +Type branch check failed ==> test-branch +It doesn't match regex: ^(feature|bugfix|hotfix|release|chore|feat|fix)\/.+|(master)|(main)|(HEAD)|(PR-.+) +The branch should follow Conventional Branch. See https://conventionalbranch.org +Suggest: Use / with allowed types or ignore_authors in config branch section to bypass +``` + +**Commit Signature Validation Failure:** + +```text +Commit rejected by Commit-Check. + + (c).-.(c) (c).-.(c) (c).-.(c) (c).-.(c) (c).-.(c) + / ._. \ / ._. \ / ._. \ / ._. \ / ._. \ + __\( C )/__ __\( H )/__ __\( E )/__ __\( C )/__ __\( K )/__ +(_.-/'-'\-._)(_.-/'-'\-._)(_.-/'-'\-._)(_.-/'-'\-._)(_.-/'-'\-._) + || E || || R || || R || || O || || R || + _.' '-' '._ _.' '-' '._ _.' '-' '._ _.' '-' '._ _.' '-' '._ +(.-./`-´\.-.)(.-./`-´\.-.)(.-./`-´\.-.)(.-./`-´\.-.)(.-./`-´\.-.) + `-´ `-´ `-´ `-´ `-´ `-´ `-´ `-´ `-´ `-´ + +Commit rejected. + +Type require_signed_off_by check failed ==> fix: add missing file +It doesn't match regex: Signed-off-by:.*[A-Za-z0-9]\s+<.+@.+> +Signed-off-by not found in latest commit +Suggest: git commit --amend --signoff or use --signoff on commit +``` + +**Commit Message Validation Failure without ASCII Banner (`--no-banner`):** + +```text +Type message check failed ==> test commit message check +It doesn't match regex: ^(chore|ci|docs|feat|fix|refactor|style|test){1}(\([\w\-\.]+\))?(!)?: ([\w ])+([\s\S]*)|(Merge).*|(fixup!.*) +The commit message should follow Conventional Commits. See https://www.conventionalcommits.org +Suggest: Use (): with allowed types +``` + +**Compact Failure Output (`--compact`):** + +```text +[FAIL] message: test commit message check +``` + +**Imperative Mood Validation Failure:** + +```text +Commit rejected by Commit-Check. + + (c).-.(c) (c).-.(c) (c).-.(c) (c).-.(c) (c).-.(c) + / ._. \ / ._. \ / ._. \ / ._. \ / ._. \ + __\( C )/__ __\( H )/__ __\( E )/__ __\( C )/__ __\( K )/__ +(_.-/'-'\-._)(_.-/'-'\-._)(_.-/'-'\-._)(_.-/'-'\-._)(_.-/'-'\-._) + || E || || R || || R || || O || || R || + _.' '-' '._ _.' '-' '._ _.' '-' '._ _.' '-' '._ _.' '-' '._ +(.-./`-´\.-.)(.-./`-´\.-.)(.-./`-´\.-.)(.-./`-´\.-.)(.-./`-´\.-.) + `-´ `-´ `-´ `-´ `-´ `-´ `-´ `-´ `-´ `-´ + +Commit rejected. + +Type imperative check failed ==> fix: added missing file +It doesn't match regex: +Commit message should use imperative mood (e.g., 'Add feature' not 'Added feature') +Suggest: Use imperative mood in the subject line +``` + +## Integration Tips + +### CI/CD Integration + +You can use commit-check in CI/CD pipelines: + +```bash +# In your CI script +git log --format="%s" -n 1 | commit-check -m + +# or just +commit-check -m + +# Keep plain-text output but remove the ASCII art banner +git log --format="%s" -n 1 | commit-check -m --no-banner + +# Emit one machine-friendly line per failure without switching to JSON +git log --format="%s" -n 1 | commit-check -m --compact +``` + +### Scripting + +Use commit-check in scripts to validate commit messages programmatically: + +```bash +#!/bin/bash +# validate-commits.sh + +# Get all commit messages from last 10 commits +for i in {0..9}; do + msg=$(git log --format="%s" -n 1 --skip=$i) + if [ -n "$msg" ]; then + echo "Validating: $msg" + echo "$msg" | commit-check -m || exit 1 + fi +done + +echo "All commits are valid!" +``` + +For more configuration options, see the [Configuration Documentation](configuration.md). diff --git a/docs/example.rst b/docs/example.rst deleted file mode 100644 index 4ac73653..00000000 --- a/docs/example.rst +++ /dev/null @@ -1,397 +0,0 @@ -Usage Examples -============== - -This guide demonstrates how to use commit-check to validate commit messages, branch names, and author information. - -There are several ways to use commit-check: as a pre-commit hook, via STDIN, or directly with files. - -Running as GitHub Action ------------------------- - -Please see `commit-check/commit-check-action `_ - -Running as pre-commit hook ---------------------------- - -1. **Install pre-commit:** - -.. tip:: - - Make sure ``pre-commit`` is `installed `_. - -.. code-block:: bash - - pip install pre-commit - -2. **Create .pre-commit-config.yaml:** - -.. code-block:: yaml - - - repo: https://github.com/commit-check/commit-check - rev: the tag or revision - hooks: - - id: check-message - stages: [commit-msg] - - id: check-branch - - id: check-author-name - - id: check-author-email - -3. **Install the hooks:** - -.. code-block:: bash - - pre-commit install --hook-type pre-commit --hook-type commit-msg - -4. **Test the integration:** - -.. code-block:: bash - - # This will trigger validation automatically - git commit -m "feat: add new user authentication system" - - -Pre-commit Validation Examples -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -**✅ Successful Validation:** - -.. code-block:: text - - $ git commit -m "feat: add user authentication system" - - check commit message.....................................................Passed - check committer name.....................................................Passed - check committer email....................................................Passed - [main abc1234] feat: add user authentication system - -**❌ Failed Validation:** - -.. code-block:: text - - $ git commit -m "bad commit message" - - check commit message.....................................................Failed - - hook id: check-message - - exit code: 1 - - Commit rejected by Commit-Check. - - (c).-.(c) (c).-.(c) (c).-.(c) (c).-.(c) (c).-.(c) - / ._. \ / ._. \ / ._. \ / ._. \ / ._. \ - __\( C )/__ __\( H )/__ __\( E )/__ __\( C )/__ __\( K )/__ - (_.-/'-'\-._)(_.-/'-'\-._)(_.-/'-'\-._)(_.-/'-'\-._)(_.-/'-'\-._) - || E || || R || || R || || O || || R || - _.' '-' '._ _.' '-' '._ _.' '-' '._ _.' '-' '._ _.' '-' '._ - (.-./`-´\.-.)(.-./`-´\.-.)(.-./`-´\.-.)(.-./`-´\.-.)(.-./`-´\.-.) - `-´ `-´ `-´ `-´ `-´ `-´ `-´ `-´ `-´ `-´ - - Commit rejected. - - Type message check failed ==> bad commit message - It doesn't match regex: ^(feat|fix|docs|style|refactor|test|chore)(\(.+\))?: .+ - The commit message should follow Conventional Commits. See https://www.conventionalcommits.org - Suggest: Use (): with allowed types - - -Running as CLI --------------- - -Commit-check provides several command-line options for different validation scenarios. via options or STDIN - -.. tip :: - Validate commit messages by piping them through STDIN. This is useful for testing or scripting. - -Available Commands see `commit-check --help `_ - -Message Validation Examples -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. code-block:: bash - - # Validate message from STDIN - echo "feat: new feature" | commit-check -m - - # Validate message from file - commit-check -m commit_message.txt - - # Validate current git commit message (from git log) - commit-check -m - - -**Reading from file:** - -.. code-block:: bash - - # Create a commit message file - cat > commit_message.txt << EOF - fix(auth): resolve login timeout issue - - Users were experiencing timeouts during login. - Increased session timeout and improved error handling. - - Fixes #123 - EOF - - # Validate from file - commit-check -m commit_message.txt - - # Or pipe file content - cat commit_message.txt | commit-check -m - - -Branch Validation Examples -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. code-block:: bash - - # Check current branch name - commit-check --branch - - # Example valid branch names: - # - feature/user-auth - # - fix/login-bug - # - hotfix/security-patch - # - release/v1.2.0 - -Push Validation Examples -~~~~~~~~~~~~~~~~~~~~~~~~ - -.. code-block:: bash - - # Check whether pushing HEAD to its configured upstream would require force - commit-check --no-force-push - -.. code-block:: yaml - - # Configure the dedicated pre-push hook - - repo: https://github.com/commit-check/commit-check - rev: the tag or revision - hooks: - - id: check-no-force-push - stages: [pre-push] - -``git push | commit-check --no-force-push`` is not a prevention mechanism. The -push has already started, and normal ``git push`` output does not include the -pre-push ref lines that Git provides to hooks. - -Author Validation Examples -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. code-block:: bash - - # Check author name - commit-check --author-name - - # Check author email - commit-check --author-email - - # Check both author name and email - commit-check --author-name --author-email - - -Configuration Examples -~~~~~~~~~~~~~~~~~~~~~~ - -.. code-block:: bash - - # Use custom configuration file - echo "feat: test" | commit-check --config my-config.toml -m - - # Use configuration from different directory - commit-check --config /path/to/config/cchk.toml -m - - -Valid Commit Message Examples -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. code-block:: bash - - # Basic feature - echo "feat: add user registration" | commit-check -m - - # Feature with scope - echo "feat(auth): implement OAuth2 login" | commit-check -m - - # Bug fix - echo "fix: resolve memory leak in parser" | commit-check -m - - # Documentation update - echo "docs: add installation guide" | commit-check -m - - # Breaking change - echo "feat!: redesign API endpoints" | commit-check -m - - # Merge commit (automatically allowed) - echo "Merge pull request #123 from feature/new-api" | commit-check -m - -Invalid Commit Message Examples -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. code-block:: bash - - # No type prefix - echo "added new feature" | commit-check -m - - # Capitalized (if configured to disallow) - echo "feat: Add new feature" | commit-check -m - - # Too short - echo "fix" | commit-check -m - - # Non-imperative mood - echo "feat: added login functionality" | commit-check -m - - # Unknown type - echo "unknown: some changes" | commit-check -m - -Error Output Examples -~~~~~~~~~~~~~~~~~~~~~ - -**Commit Message Validation Failure:** - -.. code-block:: text - - Commit rejected by Commit-Check. - - (c).-.(c) (c).-.(c) (c).-.(c) (c).-.(c) (c).-.(c) - / ._. \ / ._. \ / ._. \ / ._. \ / ._. \ - __\( C )/__ __\( H )/__ __\( E )/__ __\( C )/__ __\( K )/__ - (_.-/'-'\-._)(_.-/'-'\-._)(_.-/'-'\-._)(_.-/'-'\-._)(_.-/'-'\-._) - || E || || R || || R || || O || || R || - _.' '-' '._ _.' '-' '._ _.' '-' '._ _.' '-' '._ _.' '-' '._ - (.-./`-´\.-.)(.-./`-´\.-.)(.-./`-´\.-.)(.-./`-´\.-.)(.-./`-´\.-.) - `-´ `-´ `-´ `-´ `-´ `-´ `-´ `-´ `-´ `-´ - - Commit rejected. - - Type message check failed ==> test commit message check - It doesn't match regex: ^(chore|ci|docs|feat|fix|refactor|style|test){1}(\([\w\-\.]+\))?(!)?: ([\w ])+([\s\S]*)|(Merge).*|(fixup!.*) - The commit message should follow Conventional Commits. See https://www.conventionalcommits.org - Suggest: Use (): with allowed types - -**Branch Name Validation Failure:** - -.. code-block:: text - - Commit rejected by Commit-Check. - - (c).-.(c) (c).-.(c) (c).-.(c) (c).-.(c) (c).-.(c) - / ._. \ / ._. \ / ._. \ / ._. \ / ._. \ - __\( C )/__ __\( H )/__ __\( E )/__ __\( C )/__ __\( K )/__ - (_.-/'-'\-._)(_.-/'-'\-._)(_.-/'-'\-._)(_.-/'-'\-._)(_.-/'-'\-._) - || E || || R || || R || || O || || R || - _.' '-' '._ _.' '-' '._ _.' '-' '._ _.' '-' '._ _.' '-' '._ - (.-./`-´\.-.)(.-./`-´\.-.)(.-./`-´\.-.)(.-./`-´\.-.)(.-./`-´\.-.) - `-´ `-´ `-´ `-´ `-´ `-´ `-´ `-´ `-´ `-´ - - Commit rejected. - - Type branch check failed ==> test-branch - It doesn't match regex: ^(feature|bugfix|hotfix|release|chore|feat|fix)\/.+|(master)|(main)|(HEAD)|(PR-.+) - The branch should follow Conventional Branch. See https://conventionalbranch.org - Suggest: Use / with allowed types or ignore_authors in config branch section to bypass - -**Commit Signature Validation Failure:** - -.. code-block:: text - - Commit rejected by Commit-Check. - - (c).-.(c) (c).-.(c) (c).-.(c) (c).-.(c) (c).-.(c) - / ._. \ / ._. \ / ._. \ / ._. \ / ._. \ - __\( C )/__ __\( H )/__ __\( E )/__ __\( C )/__ __\( K )/__ - (_.-/'-'\-._)(_.-/'-'\-._)(_.-/'-'\-._)(_.-/'-'\-._)(_.-/'-'\-._) - || E || || R || || R || || O || || R || - _.' '-' '._ _.' '-' '._ _.' '-' '._ _.' '-' '._ _.' '-' '._ - (.-./`-´\.-.)(.-./`-´\.-.)(.-./`-´\.-.)(.-./`-´\.-.)(.-./`-´\.-.) - `-´ `-´ `-´ `-´ `-´ `-´ `-´ `-´ `-´ `-´ - - Commit rejected. - - Type require_signed_off_by check failed ==> fix: add missing file - It doesn't match regex: Signed-off-by:.*[A-Za-z0-9]\s+<.+@.+> - Signed-off-by not found in latest commit - Suggest: git commit --amend --signoff or use --signoff on commit - -**Commit Message Validation Failure without ASCII Banner (``--no-banner``):** - -.. code-block:: text - - Type message check failed ==> test commit message check - It doesn't match regex: ^(chore|ci|docs|feat|fix|refactor|style|test){1}(\([\w\-\.]+\))?(!)?: ([\w ])+([\s\S]*)|(Merge).*|(fixup!.*) - The commit message should follow Conventional Commits. See https://www.conventionalcommits.org - Suggest: Use (): with allowed types - -**Compact Failure Output (``--compact``):** - -.. code-block:: text - - [FAIL] message: test commit message check - -**Imperative Mood Validation Failure:** - -.. code-block:: text - - Commit rejected by Commit-Check. - - (c).-.(c) (c).-.(c) (c).-.(c) (c).-.(c) (c).-.(c) - / ._. \ / ._. \ / ._. \ / ._. \ / ._. \ - __\( C )/__ __\( H )/__ __\( E )/__ __\( C )/__ __\( K )/__ - (_.-/'-'\-._)(_.-/'-'\-._)(_.-/'-'\-._)(_.-/'-'\-._)(_.-/'-'\-._) - || E || || R || || R || || O || || R || - _.' '-' '._ _.' '-' '._ _.' '-' '._ _.' '-' '._ _.' '-' '._ - (.-./`-´\.-.)(.-./`-´\.-.)(.-./`-´\.-.)(.-./`-´\.-.)(.-./`-´\.-.) - `-´ `-´ `-´ `-´ `-´ `-´ `-´ `-´ `-´ `-´ - - Commit rejected. - - Type imperative check failed ==> fix: added missing file - It doesn't match regex: - Commit message should use imperative mood (e.g., 'Add feature' not 'Added feature') - Suggest: Use imperative mood in the subject line - - -Integration Tips ----------------- - -CI/CD Integration -~~~~~~~~~~~~~~~~~ - -You can use commit-check in CI/CD pipelines: - -.. code-block:: bash - - # In your CI script - git log --format="%s" -n 1 | commit-check -m - - # or just - commit-check -m - - # Keep plain-text output but remove the ASCII art banner - git log --format="%s" -n 1 | commit-check -m --no-banner - - # Emit one machine-friendly line per failure without switching to JSON - git log --format="%s" -n 1 | commit-check -m --compact - -Scripting -~~~~~~~~~ - -Use commit-check in scripts to validate commit messages programmatically: - -.. code-block:: bash - - #!/bin/bash - # validate-commits.sh - - # Get all commit messages from last 10 commits - for i in {0..9}; do - msg=$(git log --format="%s" -n 1 --skip=$i) - if [ -n "$msg" ]; then - echo "Validating: $msg" - echo "$msg" | commit-check -m || exit 1 - fi - done - - echo "All commits are valid!" - -For more configuration options, see the `Configuration Documentation `_. diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md new file mode 100644 index 00000000..c915fc6a --- /dev/null +++ b/docs/getting-started/installation.md @@ -0,0 +1,78 @@ +# Installation + +Commit Check runs anywhere Python does, and ships as a GitHub Action and an MCP +server for the places it doesn't. + +## Command line + +=== "pip" + + ```console + $ pip install commit-check + ``` + +=== "uv" + + ```console + $ uv tool install commit-check + ``` + +=== "pipx" + + ```console + $ pipx install commit-check + ``` + +Verify the install: + +```console +$ commit-check --version +``` + +The CLI is also available as `cchk`, which is the same program under a shorter +name. + +!!! tip "Supported Python versions" + + Commit Check supports Python 3.10 through 3.14, on Linux, macOS and Windows. + +## As a pre-commit hook + +No installation step — [pre-commit](https://pre-commit.com) fetches it for you. +See the [pre-commit guide](../guides/pre-commit.md). + +## As a GitHub Action + +No installation step. See the +[GitHub Actions guide](../guides/github-actions.md). + +## Verifying the download + +Releases are built with [SLSA Level 3](https://slsa.dev) provenance. To verify a +release artifact came from this repository's build pipeline: + +```console +$ gh attestation verify commit_check-*.whl --repo commit-check/commit-check +``` + +## Next steps + +
+ +- :material-rocket-launch-outline:{ .lg .middle } __Quick start__ + + --- + + Catch your first bad commit in five minutes. + + [:octicons-arrow-right-24: Quick start](quickstart.md) + +- :material-book-open-variant:{ .lg .middle } __Rules reference__ + + --- + + Every rule, what it does, and why it matters. + + [:octicons-arrow-right-24: Rules](../rules.md) + +
diff --git a/docs/getting-started/quickstart.md b/docs/getting-started/quickstart.md new file mode 100644 index 00000000..11eef029 --- /dev/null +++ b/docs/getting-started/quickstart.md @@ -0,0 +1,148 @@ +# Quick start + +By the end of this page you will have Commit Check rejecting a bad commit +message on your machine, and you will understand what it is telling you. + +It takes about five minutes and needs nothing but a Git repository. + +## 1. Install + +```console +$ pip install commit-check +``` + +## 2. Watch it reject something + +Commit Check works with no configuration at all. Make a deliberately bad commit +in a scratch repository: + +```console +$ git init demo && cd demo +$ git commit --allow-empty -m "updated the parser" +``` + +Now check it: + +```console +$ commit-check --message +``` + +```text +CC001 message check failed ==> updated the parser +The commit message should follow Conventional Commits. See https://www.conventionalcommits.org +Suggest: Use (): , where is one of: feat, fix, docs, ... +Docs: https://docs.commit-check.com/rules/#cc001 +``` + +Four things are happening in that output, and each is deliberate: + +| Part | What it gives you | +|---|---| +| `CC001` | A stable rule ID. It will mean the same thing in five years. | +| `==> updated the parser` | The exact value that failed, not just "invalid message". | +| `Suggest:` | What to do about it. | +| `Docs:` | Why the rule exists, and how to configure or disable it. | + +## 3. Fix it + +```console +$ git commit --amend -m "fix(parser): handle empty input" +$ commit-check --message +``` + +No output and an exit code of `0`. Commit Check is quiet when it is happy. + +## 4. Check the branch too + +```console +$ git switch -c my-changes +$ commit-check --branch +``` + +```text +CC201 branch check failed ==> my-changes +The branch should follow Conventional Branch. See https://conventionalbranch.org +Suggest: Use / with allowed types +Docs: https://docs.commit-check.com/rules/#cc201 +``` + +Rename it to something structured and it passes: + +```console +$ git branch -m fix/empty-input +$ commit-check --branch +``` + +!!! tip "Checks are opt-in per run" + + `commit-check --message` never evaluates branch rules, and vice versa. Each + check is selected by its own flag, so you can run exactly what a given hook + or CI job needs. The + [rules reference](../rules.md) lists which flag activates each rule. + +## 5. Write down your policy + +So far you have been running the defaults. Create a `cchk.toml` in the +repository root — or in `.github/` — to make the policy explicit: + +```toml title="cchk.toml" +[commit] +conventional_commits = true +subject_imperative = true # (1)! +subject_max_length = 72 +allow_wip_commits = false # (2)! + +[branch] +conventional_branch = true +``` + +1. Off by default. Turning it on rejects `fixed a bug` in favour of `fix a bug`. +2. `allow_*` options describe what is *permitted*. Set to `false` to enforce. + +Run it again and the new rules apply: + +```console +$ commit-check --message +``` + +!!! warning "Defaults are not "nothing"" + + Even with no config file, Conventional Commits, Conventional Branch, subject + length limits of 5–80 characters, and author name/email patterns are + enforced. Check the *Default* column in the + [rules reference](../rules.md) before assuming a rule is off. + +## 6. Make it automatic + +Running the command by hand does not scale. Wire it into the two places it +belongs: + +
+ +- :material-git:{ .lg .middle } __Before the commit lands__ + + --- + + A pre-commit hook rejects the message as you write it, so nothing bad + reaches the branch in the first place. + + [:octicons-arrow-right-24: Pre-commit guide](../guides/pre-commit.md) + +- :material-github:{ .lg .middle } __On every pull request__ + + --- + + A GitHub Action checks every commit in the PR and can comment on the PR + with what needs fixing. + + [:octicons-arrow-right-24: GitHub Actions guide](../guides/github-actions.md) + +
+ +## Where to go next + +- **[Rules reference](../rules.md)** — every rule, what it does, why it matters, + and how to configure it. +- **[Configuration](../configuration.md)** — every option, its type and default, + plus the environment variable and CLI flag that override it. +- **[Why Commit Check](why.md)** — the reasoning behind the tool. diff --git a/docs/getting-started/why.md b/docs/getting-started/why.md new file mode 100644 index 00000000..f1143126 --- /dev/null +++ b/docs/getting-started/why.md @@ -0,0 +1,56 @@ +# Why Commit Check + +## The problem + +Git history is a database that every team writes to and almost nobody validates. + +The cost shows up later, and indirectly. Release notes get written by hand +because commit subjects cannot be grouped. `git bisect` walks through merge +commits that record nothing but a sync. A commit is attributed to `ec2-user` +because a build box had no `user.name`. A contribution has to be rejected +months after the fact because it never carried a `Signed-off-by` trailer. + +None of these are caught by a linter, a type checker, or a test suite. They are +all caught by review, which means they are caught inconsistently, by whoever +happens to be looking, and only after the work is done. + +## The approach + +Commit Check treats commit metadata the way linters treat code: a policy written +down once, enforced identically everywhere, with a stable identifier for every +diagnostic so that findings can be discussed, suppressed, and tracked. + +**One config.** A single `cchk.toml` drives the CLI, the pre-commit hook, the +GitHub Action, and the MCP server. There is no second place where the rules can +disagree with themselves. + +**Fails where it is cheap.** The same check that runs in CI runs in your +`commit-msg` hook. Finding out that a subject is malformed takes a second +locally and a full CI cycle plus a force-push remotely. + +**Stable rule IDs.** Every rule has an ID like [CC003](../rules.md#cc003) that +never changes once released. You can cite it in a review comment, link to its +documentation, and eventually suppress it per-rule. + +**Explains itself.** A failure names the rule, quotes the offending value, says +how to fix it, and links to the reasoning. + +## Where it fits + +Commit Check is deliberately narrow: it validates *metadata*, not code. It is a +lightweight, open alternative to +[GitHub Enterprise metadata restrictions](https://docs.github.com/en/enterprise-server@3.11/repositories/configuring-branches-and-merges-in-your-repository/managing-rulesets/available-rules-for-rulesets#metadata-restrictions) +and Bitbucket's paid +[Yet Another Commit Checker](https://marketplace.atlassian.com/apps/1211854/yet-another-commit-checker), +without requiring a particular forge or an enterprise plan. + +If you already run `ruff`, `eslint`, or `golangci-lint` on your source, Commit +Check is the equivalent for the commits that carry it. + +## What it is not + +- **Not a code linter.** It never reads your source files. +- **Not a replacement for review.** It enforces the mechanical rules so review + can spend its attention on the change itself. +- **Not opinionated by default.** Most rules are off until you turn them on. See + the [rules reference](../rules.md) for what applies out of the box. diff --git a/docs/guides/ai-attribution.md b/docs/guides/ai-attribution.md new file mode 100644 index 00000000..3f627e16 --- /dev/null +++ b/docs/guides/ai-attribution.md @@ -0,0 +1,75 @@ +# Set an AI attribution policy + +AI coding tools add trailers to commit messages identifying themselves. Whether +that is welcome, required, or unacceptable is a decision each project makes for +itself — and the industry has landed in different places: + +- The **Linux kernel** added an `Assisted-by:` trailer, treating AI assistance + as something to disclose. +- **Some projects disallow AI-assisted contributions outright**, usually over + provenance and licensing. +- **Most projects have no stated position**, which means the question resurfaces + in every code review. + +Commit Check does not take a side. It gives you a way to enforce whichever +position your project has already taken, so it stops being relitigated. + +## The default: no opinion + +```toml +[commit] +ai_attribution = "ignore" # the default +``` + +[CC013](../rules.md#cc013) is off. Commits carrying AI trailers pass, and so do +commits without them. + +## Forbidding AI-attributed commits + +```toml title="cchk.toml" +[commit] +ai_attribution = "forbid" +``` + +Commits carrying a recognised AI signature now fail: + +```text +CC013 ai_attribution check failed ==> feat: add caching layer +AI attribution policy violation +Suggest: This project forbids AI-assisted commits. Remove AI trailers and re-commit. +Docs: https://docs.commit-check.com/rules/#cc013 +``` + +### What counts as a signature + +Trailers and co-author lines naming Claude Code, GitHub Copilot, Codex, Gemini, +Cursor, Devin, Aider, Windsurf and Tabby, plus generic AI model patterns. + +!!! warning "This checks disclosure, not authorship" + + CC013 reads commit metadata. It detects a commit that *says* it was + AI-assisted; it cannot detect one that was AI-assisted and did not say so. + + Set against a policy of "no AI contributions", it is an honesty check on + contributors who are already following the rules — not an enforcement + mechanism against those who aren't. Be clear with yourself about which of + those you are buying. + +## Exempting automation + +Bots that legitimately carry AI trailers can be excluded: + +```toml title="cchk.toml" +[commit] +ai_attribution = "forbid" +ignore_authors = ["dependabot[bot]", "renovate[bot]"] +``` + +## Documenting the decision + +Whichever way you go, the config file is not where contributors look. State the +policy where they will see it — `CONTRIBUTING.md`, the pull request template — +and let Commit Check be the mechanism rather than the announcement. + +Enforcing an undocumented policy produces a confusing failure for somebody +acting in good faith. diff --git a/docs/guides/github-actions.md b/docs/guides/github-actions.md new file mode 100644 index 00000000..4dc95ae1 --- /dev/null +++ b/docs/guides/github-actions.md @@ -0,0 +1,102 @@ +# Run in GitHub Actions + +Local hooks can be skipped with `--no-verify`. A CI check cannot, which makes +GitHub Actions the place where your policy is actually a policy. + +## Minimal setup + +```yaml title=".github/workflows/commit-check.yml" +name: Commit Check + +on: + push: + pull_request: + branches: [main] + +jobs: + commit-check: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v5 + with: + ref: ${{ github.event.pull_request.head.sha }} + fetch-depth: 0 # (1)! + - uses: commit-check/commit-check-action@v1 + with: + message: true + branch: true + author-name: true + author-email: true +``` + +1. Commit Check needs the full history to inspect every commit in the pull + request. Without this it only sees the most recent one. + +## Commenting on the pull request + +Instead of making contributors open the job log, have the Action post what +needs fixing directly on the PR: + +```yaml + - uses: commit-check/commit-check-action@v1 + with: + message: true + branch: true + pr-comments: ${{ github.event_name == 'pull_request' }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} +``` + +This needs extra permissions on the job: + +```yaml + permissions: + contents: read + pull-requests: write +``` + +## Reporting without failing + +While a team is adopting the policy, it is often better to report problems +without blocking merges. `dry-run` always exits `0`: + +```yaml + - uses: commit-check/commit-check-action@v1 + with: + message: true + dry-run: true +``` + +Turn it off once the history is clean. + +## Sharing config with local hooks + +The Action reads the same `cchk.toml` as the CLI, so a repository that already +has one needs no Action-specific configuration. That is the point: the rules +cannot drift between what a developer sees locally and what CI enforces. + +See [Configuration](../configuration.md) for where the file may live, and +[Organization-wide policy](organization.md) for sharing one across repositories. + +## Pull requests from forks + +A `pull_request` workflow triggered by a fork receives a **read-only** +`GITHUB_TOKEN`, and `permissions: pull-requests: write` does not override that. +`pr-comments` will therefore fail to post on fork pull requests unless the +repository has *Send write tokens to workflows from pull requests* enabled under +**Settings → Actions → General**. + +!!! warning "Do not reach for `pull_request_target` casually" + + `pull_request_target` does get a write token, but it runs in the context of + the base repository with access to its secrets. Checking out and executing + the fork's code under that trigger is the "pwn request" pattern and hands + repository access to anyone who can open a pull request. + + If you use it, check out the base branch only and never run code from the + pull request. + +The checks themselves still run on fork pull requests and still fail the build; +only the commenting is affected. diff --git a/docs/guides/organization.md b/docs/guides/organization.md new file mode 100644 index 00000000..1da9fd0e --- /dev/null +++ b/docs/guides/organization.md @@ -0,0 +1,100 @@ +# Enforce one policy across an organization + +Copying `cchk.toml` into forty repositories works until the day you want to +change it. `inherit_from` lets each repository pull a shared base config and +override only what it genuinely needs. + +## The shared config + +Put the policy in a repository every project can read — GitHub's `.github` +repository is the conventional home: + +```toml title="my-org/.github → cchk.toml" +[commit] +conventional_commits = true +subject_imperative = true +subject_max_length = 72 +allow_merge_commits = false + +[branch] +conventional_branch = true +allow_branch_types = ["feature", "bugfix", "hotfix", "release", "chore"] +``` + +## Inheriting it + +Each repository then needs one line: + +```toml title="any-repo → .github/cchk.toml" +inherit_from = "github:my-org/.github:cchk.toml" +``` + +Local settings win, so a project with a different constraint overrides just that +one option: + +```toml title="a-repo-with-longer-subjects → .github/cchk.toml" +inherit_from = "github:my-org/.github:cchk.toml" + +[commit] +subject_max_length = 100 # everything else comes from the org config +``` + +## Pinning the version + +By default the shorthand resolves to the parent repository's default branch, +which means a change to the org config takes effect everywhere on the next run. +That is usually what you want. When it isn't, pin to a ref: + +```toml +inherit_from = "github:my-org/.github@v1:cchk.toml" +``` + +## Other sources + +=== "GitHub shorthand" + + ```toml + inherit_from = "github:my-org/.github:cchk.toml" + ``` + +=== "Local path" + + ```toml + inherit_from = "../../shared/org-cchk.toml" + ``` + + Useful in a monorepo, where the shared config is already checked out. + +=== "HTTPS URL" + + ```toml + inherit_from = "https://example.com/shared/cchk.toml" + ``` + + Plain HTTP is rejected. + +!!! warning "Inheritance fails open" + + If the parent config is unreachable — a network blip, a renamed file, a + private repository — Commit Check silently falls back to the local config + rather than failing the build. This keeps CI green during an outage, but it + also means a typo in `inherit_from` is easy to miss. Verify the merged + result when you first set it up: + + ```console + $ commit-check --message --format json + ``` + +## Rolling it out + +Turning on a strict policy across an organization at once produces a wall of +red. A gentler sequence: + +1. Ship the org config with `dry-run` enabled in CI, so violations are reported + but nothing blocks. +2. Look at what actually fails. Some rules will turn out to be wrong for some + teams — that is information, not an obstacle. +3. Turn off `dry-run` for repositories whose history is already clean. +4. Tighten the shared config over time. + +See the [GitHub Actions guide](github-actions.md) for the `dry-run` input. diff --git a/docs/guides/pre-commit.md b/docs/guides/pre-commit.md new file mode 100644 index 00000000..193f0f97 --- /dev/null +++ b/docs/guides/pre-commit.md @@ -0,0 +1,89 @@ +# Run as a pre-commit hook + +A pre-commit hook is the cheapest place to enforce commit policy: the developer +finds out while they are still writing the message, not after a CI round trip. + +## Setup + +Add Commit Check to `.pre-commit-config.yaml`: + +```yaml title=".pre-commit-config.yaml" +repos: + - repo: https://github.com/commit-check/commit-check + rev: v2.11.0 + hooks: + - id: check-message + - id: check-branch + - id: check-author-name + - id: check-author-email +``` + +Then install the hooks. `check-message` runs at the `commit-msg` stage, so it +needs its own install step: + +```console +$ pre-commit install --hook-type commit-msg +$ pre-commit install +``` + +That is it. The next malformed commit message is rejected before it exists. + +## Available hooks + +| Hook ID | Stage | Rules | +|---|---|---| +| `check-message` | `commit-msg` | [CC001–CC013](../rules.md#commit-message-rules) | +| `check-branch` | `pre-commit` | [CC201–CC202](../rules.md#branch-rules) | +| `check-author-name` | `pre-commit` | [CC101](../rules.md#cc101) | +| `check-author-email` | `pre-commit` | [CC102](../rules.md#cc102) | +| `check-no-force-push` | `pre-push` | [CC301](../rules.md#cc301) | + +`check-no-force-push` also needs its own install: + +```console +$ pre-commit install --hook-type pre-push +``` + +## Configuring without a TOML file + +Options can be passed as hook arguments, which keeps everything in one file: + +```yaml title=".pre-commit-config.yaml" +repos: + - repo: https://github.com/commit-check/commit-check + rev: v2.11.0 + hooks: + - id: check-message + args: + - --subject-imperative=true + - --subject-max-length=72 + - --allow-merge-commits=false +``` + +A `cchk.toml` is usually the better choice once you have more than a couple of +options, because CI and the CLI read it too. See +[Configuration](../configuration.md) for the precedence rules. + +## Skipping a hook + +Occasionally you need to get a commit through — a mid-rebase fixup, an +automated migration. `pre-commit` supports this natively: + +```console +$ SKIP=check-message git commit -m "wip" +``` + +!!! warning "Local hooks are not a policy boundary" + + Anyone can pass `--no-verify`. Hooks exist to give fast feedback to people + who want to follow the policy, not to stop people who don't. Pair them with + the [GitHub Action](github-actions.md), which runs where it cannot be + skipped. + +## Troubleshooting + +If `check-message` never seems to run, it is almost always because +`pre-commit install --hook-type commit-msg` was not run — a plain +`pre-commit install` only wires up the `pre-commit` stage. + +More in [Troubleshooting](../troubleshoot.md). diff --git a/docs/guides/signoff.md b/docs/guides/signoff.md new file mode 100644 index 00000000..578c37fb --- /dev/null +++ b/docs/guides/signoff.md @@ -0,0 +1,81 @@ +# Require signoff (DCO) + +Projects that use the [Developer Certificate of Origin](https://developercertificate.org/) +require every commit to carry a `Signed-off-by` trailer. The Linux kernel and +much of the CNCF work this way. + +A DCO bot rejecting a pull request after the fact is a poor experience: the +contributor has to rewrite history for every commit in the branch. Checking +locally fixes it before it becomes a problem. + +## Turn it on + +```toml title="cchk.toml" +[commit] +require_signed_off_by = true +``` + +This enables [CC012](../rules.md#cc012), which is off by default. + +## Signing off + +```console +$ git commit --signoff -m "fix: handle an empty config file" +``` + +The trailer is appended automatically from your `user.name` and `user.email`: + +```text +fix: handle an empty config file + +Signed-off-by: Your Name +``` + +Forgot it? Fix the last commit in place: + +```console +$ git commit --amend --signoff --no-edit +``` + +Fix a whole branch: + +```console +$ git rebase --signoff main +``` + +!!! tip "Make it automatic" + + Signing off is easy to forget. Combine this rule with the + [pre-commit hook](pre-commit.md) so a missing trailer is caught at commit + time, not at review time. + +## Identity matters + +The DCO is a statement about who wrote the code, so it only means something if +the identity is real. [CC101](../rules.md#cc101) and +[CC102](../rules.md#cc102) check the committer name and email, and are enabled +by default when their check runs: + +```console +$ commit-check --author-name --author-email +``` + +To require a company address: + +```toml title="cchk.toml" +[commit] +author_email_pattern = "^.+@example\\.com$" +``` + +## Bots + +Automation cannot meaningfully sign the DCO, and forcing it to produces +meaningless trailers. Exempt bots instead: + +```toml title="cchk.toml" +[commit] +require_signed_off_by = true +ignore_authors = ["dependabot[bot]", "renovate[bot]"] +``` + +`ignore_authors` matches the commit author and any `Co-authored-by:` trailers. diff --git a/docs/index.md b/docs/index.md index 69e2336c..1e203451 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,15 +1,169 @@ -```{include} ../README.md -``` +--- +title: Commit Check +description: Enforce commit message, branch naming, author and signoff standards across your CLI, pre-commit hooks, CI, and AI agents. +hide: + - navigation + - toc +--- + +
+ +# Clean commits. Clear standards. + +Commit Check enforces the rules your Git history already depends on — commit +messages, branch names, committer identity, signoff — from one config, in every +place your team writes code. + +[Get started :octicons-arrow-right-24:](getting-started/quickstart.md){ .md-button .md-button--primary } +[Browse the rules](rules.md){ .md-button } + +
+ +--- + +## One config, enforced everywhere + +Write the policy once. The same rules run on a developer's laptop, in CI, and in +whatever your AI agent is committing on your behalf. + +=== "Command line" + + ```console + $ commit-check --message --branch + CC003 subject_imperative check failed ==> docs: revamped the profile + Commit message should use imperative mood (e.g., 'fix bug' not 'fixed bug') + Suggest: Change the first verb to imperative form + Docs: https://docs.commit-check.com/rules/#cc003 + ``` + +=== "pre-commit" + + ```yaml title=".pre-commit-config.yaml" + repos: + - repo: https://github.com/commit-check/commit-check + rev: v2.11.0 + hooks: + - id: check-message + - id: check-branch + - id: check-author-email + ``` + +=== "GitHub Actions" + + ```yaml title=".github/workflows/commit-check.yml" + - uses: commit-check/commit-check-action@v1 + with: + message: true + branch: true + pr-comments: ${{ github.event_name == 'pull_request' }} + ``` + +=== "AI agents" + + ```json title="MCP server" + { + "mcpServers": { + "commit-check": { "command": "commit-check-mcp" } + } + } + ``` + +## What it checks + +
+ +- :material-message-text-outline:{ .lg .middle } __Commit messages__ + + --- + + Conventional Commits by default, or your own pattern. Subject length, mood, + capitalisation, required body, forbidden merge/fixup/WIP commits. + + [:octicons-arrow-right-24: CC001–CC013](rules.md#commit-message-rules) + +- :material-source-branch:{ .lg .middle } __Branch names__ + + --- + + Conventional Branch naming, plus rebase checks that catch a branch drifting + behind its target before CI wastes a run on stale code. + + [:octicons-arrow-right-24: CC201–CC202](rules.md#branch-rules) + +- :material-account-check-outline:{ .lg .middle } __Committer identity__ + + --- + + Catch commits authored by `ec2-user` on a build box, or require everyone to + contribute from a company address. -```{toctree} -:hidden: -self -what-is-new -configuration -rules -example -migration -troubleshoot -changelog -cli_args + [:octicons-arrow-right-24: CC101–CC102](rules.md#author-rules) + +- :material-file-sign:{ .lg .middle } __Signoff and DCO__ + + --- + + Require the `Signed-off-by` trailer locally, so contributors find out before + CI rejects the pull request. + + [:octicons-arrow-right-24: Signoff guide](guides/signoff.md) + +- :material-robot-outline:{ .lg .middle } __AI attribution__ + + --- + + Whatever your project has decided about AI-assisted commits, enforce it + mechanically instead of relitigating it in review. + + [:octicons-arrow-right-24: AI attribution guide](guides/ai-attribution.md) + +- :material-office-building-outline:{ .lg .middle } __Org-wide policy__ + + --- + + Inherit a base config from a shared repository, then let each project + override only what it needs. + + [:octicons-arrow-right-24: Organization guide](guides/organization.md) + +
+ +## Built to be trusted + +
+ +- :material-shield-check:{ .lg .middle } __SLSA Level 3__ + + --- + + Build provenance with artifact attestation you can verify before + installing. + +- :material-tag-outline:{ .lg .middle } __Stable rule IDs__ + + --- + + Every diagnostic carries an ID like `CC003` that never changes, so you can + cite it in review, suppress it, or feed it to tooling. + +- :material-source-commit:{ .lg .middle } __Used in production__ + + --- + + Running at Apache, Texas Instruments, Mila, and + [many more](https://github.com/commit-check/commit-check-action/network/dependents). + +
+ +## Ready in two minutes + +```console +$ pip install commit-check +$ commit-check --message --branch ``` + +No configuration file needed to start — sensible defaults apply immediately, and +you tighten them when you are ready. + +[Install :octicons-arrow-right-24:](getting-started/installation.md){ .md-button .md-button--primary } +[Why Commit Check?](getting-started/why.md){ .md-button } diff --git a/docs/migration.md b/docs/migration.md new file mode 100644 index 00000000..45d57ffa --- /dev/null +++ b/docs/migration.md @@ -0,0 +1,189 @@ +# Migration Guide + +This guide helps you migrate from commit-check v1.x (YAML configuration) to v2.0+ (TOML configuration). + +## Overview + +Version 2.0 introduces significant changes to commit-check: + +* **Configuration format**: `.commit-check.yml` → `cchk.toml` or `commit-check.toml` +* **Simplified architecture**: New validation engine with cleaner design +* **Enhanced functionality**: Better error messages and more flexible configuration options + +## Quick Migration Steps + +1. **Backup your existing configuration**: + +```bash +cp .commit-check.yml .commit-check.yml.backup +``` + +2. **Create new TOML configuration**: + +```bash +touch cchk.toml # or commit-check.toml +``` + +3. **Convert YAML to TOML format** (see examples below) + +4. **Test the new configuration**: + +```bash +commit-check --help +commit-check --message --branch --author-name --author-email --dry-run +``` + +5. **Remove old YAML file**: + +```bash +rm .commit-check.yml.backup # after confirming everything works +``` + +## Configuration Format Changes + +The configuration structure has changed from YAML to TOML format + +### YAML (v1.x) vs TOML (v2.0+) + +**Old YAML format** (`.commit-check.yml`): + +```yaml +checks: +- check: message + regex: '^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test){1}(\([\w\-\.]+\))?(!)?: ([\w ])+([\s\S]*)|(Merge).*|(fixup!.*)' + error: "The commit message should be structured as follows:\n\n + [optional scope]: \n + [optional body]\n + [optional footer(s)]\n\n + More details please refer to https://www.conventionalcommits.org" + suggest: please check your commit message whether matches above regex + +- check: branch + regex: ^(bugfix|feature|release|hotfix|task|chore)\/.+|(master)|(main)|(HEAD)|(PR-.+) + error: "Branches must begin with these types: bugfix/ feature/ release/ hotfix/ task/ chore/" + suggest: run command `git checkout -b type/branch_name` + +- check: author_name + regex: ^[A-Za-zÀ-ÖØ-öø-ÿ\u0100-\u017F\u0180-\u024F ,.\'-]+$|.*(\[bot]) + error: The committer name seems invalid + suggest: run command `git config user.name "Your Name"` + +- check: author_email + regex: ^.+@.+$ + error: The committer email seems invalid + suggest: run command `git config user.email yourname@example.com` + +- check: commit_signoff + regex: Signed-off-by:.*[A-Za-z0-9]\s+<.+@.+> + error: Signed-off-by not found in latest commit + suggest: run command `git commit -m "conventional commit message" --signoff` + +- check: merge_base + 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"' +``` + +**New TOML format** (`cchk.toml` or `commit-check.toml`): + +```toml +[commit] +# https://www.conventionalcommits.org +conventional_commits = true +subject_capitalized = false +subject_imperative = true +subject_max_length = 80 +subject_min_length = 5 +allow_commit_types = ["feat", "fix", "docs", "style", "refactor", "test", "chore", "ci"] +allow_merge_commits = true +allow_revert_commits = true +allow_empty_commits = false +allow_fixup_commits = true +allow_wip_commits = false +require_body = false +require_signed_off_by = false +ignore_authors = ["dependabot[bot]", "copilot[bot]"] + +[branch] +# https://conventionalbranch.org +conventional_branch = true +allow_branch_types = ["feature", "bugfix", "hotfix", "release", "chore", "feat", "fix"] +require_rebase_target = "main" +``` + +### CLI Changes + +The command-line interface has been simplified: + +**Old CLI** (v1.x): + +```bash +commit-check --config .commit-check.yml +``` + +**New CLI** (v2.0+): + +```bash +commit-check --config cchk.toml # or commit-check.toml +# Or use defaults (no config file needed) +commit-check --message --branch +``` + +### Custom Regex (`message_pattern`) + +If you relied on the custom `regex` field in v1.x to enforce a non-Conventional-Commits +format (e.g. JIRA smart commits `PROJ-123: description`), use the `message_pattern` +option in the `[commit]` section: + +```toml +[commit] +message_pattern = "^PROJ-\\d+: .+" +``` + +When `message_pattern` is set (non-empty), it replaces the auto-generated Conventional +Commits regex entirely, giving you full control over the accepted message format. + +## Troubleshooting + +### Common Issues + +**Issue**: "Configuration file not found" + +**Solution**: Ensure your file is named `cchk.toml` or `commit-check.toml` and placed in the repository root or in the `.github` folder. + +**Issue**: "Invalid TOML syntax" + +**Solution**: Use a TOML validator or check the syntax. Common issues include: + +* Missing quotes around strings +* Incorrect boolean values (use `true`/`false`, not `True`/`False`) +* Invalid array syntax + +**Issue**: "Validation rules not working as expected" + +**Solution**: Check the [Configuration Documentation](configuration.md) for the correct option names and formats. + +### Validation and Testing + +After migration, test your configuration: + +```bash +# Test commit message validation +echo "feat: test commit message" | commit-check --message + +# Test branch validation +commit-check --branch + +# Test with dry-run flag +commit-check --message --branch --author-name --author-email --dry-run +``` + +## Getting Help + +* **Documentation**: Check the [Configuration Guide](configuration.md) +* **Issues**: Report problems on [GitHub Issues](https://github.com/commit-check/commit-check/issues) diff --git a/docs/migration.rst b/docs/migration.rst deleted file mode 100644 index c0118de1..00000000 --- a/docs/migration.rst +++ /dev/null @@ -1,203 +0,0 @@ -Migration Guide -=============== - -This guide helps you migrate from commit-check v1.x (YAML configuration) to v2.0+ (TOML configuration). - -Overview --------- - -Version 2.0 introduces significant changes to commit-check: - -* **Configuration format**: ``.commit-check.yml`` → ``cchk.toml`` or ``commit-check.toml`` -* **Simplified architecture**: New validation engine with cleaner design -* **Enhanced functionality**: Better error messages and more flexible configuration options - -Quick Migration Steps ---------------------- - -1. **Backup your existing configuration**: - - .. code-block:: bash - - cp .commit-check.yml .commit-check.yml.backup - -2. **Create new TOML configuration**: - - .. code-block:: bash - - touch cchk.toml # or commit-check.toml - -3. **Convert YAML to TOML format** (see examples below) - -4. **Test the new configuration**: - - .. code-block:: bash - - commit-check --help - commit-check --message --branch --author-name --author-email --dry-run - -5. **Remove old YAML file**: - - .. code-block:: bash - - rm .commit-check.yml.backup # after confirming everything works - -Configuration Format Changes ----------------------------- - -The configuration structure has changed from YAML to TOML format - -YAML (v1.x) vs TOML (v2.0+) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -**Old YAML format** (``.commit-check.yml``): - -.. code-block:: yaml - - checks: - - check: message - regex: '^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test){1}(\([\w\-\.]+\))?(!)?: ([\w ])+([\s\S]*)|(Merge).*|(fixup!.*)' - error: "The commit message should be structured as follows:\n\n - [optional scope]: \n - [optional body]\n - [optional footer(s)]\n\n - More details please refer to https://www.conventionalcommits.org" - suggest: please check your commit message whether matches above regex - - - check: branch - regex: ^(bugfix|feature|release|hotfix|task|chore)\/.+|(master)|(main)|(HEAD)|(PR-.+) - error: "Branches must begin with these types: bugfix/ feature/ release/ hotfix/ task/ chore/" - suggest: run command `git checkout -b type/branch_name` - - - check: author_name - regex: ^[A-Za-zÀ-ÖØ-öø-ÿ\u0100-\u017F\u0180-\u024F ,.\'-]+$|.*(\[bot]) - error: The committer name seems invalid - suggest: run command `git config user.name "Your Name"` - - - check: author_email - regex: ^.+@.+$ - error: The committer email seems invalid - suggest: run command `git config user.email yourname@example.com` - - - check: commit_signoff - regex: Signed-off-by:.*[A-Za-z0-9]\s+<.+@.+> - error: Signed-off-by not found in latest commit - suggest: run command `git commit -m "conventional commit message" --signoff` - - - check: merge_base - 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"' - -**New TOML format** (``cchk.toml`` or ``commit-check.toml``): - -.. code-block:: toml - - [commit] - # https://www.conventionalcommits.org - conventional_commits = true - subject_capitalized = false - subject_imperative = true - subject_max_length = 80 - subject_min_length = 5 - allow_commit_types = ["feat", "fix", "docs", "style", "refactor", "test", "chore", "ci"] - allow_merge_commits = true - allow_revert_commits = true - allow_empty_commits = false - allow_fixup_commits = true - allow_wip_commits = false - require_body = false - require_signed_off_by = false - ignore_authors = ["dependabot[bot]", "copilot[bot]"] - - [branch] - # https://conventionalbranch.org - conventional_branch = true - allow_branch_types = ["feature", "bugfix", "hotfix", "release", "chore", "feat", "fix"] - require_rebase_target = "main" - - - -CLI Changes -~~~~~~~~~~~ - -The command-line interface has been simplified: - -**Old CLI** (v1.x): - -.. code-block:: bash - - commit-check --config .commit-check.yml - -**New CLI** (v2.0+): - -.. code-block:: bash - - commit-check --config cchk.toml # or commit-check.toml - # Or use defaults (no config file needed) - commit-check --message --branch - - -Custom Regex (``message_pattern``) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -If you relied on the custom ``regex`` field in v1.x to enforce a non-Conventional-Commits -format (e.g. JIRA smart commits ``PROJ-123: description``), use the ``message_pattern`` -option in the ``[commit]`` section: - -.. code-block:: toml - - [commit] - message_pattern = "^PROJ-\\d+: .+" - -When ``message_pattern`` is set (non-empty), it replaces the auto-generated Conventional -Commits regex entirely, giving you full control over the accepted message format. - -Troubleshooting ---------------- - -Common Issues -~~~~~~~~~~~~~ - -**Issue**: "Configuration file not found" - -**Solution**: Ensure your file is named ``cchk.toml`` or ``commit-check.toml`` and placed in the repository root or in the ``.github`` folder. - -**Issue**: "Invalid TOML syntax" - -**Solution**: Use a TOML validator or check the syntax. Common issues include: - -* Missing quotes around strings -* Incorrect boolean values (use ``true``/``false``, not ``True``/``False``) -* Invalid array syntax - -**Issue**: "Validation rules not working as expected" - -**Solution**: Check the `Configuration Documentation `_ for the correct option names and formats. - -Validation and Testing -~~~~~~~~~~~~~~~~~~~~~~ - -After migration, test your configuration: - -.. code-block:: bash - - # Test commit message validation - echo "feat: test commit message" | commit-check --message - - # Test branch validation - commit-check --branch - - # Test with dry-run flag - commit-check --message --branch --author-name --author-email --dry-run - -Getting Help ------------- - -* **Documentation**: Check the `Configuration Guide `_ -* **Issues**: Report problems on `GitHub Issues `_ diff --git a/docs/rules.md b/docs/rules.md new file mode 100644 index 00000000..fdd0db18 --- /dev/null +++ b/docs/rules.md @@ -0,0 +1,696 @@ +# Rules + +Every check that can report a failure has a **stable rule ID**. Rule IDs never +change once released, so they are safe to reference in commit messages, code +review comments, issue templates, and tooling. + +Rule IDs appear in commit-check's output and in `--format json` results: + +```text +CC003 subject_imperative check failed ==> docs: revamped the profile +Commit message should use imperative mood (e.g., 'fix bug' not 'fixed bug') +Suggest: Change the first verb to imperative form, e.g., 'fix' instead of 'fixed' +Docs: https://docs.commit-check.com/rules.html#cc003 +``` + +`--compact` prints one line per failure, keeping the rule ID and dropping the +explanation, suggestion, and documentation link: + +```text +[FAIL] CC003 subject_imperative: docs: revamped the profile +``` + +## How to read this page + +Rule IDs are grouped by what they inspect: + +| Range | Category | Inspects | +|---|---|---| +| `CC0xx` | [Commit message](#commit-message-rules) | The subject, body, and trailers of a commit message | +| `CC1xx` | [Author](#author-rules) | The committer's configured name and email | +| `CC2xx` | [Branch](#branch-rules) | The current branch's name and its position relative to a target branch | +| `CC3xx` | [Push](#push-rules) | The push operation itself | + +Two things determine whether a rule runs: + +**The check you select.** commit-check only evaluates the checks you ask for on +the command line. `commit-check --message` never reports a branch rule. The +*Check* column in the tables below shows which flag activates each rule. + +**Its configuration.** Within a selected check, the *Default* column shows +whether the rule is active with no configuration at all: + +| Default | Meaning | +|---|---| +| ✅ On | Enforced out of the box. Disable it through the listed option. | +| ⚪ Off | Not enforced until you opt in through the listed option. | + +Rules that are off by default are not lesser rules — they encode conventions +that are right for some projects and wrong for others. Turning on +[CC002](#cc002) makes sense for a project that capitalizes subjects, and is +actively harmful for one that does not. + +!!! tip + + Every option named below is documented in full — with its type, default, and + the matching environment variable and CLI flag — in + [configuration](configuration.md). + +## Rule index + +### Commit message rules (`CC0xx`) { #commit-message-rules } + +Run with `-m` / `--message`. + +| Code | Name | Message | Check | Default | +|---|---|---|---|---| +| [CC001](#cc001) | `message` | The commit message should follow Conventional Commits | `-m` | ✅ On | +| [CC002](#cc002) | `subject-capitalized` | Subject must start with a capital letter | `-m` | ⚪ Off | +| [CC003](#cc003) | `subject-imperative` | Commit message should use imperative mood | `-m` | ⚪ Off | +| [CC004](#cc004) | `subject-max-length` | Subject must be at most `{max_len}` characters | `-m` | ✅ On | +| [CC005](#cc005) | `subject-min-length` | Subject must be at least `{min_len}` characters | `-m` | ✅ On | +| [CC006](#cc006) | `allow-merge-commits` | Merge commits are not allowed | `-m` | ⚪ Off | +| [CC007](#cc007) | `allow-revert-commits` | Revert commits are not allowed | `-m` | ⚪ Off | +| [CC008](#cc008) | `allow-empty-commits` | Empty commit messages are not allowed | `-m` | ⚪ Off | +| [CC009](#cc009) | `allow-fixup-commits` | Fixup commits are not allowed | `-m` | ⚪ Off | +| [CC010](#cc010) | `allow-wip-commits` | WIP commits are not allowed | `-m` | ⚪ Off | +| [CC011](#cc011) | `require-body` | Commit body is required | `-m` | ⚪ Off | +| [CC012](#cc012) | `require-signed-off-by` | Signed-off-by not found in latest commit | `-m` | ⚪ Off | +| [CC013](#cc013) | `ai-attribution` | AI attribution policy violation | `-m` | ⚪ Off | + +### Author rules (`CC1xx`) { #author-rules } + +| Code | Name | Message | Check | Default | +|---|---|---|---|---| +| [CC101](#cc101) | `author-name` | The committer name seems invalid | `-n` | ✅ On | +| [CC102](#cc102) | `author-email` | The committer's email seems invalid | `-e` | ✅ On | + +### Branch rules (`CC2xx`) { #branch-rules } + +Run with `-b` / `--branch`. + +| Code | Name | Message | Check | Default | +|---|---|---|---|---| +| [CC201](#cc201) | `branch` | The branch should follow Conventional Branch | `-b` | ✅ On | +| [CC202](#cc202) | `merge-base` | Current branch is not rebased onto target branch | `-b` | ⚪ Off | + +### Push rules (`CC3xx`) { #push-rules } + +| Code | Name | Message | Check | Default | +|---|---|---|---|---| +| [CC301](#cc301) | `no-force-push` | Force push is not allowed | `--no-force-push` | ⚪ Off | + +## Commit message rules + +### message (CC001) { #cc001 } + +**What it does** + +Checks that the commit message subject follows the +[Conventional Commits](https://www.conventionalcommits.org) specification: +`()!: `. + +**Why is this bad?** + +A free-form subject can only be read by a human. A structured one can be read by +tooling: release-drafting can group changes by type, semantic versioning can +infer whether a release is a patch, minor, or major, and `git log` becomes +filterable by area of the codebase. Once a fraction of the history is +unstructured, every consumer of that history needs a fallback path. + +**Example** + +```text +updated the parser +``` + +Use instead: + +```text +fix(parser): handle empty input +``` + +**Options** + +* `commit.conventional_commits` — set to `false` to disable this rule. +* `commit.allow_commit_types` — the accepted `` values. +* `commit.message_pattern` — a custom regex that replaces the generated + Conventional Commits pattern entirely, for formats such as JIRA smart commits + (`"^PROJ-\\d+: .+"`). + +### subject-capitalized (CC002) { #cc002 } + +**What it does** + +Checks that the description in the subject line starts with a capital letter. + +**Why is this bad?** + +Nothing is inherently wrong with either casing — but mixing them is. A history +where half the subjects read `fix: handle empty input` and the other half read +`fix: Handle empty input` looks careless in `git log --oneline`, and gives +reviewers a pointless thing to comment on. This rule picks the capitalized +convention and enforces it. + +Leave it off if your project deliberately uses lowercase descriptions, which is +the more common convention among projects that follow Conventional Commits. + +**Example** + +```text +fix: handle empty input +``` + +Use instead: + +```text +fix: Handle empty input +``` + +**Options** + +* `commit.subject_capitalized` — set to `true` to enable this rule. + +### subject-imperative (CC003) { #cc003 } + +**What it does** + +Checks that the first word of the description is in the imperative mood — +`fix`, not `fixed`, `fixes`, or `fixing`. + +**Why is this bad?** + +This is Git's own convention: a subject should complete the sentence *"If +applied, this commit will ___"*. `If applied, this commit will fixed a crash` +does not read as English. Beyond grammar, the imperative form is the shortest of +the three, which matters on a line that tooling truncates around 50 characters. + +**Example** + +```text +fix: fixed a crash when the config file is empty +``` + +Use instead: + +```text +fix: handle an empty config file +``` + +**Options** + +* `commit.subject_imperative` — set to `true` to enable this rule. + +The list of recognised non-imperative verb forms lives in +[imperatives.py](https://github.com/commit-check/commit-check/blob/main/commit_check/imperatives.py). + +### subject-max-length (CC004) { #cc004 } + +**What it does** + +Checks that the subject line is at most a configured number of characters. + +**Why is this bad?** + +Long subjects get truncated by the tools that display them — +`git log --oneline`, `git shortlog`, GitHub's commit list, and most Git +GUIs all cut off somewhere between 50 and 72 columns. A subject that carries its +meaning past that point loses it exactly where people skim. Detail belongs in +the body, which nothing truncates. + +**Example** + +```text +fix: handle an empty config file and also fix the unrelated crash in the branch parser that happens on Windows +``` + +Use instead: + +```text +fix: handle an empty config file + +Also fixes the branch parser crash on Windows, which shared the +same root cause. +``` + +**Options** + +* `commit.subject_max_length` — the limit, in characters. Defaults to `80`. + `50` and `72` are the other conventional choices. + +### subject-min-length (CC005) { #cc005 } + +**What it does** + +Checks that the subject line is at least a configured number of characters. + +**Why is this bad?** + +Subjects like `fix`, `wip`, or `.` describe nothing. They are invisible in +a blame view and useless in a bisect session, and they are almost always the +result of a hurried commit rather than a deliberate one. + +**Example** + +```text +fix: bug +``` + +Use instead: + +```text +fix: reject config files with a null inherit_from +``` + +**Options** + +* `commit.subject_min_length` — the minimum, in characters. Defaults to `5`. + +### allow-merge-commits (CC006) { #cc006 } + +**What it does** + +Rejects merge commits — the `Merge branch '...'` commits that `git pull` +creates. + +**Why is this bad?** + +Merge commits created by `git pull` carry no information: they record that +someone synced, not that anything was decided. On a busy repository they can +outnumber real commits, which makes `git log` unreadable, adds branches for +`git bisect` to walk, and breaks the assumption behind +`git log --first-parent`. Projects that want a linear history rebase instead. + +**Example** + +```bash +git pull +``` + +Use instead: + +```bash +git pull --rebase + +# or make it the default +git config --global pull.rebase true +``` + +**Options** + +* `commit.allow_merge_commits` — set to `false` to enable this rule. + +### allow-revert-commits (CC007) { #cc007 } + +**What it does** + +Rejects the `Revert "..."` commits that `git revert` generates. + +**Why is this bad?** + +A generated revert subject describes the mechanics of the change and nothing +about the reason for it. Six months later, `Revert "feat: add caching layer"` +answers "what happened" but not the only question that matters: why the feature +was backed out, and whether it is safe to try again. + +**Example** + +```text +Revert "feat: add caching layer" +``` + +Use instead: + +```text +fix: remove the caching layer + +The cache served stale permissions after a role change (#412). +Reverts 4a1c9f2; re-land once invalidation is keyed on role version. +``` + +**Options** + +* `commit.allow_revert_commits` — set to `false` to enable this rule. + +### allow-empty-commits (CC008) { #cc008 } + +**What it does** + +Rejects commits with an empty message. + +**Why is this bad?** + +A commit with no subject cannot be searched for, summarised, or reviewed. It is +a gap in the history that nobody can fill in later. + +**Options** + +* `commit.allow_empty_commits` — set to `false` to enable this rule. + +### allow-fixup-commits (CC009) { #cc009 } + +**What it does** + +Rejects `fixup!` and `squash!` commits. + +**Why is this bad?** + +These commits exist to be consumed by `git rebase --autosquash` before a +branch is merged. One that survives to the target branch means the autosquash +was forgotten — leaving behind a commit that, by construction, does not stand on +its own. + +**Example** + +```text +fixup! feat: add the caching layer +``` + +Use instead: + +```bash +git rebase -i --autosquash main +``` + +**Options** + +* `commit.allow_fixup_commits` — set to `false` to enable this rule. + +### allow-wip-commits (CC010) { #cc010 } + +**What it does** + +Rejects work-in-progress commits — subjects beginning with `WIP`. + +**Why is this bad?** + +A WIP commit is an explicit statement that the change is not finished. That is +useful on a local branch and wrong on a shared one, where every commit is +something another developer may bisect through or build on. + +**Example** + +```text +WIP: caching +``` + +Use instead: + +```bash +# keep the work, drop the marker +git commit --amend -m "feat: add a caching layer for role lookups" +``` + +**Options** + +* `commit.allow_wip_commits` — set to `false` to enable this rule. + +### require-body (CC011) { #cc011 } + +**What it does** + +Requires a non-empty body after the subject line. + +**Why is this bad?** + +The subject says *what* changed; the diff already says that too. The body says +*why* — the constraint, the bug report, the rejected alternative. That reasoning +is the one thing that cannot be recovered from the code later, and it is exactly +what the next person to touch the change needs. + +**Example** + +```text +fix: cap the retry backoff at 30s +``` + +Use instead: + +```text +fix: cap the retry backoff at 30s + +The unbounded exponential backoff reached 45 minutes during the +incident on 2026-05-11, long after the upstream had recovered. +``` + +**Options** + +* `commit.require_body` — set to `true` to enable this rule. + +### require-signed-off-by (CC012) { #cc012 } + +**What it does** + +Requires a `Signed-off-by:` trailer in the commit message. + +**Why is this bad?** + +Projects that use the +[Developer Certificate of Origin](https://developercertificate.org/) — the +Linux kernel, and much of the +CNCF — treat that trailer as the contributor's statement that they have the +right to submit the code. A commit without it cannot be merged, so catching it +locally saves a round trip through CI. + +**Example** + +```bash +git commit -m "fix: handle an empty config file" +``` + +Use instead: + +```bash +git commit --signoff -m "fix: handle an empty config file" + +# or fix the commit you already made +git commit --amend --signoff +``` + +**Options** + +* `commit.require_signed_off_by` — set to `true` to enable this rule. + +### ai-attribution (CC013) { #cc013 } + +**What it does** + +Rejects commits carrying the signatures that AI coding tools add to commit +messages — trailers naming Claude Code, Copilot, Codex, Gemini, Cursor, Devin, +Aider, Windsurf, Tabby, and generic AI model patterns. + +**Why is this bad?** + +Whether AI-assisted commits are acceptable is a policy question, and projects +have landed on different answers: the Linux kernel added an `Assisted-by:` +trailer, while others disallow the practice outright. This rule exists for +projects that have made that decision and want it enforced mechanically rather +than relitigated in every code review. + +It is off by default, and the default policy is `"ignore"`. Enable it only if +your project has a stated position. + +**Options** + +* `commit.ai_attribution` — `"forbid"` enables this rule, `"ignore"` + (the default) disables it. + +## Author rules + +### author-name (CC101) { #cc101 } + +**What it does** + +Checks the committer's configured name against a pattern. The built-in pattern +accepts letters (including accented Latin characters), spaces, and +`, . ' -`, and always allows `[bot]` accounts. + +**Why is this bad?** + +When `user.name` is unset, Git falls back to the machine's account name. +Histories built in CI containers and on fresh VMs fill up with commits by +machine accounts — authorship that cannot be traced back to a person, which +matters for both code archaeology and compliance. + +**Example** + +```bash +git config user.name ec2-user +``` + +Use instead: + +```bash +git config --global user.name "Your Name" +``` + +**Options** + +* `commit.author_name_pattern` — a custom regex replacing the built-in + pattern. For example, `"^.+ .+$"` to require a full name. + +!!! note + + The built-in pattern accepts any name made of letters, spaces, and + `, . ' -`, so plain account names such as `root` or `ubuntu` still + pass it — only names containing digits or other symbols are rejected. Set + `author_name_pattern` if you need something stricter. + +### author-email (CC102) { #cc102 } + +**What it does** + +Checks the committer's configured email against a pattern. The built-in pattern +is `^.+@.+$`, which only requires an `@` with something on either side. + +**Why is this bad?** + +An address with no `@` is not routable, so it breaks the link between a commit +and its author: forges cannot attribute the commit to an account, and +mailmap-based tooling cannot merge identities. + +The built-in pattern is deliberately permissive — it is a sanity check, not a +policy. Its real value comes from replacing it, which is how organisations +require contributions to come from a corporate address. + +**Example** + +With `author_email_pattern = "^.+@example\\.com$"` configured: + +```bash +git config user.email you@gmail.com +``` + +Use instead: + +```bash +git config --global user.email you@example.com +``` + +**Options** + +* `commit.author_email_pattern` — the regex to match against. Defaults to + `^.+@.+$`; set something like `"^.+@example\\.com$"` to require a company + domain. + +!!! note + + Because the built-in pattern only looks for an `@`, local and placeholder + addresses such as `root@localhost` pass it. Set `author_email_pattern` + if you need to reject those. + +## Branch rules + +### branch (CC201) { #cc201 } + +**What it does** + +Checks that the current branch name follows the +[Conventional Branch](https://conventionalbranch.org/) specification: +`/`. + +`master`, `main`, `HEAD`, and `PR-*` are always accepted. + +**Why is this bad?** + +A predictable prefix is something automation can act on: CI can skip expensive +jobs for `docs/` branches, deployment workflows can key off `release/`, and +branch protection rules can be written per type. It also makes a list of a +hundred open branches scannable, which an unstructured list never is. + +**Example** + +```text +my-fix +johns-branch-2 +``` + +Use instead: + +```text +fix/empty-config-crash +feature/role-caching +``` + +**Options** + +* `branch.conventional_branch` — set to `false` to disable this rule. +* `branch.allow_branch_types` — the accepted `` values. The default is + a superset of the specification: the spec types plus the Conventional Commit + types, AI agent prefixes (`ai`, `claude`, `codex`, `copilot`, + `cursor`), and bot prefixes (`dependabot`, `renovate`). Set it + explicitly for strict spec-only validation. +* `branch.allow_branch_names` — additional standalone names to accept, such as + `["develop", "staging"]`. +* `branch.ignore_authors` — bypass the check for specific authors. + +### merge-base (CC202) { #cc202 } + +**What it does** + +Checks that the current branch is rebased onto a target branch. + +**Why is this bad?** + +A branch that has fallen behind is tested against code that no longer exists on +the target. CI passing on it says little about whether it will pass after +merging, and the failures it hides — a renamed function, a changed migration — +surface on the target branch instead of the pull request. + +**Example** + +```bash +# branch was cut from main three weeks ago +git push +``` + +Use instead: + +```bash +git fetch origin +git rebase origin/main +git push --force-with-lease +``` + +**Options** + +* `branch.require_rebase_target` — the target branch, for example `"main"`. + Unset by default, meaning no rebase requirement. + +## Push rules + +### no-force-push (CC301) { #cc301 } + +**What it does** + +Blocks force pushes. Run it as a `pre-push` hook, where it reads the push +details from stdin, or with `--no-force-push`, where it compares the current +branch against its upstream. + +**Why is this bad?** + +A force push to a shared branch rewrites history that other people have already +based work on. Their next pull produces conflicts against commits that no longer +exist, and any commit pushed between their fetch and the force push is silently +dropped. On a personal branch this is a routine part of rebasing; on a shared +one it is a data-loss event. + +**Example** + +```bash +git push --force +``` + +Use instead: + +```bash +# on a shared branch, add a commit rather than rewriting +git revert + +# on your own branch, at least refuse to clobber someone else's work +git push --force-with-lease +``` + +**Options** + +* `push.allow_force_push` — set to `false` to enable this rule. diff --git a/docs/rules.rst b/docs/rules.rst deleted file mode 100644 index a850316f..00000000 --- a/docs/rules.rst +++ /dev/null @@ -1,316 +0,0 @@ -Rules Reference -=============== - -Every check that can report a failure has a **stable rule ID**. Rule IDs never -change once released, so they are safe to reference in documentation, code -review comments, and tooling. - -Rule IDs appear in commit-check output and in ``--format json`` results. - -Default output leads with the rule ID and ends with a link to that rule's -section on this page: - -.. code-block:: text - - CC003 subject_imperative check failed ==> docs: revamped the profile - Commit message should use imperative mood (e.g., 'fix bug' not 'fixed bug') - Suggest: Change the first verb to imperative form, e.g., 'fix' instead of 'fixed' - Docs: https://docs.commit-check.com/rules.html#cc003 - -``--compact`` prints one line per failure, keeping the rule ID and omitting the -explanation, suggestion, and documentation link: - -.. code-block:: text - - [FAIL] CC003 subject_imperative: docs: revamped the profile - -ID ranges ---------- - -.. list-table:: - :header-rows: 1 - - * - Range - - Category - * - ``CC0xx`` - - Commit message - * - ``CC1xx`` - - Author - * - ``CC2xx`` - - Branch - * - ``CC3xx`` - - Push - -All rules ---------- - -.. list-table:: - :header-rows: 1 - - * - ID - - Name - - Description - * - :ref:`CC001 ` - - ``message`` - - The commit message should follow Conventional Commits - * - :ref:`CC002 ` - - ``subject-capitalized`` - - Subject must start with a capital letter - * - :ref:`CC003 ` - - ``subject-imperative`` - - Commit message should use imperative mood (e.g., 'fix bug' not 'fixed bug', 'add feature' not 'adding feature') - * - :ref:`CC004 ` - - ``subject-max-length`` - - Subject must be at most {max_len} characters - * - :ref:`CC005 ` - - ``subject-min-length`` - - Subject must be at least {min_len} characters - * - :ref:`CC006 ` - - ``allow-merge-commits`` - - Merge commits are not allowed - * - :ref:`CC007 ` - - ``allow-revert-commits`` - - Revert commits are not allowed - * - :ref:`CC008 ` - - ``allow-empty-commits`` - - Empty commit messages are not allowed - * - :ref:`CC009 ` - - ``allow-fixup-commits`` - - Fixup commits are not allowed - * - :ref:`CC010 ` - - ``allow-wip-commits`` - - WIP commits are not allowed - * - :ref:`CC011 ` - - ``require-body`` - - Commit body is required - * - :ref:`CC012 ` - - ``require-signed-off-by`` - - Signed-off-by not found in latest commit - * - :ref:`CC013 ` - - ``ai-attribution`` - - AI attribution policy violation - * - :ref:`CC101 ` - - ``author-name`` - - The committer name seems invalid - * - :ref:`CC102 ` - - ``author-email`` - - The committer's email seems invalid - * - :ref:`CC201 ` - - ``branch`` - - The branch should follow Conventional Branch - * - :ref:`CC202 ` - - ``merge-base`` - - Current branch is not rebased onto target branch - * - :ref:`CC301 ` - - ``no-force-push`` - - Force push is not allowed - -Commit message rules --------------------- - -.. _cc001: - -CC001 — message -~~~~~~~~~~~~~~~ - -**Config key:** ``message`` - -**Message:** The commit message should follow Conventional Commits. See https://www.conventionalcommits.org - -**How to fix:** Use (): with allowed types - -.. _cc002: - -CC002 — subject-capitalized -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -**Config key:** ``subject_capitalized`` - -**Message:** Subject must start with a capital letter - -**How to fix:** Capitalize the first word of the subject - -.. _cc003: - -CC003 — subject-imperative -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -**Config key:** ``subject_imperative`` - -**Message:** Commit message should use imperative mood (e.g., 'fix bug' not 'fixed bug', 'add feature' not 'adding feature') - -**How to fix:** Change the first verb to imperative form, e.g., 'fix' instead of 'fixed'/'fixes'/'fixing' - -.. _cc004: - -CC004 — subject-max-length -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -**Config key:** ``subject_max_length`` - -**Message:** Subject must be at most {max_len} characters - -**How to fix:** Keep the subject concise (<= configured max) - -.. _cc005: - -CC005 — subject-min-length -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -**Config key:** ``subject_min_length`` - -**Message:** Subject must be at least {min_len} characters - -**How to fix:** Provide a meaningful subject (>= configured min) - -.. _cc006: - -CC006 — allow-merge-commits -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -**Config key:** ``allow_merge_commits`` - -**Message:** Merge commits are not allowed - -**How to fix:** Rebase or squash your changes instead of merging - -.. _cc007: - -CC007 — allow-revert-commits -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -**Config key:** ``allow_revert_commits`` - -**Message:** Revert commits are not allowed - -**How to fix:** Avoid using 'revert' commits; rewrite history if necessary - -.. _cc008: - -CC008 — allow-empty-commits -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -**Config key:** ``allow_empty_commits`` - -**Message:** Empty commit messages are not allowed - -**How to fix:** Provide a non-empty subject - -.. _cc009: - -CC009 — allow-fixup-commits -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -**Config key:** ``allow_fixup_commits`` - -**Message:** Fixup commits are not allowed - -**How to fix:** Use interactive rebase to clean up fixup commits - -.. _cc010: - -CC010 — allow-wip-commits -~~~~~~~~~~~~~~~~~~~~~~~~~ - -**Config key:** ``allow_wip_commits`` - -**Message:** WIP commits are not allowed - -**How to fix:** Complete the work before committing or remove 'WIP' - -.. _cc011: - -CC011 — require-body -~~~~~~~~~~~~~~~~~~~~ - -**Config key:** ``require_body`` - -**Message:** Commit body is required - -**How to fix:** Add a body explaining the change - -.. _cc012: - -CC012 — require-signed-off-by -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -**Config key:** ``require_signed_off_by`` - -**Message:** Signed-off-by not found in latest commit - -**How to fix:** git commit --amend --signoff or use --signoff on commit - -.. _cc013: - -CC013 — ai-attribution -~~~~~~~~~~~~~~~~~~~~~~ - -**Config key:** ``ai_attribution`` - -**Message:** AI attribution policy violation - -**How to fix:** This project forbids AI-assisted commits. Remove AI trailers and re-commit. - -Author rules ------------- - -.. _cc101: - -CC101 — author-name -~~~~~~~~~~~~~~~~~~~ - -**Config key:** ``author_name`` - -**Message:** The committer name seems invalid - -**How to fix:** git config user.name 'Your Name' - -.. _cc102: - -CC102 — author-email -~~~~~~~~~~~~~~~~~~~~ - -**Config key:** ``author_email`` - -**Message:** The committer's email seems invalid - -**How to fix:** git config user.email yourname@example.com - -Branch rules ------------- - -.. _cc201: - -CC201 — branch -~~~~~~~~~~~~~~ - -**Config key:** ``branch`` - -**Message:** The branch should follow Conventional Branch. See https://conventionalbranch.org - -**How to fix:** Use / with allowed types or add branch name to allow_branch_names in config, or use ignore_authors in config branch section to bypass - -.. _cc202: - -CC202 — merge-base -~~~~~~~~~~~~~~~~~~ - -**Config key:** ``merge_base`` - -**Message:** Current branch is not rebased onto target branch - -**How to fix:** Rebase or merge with the target branch - -Push rules ----------- - -.. _cc301: - -CC301 — no-force-push -~~~~~~~~~~~~~~~~~~~~~ - -**Config key:** ``no_force_push`` - -**Message:** Force push is not allowed - -**How to fix:** Use a normal push instead of --force or --force-with-lease diff --git a/docs/troubleshoot.md b/docs/troubleshoot.md new file mode 100644 index 00000000..a84ca418 --- /dev/null +++ b/docs/troubleshoot.md @@ -0,0 +1,41 @@ +# Troubleshooting + +## How to Skip Author Name Check + +In some cases, Commit Check may fail due to an invalid `author_name`, as shown below: + +```shell +check committer name.....................................................Failed +- hook id: check-author-name +- exit code: 1 + +Commit rejected by Commit-Check. + +Type author_name check failed => 12 +It doesn't match regex: ^[A-Za-zÀ-ÖØ-öø-ÿ\u0100-\u017F\u0180-\u024F ,.\'-]+$|.*(\[bot]) +The committer name seems invalid +Suggest: run command `git config user.name "Your Name"` +``` + +To fix it, you can either update your Git config or temporarily skip the check using one of the following methods. + +### Bypass All Hooks + +Use the `--no-verify` flag to skip the pre-commit hook: + +```shell +# Amend the commit without running hooks +git commit --amend --author="Xianpeng Shen " --no-edit --no-verify +``` + +### Bypass A Specific Hook + +Alternatively, use the `SKIP=your-hook-name` environment variable, like below: + +```shell +# Set the correct Git author name +git config user.name "Xianpeng Shen" + +# Force amend while skipping the specified hook +SKIP=check-author-name git commit --amend --author="Xianpeng Shen " --no-edit +``` diff --git a/docs/troubleshoot.rst b/docs/troubleshoot.rst deleted file mode 100644 index 66df7c48..00000000 --- a/docs/troubleshoot.rst +++ /dev/null @@ -1,45 +0,0 @@ -Troubleshooting -=============== - -How to Skip Author Name Check ------------------------------ - -In some cases, Commit Check may fail due to an invalid ``author_name``, as shown below: - -.. code-block:: shell - - check committer name.....................................................Failed - - hook id: check-author-name - - exit code: 1 - - Commit rejected by Commit-Check. - - Type author_name check failed => 12 - It doesn't match regex: ^[A-Za-zÀ-ÖØ-öø-ÿ\u0100-\u017F\u0180-\u024F ,.\'-]+$|.*(\[bot]) - The committer name seems invalid - Suggest: run command `git config user.name "Your Name"` - -To fix it, you can either update your Git config or temporarily skip the check using one of the following methods. - -Bypass Specific Hook -~~~~~~~~~~~~~~~~~~~~ - -Use the ``--no-verify`` flag to skip the pre-commit hook: - -.. code-block:: shell - - # Amend the commit without running hooks - git commit --amend --author="Xianpeng Shen " --no-edit --no-verify - -Bypass All Hooks -~~~~~~~~~~~~~~~~ - -Alternatively, use the ``SKIP=your-hook-name`` environment variable, like below: - -.. code-block:: shell - - # Set the correct Git author name - git config user.name "Xianpeng Shen" - - # Force amend while skipping the specified hook - SKIP=check-author-name git commit --amend --author="Xianpeng Shen " --no-edit diff --git a/docs/what-is-new.md b/docs/what-is-new.md new file mode 100644 index 00000000..b45c3a02 --- /dev/null +++ b/docs/what-is-new.md @@ -0,0 +1,365 @@ +# What's New + +This document highlights the major changes and improvements in each version of commit-check. + +## Version 2.11.0 — AI Attribution Governance + +### Enforce Your Project's AI Contribution Policy + +commit-check now supports **AI attribution governance** — a neutral enforcement +layer for the industry-wide discussion on AI disclosure in open source. + +Configured under `[commit]`: + +```toml +[commit] +# "ignore" (default) | "forbid" +ai_attribution = "forbid" +``` + +When set to `"forbid"`, any commit containing known AI tool signatures is +rejected. The built-in signature database detects trailers and markers from: + +* **Claude Code** — `Co-authored-by: Claude`, `Assisted-by: Claude:...`, + `🤖 Generated with Claude`, `Claude-Session:`, `Claude-Workflow:` +* **GitHub Copilot** — `Co-authored-by: Copilot` +* **OpenAI Codex** — `Co-authored-by: Codex` +* **Gemini** — `Co-authored-by: Gemini` +* **Cursor** — `Co-authored-by: Cursor` +* **Devin** — `Co-authored-by: Devin` +* **Aider** — `Co-authored-by: Aider`, `Co-authored-by: ... (aider)` +* **Windsurf** — `Co-authored-by: Windsurf` +* **Tabby** — `Co-authored-by: Tabby` +* **Generic AI** — `Assisted-by:` (Linux kernel style, with tool list), + model names like `claude-sonnet-4`, `gpt-4-turbo` + +The signature database is designed to be extensible — adding a new tool is as +simple as adding a `KnownAiTool` entry with the tool's patterns. + +This feature is motivated by ongoing discussions in the CPython core +development community, the Linux kernel's `Assisted-by:` trailer standard, +VS Code, Apache, Fedora, and other foundations. + +See [Configuration Documentation](configuration.md) for details. + +## Version 2.10.0 — Bot Branch Types as Default + +### `dependabot/` and `renovate/` branches now pass by default + +`dependabot` and `renovate` are now included in `DEFAULT_BRANCH_TYPES`, +so branches like `dependabot/go_modules/go-deps-c57c3fe1e0` and +`renovate/lodash-5.x` are automatically accepted without manual +`allow_branch_types` configuration. + +## Version 2.9.0 — AI Agent Branch Prefixes + +### Conventional Branch v1.1.0 AI agent prefixes supported by default + +`ai/`, `claude/`, `codex/`, `copilot/`, and `cursor/` have been +added to `DEFAULT_BRANCH_TYPES` as defined in [Conventional Branch v1.1.0](https://conventional-branch.github.io/). Branches created by AI +coding agents are now valid out of the box without extra configuration. + +## Version 2.7.0 — Force Push Blocking + +### Force Push Detection and Prevention + +commit-check now includes a **force push detection** feature that blocks +accidental `git push --force` / `git push -f` by inspecting pushed ref +ancestry via `git merge-base --is-ancestor`. + +**How it works:** + +* Runs inside a Git `pre-push` hook — receives pushed ref metadata on stdin + and inspects the ancestry relationship. +* New branch pushes (remote SHA is all zeros) always pass. +* Fast-forward pushes (remote is ancestor of local) pass. +* When the remote commit is **not** an ancestor of the local commit, a force + push is detected and **blocked**. +* Git errors (e.g., unknown SHA) result in a safe pass. + +**Usage:** + +```bash +# Standalone: check whether pushing HEAD to its upstream requires force +commit-check --no-force-push +``` + +```yaml +# As a pre-commit pre-push hook +repos: + - repo: https://github.com/commit-check/commit-check + rev: v2.7.0 + hooks: + - id: check-no-force-push + stages: [pre-push] +``` + +```toml +# Configurable in cchk.toml +[push] +allow_force_push = false # default: true (force pushes allowed) +``` + +**New Python API:** + +```python +from commit_check.api import validate_push + +zero = "0000000000000000000000000000000000000000" +result = validate_push(f"refs/heads/main abc123 refs/heads/main {zero}") +print(result["status"]) # "pass" +``` + +See the [Push Safety section in README](https://github.com/commit-check/commit-check#check-push-safety) +and [Push Validation Examples](https://docs.commit-check.com/example.html#push-validation-examples) +for more details. + +## Version 2.6.0 — Output Controls for CLI Workflows + +### Quieter Human-Readable Failure Output + +commit-check now includes two CLI flags for workflows that want less verbose +terminal output without switching to JSON mode: + +* `--no-banner` suppresses the ASCII art failure banner while keeping the + detailed error message and suggestion output. +* `--compact` prints a single `[FAIL]` line per failing check and implies + `--no-banner`. + +These flags are useful in CI logs, pre-commit output, and agent-driven terminal +sessions where the full banner is noisy but plain-text diagnostics are still +helpful. + +## Version 2.5.0 — New Features + +### Co-author Bypass in `ignore_authors` + +commit-check can now skip validation when a **co-author** of the commit matches an entry in `ignore_authors`, not just the primary commit author. + +This is especially useful for AI-assisted workflows where a bot (e.g., `coderabbitai[bot]`, `copilot[bot]`) co-authors a commit that does not follow Conventional Commits format: + +```toml +[commit] +ignore_authors = ["dependabot[bot]", "renovate[bot]", "coderabbitai[bot]", "copilot[bot]"] +``` + +When a `Co-authored-by:` trailer in the commit message body matches any entry in the list, all commit checks are skipped for that commit. + +### Organization-Level Config Inheritance (`inherit_from`) + +Teams can now share a **centralized base configuration** across all repositories in an organization using the new `inherit_from` top-level key. + +```toml +# .github/cchk.toml — in every repo +inherit_from = "github:my-org/.github:cchk.toml" + +[commit] +subject_max_length = 72 # Local override +``` + +**Supported source formats:** + +* `github:owner/repo:path/to/cchk.toml` — fetches from the default branch via `raw.githubusercontent.com` +* `github:owner/repo@main:path/to/cchk.toml` — pins to a specific branch, tag, or SHA +* A local file path (relative or absolute) +* An HTTPS URL + +Local settings always **override** the inherited configuration. HTTP (non-TLS) URLs are rejected for security. If the source is unreachable, the local config is used as-is. + +### Git Config Author Validation + +Author name and email validation now checks **`git config user.name` / `user.email`** first — the identity that will be used for the *next* commit — and falls back to the last commit's author only if git config is unset. + +Previously, a developer with a misconfigured `user.name` (e.g., starting with a digit) would pass validation as long as their most recent commit had a valid author name. This fix closes that gap. + +## Version 2.0.0 - Major Release + +Version 2.0.0 represents a complete architectural overhaul of commit-check, introducing significant improvements in configuration, usability, and maintainability. + +### **Overview** + +The most significant change in v2.0.0 is the transition from YAML to TOML configuration format, along with a complete redesign of the validation engine using SOLID principles. + +**Key Benefits:** + +* **Simplified Configuration**: More intuitive TOML syntax +* **Better Defaults**: Sensible out-of-the-box behavior +* **Enhanced Validation**: Built-in support for Conventional Commits and Conventional Branches +* **Improved Architecture**: Modular, maintainable codebase +* **Better Documentation**: Comprehensive guides and examples + +### **Documentation & Migration** + +* **Configuration Guide**: Updated [Configuration Documentation](configuration.md) with comprehensive examples +* **Migration Support**: Complete [Migration Guide](migration.md) for upgrading from v1.x to v2.0+ + +### **Configuration Format Migration** + +The configuration format has changed from YAML to TOML, providing better readability and easier maintenance. + +**Format Comparison:** + +| Feature | YAML (v1.x) | TOML (v2.0+) | +|---|---|---| +| **Syntax** | Complex nested structure | Simple key-value pairs | +| **Validation** | Custom regex patterns | Built-in conventional standards | +| **Configuration** | `.commit-check.yml` | `cchk.toml` or `commit-check.toml` | +| **Maintainability** | Manual regex maintenance | Standardized patterns | + +### **Configuration Examples** + +Below are side-by-side comparisons showing how common configurations translate from v1.x to v2.0+. + +#### Commit Message Validation + +Transform complex regex patterns into simple, standardized configuration. + +**Before (YAML v1.x):** + +```yaml +checks: + - check: message + regex: '^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test){1}(\([\w\-\.]+\))?(!)?: ([\w ])+([\s\S]*)|(Merge).*|(fixup!.*)' + error: "The commit message should be structured as follows:\n\n + [optional scope]: \n + [optional body]\n + [optional footer(s)]\n\n + More details please refer to https://www.conventionalcommits.org" + suggest: please check your commit message whether matches above regex +``` + +**After (TOML v2.0+):** + +```toml +[commit] +conventional_commits = true +allow_commit_types = ["build", "chore", "ci", "docs", "feat", "fix", "perf", "refactor", "style", "test"] +``` + +**Benefits**: No more complex regex patterns, built-in [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) support, clearer configuration. + +#### Branch Naming Validation + +Standardize branch naming with conventional patterns. + +**Before (YAML v1.x):** + +```yaml +checks: + - check: branch + regex: ^(bugfix|feature|release|hotfix|task|chore)\/.+|(master)|(main)|(HEAD)|(PR-.+) + error: "Branches must begin with these types: bugfix/ feature/ release/ hotfix/ task/ chore/" + suggest: run command `git checkout -b type/branch_name` +``` + +**After (TOML v2.0+):** + +```toml +[branch] +conventional_branch = true +allow_branch_types = ["bugfix", "feature", "release", "hotfix", "task", "chore"] +``` + +**Benefits**: Built-in [Conventional Branch](https://conventionalbranch.org) support, automatic handling of special branches (main, master, HEAD, PR-\*). + +#### Author Validation + +Flexible author validation with allow/ignore lists. + +**Before (YAML v1.x):** + +```yaml +checks: + - check: author_name + regex: ^[A-Za-zÀ-ÖØ-öø-ÿ\u0100-\u017F\u0180-\u024F ,.\'-]+$|.*(\[bot]) + error: The committer name seems invalid + suggest: run command `git config user.name "Your Name"` +``` + +**After (TOML v2.0+):** + +```toml +[commit] +# Built-in validation with sensible defaults for author name/email +# Optional: ignore specific authors (e.g., bots) +ignore_authors = ["dependabot[bot]", "renovate[bot]"] +``` + +**Benefits**: Built-in validation patterns, flexible ignore lists, automatic bot detection. + +#### Signed-off-by Requirements + +Simple boolean flag for DCO compliance. + +**Before (YAML v1.x):** + +```yaml +checks: + - check: commit_signoff + regex: Signed-off-by:.*[A-Za-z0-9]\s+<.+@.+> + error: Signed-off-by not found in latest commit + suggest: run command `git commit -m "conventional commit message" --signoff` +``` + +**After (TOML v2.0+):** + +```toml +[commit] +require_signed_off_by = true +``` + +**Benefits**: Simple boolean configuration, built-in DCO validation, clear error messages. + +### **Architecture Improvements** + +#### **New Validation Engine** + +* **SOLID Principles**: Maintainable, extensible design +* **Specialized Validators**: Dedicated classes for each validation type +* **Centralized Rules**: Rule catalog with consistent error messages +* **Flexible Configuration**: Dynamic rule building from configuration + +#### **Module Organization** + +| Module | Purpose | +|---|---| +| `config.py` | TOML configuration loading and validation | +| `engine.py` | Core validation engine and specialized validators | +| `rule_builder.py` | Builds validation rules from configuration | +| `rules_catalog.py` | Centralized catalog of validation rules and messages | +| `main.py` | CLI interface and orchestration | + +### **Getting Started with v2.0** + +#### **For New Users:** + +1. **Install commit-check v2.0+**: + +```bash +pip install commit-check>=2.0.0 +``` + +2. **Start with defaults** (no configuration needed): + +```bash +commit-check --message --branch +``` + +3. **Customize as needed** with `cchk.toml`: + +```toml +[commit] +conventional_commits = true +subject_max_length = 72 +``` + +#### For Existing Users + +1. **Follow the Migration Guide**: See [Migration Guide](migration.md) +2. **Test thoroughly**: Validate your new configuration before deploying + +### **Additional Resources** + +* [Configuration Reference](configuration.md) - Complete configuration options +* [Migration Guide](migration.md) - Step-by-step upgrade instructions +* [CLI Reference](cli.md) - Command-line interface documentation diff --git a/docs/what-is-new.rst b/docs/what-is-new.rst deleted file mode 100644 index 81d5ebab..00000000 --- a/docs/what-is-new.rst +++ /dev/null @@ -1,420 +0,0 @@ -What's New -========== - -This document highlights the major changes and improvements in each version of commit-check. - -Version 2.11.0 — AI Attribution Governance --------------------------------------------- - -Enforce Your Project's AI Contribution Policy -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -commit-check now supports **AI attribution governance** — a neutral enforcement -layer for the industry-wide discussion on AI disclosure in open source. - -Configured under ``[commit]``: - -.. code-block:: toml - - [commit] - # "ignore" (default) | "forbid" - ai_attribution = "forbid" - -When set to ``"forbid"``, any commit containing known AI tool signatures is -rejected. The built-in signature database detects trailers and markers from: - -* **Claude Code** — ``Co-authored-by: Claude``, ``Assisted-by: Claude:...``, - ``🤖 Generated with Claude``, ``Claude-Session:``, ``Claude-Workflow:`` -* **GitHub Copilot** — ``Co-authored-by: Copilot`` -* **OpenAI Codex** — ``Co-authored-by: Codex`` -* **Gemini** — ``Co-authored-by: Gemini`` -* **Cursor** — ``Co-authored-by: Cursor`` -* **Devin** — ``Co-authored-by: Devin`` -* **Aider** — ``Co-authored-by: Aider``, ``Co-authored-by: ... (aider)`` -* **Windsurf** — ``Co-authored-by: Windsurf`` -* **Tabby** — ``Co-authored-by: Tabby`` -* **Generic AI** — ``Assisted-by:`` (Linux kernel style, with tool list), - model names like ``claude-sonnet-4``, ``gpt-4-turbo`` - -The signature database is designed to be extensible — adding a new tool is as -simple as adding a ``KnownAiTool`` entry with the tool's patterns. - -This feature is motivated by ongoing discussions in the CPython core -development community, the Linux kernel's ``Assisted-by:`` trailer standard, -VS Code, Apache, Fedora, and other foundations. - -See `Configuration Documentation `_ for details. - -Version 2.10.0 — Bot Branch Types as Default ---------------------------------------------- - -``dependabot/`` and ``renovate/`` branches now pass by default -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -``dependabot`` and ``renovate`` are now included in ``DEFAULT_BRANCH_TYPES``, -so branches like ``dependabot/go_modules/go-deps-c57c3fe1e0`` and -``renovate/lodash-5.x`` are automatically accepted without manual -``allow_branch_types`` configuration. - -Version 2.9.0 — AI Agent Branch Prefixes ----------------------------------------- - -Conventional Branch v1.1.0 AI agent prefixes supported by default -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -``ai/``, ``claude/``, ``codex/``, ``copilot/``, and ``cursor/`` have been -added to ``DEFAULT_BRANCH_TYPES`` as defined in `Conventional Branch -v1.1.0 `_. Branches created by AI -coding agents are now valid out of the box without extra configuration. - -Version 2.7.0 — Force Push Blocking ------------------------------------ - -Force Push Detection and Prevention -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -commit-check now includes a **force push detection** feature that blocks -accidental ``git push --force`` / ``git push -f`` by inspecting pushed ref -ancestry via ``git merge-base --is-ancestor``. - -**How it works:** - -* Runs inside a Git ``pre-push`` hook — receives pushed ref metadata on stdin - and inspects the ancestry relationship. -* New branch pushes (remote SHA is all zeros) always pass. -* Fast-forward pushes (remote is ancestor of local) pass. -* When the remote commit is **not** an ancestor of the local commit, a force - push is detected and **blocked**. -* Git errors (e.g., unknown SHA) result in a safe pass. - -**Usage:** - -.. code-block:: bash - - # Standalone: check whether pushing HEAD to its upstream requires force - commit-check --no-force-push - -.. code-block:: yaml - - # As a pre-commit pre-push hook - repos: - - repo: https://github.com/commit-check/commit-check - rev: v2.7.0 - hooks: - - id: check-no-force-push - stages: [pre-push] - -.. code-block:: toml - - # Configurable in cchk.toml - [push] - allow_force_push = false # default: true (force pushes allowed) - -**New Python API:** - -.. code-block:: python - - from commit_check.api import validate_push - - zero = "0000000000000000000000000000000000000000" - result = validate_push(f"refs/heads/main abc123 refs/heads/main {zero}") - print(result["status"]) # "pass" - -See the `Push Safety section in README `_ -and `Push Validation Examples `_ -for more details. - - -Version 2.6.0 — Output Controls for CLI Workflows -------------------------------------------------- - -Quieter Human-Readable Failure Output -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -commit-check now includes two CLI flags for workflows that want less verbose -terminal output without switching to JSON mode: - -* ``--no-banner`` suppresses the ASCII art failure banner while keeping the - detailed error message and suggestion output. -* ``--compact`` prints a single ``[FAIL]`` line per failing check and implies - ``--no-banner``. - -These flags are useful in CI logs, pre-commit output, and agent-driven terminal -sessions where the full banner is noisy but plain-text diagnostics are still -helpful. - -Version 2.5.0 — New Features ------------------------------- - -Co-author Bypass in ``ignore_authors`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -commit-check can now skip validation when a **co-author** of the commit matches an entry in ``ignore_authors``, not just the primary commit author. - -This is especially useful for AI-assisted workflows where a bot (e.g., ``coderabbitai[bot]``, ``copilot[bot]``) co-authors a commit that does not follow Conventional Commits format: - -.. code-block:: toml - - [commit] - ignore_authors = ["dependabot[bot]", "renovate[bot]", "coderabbitai[bot]", "copilot[bot]"] - -When a ``Co-authored-by:`` trailer in the commit message body matches any entry in the list, all commit checks are skipped for that commit. - -Organization-Level Config Inheritance (``inherit_from``) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Teams can now share a **centralized base configuration** across all repositories in an organization using the new ``inherit_from`` top-level key. - -.. code-block:: toml - - # .github/cchk.toml — in every repo - inherit_from = "github:my-org/.github:cchk.toml" - - [commit] - subject_max_length = 72 # Local override - -**Supported source formats:** - -* ``github:owner/repo:path/to/cchk.toml`` — fetches from the default branch via ``raw.githubusercontent.com`` -* ``github:owner/repo@main:path/to/cchk.toml`` — pins to a specific branch, tag, or SHA -* A local file path (relative or absolute) -* An HTTPS URL - -Local settings always **override** the inherited configuration. HTTP (non-TLS) URLs are rejected for security. If the source is unreachable, the local config is used as-is. - -Git Config Author Validation -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Author name and email validation now checks **``git config user.name`` / ``user.email``** first — the identity that will be used for the *next* commit — and falls back to the last commit's author only if git config is unset. - -Previously, a developer with a misconfigured ``user.name`` (e.g., starting with a digit) would pass validation as long as their most recent commit had a valid author name. This fix closes that gap. - -Version 2.0.0 - Major Release ------------------------------ - -Version 2.0.0 represents a complete architectural overhaul of commit-check, introducing significant improvements in configuration, usability, and maintainability. - -**Overview** -~~~~~~~~~~~~~~~ - -The most significant change in v2.0.0 is the transition from YAML to TOML configuration format, along with a complete redesign of the validation engine using SOLID principles. - -**Key Benefits:** - -* **Simplified Configuration**: More intuitive TOML syntax -* **Better Defaults**: Sensible out-of-the-box behavior -* **Enhanced Validation**: Built-in support for Conventional Commits and Conventional Branches -* **Improved Architecture**: Modular, maintainable codebase -* **Better Documentation**: Comprehensive guides and examples - -**Documentation & Migration** -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -* **Configuration Guide**: Updated `Configuration Documentation `_ with comprehensive examples -* **Migration Support**: Complete `Migration Guide `_ for upgrading from v1.x to v2.0+ - -**Configuration Format Migration** -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The configuration format has changed from YAML to TOML, providing better readability and easier maintenance. - -**Format Comparison:** - -.. list-table:: - :header-rows: 1 - :widths: 20 40 40 - - * - Feature - - YAML (v1.x) - - TOML (v2.0+) - * - **Syntax** - - Complex nested structure - - Simple key-value pairs - * - **Validation** - - Custom regex patterns - - Built-in conventional standards - * - **Configuration** - - ``.commit-check.yml`` - - ``cchk.toml`` or ``commit-check.toml`` - * - **Maintainability** - - Manual regex maintenance - - Standardized patterns - - -**Configuration Examples** -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Below are side-by-side comparisons showing how common configurations translate from v1.x to v2.0+. - -Commit Message Validation -^^^^^^^^^^^^^^^^^^^^^^^^^ - -Transform complex regex patterns into simple, standardized configuration. - -**Before (YAML v1.x):** - -.. code-block:: yaml - - checks: - - check: message - regex: '^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test){1}(\([\w\-\.]+\))?(!)?: ([\w ])+([\s\S]*)|(Merge).*|(fixup!.*)' - error: "The commit message should be structured as follows:\n\n - [optional scope]: \n - [optional body]\n - [optional footer(s)]\n\n - More details please refer to https://www.conventionalcommits.org" - suggest: please check your commit message whether matches above regex - -**After (TOML v2.0+):** - -.. code-block:: toml - - [commit] - conventional_commits = true - allow_commit_types = ["build", "chore", "ci", "docs", "feat", "fix", "perf", "refactor", "style", "test"] - -**Benefits**: No more complex regex patterns, built-in `Conventional Commits `_ support, clearer configuration. - -Branch Naming Validation -^^^^^^^^^^^^^^^^^^^^^^^^ - -Standardize branch naming with conventional patterns. - -**Before (YAML v1.x):** - -.. code-block:: yaml - - checks: - - check: branch - regex: ^(bugfix|feature|release|hotfix|task|chore)\/.+|(master)|(main)|(HEAD)|(PR-.+) - error: "Branches must begin with these types: bugfix/ feature/ release/ hotfix/ task/ chore/" - suggest: run command `git checkout -b type/branch_name` - -**After (TOML v2.0+):** - -.. code-block:: toml - - [branch] - conventional_branch = true - allow_branch_types = ["bugfix", "feature", "release", "hotfix", "task", "chore"] - -**Benefits**: Built-in `Conventional Branch `_ support, automatic handling of special branches (main, master, HEAD, PR-\*). - -Author Validation -^^^^^^^^^^^^^^^^^ - -Flexible author validation with allow/ignore lists. - -**Before (YAML v1.x):** - -.. code-block:: yaml - - checks: - - check: author_name - regex: ^[A-Za-zÀ-ÖØ-öø-ÿ\u0100-\u017F\u0180-\u024F ,.\'-]+$|.*(\[bot]) - error: The committer name seems invalid - suggest: run command `git config user.name "Your Name"` - -**After (TOML v2.0+):** - -.. code-block:: toml - - [commit] - # Built-in validation with sensible defaults for author name/email - # Optional: ignore specific authors (e.g., bots) - ignore_authors = ["dependabot[bot]", "renovate[bot]"] - -**Benefits**: Built-in validation patterns, flexible ignore lists, automatic bot detection. - -Signed-off-by Requirements -^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Simple boolean flag for DCO compliance. - -**Before (YAML v1.x):** - -.. code-block:: yaml - - checks: - - check: commit_signoff - regex: Signed-off-by:.*[A-Za-z0-9]\s+<.+@.+> - error: Signed-off-by not found in latest commit - suggest: run command `git commit -m "conventional commit message" --signoff` - -**After (TOML v2.0+):** - -.. code-block:: toml - - [commit] - require_signed_off_by = true - -**Benefits**: Simple boolean configuration, built-in DCO validation, clear error messages. - -**Architecture Improvements** -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -**New Validation Engine** -^^^^^^^^^^^^^^^^^^^^^^^^^ - -* **SOLID Principles**: Maintainable, extensible design -* **Specialized Validators**: Dedicated classes for each validation type -* **Centralized Rules**: Rule catalog with consistent error messages -* **Flexible Configuration**: Dynamic rule building from configuration - -**Module Organization** -^^^^^^^^^^^^^^^^^^^^^^^ - -.. list-table:: - :header-rows: 1 - :widths: 30 70 - - * - Module - - Purpose - * - ``config.py`` - - TOML configuration loading and validation - * - ``engine.py`` - - Core validation engine and specialized validators - * - ``rule_builder.py`` - - Builds validation rules from configuration - * - ``rules_catalog.py`` - - Centralized catalog of validation rules and messages - * - ``main.py`` - - CLI interface and orchestration - -**Getting Started with v2.0** -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -**For New Users:** -^^^^^^^^^^^^^^^^^^ - -1. **Install commit-check v2.0+**: - - .. code-block:: bash - - pip install commit-check>=2.0.0 - -2. **Start with defaults** (no configuration needed): - - .. code-block:: bash - - commit-check --message --branch - -3. **Customize as needed** with ``cchk.toml``: - - .. code-block:: toml - - [commit] - conventional_commits = true - subject_max_length = 72 - -For Existing Users -^^^^^^^^^^^^^^^^^^ -1. **Follow the Migration Guide**: See `Migration Guide `_ -2. **Test thoroughly**: Validate your new configuration before deploying - -**Additional Resources** -~~~~~~~~~~~~~~~~~~~~~~~~ - -* `Configuration Reference `_ - Complete configuration options -* `Migration Guide `_ - Step-by-step upgrade instructions -* `CLI Reference `_ - Command-line interface documentation diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 00000000..04669a01 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,130 @@ +site_name: Commit Check +# Deploy previews override this so the preview is self-consistent; see +# netlify.toml. +site_url: !ENV [SITE_URL, 'https://docs.commit-check.com/'] +site_description: >- + Enforce commit message, branch naming, author and signoff standards — + one policy, across your CLI, pre-commit hooks, CI, and AI agents. +site_author: shenxianpeng +copyright: Copyright © 2022 Commit Check + +repo_url: https://github.com/commit-check/commit-check +repo_name: commit-check/commit-check +edit_uri: edit/main/docs/ + +docs_dir: docs + +theme: + name: material + language: en + logo: assets/logo.svg + favicon: assets/favicon.svg + icon: + repo: fontawesome/brands/github + font: + text: Inter + code: JetBrains Mono + palette: + - media: "(prefers-color-scheme)" + toggle: + icon: material/brightness-auto + name: Follow system theme + - media: "(prefers-color-scheme: light)" + scheme: default + primary: custom + accent: custom + toggle: + icon: material/brightness-7 + name: Switch to dark mode + - media: "(prefers-color-scheme: dark)" + scheme: slate + primary: custom + accent: custom + toggle: + icon: material/brightness-4 + name: Switch to light mode + features: + # Navigation lives in the left sidebar, grouped into sections. + - navigation.sections + - navigation.top + - navigation.tracking + - navigation.instant + - navigation.instant.progress + - navigation.footer + - toc.follow + - search.suggest + - search.highlight + - search.share + - content.code.copy + - content.code.annotate + - content.tabs.link + - content.action.edit + +extra_css: + - assets/extra.css + +extra: + social: + - icon: fontawesome/brands/github + link: https://github.com/commit-check + name: Commit Check on GitHub + - icon: fontawesome/brands/python + link: https://pypi.org/project/commit-check/ + name: commit-check on PyPI + +markdown_extensions: + - abbr + - admonition + - attr_list + - def_list + - footnotes + - md_in_html + - tables + - toc: + permalink: true + permalink_title: Link to this section + - pymdownx.details + - pymdownx.emoji: + emoji_index: !!python/name:material.extensions.emoji.twemoji + emoji_generator: !!python/name:material.extensions.emoji.to_svg + - pymdownx.highlight: + anchor_linenums: true + line_spans: __span + pygments_lang_class: true + - pymdownx.inlinehilite + - pymdownx.keys + - pymdownx.snippets + - pymdownx.superfences + - pymdownx.tabbed: + alternate_style: true + - pymdownx.tasklist: + custom_checkbox: true + +plugins: + - search + +hooks: + - scripts/mkdocs_hooks.py + +nav: + - Home: index.md + - Getting started: + - Installation: getting-started/installation.md + - Quick start: getting-started/quickstart.md + - Why Commit Check: getting-started/why.md + - Guides: + - Pre-commit hook: guides/pre-commit.md + - GitHub Actions: guides/github-actions.md + - Organization-wide policy: guides/organization.md + - Signoff and DCO: guides/signoff.md + - AI attribution policy: guides/ai-attribution.md + - Command-line recipes: example.md + - Reference: + - Rules: rules.md + - Configuration: configuration.md + - CLI: cli.md + - About: + - Migrating from v1: migration.md + - Troubleshooting: troubleshoot.md + - Release highlights: what-is-new.md + - Changelog: changelog.md diff --git a/netlify.toml b/netlify.toml new file mode 100644 index 00000000..b0e18c1e --- /dev/null +++ b/netlify.toml @@ -0,0 +1,100 @@ +# Netlify builds the documentation for deploy previews on pull requests. +# +# This file exists so the build is defined in version control rather than in +# Netlify's web UI: when the docs toolchain changes, the preview build changes +# with it in the same commit, and a mismatch shows up in review instead of +# silently producing an empty preview. + +[build] + command = "pip install '.[docs]' && mkdocs build --strict" + publish = "site" + +[build.environment] + # MkDocs Material needs a modern Python; the package itself supports 3.10+. + PYTHON_VERSION = "3.12" + +# On a deploy preview, point the site at the preview itself. Without this the +# canonical links and the generated redirect stubs would send reviewers from +# the preview back to the production site, which defeats the purpose. +# +# SITE_URL has to be set here rather than in a [context.*.environment] block: +# those values are literals, so "${DEPLOY_PRIME_URL}" would reach MkDocs +# unexpanded and be rejected as a URL without a scheme. The build command runs +# in a shell, where the variable actually expands. +[context.deploy-preview] + command = "pip install '.[docs]' && SITE_URL=\"$DEPLOY_PRIME_URL/\" mkdocs build --strict" + +[context.branch-deploy] + command = "pip install '.[docs]' && SITE_URL=\"$DEPLOY_PRIME_URL/\" mkdocs build --strict" + +# The URLs the Sphinx site served. The build does not emit redirect stubs on +# Netlify (see scripts/mkdocs_hooks.py): a stub at rules.html would be served +# for /rules/ requests too, shadowing the real page and redirecting to itself. +# Declaring the URLs here resolves them before file lookup, so they cannot +# collide with the pages they point at. Browsers carry the fragment of the +# requested URL across the redirect, so /rules.html#cc003 lands on /rules/#cc003. +[[redirects]] + from = "/configuration.html" + to = "/configuration/" + status = 301 + force = true + +[[redirects]] + from = "/rules.html" + to = "/rules/" + status = 301 + force = true + +[[redirects]] + from = "/example.html" + to = "/example/" + status = 301 + force = true + +[[redirects]] + from = "/migration.html" + to = "/migration/" + status = 301 + force = true + +[[redirects]] + from = "/troubleshoot.html" + to = "/troubleshoot/" + status = 301 + force = true + +[[redirects]] + from = "/changelog.html" + to = "/changelog/" + status = 301 + force = true + +[[redirects]] + from = "/what-is-new.html" + to = "/what-is-new/" + status = 301 + force = true + +[[redirects]] + from = "/cli_args.html" + to = "/cli/" + status = 301 + force = true + +[[redirects]] + from = "/README.html" + to = "/getting-started/installation/" + status = 301 + force = true + +[[redirects]] + from = "/index.html" + to = "/" + status = 301 + force = true + +[[redirects]] + from = "/genindex.html" + to = "/" + status = 301 + force = true diff --git a/noxfile.py b/noxfile.py index 44072ced..3ed32045 100644 --- a/noxfile.py +++ b/noxfile.py @@ -53,12 +53,10 @@ def coverage(session): @nox.session() def docs(session): session.install(".[docs]") - session.run("sphinx-build", "-E", "-b", "html", "docs", "_build/html") + session.run("mkdocs", "build", "--strict") @nox.session(name="docs-live") def docs_live(session): session.install(".[docs]") - session.run( - "sphinx-autobuild", "-b", "html", "docs", "_build/html", "--watch", "docs/" - ) + session.run("mkdocs", "serve") diff --git a/pyproject.toml b/pyproject.toml index cc58e8b0..8a0a62eb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,7 +48,7 @@ tracker = "https://github.com/commit-check/commit-check/issues" [project.optional-dependencies] dev = ['nox==2026.7.11'] test = ['coverage', 'pytest', 'pytest-mock', 'pytest-codspeed'] -docs = ['sphinx<9', 'sphinx-immaterial', 'sphinx-autobuild', 'sphinx_issues', 'myst-parser'] +docs = ['mkdocs-material>=9.7'] ci = ['twine==7.0.0'] [tool.setuptools] diff --git a/scripts/mkdocs_hooks.py b/scripts/mkdocs_hooks.py new file mode 100644 index 00000000..d8b439e7 --- /dev/null +++ b/scripts/mkdocs_hooks.py @@ -0,0 +1,117 @@ +"""MkDocs build hooks. + +Two jobs: + +* generate ``docs/cli.md`` from the CLI's own ``--help`` output, so the + documented interface cannot drift from the shipped one; +* emit redirect stubs for the ``.html`` URLs the previous Sphinx site served, + so links already published elsewhere keep working on hosts that have no + other redirect mechanism (GitHub Pages). Netlify skips the stubs: it + declares the same URLs as ``[[redirects]]`` in ``netlify.toml``, which + resolve before file lookup and cannot collide with the pages they target. +""" + +from __future__ import annotations + +import os +import subprocess +from pathlib import Path + +#: Sphinx page name -> path under the MkDocs site. +LEGACY_URLS = { + "configuration": "configuration/", + "rules": "rules/", + "example": "example/", + "migration": "migration/", + "troubleshoot": "troubleshoot/", + "changelog": "changelog/", + "what-is-new": "what-is-new/", + "cli_args": "cli/", + "README": "getting-started/installation/", + "genindex": "", +} + +# Redirect stubs for the URLs the Sphinx site served. +# The script carries the fragment across, because the links most worth keeping +# alive are the per-rule ones (``rules.html#cc003``) and a plain redirect drops +# the ``#cc003``. It also refuses to redirect a page to itself: a host that +# normalises ``/rules`` and ``/rules/`` to the same resource would otherwise +# serve this stub in place of the real page and loop forever. +REDIRECT = """ + + + +Redirecting… + + + + +Redirecting to {url}… + +""" + +PAGE = """# CLI reference + +Generated from `commit-check --help`. The CLI is also available as `cchk`. + +```console +$ commit-check --help +``` + +```text +{help} +``` + +## See also + +- [Configuration](configuration.md) — every option, with the environment + variable and TOML key that set it. +- [Rules](rules.md) — which flag activates which rule. +""" + + +def on_pre_build(config, **kwargs) -> None: + """Write ``cli.md`` before the build reads the docs directory.""" + try: + result = subprocess.run( + ["commit-check", "--help"], + capture_output=True, + encoding="utf-8", + check=True, + ) + except (OSError, subprocess.CalledProcessError) as exc: # pragma: no cover + raise RuntimeError( + "could not run 'commit-check --help' to generate the CLI reference; " + "install the package first (pip install -e .)" + ) from exc + + text = result.stdout.rstrip() + target = Path(config["docs_dir"], "cli.md") + target.write_text(PAGE.format(help=text), encoding="utf-8") + + +def on_post_build(config, **kwargs) -> None: + """Write a redirect stub for each URL the Sphinx site used to serve.""" + site = Path(config["site_dir"]) + # On Netlify the stubs would be served in place of the real pages: Netlify + # resolves ``/rules/`` to the file ``rules.html``, so the stub that + # redirects to ``/rules/`` would shadow ``rules/index.html`` and loop. The + # ``[[redirects]]`` table in netlify.toml covers the same URLs with real + # 301s, so the stubs are only written for hosts without redirects. + if os.environ.get("NETLIFY") == "true": + return + # Deploy previews pass their own URL in, and it may arrive without the + # trailing slash the targets below are joined onto. + base = (config["site_url"] or "/").rstrip("/") + "/" + for legacy, target in LEGACY_URLS.items(): + (site / f"{legacy}.html").write_text( + REDIRECT.format(url=base + target), encoding="utf-8" + ) diff --git a/tests/rules_catalog_test.py b/tests/rules_catalog_test.py index 53bb24a4..e8c330be 100644 --- a/tests/rules_catalog_test.py +++ b/tests/rules_catalog_test.py @@ -2,6 +2,7 @@ import re from pathlib import Path +from typing import Any import pytest @@ -15,6 +16,7 @@ RuleCatalogEntry, ) from commit_check.rule_builder import RuleBuilder +from commit_check.config_merger import get_default_config ALL_ENTRIES = [*COMMIT_RULES, *BRANCH_RULES, *PUSH_RULES] @@ -107,18 +109,167 @@ def test_internal_entries_have_no_id(self): class TestRulesDocumentation: - """Anti-drift guard: every documented rule stays documented.""" + """Anti-drift guard: every documented rule stays documented. + + These tests read files rather than exercising the package, so they carry no + ``benchmark`` marker: their cost tracks the size of the documentation, and + benchmarking them would report a performance regression every time somebody + writes more of it. + """ - @pytest.mark.benchmark def test_every_rule_is_documented(self): """Each rule ID must have an anchor in the rules reference page. This prevents shipping a new rule without documenting it. """ - docs = Path(__file__).parent.parent / "docs" / "rules.rst" - content = docs.read_text(encoding="utf-8") + content = _read_doc("rules.md") for entry in ALL_RULES: - anchor = f".. _{entry.rule_id.lower()}:" + anchor = f"{{ #{entry.rule_id.lower()} }}" assert anchor in content, ( - f"{entry.rule_id} ({entry.check}) is missing from docs/rules.rst" + f"{entry.rule_id} ({entry.check}) is missing from docs/rules.md" + ) + + def test_every_rule_has_a_section_heading(self): + """Each rule needs a ``name (CCxxx)`` heading, not just an anchor. + + Checked inside the rule's own section: an anchor alone, or a heading + that survives elsewhere on the page, would otherwise pass. + """ + content = _read_doc("rules.md") + for entry in ALL_RULES: + heading = ( + f"### {entry.name} ({entry.rule_id}) {{ #{entry.rule_id.lower()} }}" + ) + assert heading in content, ( + f"docs/rules.md has no section titled '{heading}'" + ) + + def test_every_rule_explains_itself(self): + """Each rule section must answer what it does and why it matters.""" + content = _read_doc("rules.md") + for entry in ALL_RULES: + section = _rule_section(content, entry.rule_id) + for required in ("**What it does**", "**Why is this bad?**", "**Options**"): + assert required in section, ( + f"{entry.rule_id} ({entry.check}) section is missing {required}" + ) + + +class TestDocumentedDefaults: + """The documented defaults must match the ones the code actually uses. + + Not benchmarked, for the same reason as :class:`TestRulesDocumentation`. + """ + + def test_every_runtime_option_is_documented(self): + """Every option the runtime defines has a row in the options table.""" + documented = _parse_options_table(_read_doc("configuration.md")) + for section, options in get_default_config().items(): + for option in options: + assert (section, option) in documented, ( + f"[{section}] {option} exists in get_default_config() but " + f"has no row in the options table of docs/configuration.md" + ) + + def test_no_invented_options_are_documented(self): + """The options table does not document options that do not exist.""" + runtime = get_default_config() + for section, option in _parse_options_table(_read_doc("configuration.md")): + assert option in runtime.get(section, {}), ( + f"docs/configuration.md documents [{section}] {option}, which " + f"does not exist in get_default_config()" ) + + def test_documented_defaults_match_the_runtime(self): + """Every documented default equals the value the runtime actually uses. + + ``get_default_config()`` is what ``ConfigMerger.from_all_sources()`` + starts from, so it is the single source of truth for "what happens with + no config file". The options table is maintained by hand and silently + drifts away from it without this guard. + """ + documented = _parse_options_table(_read_doc("configuration.md")) + runtime = get_default_config() + + for (section, option), (type_, cell) in sorted(documented.items()): + if option not in runtime.get(section, {}): + continue # reported by test_no_invented_options_are_documented + expected = runtime[section][option] + actual = _documented_default(type_, cell) + if isinstance(expected, list): + # Allow-lists: order carries no meaning, membership does. + assert set(actual or []) == set(expected), ( + f"docs/configuration.md documents [{section}] {option} " + f"with {sorted(set(actual or []) - set(expected))} that are " + f"not defaults, and is missing " + f"{sorted(set(expected) - set(actual or []))}" + ) + else: + assert actual == expected, ( + f"docs/configuration.md documents [{section}] {option} as " + f"{cell.strip()!r}, but the runtime default is {expected!r}" + ) + + +def _read_doc(name: str) -> str: + """Read a file from the ``docs`` directory.""" + return (Path(__file__).parent.parent / "docs" / name).read_text(encoding="utf-8") + + +def _rule_section(content: str, rule_id: str) -> str: + """Return just the part of the rules page belonging to one rule.""" + _, _, after = content.partition(f"{{ #{rule_id.lower()} }}") + return re.split(r"\{ #cc\d{3} \}", after)[0] + + +_OPTIONS_ROW = re.compile( + r"^\|\s*(commit|branch|push)\s*" # section + r"\|\s*(\w+)\s*" # option name + r"\|\s*(bool|int|str|list\[str\])\s*" # type + r"\|\s*(.+?)\s*\|", # documented default + re.M, +) + + +def _parse_options_table(content: str) -> dict[tuple[str, str], tuple[str, str]]: + """Map ``(section, option) -> (type, raw default cell)``. + + Parses the five-cell ``list-table`` rows of the options table, e.g.:: + + * - commit + - subject_max_length + - int + - 80 + - Maximum length of the subject line. + """ + return { + (section, option): (type_, cell) + for section, option, type_, cell in _OPTIONS_ROW.findall(content) + } + + +# Markdown code spans use single backticks; RST used double. +_QUOTED = re.compile(r'^(?:`+(.*?)`+|"(.*?)")') + + +def _documented_default(type_: str, cell: str) -> Any: + """Turn a documented default cell into a comparable Python value. + + Cells carry a human annotation after the value itself (``"" (disabled)``), + so the value is read from the front of the cell and the rest ignored. + """ + cell = cell.strip() + if type_ == "bool": + return cell.startswith("true") + if type_ == "int": + match = re.match(r"-?\d+", cell) + return int(match.group()) if match else None + if type_ == "list[str]": + return re.findall(r'"(.*?)"', cell) + quoted = _QUOTED.match(cell) + if quoted is None: + return cell + # Group 1 is the ``literal`` form, group 2 the "literal" form; exactly one + # of them matched. + backticked, double_quoted = quoted.groups() + return backticked if backticked is not None else double_quoted