From 0eb515a487aa0841505161a9df1ad91776b7ccac Mon Sep 17 00:00:00 2001 From: shenxianpeng Date: Mon, 3 Aug 2026 14:21:53 +0000 Subject: [PATCH 1/9] docs: move navigation to the sidebar and expand the rules reference Replace the top tab bar with a left sidebar, and group the pages into Getting started / Configuring / Reference / About so the whole documentation set is visible at a glance instead of one tab at a time. On a rule page the sidebar also nests that page's own table of contents, putting every rule one click away. Rewrite the rules reference along the lines of a linter's rule documentation: an index table per category giving each rule's code, name, message, the CLI flag that runs it, and whether it is on by default, followed by a section per rule answering what it does, why it matters, and how to fix it, with a before/after example and the options that control it. Correct several defaults in the configuration reference that had drifted from the source: subject_capitalized and subject_imperative are off by default (documented as on), allow_empty_commits and allow_wip_commits are on (documented as off), and allow_commit_types was missing perf, build, and ci. The "Default Behavior" tip repeated the same mistake. Guard all of it with tests, so the documentation cannot drift again: documented boolean and list defaults are compared against the values in commit_check/__init__.py, and every rule must have a section heading that answers what it does, why it is bad, and which options apply. Also fix four reStructuredText title underlines that were shorter than their titles, letting the docs build cleanly under -W. --- docs/_static/extra_css.css | 18 +- docs/changelog.rst | 8 +- docs/conf.py | 12 +- docs/configuration.rst | 18 +- docs/index.md | 22 +- docs/migration.rst | 2 +- docs/rules.rst | 743 +++++++++++++++++++++++++++++++----- tests/rules_catalog_test.py | 131 ++++++- 8 files changed, 839 insertions(+), 115 deletions(-) diff --git a/docs/_static/extra_css.css b/docs/_static/extra_css.css index 34f05968..4d3be828 100644 --- a/docs/_static/extra_css.css +++ b/docs/_static/extra_css.css @@ -5,11 +5,16 @@ thead { } .md-header, -.md-tabs, .md-nav--primary .md-nav__title[for="__drawer"] { background-color: #2c9ccd; } +/* Sidebar section headings ("Getting started", "Reference", ...) */ +.md-nav__item--section > .md-nav__link { + font-weight: 700; + color: var(--md-default-fg-color); +} + /* Fix table header visibility for both light and dark modes */ .md-content table th { color: var(--md-typeset-color) !important; @@ -51,3 +56,14 @@ thead { background-color: rgba(44, 156, 205, 0.1); border-color: #2c9ccd; } + +/* Rule index tables: keep rule codes, names, and CLI flags on one line so the + tables stay scannable, and align cells to the top when a message wraps. */ +.md-typeset table td, +.md-typeset table th { + vertical-align: top; +} + +.md-typeset table td code { + white-space: nowrap; +} diff --git a/docs/changelog.rst b/docs/changelog.rst index 89d87726..a64330e8 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -6,7 +6,7 @@ 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 ~~~~~~~~~~~~ @@ -32,7 +32,7 @@ Chores v2.10.1 (2026-06-30) -------------------- +-------------------- Bug Fixes ~~~~~~~~~ @@ -54,7 +54,7 @@ Refactors v2.10.0 (2026-06-26) -------------------- +-------------------- New Features ~~~~~~~~~~~~ @@ -222,6 +222,6 @@ 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 index 898349b0..405949dc 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -83,14 +83,22 @@ }, }, ], + # The global navigation lives in the left sidebar (no top tab bar), with + # each toctree caption rendered as a section heading. This keeps every + # page one click away and leaves the right-hand column for the page's own + # table of contents. "features": [ + "navigation.sections", "navigation.top", - "navigation.tabs", - "navigation.tabs.sticky", + "navigation.tracking", "toc.sticky", "toc.follow", + "search.highlight", "search.share", ], + # Keep the sidebar sections expanded rather than collapsing everything but + # the current page, so the whole documentation set is visible at a glance. + "globaltoc_collapse": False, } object_description_options = [ diff --git a/docs/configuration.rst b/docs/configuration.rst index 0c658df9..4ba5381c 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -24,8 +24,10 @@ Configuration Files **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. + * Only the Conventional Commits format (:ref:`CC001 `), the Conventional Branch format (:ref:`CC201 `), and the author name and email patterns (:ref:`CC101 `, :ref:`CC102 `) are enforced by default. + * Subject capitalization and imperative mood are **off** by default, as are all length limits, body and signoff requirements, and rebase requirements. + + See :doc:`rules` for the default state of every rule. commit-check can be configured via a ``cchk.toml`` or ``commit-check.toml`` file. @@ -365,12 +367,12 @@ Options Table Description * - commit - subject_capitalized - bool - - true + - false - Subject must start with a capital letter. * - commit - subject_imperative - bool - - true + - false - Subject must be in imperative mood. Forms of verbs can be found at `imperatives.py `_ * - commit - subject_max_length @@ -385,7 +387,7 @@ Options Table Description * - commit - allow_commit_types - list[str] - - ["feat", "fix", "docs", "style", "refactor", "test", "chore"] + - ["feat", "fix", "docs", "style", "refactor", "test", "chore", "perf", "build", "ci"] - Allowed commit types when conventional_commits is true. * - commit - allow_merge_commits @@ -400,7 +402,7 @@ Options Table Description * - commit - allow_empty_commits - bool - - false + - true - Allow empty commits. * - commit - allow_fixup_commits @@ -410,7 +412,7 @@ Options Table Description * - commit - allow_wip_commits - bool - - false + - true - Allow work-in-progress commits (e.g., "WIP: "). * - commit - require_body @@ -425,7 +427,7 @@ Options Table Description * - commit - author_email_pattern - str - - ^.+@.+$ + - "" (built-in default ``^.+@.+$``) - 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 diff --git a/docs/index.md b/docs/index.md index 69e2336c..b7b74a4f 100644 --- a/docs/index.md +++ b/docs/index.md @@ -3,13 +3,29 @@ ```{toctree} :hidden: +:caption: Getting started self what-is-new -configuration -rules example +``` + +```{toctree} +:hidden: +:caption: Configuring +configuration migration +``` + +```{toctree} +:hidden: +:caption: Reference +rules +cli_args +``` + +```{toctree} +:hidden: +:caption: About troubleshoot changelog -cli_args ``` diff --git a/docs/migration.rst b/docs/migration.rst index c0118de1..5dd13895 100644 --- a/docs/migration.rst +++ b/docs/migration.rst @@ -144,7 +144,7 @@ The command-line interface has been simplified: 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`` diff --git a/docs/rules.rst b/docs/rules.rst index a850316f..c19d9047 100644 --- a/docs/rules.rst +++ b/docs/rules.rst @@ -1,14 +1,11 @@ -Rules Reference -=============== +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 documentation, code -review comments, and tooling. +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 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: +Rule IDs appear in commit-check's output and in ``--format json`` results: .. code-block:: text @@ -17,300 +14,858 @@ section on this page: 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 +``--compact`` prints one line per failure, keeping the rule ID and dropping the explanation, suggestion, and documentation link: .. code-block:: text [FAIL] CC003 subject_imperative: docs: revamped the profile -ID ranges ---------- +How to read this page +--------------------- + +Rule IDs are grouped by what they inspect: .. list-table:: :header-rows: 1 + :widths: 12 25 63 * - Range - Category + - Inspects * - ``CC0xx`` - - Commit message + - :ref:`Commit message ` + - The subject, body, and trailers of a commit message * - ``CC1xx`` - - Author + - :ref:`Author ` + - The committer's configured name and email * - ``CC2xx`` - - Branch + - :ref:`Branch ` + - The current branch's name and its position relative to a target branch * - ``CC3xx`` - - Push + - :ref:`Push ` + - 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: + +.. list-table:: + :header-rows: 1 + :widths: 20 80 + + * - 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 +:ref:`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 + :doc:`configuration`. + +Rule index +---------- -All rules ---------- +.. _commit-message-rules: + +Commit message rules (``CC0xx``) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Run with ``-m`` / ``--message``. .. list-table:: :header-rows: 1 + :widths: 10 26 44 10 10 - * - ID + * - Code - Name - - Description + - Message + - Check + - Default * - :ref:`CC001 ` - ``message`` - The commit message should follow Conventional Commits + - ``-m`` + - ✅ On * - :ref:`CC002 ` - ``subject-capitalized`` - Subject must start with a capital letter + - ``-m`` + - ⚪ Off * - :ref:`CC003 ` - ``subject-imperative`` - - Commit message should use imperative mood (e.g., 'fix bug' not 'fixed bug', 'add feature' not 'adding feature') + - Commit message should use imperative mood + - ``-m`` + - ⚪ Off * - :ref:`CC004 ` - ``subject-max-length`` - - Subject must be at most {max_len} characters + - Subject must be at most ``{max_len}`` characters + - ``-m`` + - ⚪ Off * - :ref:`CC005 ` - ``subject-min-length`` - - Subject must be at least {min_len} characters + - Subject must be at least ``{min_len}`` characters + - ``-m`` + - ⚪ Off * - :ref:`CC006 ` - ``allow-merge-commits`` - Merge commits are not allowed + - ``-m`` + - ⚪ Off * - :ref:`CC007 ` - ``allow-revert-commits`` - Revert commits are not allowed + - ``-m`` + - ⚪ Off * - :ref:`CC008 ` - ``allow-empty-commits`` - Empty commit messages are not allowed + - ``-m`` + - ⚪ Off * - :ref:`CC009 ` - ``allow-fixup-commits`` - Fixup commits are not allowed + - ``-m`` + - ⚪ Off * - :ref:`CC010 ` - ``allow-wip-commits`` - WIP commits are not allowed + - ``-m`` + - ⚪ Off * - :ref:`CC011 ` - ``require-body`` - Commit body is required + - ``-m`` + - ⚪ Off * - :ref:`CC012 ` - ``require-signed-off-by`` - Signed-off-by not found in latest commit + - ``-m`` + - ⚪ Off * - :ref:`CC013 ` - ``ai-attribution`` - AI attribution policy violation + - ``-m`` + - ⚪ Off + +.. _author-rules: + +Author rules (``CC1xx``) +~~~~~~~~~~~~~~~~~~~~~~~~ + +.. list-table:: + :header-rows: 1 + :widths: 10 26 44 10 10 + + * - Code + - Name + - Message + - Check + - Default * - :ref:`CC101 ` - ``author-name`` - The committer name seems invalid + - ``-n`` + - ✅ On * - :ref:`CC102 ` - ``author-email`` - The committer's email seems invalid + - ``-e`` + - ✅ On + +.. _branch-rules: + +Branch rules (``CC2xx``) +~~~~~~~~~~~~~~~~~~~~~~~~ + +Run with ``-b`` / ``--branch``. + +.. list-table:: + :header-rows: 1 + :widths: 10 26 44 10 10 + + * - Code + - Name + - Message + - Check + - Default * - :ref:`CC201 ` - ``branch`` - The branch should follow Conventional Branch + - ``-b`` + - ✅ On * - :ref:`CC202 ` - ``merge-base`` - Current branch is not rebased onto target branch + - ``-b`` + - ⚪ Off + +.. _push-rules: + +Push rules (``CC3xx``) +~~~~~~~~~~~~~~~~~~~~~~ + +.. list-table:: + :header-rows: 1 + :widths: 10 26 44 10 10 + + * - Code + - Name + - Message + - Check + - Default * - :ref:`CC301 ` - ``no-force-push`` - Force push is not allowed + - ``--no-force-push`` + - ⚪ Off Commit message rules -------------------- .. _cc001: -CC001 — message +message (CC001) ~~~~~~~~~~~~~~~ -**Config key:** ``message`` +**What it does** -**Message:** The commit message should follow Conventional Commits. See https://www.conventionalcommits.org +Checks that the commit message subject follows the +`Conventional Commits `_ specification: +``()!: ``. -**How to fix:** Use (): with allowed types +**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** + +.. code-block:: text + + updated the parser + +Use instead: + +.. code-block:: 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+: .+"``). .. _cc002: -CC002 — subject-capitalized +subject-capitalized (CC002) ~~~~~~~~~~~~~~~~~~~~~~~~~~~ -**Config key:** ``subject_capitalized`` +**What it does** + +Checks that the description in the subject line starts with a capital letter. + +**Why is this bad?** -**Message:** Subject must start with a capital letter +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. -**How to fix:** Capitalize the first word of the subject +Leave it off if your project deliberately uses lowercase descriptions, which is +the more common convention among projects that follow Conventional Commits. + +**Example** + +.. code-block:: text + + fix: handle empty input + +Use instead: + +.. code-block:: text + + fix: Handle empty input + +**Options** + +* ``commit.subject_capitalized`` — set to ``true`` to enable this rule. .. _cc003: -CC003 — subject-imperative +subject-imperative (CC003) ~~~~~~~~~~~~~~~~~~~~~~~~~~ -**Config key:** ``subject_imperative`` +**What it does** -**Message:** Commit message should use imperative mood (e.g., 'fix bug' not 'fixed bug', 'add feature' not 'adding feature') +Checks that the first word of the description is in the imperative mood — +``fix``, not ``fixed``, ``fixes``, or ``fixing``. -**How to fix:** Change the first verb to imperative form, e.g., 'fix' instead of 'fixed'/'fixes'/'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** + +.. code-block:: text + + fix: fixed a crash when the config file is empty + +Use instead: + +.. code-block:: 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 `_. .. _cc004: -CC004 — subject-max-length +subject-max-length (CC004) ~~~~~~~~~~~~~~~~~~~~~~~~~~ -**Config key:** ``subject_max_length`` +**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** + +.. code-block:: text + + fix: handle an empty config file and also fix the unrelated crash in the branch parser that happens on Windows + +Use instead: -**Message:** Subject must be at most {max_len} characters +.. code-block:: text + + fix: handle an empty config file + + Also fixes the branch parser crash on Windows, which shared the + same root cause. + +**Options** -**How to fix:** Keep the subject concise (<= configured max) +* ``commit.subject_max_length`` — the limit, in characters. Unset by default, + meaning no limit. ``50`` and ``72`` are the conventional choices. .. _cc005: -CC005 — subject-min-length +subject-min-length (CC005) ~~~~~~~~~~~~~~~~~~~~~~~~~~ -**Config key:** ``subject_min_length`` +**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** + +.. code-block:: text + + fix: bug + +Use instead: + +.. code-block:: text -**Message:** Subject must be at least {min_len} characters + fix: reject config files with a null inherit_from -**How to fix:** Provide a meaningful subject (>= configured min) +**Options** + +* ``commit.subject_min_length`` — the minimum, in characters. Unset by default, + meaning no minimum. .. _cc006: -CC006 — allow-merge-commits +allow-merge-commits (CC006) ~~~~~~~~~~~~~~~~~~~~~~~~~~~ -**Config key:** ``allow_merge_commits`` +**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** + +.. code-block:: bash + + git pull -**Message:** Merge commits are not allowed +Use instead: -**How to fix:** Rebase or squash your changes instead of merging +.. code-block:: 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. .. _cc007: -CC007 — allow-revert-commits +allow-revert-commits (CC007) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -**Config key:** ``allow_revert_commits`` +**What it does** + +Rejects the ``Revert "..."`` commits that ``git revert`` generates. + +**Why is this bad?** -**Message:** Revert commits are not allowed +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. -**How to fix:** Avoid using 'revert' commits; rewrite history if necessary +**Example** + +.. code-block:: text + + Revert "feat: add caching layer" + +Use instead: + +.. code-block:: 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. .. _cc008: -CC008 — allow-empty-commits +allow-empty-commits (CC008) ~~~~~~~~~~~~~~~~~~~~~~~~~~~ -**Config key:** ``allow_empty_commits`` +**What it does** + +Rejects commits with an empty message. -**Message:** Empty commit messages are not allowed +**Why is this bad?** -**How to fix:** Provide a non-empty subject +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. .. _cc009: -CC009 — allow-fixup-commits +allow-fixup-commits (CC009) ~~~~~~~~~~~~~~~~~~~~~~~~~~~ -**Config key:** ``allow_fixup_commits`` +**What it does** + +Rejects ``fixup!`` and ``squash!`` commits. -**Message:** Fixup commits are not allowed +**Why is this bad?** -**How to fix:** Use interactive rebase to clean up fixup commits +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** + +.. code-block:: text + + fixup! feat: add the caching layer + +Use instead: + +.. code-block:: bash + + git rebase -i --autosquash main + +**Options** + +* ``commit.allow_fixup_commits`` — set to ``false`` to enable this rule. .. _cc010: -CC010 — allow-wip-commits +allow-wip-commits (CC010) ~~~~~~~~~~~~~~~~~~~~~~~~~ -**Config key:** ``allow_wip_commits`` +**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** -**Message:** WIP commits are not allowed +.. code-block:: text + + WIP: caching + +Use instead: + +.. code-block:: bash -**How to fix:** Complete the work before committing or remove 'WIP' + # 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. .. _cc011: -CC011 — require-body +require-body (CC011) ~~~~~~~~~~~~~~~~~~~~ -**Config key:** ``require_body`` +**What it does** + +Requires a non-empty body after the subject line. -**Message:** Commit body is required +**Why is this bad?** -**How to fix:** Add a body explaining the change +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** + +.. code-block:: text + + fix: cap the retry backoff at 30s + +Use instead: + +.. code-block:: 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. .. _cc012: -CC012 — require-signed-off-by +require-signed-off-by (CC012) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -**Config key:** ``require_signed_off_by`` +**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 +`_ — 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** + +.. code-block:: bash + + git commit -m "fix: handle an empty config file" + +Use instead: + +.. code-block:: bash + + git commit --signoff -m "fix: handle an empty config file" -**Message:** Signed-off-by not found in latest commit + # or fix the commit you already made + git commit --amend --signoff -**How to fix:** git commit --amend --signoff or use --signoff on commit +**Options** + +* ``commit.require_signed_off_by`` — set to ``true`` to enable this rule. +* ``commit.required_signoff_name`` — require the trailer to carry a specific name. +* ``commit.required_signoff_email`` — require the trailer to carry a specific email. .. _cc013: -CC013 — ai-attribution +ai-attribution (CC013) ~~~~~~~~~~~~~~~~~~~~~~ -**Config key:** ``ai_attribution`` +**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. -**Message:** AI attribution policy violation +It is off by default, and the default policy is ``"ignore"``. Enable it only if +your project has a stated position. -**How to fix:** This project forbids AI-assisted commits. Remove AI trailers and re-commit. +**Options** + +* ``commit.ai_attribution`` — ``"forbid"`` enables this rule, ``"ignore"`` + (the default) disables it. Author rules ------------ .. _cc101: -CC101 — author-name +author-name (CC101) ~~~~~~~~~~~~~~~~~~~ -**Config key:** ``author_name`` +**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 +``root``, ``ubuntu``, and ``ec2-user`` — authorship that cannot be traced back +to a person, which matters for both code archaeology and compliance. + +**Example** + +.. code-block:: bash + + git config user.name root + +Use instead: + +.. code-block:: bash + + git config --global user.name "Your Name" -**Message:** The committer name seems invalid +**Options** -**How to fix:** git config user.name 'Your Name' +* ``commit.author_name_pattern`` — a custom regex replacing the built-in + pattern. For example, ``"^.+ .+$"`` to require a full name. .. _cc102: -CC102 — author-email +author-email (CC102) ~~~~~~~~~~~~~~~~~~~~ -**Config key:** ``author_email`` +**What it does** -**Message:** The committer's email seems invalid +Checks the committer's configured email against a pattern. The built-in pattern +(``^.+@.+$``) only requires something that looks like an address. -**How to fix:** git config user.email yourname@example.com +**Why is this bad?** + +An unset or placeholder email breaks the link between a commit and its author: +forges cannot attribute the commit to an account, and mailmap-based tooling +cannot merge identities. Organisations that require contributions from a +corporate address can tighten the pattern to enforce it. + +**Example** + +.. code-block:: bash + + git config user.email root@localhost + +Use instead: + +.. code-block:: bash + + git config --global user.email you@example.com + +**Options** + +* ``commit.author_email_pattern`` — a custom regex replacing the built-in + pattern. For example, ``"^.+@example\\.com$"`` to require a company domain. Branch rules ------------ .. _cc201: -CC201 — branch +branch (CC201) ~~~~~~~~~~~~~~ -**Config key:** ``branch`` +**What it does** + +Checks that the current branch name follows the +`Conventional Branch `_ 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. -**Message:** The branch should follow Conventional Branch. See https://conventionalbranch.org +**Example** -**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 +.. code-block:: text + + my-fix + johns-branch-2 + +Use instead: + +.. code-block:: text + + fix/empty-config-crash + feature/role-caching + +**Options** + +* ``commit.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. .. _cc202: -CC202 — merge-base +merge-base (CC202) ~~~~~~~~~~~~~~~~~~ -**Config key:** ``merge_base`` +**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** + +.. code-block:: bash + + # branch was cut from main three weeks ago + git push -**Message:** Current branch is not rebased onto target branch +Use instead: -**How to fix:** Rebase or merge with the target branch +.. code-block:: 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 ---------- .. _cc301: -CC301 — no-force-push +no-force-push (CC301) ~~~~~~~~~~~~~~~~~~~~~ -**Config key:** ``no_force_push`` +**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** + +.. code-block:: bash + + git push --force + +Use instead: + +.. code-block:: 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 -**Message:** Force push is not allowed +**Options** -**How to fix:** Use a normal push instead of --force or --force-with-lease +* ``push.allow_force_push`` — set to ``false`` to enable this rule. diff --git a/tests/rules_catalog_test.py b/tests/rules_catalog_test.py index 53bb24a4..353d72e4 100644 --- a/tests/rules_catalog_test.py +++ b/tests/rules_catalog_test.py @@ -15,6 +15,12 @@ RuleCatalogEntry, ) from commit_check.rule_builder import RuleBuilder +from commit_check import ( + DEFAULT_BOOLEAN_RULES, + DEFAULT_BRANCH_TYPES, + DEFAULT_COMMIT_TYPES, + DEFAULT_PUSH_RULES, +) ALL_ENTRIES = [*COMMIT_RULES, *BRANCH_RULES, *PUSH_RULES] @@ -115,10 +121,131 @@ def test_every_rule_is_documented(self): 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.rst") for entry in ALL_RULES: anchor = f".. _{entry.rule_id.lower()}:" assert anchor in content, ( f"{entry.rule_id} ({entry.check}) is missing from docs/rules.rst" ) + + @pytest.mark.benchmark + def test_every_rule_has_a_section_heading(self): + """Each rule needs a ``name (CCxxx)`` heading, not just an anchor. + + An anchor alone would satisfy the test above while linking readers to + an empty part of the page. + """ + content = _read_doc("rules.rst") + for entry in ALL_RULES: + heading = f"{entry.name} ({entry.rule_id})" + assert heading in content, ( + f"docs/rules.rst has no section titled '{heading}'" + ) + + @pytest.mark.benchmark + def test_every_rule_explains_itself(self): + """Each rule section must answer what it does and why it matters.""" + content = _read_doc("rules.rst") + # Split on the anchors so each rule's prose is checked in isolation. + for entry in ALL_RULES: + _, _, after = content.partition(f".. _{entry.rule_id.lower()}:") + section = re.split(r"\n\.\. _cc\d{3}:", after)[0] + 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.""" + + @pytest.mark.benchmark + def test_boolean_defaults_match_configuration_docs(self): + """Every boolean option's documented default matches the source. + + The options table in ``docs/configuration.rst`` is maintained by hand. + Without this guard it silently drifts away from + ``DEFAULT_BOOLEAN_RULES`` whenever a default changes. + """ + documented = _parse_options_table(_read_doc("configuration.rst")) + expected = {**DEFAULT_BOOLEAN_RULES, **DEFAULT_PUSH_RULES} + + for option, default in expected.items(): + assert option in documented, ( + f"'{option}' has a default in the source but no row in the " + f"options table of docs/configuration.rst" + ) + assert documented[option] == default, ( + f"docs/configuration.rst documents {option} as " + f"{str(documented[option]).lower()}, but the default is " + f"{str(default).lower()}" + ) + + @pytest.mark.parametrize( + ("option", "expected"), + [ + ("allow_commit_types", DEFAULT_COMMIT_TYPES), + ("allow_branch_types", DEFAULT_BRANCH_TYPES), + ], + ) + @pytest.mark.benchmark + def test_list_defaults_match_configuration_docs(self, option, expected): + """The documented list defaults contain exactly the real values. + + Compared as sets: these are allow-lists, so the order they are listed + in carries no meaning and should not fail the build. + """ + documented = _parse_list_default(_read_doc("configuration.rst"), option) + assert documented is not None, ( + f"'{option}' has no list[str] row in the options table of " + f"docs/configuration.rst" + ) + assert set(documented) == set(expected), ( + f"docs/configuration.rst documents {option} with " + f"{sorted(set(documented) - set(expected))} that are not defaults, " + f"and is missing {sorted(set(expected) - set(documented))}" + ) + + +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 _parse_options_table(content: str) -> dict[str, bool]: + """Extract ``option -> documented default`` for boolean rows. + + Matches the five-cell ``list-table`` rows in the options table, e.g.:: + + * - commit + - allow_wip_commits + - bool + - true + - Allow work-in-progress commits. + """ + row = re.compile( + r"\*\s+-\s+(?:commit|branch|push)\s*\n" + r"\s+-\s+(\w+)\s*\n" + r"\s+-\s+bool\s*\n" + r"\s+-\s+(true|false)\s*\n" + ) + return {name: value == "true" for name, value in row.findall(content)} + + +def _parse_list_default(content: str, option: str) -> list[str] | None: + """Extract the documented default for a ``list[str]`` option. + + Returns ``None`` when the option has no ``list[str]`` row, so the caller + can tell "undocumented" apart from "documented as empty". + """ + row = re.search( + rf"\*\s+-\s+(?:commit|branch|push)\s*\n" + rf"\s+-\s+{re.escape(option)}\s*\n" + rf"\s+-\s+list\[str\]\s*\n" + rf"\s+-\s+(\[.*?\])\s*\n", + content, + re.S, + ) + if row is None: + return None + return re.findall(r'"([^"]+)"', row.group(1)) From 0f4c3ca5912063354fd4e0f24e3823d74e89504d Mon Sep 17 00:00:00 2001 From: shenxianpeng Date: Mon, 3 Aug 2026 14:35:29 +0000 Subject: [PATCH 2/9] docs: correct the subject-length defaults and verify docs against the runtime MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The subject-length rules are on by default, not off. `get_default_config()` sets subject_max_length to 80 and subject_min_length to 5, and `ConfigMerger.from_all_sources()` starts from it, so with no config file at all a subject over 80 characters fails CC004. Both the rules reference and the configuration reference claimed there was no limit. The same review turned up four more inaccuracies: - author_email_pattern defaults to `^.+@.+$`, not an empty string. - require_rebase_target defaults to `""`, not None. - CC201's option is branch.conventional_branch; it was documented under the commit section, where configuration merging would ignore it. - required_signoff_name and required_signoff_email were documented in the example config and in CC012's options, but no such options exist anywhere in the code. Two rule examples also demonstrated values that pass the very check they illustrate: `root` satisfies the built-in author name pattern, and `root@localhost` satisfies `^.+@.+$`. Both now use values that actually fail, with a note explaining how permissive the built-in patterns are and when to replace them. Rework the drift guard so this class of error is caught mechanically. Rather than comparing against a couple of constants, the options table is now parsed and every row checked against `get_default_config()` — the same dict the CLI builds its configuration from. Two further tests assert the table and the runtime describe the same set of options, in both directions, which is what catches an invented option like required_signoff_name or a newly added one that nobody documented. Also scope the rule-index table styling to those tables so long regexes in the configuration table stay wrappable, and check each rule's heading inside its own section instead of anywhere on the page. --- docs/_static/extra_css.css | 8 +- docs/configuration.rst | 18 ++-- docs/rules.rst | 60 ++++++++----- tests/rules_catalog_test.py | 174 ++++++++++++++++++++---------------- 4 files changed, 149 insertions(+), 111 deletions(-) diff --git a/docs/_static/extra_css.css b/docs/_static/extra_css.css index 4d3be828..cc322201 100644 --- a/docs/_static/extra_css.css +++ b/docs/_static/extra_css.css @@ -57,13 +57,15 @@ thead { border-color: #2c9ccd; } -/* Rule index tables: keep rule codes, names, and CLI flags on one line so the - tables stay scannable, and align cells to the top when a message wraps. */ +/* Align table cells to the top so short cells line up with a wrapped message. */ .md-typeset table td, .md-typeset table th { vertical-align: top; } -.md-typeset table td code { +/* In the rule index tables only, keep rule codes, names, and CLI flags on one + line so the tables stay scannable. Other tables (such as the configuration + options table) hold long regexes and lists that must stay wrappable. */ +.md-typeset table.rules-index td code { white-space: nowrap; } diff --git a/docs/configuration.rst b/docs/configuration.rst index 4ba5381c..27e05772 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -24,8 +24,8 @@ Configuration Files **Default Behavior** * When no configuration file exists, commit-check uses sensible defaults with minimal restrictions. - * Only the Conventional Commits format (:ref:`CC001 `), the Conventional Branch format (:ref:`CC201 `), and the author name and email patterns (:ref:`CC101 `, :ref:`CC102 `) are enforced by default. - * Subject capitalization and imperative mood are **off** by default, as are all length limits, body and signoff requirements, and rebase requirements. + * Enforced by default: the Conventional Commits format (:ref:`CC001 `), the Conventional Branch format (:ref:`CC201 `), the subject length limits of 5–80 characters (:ref:`CC004 `, :ref:`CC005 `), and the author name and email patterns (:ref:`CC101 `, :ref:`CC102 `). + * **Off** by default: subject capitalization, imperative mood, body and signoff requirements, rebase requirements, and every ``allow_*`` restriction. See :doc:`rules` for the default state of every rule. @@ -115,8 +115,8 @@ Example Configuration # 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 + 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 @@ -126,8 +126,6 @@ Example Configuration 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] @@ -377,12 +375,12 @@ Options Table Description * - commit - subject_max_length - int - - None (no limit) + - 80 - Maximum length of the subject line. * - commit - subject_min_length - int - - None (no limit) + - 5 - Minimum length of the subject line. * - commit - allow_commit_types @@ -427,7 +425,7 @@ Options Table Description * - commit - author_email_pattern - str - - "" (built-in default ``^.+@.+$``) + - ``^.+@.+$`` - 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 @@ -464,7 +462,7 @@ Options Table Description * - branch - require_rebase_target - str - - None (no requirement) + - "" (no requirement) - Target branch for rebase requirement. If not set, no rebase validation is performed. * - push - allow_force_push diff --git a/docs/rules.rst b/docs/rules.rst index c19d9047..0f968695 100644 --- a/docs/rules.rst +++ b/docs/rules.rst @@ -90,6 +90,7 @@ Run with ``-m`` / ``--message``. .. list-table:: :header-rows: 1 :widths: 10 26 44 10 10 + :class: rules-index * - Code - Name @@ -115,12 +116,12 @@ Run with ``-m`` / ``--message``. - ``subject-max-length`` - Subject must be at most ``{max_len}`` characters - ``-m`` - - ⚪ Off + - ✅ On * - :ref:`CC005 ` - ``subject-min-length`` - Subject must be at least ``{min_len}`` characters - ``-m`` - - ⚪ Off + - ✅ On * - :ref:`CC006 ` - ``allow-merge-commits`` - Merge commits are not allowed @@ -170,6 +171,7 @@ Author rules (``CC1xx``) .. list-table:: :header-rows: 1 :widths: 10 26 44 10 10 + :class: rules-index * - Code - Name @@ -197,6 +199,7 @@ Run with ``-b`` / ``--branch``. .. list-table:: :header-rows: 1 :widths: 10 26 44 10 10 + :class: rules-index * - Code - Name @@ -222,6 +225,7 @@ Push rules (``CC3xx``) .. list-table:: :header-rows: 1 :widths: 10 26 44 10 10 + :class: rules-index * - Code - Name @@ -382,8 +386,8 @@ Use instead: **Options** -* ``commit.subject_max_length`` — the limit, in characters. Unset by default, - meaning no limit. ``50`` and ``72`` are the conventional choices. +* ``commit.subject_max_length`` — the limit, in characters. Defaults to ``80``. + ``50`` and ``72`` are the other conventional choices. .. _cc005: @@ -414,8 +418,7 @@ Use instead: **Options** -* ``commit.subject_min_length`` — the minimum, in characters. Unset by default, - meaning no minimum. +* ``commit.subject_min_length`` — the minimum, in characters. Defaults to ``5``. .. _cc006: @@ -641,8 +644,6 @@ Use instead: **Options** * ``commit.require_signed_off_by`` — set to ``true`` to enable this rule. -* ``commit.required_signoff_name`` — require the trailer to carry a specific name. -* ``commit.required_signoff_email`` — require the trailer to carry a specific email. .. _cc013: @@ -689,14 +690,14 @@ accepts letters (including accented Latin characters), spaces, and 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 -``root``, ``ubuntu``, and ``ec2-user`` — authorship that cannot be traced back -to a person, which matters for both code archaeology and compliance. +machine accounts — authorship that cannot be traced back to a person, which +matters for both code archaeology and compliance. **Example** .. code-block:: bash - git config user.name root + git config user.name ec2-user Use instead: @@ -709,6 +710,13 @@ Use instead: * ``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. + .. _cc102: author-email (CC102) @@ -717,20 +725,25 @@ author-email (CC102) **What it does** Checks the committer's configured email against a pattern. The built-in pattern -(``^.+@.+$``) only requires something that looks like an address. +is ``^.+@.+$``, which only requires an ``@`` with something on either side. **Why is this bad?** -An unset or placeholder email breaks the link between a commit and its author: -forges cannot attribute the commit to an account, and mailmap-based tooling -cannot merge identities. Organisations that require contributions from a -corporate address can tighten the pattern to enforce it. +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: + .. code-block:: bash - git config user.email root@localhost + git config user.email you@gmail.com Use instead: @@ -740,8 +753,15 @@ Use instead: **Options** -* ``commit.author_email_pattern`` — a custom regex replacing the built-in - pattern. For example, ``"^.+@example\\.com$"`` to require a company domain. +* ``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 ------------ @@ -782,7 +802,7 @@ Use instead: **Options** -* ``commit.conventional_branch`` — set to ``false`` to disable this rule. +* ``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``, diff --git a/tests/rules_catalog_test.py b/tests/rules_catalog_test.py index 353d72e4..9d7cecd2 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,12 +16,7 @@ RuleCatalogEntry, ) from commit_check.rule_builder import RuleBuilder -from commit_check import ( - DEFAULT_BOOLEAN_RULES, - DEFAULT_BRANCH_TYPES, - DEFAULT_COMMIT_TYPES, - DEFAULT_PUSH_RULES, -) +from commit_check.config_merger import get_default_config ALL_ENTRIES = [*COMMIT_RULES, *BRANCH_RULES, *PUSH_RULES] @@ -132,13 +128,13 @@ def test_every_rule_is_documented(self): def test_every_rule_has_a_section_heading(self): """Each rule needs a ``name (CCxxx)`` heading, not just an anchor. - An anchor alone would satisfy the test above while linking readers to - an empty part of the page. + 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.rst") for entry in ALL_RULES: heading = f"{entry.name} ({entry.rule_id})" - assert heading in content, ( + assert heading in _rule_section(content, entry.rule_id), ( f"docs/rules.rst has no section titled '{heading}'" ) @@ -146,10 +142,8 @@ def test_every_rule_has_a_section_heading(self): def test_every_rule_explains_itself(self): """Each rule section must answer what it does and why it matters.""" content = _read_doc("rules.rst") - # Split on the anchors so each rule's prose is checked in isolation. for entry in ALL_RULES: - _, _, after = content.partition(f".. _{entry.rule_id.lower()}:") - section = re.split(r"\n\.\. _cc\d{3}:", after)[0] + 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}" @@ -160,51 +154,56 @@ class TestDocumentedDefaults: """The documented defaults must match the ones the code actually uses.""" @pytest.mark.benchmark - def test_boolean_defaults_match_configuration_docs(self): - """Every boolean option's documented default matches the source. - - The options table in ``docs/configuration.rst`` is maintained by hand. - Without this guard it silently drifts away from - ``DEFAULT_BOOLEAN_RULES`` whenever a default changes. - """ + 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.rst")) - expected = {**DEFAULT_BOOLEAN_RULES, **DEFAULT_PUSH_RULES} + 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.rst" + ) - for option, default in expected.items(): - assert option in documented, ( - f"'{option}' has a default in the source but no row in the " - f"options table of docs/configuration.rst" - ) - assert documented[option] == default, ( - f"docs/configuration.rst documents {option} as " - f"{str(documented[option]).lower()}, but the default is " - f"{str(default).lower()}" + @pytest.mark.benchmark + 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.rst")): + assert option in runtime.get(section, {}), ( + f"docs/configuration.rst documents [{section}] {option}, which " + f"does not exist in get_default_config()" ) - @pytest.mark.parametrize( - ("option", "expected"), - [ - ("allow_commit_types", DEFAULT_COMMIT_TYPES), - ("allow_branch_types", DEFAULT_BRANCH_TYPES), - ], - ) @pytest.mark.benchmark - def test_list_defaults_match_configuration_docs(self, option, expected): - """The documented list defaults contain exactly the real values. + def test_documented_defaults_match_the_runtime(self): + """Every documented default equals the value the runtime actually uses. - Compared as sets: these are allow-lists, so the order they are listed - in carries no meaning and should not fail the build. + ``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_list_default(_read_doc("configuration.rst"), option) - assert documented is not None, ( - f"'{option}' has no list[str] row in the options table of " - f"docs/configuration.rst" - ) - assert set(documented) == set(expected), ( - f"docs/configuration.rst documents {option} with " - f"{sorted(set(documented) - set(expected))} that are not defaults, " - f"and is missing {sorted(set(expected) - set(documented))}" - ) + documented = _parse_options_table(_read_doc("configuration.rst")) + 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.rst 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.rst documents [{section}] {option} as " + f"{cell.strip()!r}, but the runtime default is {expected!r}" + ) def _read_doc(name: str) -> str: @@ -212,40 +211,59 @@ def _read_doc(name: str) -> str: return (Path(__file__).parent.parent / "docs" / name).read_text(encoding="utf-8") -def _parse_options_table(content: str) -> dict[str, bool]: - """Extract ``option -> documented default`` for boolean rows. +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"\n\.\. _cc\d{3}:", after)[0] + + +_OPTIONS_ROW = re.compile( + r"\*\s+-\s+(commit|branch|push)\s*\n" # section + r"\s+-\s+(\w+)\s*\n" # option name + r"\s+-\s+(bool|int|str|list\[str\])\s*\n" # type + r"\s+-\s+(.+)\n" # documented default +) + - Matches the five-cell ``list-table`` rows in the options table, e.g.:: +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 - - allow_wip_commits - - bool - - true - - Allow work-in-progress commits. + - subject_max_length + - int + - 80 + - Maximum length of the subject line. """ - row = re.compile( - r"\*\s+-\s+(?:commit|branch|push)\s*\n" - r"\s+-\s+(\w+)\s*\n" - r"\s+-\s+bool\s*\n" - r"\s+-\s+(true|false)\s*\n" - ) - return {name: value == "true" for name, value in row.findall(content)} + return { + (section, option): (type_, cell) + for section, option, type_, cell in _OPTIONS_ROW.findall(content) + } + + +_QUOTED = re.compile(r'^(?:``(.*?)``|"(.*?)")') -def _parse_list_default(content: str, option: str) -> list[str] | None: - """Extract the documented default for a ``list[str]`` option. +def _documented_default(type_: str, cell: str) -> Any: + """Turn a documented default cell into a comparable Python value. - Returns ``None`` when the option has no ``list[str]`` row, so the caller - can tell "undocumented" apart from "documented as empty". + 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. """ - row = re.search( - rf"\*\s+-\s+(?:commit|branch|push)\s*\n" - rf"\s+-\s+{re.escape(option)}\s*\n" - rf"\s+-\s+list\[str\]\s*\n" - rf"\s+-\s+(\[.*?\])\s*\n", - content, - re.S, + 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) + return ( + quoted.group(1) + if quoted.group(1) is not None + else quoted.group(2) + if quoted + else cell ) - if row is None: - return None - return re.findall(r'"([^"]+)"', row.group(1)) From c76cfc616de73fe1f758e1fc67d4b93f3e16f8ac Mon Sep 17 00:00:00 2001 From: shenxianpeng Date: Mon, 3 Aug 2026 14:37:39 +0000 Subject: [PATCH 3/9] test: narrow the documented-default parser to a type mypy accepts The regex match may be None, so reading .group() from it before the None check failed mypy and took the lint session down with it. --- tests/rules_catalog_test.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/tests/rules_catalog_test.py b/tests/rules_catalog_test.py index 9d7cecd2..985870b5 100644 --- a/tests/rules_catalog_test.py +++ b/tests/rules_catalog_test.py @@ -260,10 +260,9 @@ def _documented_default(type_: str, cell: str) -> Any: if type_ == "list[str]": return re.findall(r'"(.*?)"', cell) quoted = _QUOTED.match(cell) - return ( - quoted.group(1) - if quoted.group(1) is not None - else quoted.group(2) - if quoted - else 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 From 8ac35f577c53f4af5cdab86e51a51862e1c581ee Mon Sep 17 00:00:00 2001 From: shenxianpeng Date: Mon, 3 Aug 2026 14:41:48 +0000 Subject: [PATCH 4/9] test: stop benchmarking the documentation consistency checks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CodSpeed reported test_every_rule_is_documented as a 27% regression. It is not one: that test reads docs/rules.rst and scans it for anchors, and this branch grows that file from 7.4 KB to 22.2 KB. A 2.99x larger file taking 1.37x longer to read is the expected, sub-linear result, and no runtime code changed on this branch at all. The marker is documented as meaning "performance-related tests", which these are not. Left in place, the performance gate would report a regression every time somebody expands the rules reference — penalising the act of writing documentation. The catalog and rule-builder tests in this file keep their marker; those do exercise the package. --- tests/rules_catalog_test.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/tests/rules_catalog_test.py b/tests/rules_catalog_test.py index 985870b5..15bd7fc6 100644 --- a/tests/rules_catalog_test.py +++ b/tests/rules_catalog_test.py @@ -109,9 +109,14 @@ 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. @@ -124,7 +129,6 @@ def test_every_rule_is_documented(self): f"{entry.rule_id} ({entry.check}) is missing from docs/rules.rst" ) - @pytest.mark.benchmark def test_every_rule_has_a_section_heading(self): """Each rule needs a ``name (CCxxx)`` heading, not just an anchor. @@ -138,7 +142,6 @@ def test_every_rule_has_a_section_heading(self): f"docs/rules.rst has no section titled '{heading}'" ) - @pytest.mark.benchmark def test_every_rule_explains_itself(self): """Each rule section must answer what it does and why it matters.""" content = _read_doc("rules.rst") @@ -151,9 +154,11 @@ def test_every_rule_explains_itself(self): class TestDocumentedDefaults: - """The documented defaults must match the ones the code actually uses.""" + """The documented defaults must match the ones the code actually uses. + + Not benchmarked, for the same reason as :class:`TestRulesDocumentation`. + """ - @pytest.mark.benchmark 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.rst")) @@ -164,7 +169,6 @@ def test_every_runtime_option_is_documented(self): f"has no row in the options table of docs/configuration.rst" ) - @pytest.mark.benchmark def test_no_invented_options_are_documented(self): """The options table does not document options that do not exist.""" runtime = get_default_config() @@ -174,7 +178,6 @@ def test_no_invented_options_are_documented(self): f"does not exist in get_default_config()" ) - @pytest.mark.benchmark def test_documented_defaults_match_the_runtime(self): """Every documented default equals the value the runtime actually uses. From d684332129f074a49b1d742ea91358d5892f7a37 Mon Sep 17 00:00:00 2001 From: Xianpeng Shen Date: Mon, 3 Aug 2026 19:51:29 +0300 Subject: [PATCH 5/9] docs: rebuild the documentation site on MkDocs Material (#516) --- .github/workflows/main.yml | 7 +- .gitignore | 4 + .pre-commit-config.yaml | 5 + commit_check/rules_catalog.py | 2 +- docs/README.rst | 21 - docs/_static/extra_css.css | 71 --- docs/_static/logo.jpg | Bin 10250 -> 0 bytes docs/assets/extra.css | 89 +++ docs/assets/favicon.svg | 8 + docs/assets/logo.svg | 8 + docs/changelog.md | 187 ++++++ docs/changelog.rst | 227 ------- docs/conf.py | 161 ----- docs/configuration.md | 305 +++++++++ docs/configuration.rst | 476 -------------- docs/example.md | 374 +++++++++++ docs/example.rst | 397 ------------ docs/getting-started/installation.md | 78 +++ docs/getting-started/quickstart.md | 148 +++++ docs/getting-started/why.md | 56 ++ docs/guides/ai-attribution.md | 75 +++ docs/guides/github-actions.md | 81 +++ docs/guides/organization.md | 100 +++ docs/guides/pre-commit.md | 89 +++ docs/guides/signoff.md | 81 +++ docs/index.md | 189 +++++- docs/migration.md | 189 ++++++ docs/migration.rst | 203 ------ docs/rules.md | 695 +++++++++++++++++++++ docs/rules.rst | 891 --------------------------- docs/troubleshoot.md | 41 ++ docs/troubleshoot.rst | 45 -- docs/what-is-new.md | 366 +++++++++++ docs/what-is-new.rst | 420 ------------- mkdocs.yml | 130 ++++ netlify.toml | 30 + noxfile.py | 6 +- pyproject.toml | 2 +- scripts/mkdocs_hooks.py | 89 +++ tests/rules_catalog_test.py | 48 +- 40 files changed, 3426 insertions(+), 2968 deletions(-) delete mode 100644 docs/README.rst delete mode 100644 docs/_static/extra_css.css delete mode 100644 docs/_static/logo.jpg create mode 100644 docs/assets/extra.css create mode 100644 docs/assets/favicon.svg create mode 100644 docs/assets/logo.svg create mode 100644 docs/changelog.md delete mode 100644 docs/changelog.rst delete mode 100644 docs/conf.py create mode 100644 docs/configuration.md delete mode 100644 docs/configuration.rst create mode 100644 docs/example.md delete mode 100644 docs/example.rst create mode 100644 docs/getting-started/installation.md create mode 100644 docs/getting-started/quickstart.md create mode 100644 docs/getting-started/why.md create mode 100644 docs/guides/ai-attribution.md create mode 100644 docs/guides/github-actions.md create mode 100644 docs/guides/organization.md create mode 100644 docs/guides/pre-commit.md create mode 100644 docs/guides/signoff.md create mode 100644 docs/migration.md delete mode 100644 docs/migration.rst create mode 100644 docs/rules.md delete mode 100644 docs/rules.rst create mode 100644 docs/troubleshoot.md delete mode 100644 docs/troubleshoot.rst create mode 100644 docs/what-is-new.md delete mode 100644 docs/what-is-new.rst create mode 100644 mkdocs.yml create mode 100644 netlify.toml create mode 100644 scripts/mkdocs_hooks.py 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 cc322201..00000000 --- a/docs/_static/extra_css.css +++ /dev/null @@ -1,71 +0,0 @@ -tbody .stub, -thead { - background-color: var(--md-accent-bg-color--light); - color: var(--md-default-bg-color); -} - -.md-header, -.md-nav--primary .md-nav__title[for="__drawer"] { - background-color: #2c9ccd; -} - -/* Sidebar section headings ("Getting started", "Reference", ...) */ -.md-nav__item--section > .md-nav__link { - font-weight: 700; - color: var(--md-default-fg-color); -} - -/* 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; -} - -/* Align table cells to the top so short cells line up with a wrapped message. */ -.md-typeset table td, -.md-typeset table th { - vertical-align: top; -} - -/* In the rule index tables only, keep rule codes, names, and CLI flags on one - line so the tables stay scannable. Other tables (such as the configuration - options table) hold long regexes and lists that must stay wrappable. */ -.md-typeset table.rules-index td code { - white-space: nowrap; -} diff --git a/docs/_static/logo.jpg b/docs/_static/logo.jpg deleted file mode 100644 index ed4cd723de56127ed77189e523cf63ebd651020d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10250 zcmeHtdpwls`~O3QEzxI7vQD+_Zd)vEku#fJZ23?aGc*l4AUPk#VH`_`-4dM;<50<9 z4vsMf!{|f~BTWwmV@OOM$7s-K_}-8Ge18A@{{Q~<^_SOk-}iN2*ZVr$*J1uX>1elg z^LLve2-l&fo1Ia#xI=pQ|1EqL(iVIQYN6 z|1AW(_U9Zlg*du1E49*sz0l+Y7ycVAa*Z00tZb(axb_CJEwP&PQseU359+@0quU+l z0(X{wodIVcj;x?Wav7O{ML5q>0-9tB(SrB6Mg<(W(u>7mwdr~#dUe84{f2VM@TK^D zP5z&{Wft?S6`+92E7&!<1VPkm65^cA);vB6;K|vg%l4k7*Ud@|X zx?{vmpxzaZ%SMxP~}~Fg|nYjS3d$4J?n?N*!%;Y4AEv|h%Lj_{@*v{AAsm`&owk*SpuR`-*tJqgH8P0c)$wx7TsJ94 zs+BlGu#=a0kgj44d8Nk@U1Yv4QY!w{KHg3(qk5Q|DsXgnfEAS-jKxDqGv&;DJCf%> z%LJ@_i+;}%Jd!YBHMl9*l*a6EEMA2_ZiA|v6=agU#rm|_3*YViWRqOgfkeV=6xLu* zfw8F~r{d=;5T}wAFLr>Jj2N3s-!AFw!{j>&e!aTQO@{VaUKc7~oe>%|jx@s}Wp1Uq z5jMEER9$nU*Undt5Y+yjOtZr%2O73}!47bt{9yO> z?!s=~1C?L!95nD+wK5X4vh*z7EY3tIB;}q2(%DE7Y1+k&p%J$&B8lQ>sA5L|4`z?H z$wD{hj=@n_E9dx$Vcpni3IfBdf@EmwK47n7Ko!WFfFpJ_yN{1lB1_6sW?xkP-ztxZ zu{xdGL*iOBQY8f_Q3)Vgj}l#&&xNyKshxb*J%6e%PFWjp$V3Gz6R;(ZC`T8Tv~s*X zN=x*L6UI>UQP-6FXI(!&hWGMU<%`*dwW*~g9;c-Jz-ZQ_!@5w54X~}nHl|T}bK+ss z)*PvWcn-aD4BXjbkFGsS^f%RC{;?prgFi{DLU|oSdEEqz%~&h{l(Y-kM~&UeJru0! zY83-;(^msrm7&TF%Ed@A;$h0o^F4f_s})00cG4D5j2xv=J&RZKkyKz5ylR+C%Rec1 ziTDHvXeRcgY113SB5s@1*Gq85h&wxs|p+RX>T zU06>`(^lEjt`_wriXH~WTR7I}jU(LR1iD}S$GQQ%T%5b&1S$g6trop4@quBK49-T;Gvu~gUcnv|4uAwcHcQt z0G}h*{Cyx9LRU%}8~xI!{O#o3Zzsbh4g-H!9RiHM1=j4f@HG+cs#ZHJN3+C87kehD z&F)af4PdY2u_mmQXMFR=-CO2YDHY!~;RsgfAt!L?aL&p2L@{sOLw}wPG~FBHNaO)F z?2ql)m?Vz6PEkv?IxUC|$*}p-g5YQiRI8UaIo?h~SgEu4e)mxbdkHWzIS~ZwV}vg8_ENY=EFxgh#$smUx2fA}4%jfOAsY*tbUq;9+T5 zW=fNsfC;Z`-5(fX)@Y;|^w6Iu_j5LIFjl#E#@M+1u|~0$vqfUWwj@{Y_MahoqrU%l zjjOi;9o4?p-#?l(>H{=|9e54TVm)1R4}$#-6(x#|A?SWzbg38T+JuWIqPGu5_?!f{ zZ$KfS@Thi%>CYTOACfMJGl5spx8QAMyu@+#yqsqJ?=;U}T5a?re_XJYmvP&N1rk=r z#q!#Qe@bsQGGZtKmB#)XFbap%Bh4@$J_dk%i>Iv-Ed0U6#PcYl*W__c*mxk*md~Y# zD%A>&7zB54Xe`oNYyrCmV%6eKxTu+DvWB&`mRoYHu5+q&J6C!)a;CT-kCb5YpU~ zqDoiSD2JlcfDKQ~BfrA4fzn+`o4~8$)nI0r-ki^H)X4F^qetV?5~3zsAl(Om!E*wZ zWCVExq-toGn15jBs*O6?Ibit*xdf^1bO`q_Ct{RaS*GZzLS7Qh;uEhM3F->37jk|(pcl9P(FWjZ1wBO&p zj!t=UC z1fO~|V6SZcp<2C&XrER4#58!aTGeL!g^$hwL+PomSlt(XogO|x{?XDp3!Y=1uaT8@ z638IjepIIGm3q9JvF{mi43JQy?~y?tPqtb1bdHD_Wpd(T+m{ha4%79<_y~P*ym#z^ z)*;c7)u5^6cBmfYD7sDevX2jc>0BZ&^Ju-*E5RRBvlia(#2b%)aW69R@%Gou|0YQj zs3&6}@)-V|QSX4xHT{deg+b@JHuRlb+HinXi^AmczoDty)(Yhf+`vx*dzraCm^q~; z#^*n6MGdw5Fs-r}da=cmUz>uiC#;$eg?~6KQw|)36K2Zv+vNLXI~hBinsF{Gj9b4W z$aB03=Xmj}Nj)*{Cn)kaAfILX$;z7CzWbs-z5V%NdXIO^c=Ju7l!cSg^wGmI2lt-a z2Q1Njy7atez;~JzO@4noe-ysr*iQ;fBlmT7=8JGek?pP5V?={__}c9Qg>&x~6Ry0f znN}$u)#T4{GDL6Fi7^x@ydiSg&@7i|Ota~T-q^Ptus0v=W#-9XJ(*DdvXJ^IT9WbF0WV+7^8~Nk2`$2-;t^Fn>EWSnK^EkGe{wu84B{ zn*243SRr_N^IBG9bvJ=Hknw!6r^8({;8b<(2|ImvrnuS{SV{%uOIW1FY)+-MN4GEnCSDk)rOFwo=;iO0}wOIH6p|8MT^57S&c+D8-Y`+^qC3;E$(+S4GjGCnhnTTh=lZxiThduaWLI`EMSA5B4s&w_ekNbx{j zMMTe8qU}vq*xHoOz>!!|y^w3%9-i|qpy|4@pNSs9y~S5{*cjvu=Q^FA>U~_+_c8IP z+HqyN``ep`W?i4Tw;a@%R22N(StD5VT3Czs`0w`7oVNiwx&=*-EDQNqe(F*WFSqU? zf1EqLs4=n_oqCR+HyMrhVMmNjjW=y>NbTCl2}L2uDl9(z_AE}2dcG%T@3p9|dH-;8 z)%K247r034*y>XsJc0e>rOMj65ra2#mv4qjMz?pl5B{Y_dU!^cMa8K4&4ZH+ie108 z$z&(px8^}r<^ZbMK9GQU<+#fg&5kuR3$hEhrMAU{yy~*d+asPOe9Kdx33m1S6nbo1jIm}h1pY^GmOe#wmeG87y5Uk85WKAz6}{g+s-yX8uB zO-c{O)R&dwy%Hz*-REnE&8_Vwch~8mZzMlQ5a9b>>urE&Dq{rr$A4TgiQi#bd8TK& zq&;M!4Gkb>Ab_MCZa3n##4ZNv>&9|z5*9r=VLZhiw{E3(tQ8J&b$x{ykGAQ7#r#n7 zaOI(M{C~9NB?rvHxQgDO*;RXmeW)GjZq2DmIoj85@0d3fuva~(9d#lUaYbMZp+4@_ zq~SpqjR9TZ$Fw$+Nzn|^oj-k}|B&ZDn|=klAMG$b8UOjf$RSZq;WqB*>4DWq+~tff z|45L!j#SStq@pgvmlcFJ0m3n+MHv`Y>`#+!A+syZy+a59Wm=rz(cE$JME3=RlzLGyBHIK2y#Hs&Tl>vShecF{BK0p{TWt1pzEBm7_>>m_zH#hHxUpn}%M&4Vv zs7yfsq!T4f(RlWE~TNPQ@dedgzi?^Y%*SIRT>|*W^rj{#|eCj^lyI)ZGcJXoK zk+%15-p53mX^+j_*8E`3D1n03zUG!*u`K>oG@v(;;oPK`S-OteUuG#v5Qkx2EC`g3 z_hM>254libUNS1IiSc1JdVfw~ES;{&@imCtG}%CT7Q5Wme*h0+g4azDSWhcql|L?b zDa?%RURjX5NPpzr5o0^5Z)VbmL3;i0W2oul1JpLTSq^ynt2N446%1IWF#}BOGMfY2 z=O!z9QR4@G~$ ztJ;#oe;;niE>S$imQ5m{Z?2F>-rV1S8`1MSKvcdE^)kpz)F65{9o%M&9C3o&)B#c# zjr%Z9Yqs=y?0h73(79tqP%*zd-Mw+4HRhB9iRXw_y;f{3_xSmlL_Zl$;UP%3Yyh`bBFRD;S!ydM6J0@Z)HkdF(T%Te zOY)AWr?8TJu}j)H-u#9FQnYv+)DLd)pkXm%$*jYP)f@&UXHLa?i1)*>G-V79r(IS` z*8_!5e68Qj?{7v~i^t&KX)Yj{Jz|7ua%G#Zt!(sC{`m^0j^*N#RU0fegssYE+y(5q zfu5$dcfTjBs=!3p)nQ=P@-(4QELlMU@oQ5SaJ}mM#vF|FxR_Xwtoz8SEYF=_WeSQI z5Mg>=79COH@Tg_M2UPZP=ZN`{DT!-&h?h7GUPT@wZxN{jeyIqy?b8eWuPpqclQWnqb; zW$}cyT2N@9_%Eayb^u9vx$O>XFE~C{Tma)RQSKa(Abe6>2peHRU!t#}f)vWz$&321 z*pds%Fc)~qAaHW52Nal&JT+^m?=S$dZPAcm9`=@_xWp&L$BQ`>>*zD$#|Q&9BXfTa z8<@d>n(&;1$edMuliFgK>%S?<0vHwziKTfK&sdnY?y)>K2_TFzARJ%FM!aEs*#6Et zgC%}*U)9tq6Mn#9tUTFgBDO(2_pBA@kIPCED#s2aUtgg!di~%;ng@JankSK4ixgmQ{6>xa5v0U;9xoIcQzchW*ud$ls!ZTX#^6Ph$C1#N1g5E-)mqR;dY_B?f7&V=o=^@{lS z51-wKDZ*Ic<3W|(j#xP0AFnmm#f0pHdO+bpzpHz|?p7GDZD(*{%ov zWrGY!A?1?>Q$5Gqo#0_Xp9t6wb>sr(Zp0eo8Vy{rV$PhDdg9Aw0}fp43N#I0&b!1) zZ2@IbNn=^vD3EH($pLQ@tf9Fvsk8hWp_|O)h&`=KOY>}!KA&V!J29VCbR_UccJ0JZ zZLT4`NE2@D++<7KV|7|nV832mC#d=ZwgJmjSqiFZf5E2C2K76V9!ioa3)g~!SuNPX z3O}XM6WArS9Pcbbe?-lEASigt-GM2(#vNbnpw{_{gK1@ew-W)UOyYdonUxVMlz;89 zx_>BI_e4P5pRHIYXlI)djzg`*)68N+v}XDvUd`!HdohVa5!|`=2=x+6Nvz^f^?An3 zwDoSRjre|sXlZNG*2lo~-lsZj=wwxIrzWz+l2jziA@j}G-^HIUv1SO;vqXfWNE?2O zh_Y)|zxz#@^9*Q*TH7#Ysl!xs6p$>6{Za+t+1w`t*02U>c#(LKPN_5B!2VUPJCcr~ z{5L!rGuz9g&oIT3Qp(w7RR^7u+60cTkO6u(1FCcGw3#tj8>>Nr-1{pZ|DOSo-7UOChxLy5eX>a&`f=QqVWx%~R| zInsf&XV(yC3eAjEnzj87T+HMxo}TkA6@ecwg3 z@0T*36dhswmN1g{1RvasX*$I=#M{WY$)aAS$Ix;Yym{(u+;uo~GN^u6f*mh%(D%sJ z+9s0x?57xbZ_Uv4g8Gt)%lWu*AWp`W!@%zAo`N~ z0Xac=we#y^M!L}vscE7Y2myVdDM3Rvz_S=-nx(mMdtGg!m<->e%?6n&N8?}4xlr>N zjWNG=V~1|RCLoa9QH1PYWH@=HYtpq`2smjOQuXSUyZRbwQv4P<2HzxOy@UHCOZgb- zN%6z4YpryT(-vhDi8QF(;H$PtIb!KyQBy~i2w_p$tt#y5EM@v(A{?PJ?uo;Ju9NgP z;_R8%*!FswXKU*YCDA?%AlO5<-c+n~u0;}jOj%9-sKva|M)M2rx!OcXeG zofR$-KVL?!(|XgCp1tb#s7N4yC|CXh7#@LL>sL5uSdd(PXUIc(oX7{SLx=%T40n|x zsX$!8f>RKcOB>39k3tzafZ2K+*6NOP>wEJu2@mN9kK(W!ptHT9(r@LXj-C9O>G)#O zA4EUByE0yzz;j254m`eRfk+U)LmuA;9k?sj3ITU}4F#4Gr2D9wy{c;(wRD%}M#u+* zcd}1nkhZraw`AxZxDERk*!s8^V>R^j){ezzo|vw(QqX*YBEj6_2&_ZRFC98)s-QD_ z1EC|yAP{gIfz$mI>kb|qvaS3XZvsJjOxzQovdL=`SfIo@@p7Iq0RYEIc=6onA2x7q zfb;;_1b1O(ztg;u9u_ZNMx^ebyJZ8KH}|Z{5*kUukGtt9BHRgDsU~_cXlB>G)W$aD zwI4OEX{QRQdR@qFNC$M#YOBM^*KQ_8L~P6GU!ZNFse@Ta(kN&G5B9?j8(wL%#pjF% zUplD1UQUyP=;l|q4R0qnSv)2=6X9W9-Ao#-1v85(xhKh5A>@1}PMNm)xXveJXGY6a z2VVz<)`s$(pi&rE90J3boE;El;E%*2Q)7?^OsN1`ABRp$!^F6PQMSl)xb({3rrLa`2eHuhx|VVknZc1ADl$gk2Gq5zo(@*t%YCDfDRj*tA*=b6ik zcn(^Hn&`(;YC_>e&#}Ugcmm%be69^xh?fUoUhw5NUT1;+vHhoU`o(!p0-!sn15JTr zRY3!@H&y9b-b%Fi0i#Kl(+?Dr+C`( zS{%S%lIeqhL_@|!q%fl3<8BRRk!vs`2n=4Jag=rSUW43y4ZHVl#`5vm*WRpHZ1ma- z>W)|u8`gI%)6%>`h;fQr+ftFzsTbzxYaTvP|&m%}L6(9#DG@wVlICPV0cmCm* z1FVLLsbO@^!ucN9$U>-xOLKYu)9*a^22pdbP(kkiB429jCc^G!ipIOWqhiD@U*cuG zHlVu~QV~_!O*zr|X?|#U6aD4B9W`d~s3-q$o zM(-k@Kyra13W3gTBWdB|ZuO7j6uhU<0H{lM10ax+X_$Mao)mF=Yxpto3%Zi*Z-9rC z&&$YulM}T+8{tlr4jn=FZUKAgo+Tcc3s)8*!^Mk8DDbVMMDUcRh-Z*YY805nj0aKk zFl^MI+28qIFf@D!ncyc=>_U!th^O!_qytUyh&U! zDf-o(scJG=?#;or ze~JDUK!5NwyHO%QZ*m7ZY9H&T;O&$FXL3QnM&kp}vtWS7nP(LPEL*_Piq{BSDV@ojZ8(*FTRVzrgOW9jFNm zd$9l~BUBJGAP7quDt_0!2iqEXXSD#`uY!s_eFbq+bF-pNQmyEgsL*s-bgyO|@g1YU zE-IWDip9uy`2hAVMu+hhoq`4o;d@lTy(~lnG15&lE#TBI4QD_S7!fX3hPTU$prmww zKRuO+ZOZ18?7|$xz9wc0kQw?h!-}f6OY`jOfHEah^cx5W^o#*R!oh{b?xPv4hH%xq zKNzop$CREyrW#?J?%?evGsrgi7AR-m1I`RB_WxLn^iw>q18)XApk=8;FII$^-KBZH zJPp^SrYQ?y(}Bxp2T@lkqzGQkYljGexCj=m?+4v-x4;v{9s-|?XF9Y6yX#PrD|%f` zGOUlARo6V;Pwn|=ktH_ukyy#3AwG7s%yilW{O(b#(~e1p+rJdm5Q4 zWDFUawra#bmE1%l1OX^za+GorObR7n_>gs-Ox;2&LgAi7;np`Y==|yHWcShW;Wi`- zZb8E;5j+9({4`A3MYi4pRtd>o{21jv7o{Y+M&Vxp+*MvLrRL$;F{rKW1Ii@Tl}es-SWnv?x)1h1qI0(MgBBjTp38Qp`KglLTRa%3ma4hqn}4ovY{jv zXc_hl5aD1<<}Ia4dm~Ju3mZ(4AU8Xdbp0ceyZ8fkL3iXj%}NufqX$*TYH5iUPWt@g zZnFp%u>^5P16&;lI7goU2qu4e7u;3kX+=gVrywsBu+DP!aa0LD5#dB9th|g@Df;o# zCnSdw9@9S4MyX2E{tjg?3y?MmqI)=7ct_kmSIYu5?PXMY0niU+LiB+Zz7sI4%^C`yy%24<$%C*6~yr)y>{el;5P;lw|~K5 zx@fDR0GB?rW9$>_BK|;8dEPyQV_%3+fckzwKh{@HT+Wgvho2TiM9zWm195Btk!Vk1 z5J>ygEZx}A!=eowTL4sTjUO=xv!|X#au7c|ny(D0gH+)mB~)2t(E0P}r%SjlP?UgQ zmjJ;7Vu@nAEvp@5TCX=y?lqvUzQsEL3&NJK9ciwK`1lK#^1p!sz)i;QUOH5Lqe^V< zSRrG7l1Y*Qqb4PBH}W@pX;WN}7!Idq>J3*xNqYeeph+Jsb^|me16nWcr%zebjN-pXjxI}X(nM2EJ0Y}}0(BtC zKx*!enRjA%OSX@?EMyNn(QBU`U5h|qC6}>kNKWFXtRG~dNRSM8?JEta3;o;x6n&0G ztpoFJG1X)otc@sv0dIMjUYSuCpgv^KJ}_VQ+YWDl8i^{{CkuBmyv9 zA4=dIANc|s;g-rA8J2944S3tG!opjd75=w=;q-s^tRql 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..3aee9298 --- /dev/null +++ b/docs/assets/logo.svg @@ -0,0 +1,8 @@ + diff --git a/docs/changelog.md b/docs/changelog.md new file mode 100644 index 00000000..f1fccb02 --- /dev/null +++ b/docs/changelog.md @@ -0,0 +1,187 @@ +# 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 `forbid_ai_attribution` (boolean, default + `false`) rejects commits co-authored by AI coding agents. 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) + +.. 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](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 a64330e8..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 405949dc..00000000 --- a/docs/conf.py +++ /dev/null @@ -1,161 +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", - }, - }, - ], - # The global navigation lives in the left sidebar (no top tab bar), with - # each toctree caption rendered as a section heading. This keeps every - # page one click away and leaves the right-hand column for the page's own - # table of contents. - "features": [ - "navigation.sections", - "navigation.top", - "navigation.tracking", - "toc.sticky", - "toc.follow", - "search.highlight", - "search.share", - ], - # Keep the sidebar sections expanded rather than collapsing everything but - # the current page, so the whole documentation set is visible at a glance. - "globaltoc_collapse": False, -} - -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..ac72ea3f --- /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=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: + +```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 27e05772..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. - * Enforced by default: the Conventional Commits format (:ref:`CC001 `), the Conventional Branch format (:ref:`CC201 `), the subject length limits of 5–80 characters (:ref:`CC004 `, :ref:`CC005 `), and the author name and email patterns (:ref:`CC101 `, :ref:`CC102 `). - * **Off** by default: subject capitalization, imperative mood, body and signoff requirements, rebase requirements, and every ``allow_*`` restriction. - - See :doc:`rules` 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 `_, - 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 = 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:** - -.. 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 - - 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 `_ - * - 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 `_: 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/example.md b/docs/example.md new file mode 100644 index 00000000..5d90ade3 --- /dev/null +++ b/docs/example.md @@ -0,0 +1,374 @@ +# 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..23c9a9e0 --- /dev/null +++ b/docs/guides/github-actions.md @@ -0,0 +1,81 @@ +# 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. diff --git a/docs/guides/organization.md b/docs/guides/organization.md new file mode 100644 index 00000000..38b69956 --- /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 three lines: + +```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 b7b74a4f..6be368cc 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,31 +1,168 @@ -```{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 +--- -```{toctree} -:hidden: -:caption: Getting started -self -what-is-new -example -``` +
-```{toctree} -:hidden: -:caption: Configuring -configuration -migration -``` +# Clean commits. Clear standards. -```{toctree} -:hidden: -:caption: Reference -rules -cli_args -``` +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: -:caption: About -troubleshoot -changelog + [: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 verified at install time. + +- :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 5dd13895..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..e8b3f587 --- /dev/null +++ b/docs/rules.md @@ -0,0 +1,695 @@ +# 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 +`_ — 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 0f968695..00000000 --- a/docs/rules.rst +++ /dev/null @@ -1,891 +0,0 @@ -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: - -.. 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 dropping the -explanation, suggestion, and documentation link: - -.. code-block:: text - - [FAIL] CC003 subject_imperative: docs: revamped the profile - -How to read this page ---------------------- - -Rule IDs are grouped by what they inspect: - -.. list-table:: - :header-rows: 1 - :widths: 12 25 63 - - * - Range - - Category - - Inspects - * - ``CC0xx`` - - :ref:`Commit message ` - - The subject, body, and trailers of a commit message - * - ``CC1xx`` - - :ref:`Author ` - - The committer's configured name and email - * - ``CC2xx`` - - :ref:`Branch ` - - The current branch's name and its position relative to a target branch - * - ``CC3xx`` - - :ref:`Push ` - - 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: - -.. list-table:: - :header-rows: 1 - :widths: 20 80 - - * - 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 -:ref:`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 - :doc:`configuration`. - -Rule index ----------- - -.. _commit-message-rules: - -Commit message rules (``CC0xx``) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Run with ``-m`` / ``--message``. - -.. list-table:: - :header-rows: 1 - :widths: 10 26 44 10 10 - :class: rules-index - - * - Code - - Name - - Message - - Check - - Default - * - :ref:`CC001 ` - - ``message`` - - The commit message should follow Conventional Commits - - ``-m`` - - ✅ On - * - :ref:`CC002 ` - - ``subject-capitalized`` - - Subject must start with a capital letter - - ``-m`` - - ⚪ Off - * - :ref:`CC003 ` - - ``subject-imperative`` - - Commit message should use imperative mood - - ``-m`` - - ⚪ Off - * - :ref:`CC004 ` - - ``subject-max-length`` - - Subject must be at most ``{max_len}`` characters - - ``-m`` - - ✅ On - * - :ref:`CC005 ` - - ``subject-min-length`` - - Subject must be at least ``{min_len}`` characters - - ``-m`` - - ✅ On - * - :ref:`CC006 ` - - ``allow-merge-commits`` - - Merge commits are not allowed - - ``-m`` - - ⚪ Off - * - :ref:`CC007 ` - - ``allow-revert-commits`` - - Revert commits are not allowed - - ``-m`` - - ⚪ Off - * - :ref:`CC008 ` - - ``allow-empty-commits`` - - Empty commit messages are not allowed - - ``-m`` - - ⚪ Off - * - :ref:`CC009 ` - - ``allow-fixup-commits`` - - Fixup commits are not allowed - - ``-m`` - - ⚪ Off - * - :ref:`CC010 ` - - ``allow-wip-commits`` - - WIP commits are not allowed - - ``-m`` - - ⚪ Off - * - :ref:`CC011 ` - - ``require-body`` - - Commit body is required - - ``-m`` - - ⚪ Off - * - :ref:`CC012 ` - - ``require-signed-off-by`` - - Signed-off-by not found in latest commit - - ``-m`` - - ⚪ Off - * - :ref:`CC013 ` - - ``ai-attribution`` - - AI attribution policy violation - - ``-m`` - - ⚪ Off - -.. _author-rules: - -Author rules (``CC1xx``) -~~~~~~~~~~~~~~~~~~~~~~~~ - -.. list-table:: - :header-rows: 1 - :widths: 10 26 44 10 10 - :class: rules-index - - * - Code - - Name - - Message - - Check - - Default - * - :ref:`CC101 ` - - ``author-name`` - - The committer name seems invalid - - ``-n`` - - ✅ On - * - :ref:`CC102 ` - - ``author-email`` - - The committer's email seems invalid - - ``-e`` - - ✅ On - -.. _branch-rules: - -Branch rules (``CC2xx``) -~~~~~~~~~~~~~~~~~~~~~~~~ - -Run with ``-b`` / ``--branch``. - -.. list-table:: - :header-rows: 1 - :widths: 10 26 44 10 10 - :class: rules-index - - * - Code - - Name - - Message - - Check - - Default - * - :ref:`CC201 ` - - ``branch`` - - The branch should follow Conventional Branch - - ``-b`` - - ✅ On - * - :ref:`CC202 ` - - ``merge-base`` - - Current branch is not rebased onto target branch - - ``-b`` - - ⚪ Off - -.. _push-rules: - -Push rules (``CC3xx``) -~~~~~~~~~~~~~~~~~~~~~~ - -.. list-table:: - :header-rows: 1 - :widths: 10 26 44 10 10 - :class: rules-index - - * - Code - - Name - - Message - - Check - - Default - * - :ref:`CC301 ` - - ``no-force-push`` - - Force push is not allowed - - ``--no-force-push`` - - ⚪ Off - -Commit message rules --------------------- - -.. _cc001: - -message (CC001) -~~~~~~~~~~~~~~~ - -**What it does** - -Checks that the commit message subject follows the -`Conventional Commits `_ 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** - -.. code-block:: text - - updated the parser - -Use instead: - -.. code-block:: 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+: .+"``). - -.. _cc002: - -subject-capitalized (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** - -.. code-block:: text - - fix: handle empty input - -Use instead: - -.. code-block:: text - - fix: Handle empty input - -**Options** - -* ``commit.subject_capitalized`` — set to ``true`` to enable this rule. - -.. _cc003: - -subject-imperative (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** - -.. code-block:: text - - fix: fixed a crash when the config file is empty - -Use instead: - -.. code-block:: 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 `_. - -.. _cc004: - -subject-max-length (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** - -.. code-block:: text - - fix: handle an empty config file and also fix the unrelated crash in the branch parser that happens on Windows - -Use instead: - -.. code-block:: 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. - -.. _cc005: - -subject-min-length (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** - -.. code-block:: text - - fix: bug - -Use instead: - -.. code-block:: text - - fix: reject config files with a null inherit_from - -**Options** - -* ``commit.subject_min_length`` — the minimum, in characters. Defaults to ``5``. - -.. _cc006: - -allow-merge-commits (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** - -.. code-block:: bash - - git pull - -Use instead: - -.. code-block:: 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. - -.. _cc007: - -allow-revert-commits (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** - -.. code-block:: text - - Revert "feat: add caching layer" - -Use instead: - -.. code-block:: 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. - -.. _cc008: - -allow-empty-commits (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. - -.. _cc009: - -allow-fixup-commits (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** - -.. code-block:: text - - fixup! feat: add the caching layer - -Use instead: - -.. code-block:: bash - - git rebase -i --autosquash main - -**Options** - -* ``commit.allow_fixup_commits`` — set to ``false`` to enable this rule. - -.. _cc010: - -allow-wip-commits (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** - -.. code-block:: text - - WIP: caching - -Use instead: - -.. code-block:: 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. - -.. _cc011: - -require-body (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** - -.. code-block:: text - - fix: cap the retry backoff at 30s - -Use instead: - -.. code-block:: 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. - -.. _cc012: - -require-signed-off-by (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 -`_ — 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** - -.. code-block:: bash - - git commit -m "fix: handle an empty config file" - -Use instead: - -.. code-block:: 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. - -.. _cc013: - -ai-attribution (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 ------------- - -.. _cc101: - -author-name (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** - -.. code-block:: bash - - git config user.name ec2-user - -Use instead: - -.. code-block:: 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. - -.. _cc102: - -author-email (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: - -.. code-block:: bash - - git config user.email you@gmail.com - -Use instead: - -.. code-block:: 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 ------------- - -.. _cc201: - -branch (CC201) -~~~~~~~~~~~~~~ - -**What it does** - -Checks that the current branch name follows the -`Conventional Branch `_ 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** - -.. code-block:: text - - my-fix - johns-branch-2 - -Use instead: - -.. code-block:: 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. - -.. _cc202: - -merge-base (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** - -.. code-block:: bash - - # branch was cut from main three weeks ago - git push - -Use instead: - -.. code-block:: 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 ----------- - -.. _cc301: - -no-force-push (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** - -.. code-block:: bash - - git push --force - -Use instead: - -.. code-block:: 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/troubleshoot.md b/docs/troubleshoot.md new file mode 100644 index 00000000..49c1653c --- /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 Specific Hook + +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 All Hooks + +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..1b480885 --- /dev/null +++ b/docs/what-is-new.md @@ -0,0 +1,366 @@ +# 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 `_. 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..a4d3c81a --- /dev/null +++ b/netlify.toml @@ -0,0 +1,30 @@ +# 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. +[context.deploy-preview.environment] + SITE_URL = "${DEPLOY_PRIME_URL}" + +[context.branch-deploy.environment] + SITE_URL = "${DEPLOY_PRIME_URL}" + +# Serve the legacy Sphinx URLs from the redirect stubs the build emits, and +# give anything genuinely missing the themed 404 page. +[[redirects]] + from = "/*" + to = "/404.html" + status = 404 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..38127834 --- /dev/null +++ b/scripts/mkdocs_hooks.py @@ -0,0 +1,89 @@ +"""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 redirects for the ``.html`` URLs the previous Sphinx site served, so + links already published elsewhere keep working. +""" + +from __future__ import annotations + +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 = """ + + + +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"]) + base = config["site_url"] or "/" + 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 15bd7fc6..e8c330be 100644 --- a/tests/rules_catalog_test.py +++ b/tests/rules_catalog_test.py @@ -122,11 +122,11 @@ def test_every_rule_is_documented(self): This prevents shipping a new rule without documenting it. """ - content = _read_doc("rules.rst") + 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): @@ -135,16 +135,18 @@ def test_every_rule_has_a_section_heading(self): 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.rst") + content = _read_doc("rules.md") for entry in ALL_RULES: - heading = f"{entry.name} ({entry.rule_id})" - assert heading in _rule_section(content, entry.rule_id), ( - f"docs/rules.rst has no section titled '{heading}'" + 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.rst") + 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**"): @@ -161,20 +163,20 @@ class TestDocumentedDefaults: 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.rst")) + 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.rst" + 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.rst")): + for section, option in _parse_options_table(_read_doc("configuration.md")): assert option in runtime.get(section, {}), ( - f"docs/configuration.rst documents [{section}] {option}, which " + f"docs/configuration.md documents [{section}] {option}, which " f"does not exist in get_default_config()" ) @@ -186,7 +188,7 @@ def test_documented_defaults_match_the_runtime(self): 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.rst")) + documented = _parse_options_table(_read_doc("configuration.md")) runtime = get_default_config() for (section, option), (type_, cell) in sorted(documented.items()): @@ -197,14 +199,14 @@ def test_documented_defaults_match_the_runtime(self): if isinstance(expected, list): # Allow-lists: order carries no meaning, membership does. assert set(actual or []) == set(expected), ( - f"docs/configuration.rst documents [{section}] {option} " + 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.rst documents [{section}] {option} as " + f"docs/configuration.md documents [{section}] {option} as " f"{cell.strip()!r}, but the runtime default is {expected!r}" ) @@ -216,15 +218,16 @@ def _read_doc(name: str) -> str: 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"\n\.\. _cc\d{3}:", after)[0] + _, _, after = content.partition(f"{{ #{rule_id.lower()} }}") + return re.split(r"\{ #cc\d{3} \}", after)[0] _OPTIONS_ROW = re.compile( - r"\*\s+-\s+(commit|branch|push)\s*\n" # section - r"\s+-\s+(\w+)\s*\n" # option name - r"\s+-\s+(bool|int|str|list\[str\])\s*\n" # type - r"\s+-\s+(.+)\n" # documented default + 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, ) @@ -245,7 +248,8 @@ def _parse_options_table(content: str) -> dict[tuple[str, str], tuple[str, str]] } -_QUOTED = re.compile(r'^(?:``(.*?)``|"(.*?)")') +# Markdown code spans use single backticks; RST used double. +_QUOTED = re.compile(r'^(?:`+(.*?)`+|"(.*?)")') def _documented_default(type_: str, cell: str) -> Any: From 6ad7945e20de5faaccf82cde9a72a87039cb3107 Mon Sep 17 00:00:00 2001 From: shenxianpeng Date: Mon, 3 Aug 2026 16:56:26 +0000 Subject: [PATCH 6/9] ci: expand DEPLOY_PRIME_URL where the shell can see it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The deploy preview failed to build: ERROR - Config value 'site_url': The URL isn't valid, it should include the http:// (scheme) Values in a [context.*.environment] block are literals — Netlify does not interpolate them — so SITE_URL reached MkDocs as the unexpanded string "${DEPLOY_PRIME_URL}", which is indeed not a URL. Set it in the context's build command instead, which runs in a shell where the variable expands. The redirect hook now also tolerates a site_url without a trailing slash, since that is the form DEPLOY_PRIME_URL takes. --- netlify.toml | 13 +++++++++---- scripts/mkdocs_hooks.py | 4 +++- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/netlify.toml b/netlify.toml index a4d3c81a..2b78261e 100644 --- a/netlify.toml +++ b/netlify.toml @@ -16,11 +16,16 @@ # 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. -[context.deploy-preview.environment] - SITE_URL = "${DEPLOY_PRIME_URL}" +# +# 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.environment] - SITE_URL = "${DEPLOY_PRIME_URL}" +[context.branch-deploy] + command = "pip install '.[docs]' && SITE_URL=\"$DEPLOY_PRIME_URL/\" mkdocs build --strict" # Serve the legacy Sphinx URLs from the redirect stubs the build emits, and # give anything genuinely missing the themed 404 page. diff --git a/scripts/mkdocs_hooks.py b/scripts/mkdocs_hooks.py index 38127834..ae9ee0f6 100644 --- a/scripts/mkdocs_hooks.py +++ b/scripts/mkdocs_hooks.py @@ -82,7 +82,9 @@ def on_pre_build(config, **kwargs) -> None: 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"]) - base = config["site_url"] or "/" + # 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" From a2dfd9ffa3daf14b595f1840ce5b45a17819866f Mon Sep 17 00:00:00 2001 From: shenxianpeng Date: Mon, 3 Aug 2026 17:04:42 +0000 Subject: [PATCH 7/9] docs: fix the redirect loop and eleven review findings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The deploy preview could not open /rules/. The build emits a redirect stub at rules.html for the old Sphinx URL, and Netlify normalises "/rules" and "/rules/" to the same resource, so that stub was served in place of the page it points at and redirected to itself forever. Turn the normalisation off and declare the legacy URLs as Netlify redirects, which resolve before file lookup and so cannot collide with the page they target. The emitted stubs stay for GitHub Pages, and now refuse to redirect a page to itself, so the same collision cannot reappear on another host. The stubs also carry the fragment across. The links most worth keeping alive are the per-rule ones — rules.html#cc003 — and a bare redirect dropped the anchor, landing the reader at the top of a 700-line page. From review, all verified against the source before fixing: - The RST converter missed four constructs: `.. Attention::` (it only matched lowercase), `.. tip ::` (a space before the colons), and two links whose text wrapped across a line. They were rendering as literal text. - The changelog documented `forbid_ai_attribution`, which does not exist; the option is `ai_attribution`, defaulting to "ignore". - The options table mapped `allow_force_push = true` to `CCHK_ALLOW_FORCE_PUSH=false`, describing opposite behaviour for equivalent settings. - Troubleshooting had its two bypass headings the wrong way round: --no-verify skips every hook, SKIP= skips one. - Link colour was #1e85a8 on white, 4.2:1, under the 4.5:1 needed for body text. Now #176b89 at 6.0:1. - The landing page claimed attestation "verified at install time"; only a manual verification step exists, so it now says what is true. - The organization guide said "three lines" above a one-line example. - pr-comments cannot post on fork pull requests, because a fork's `pull_request` run gets a read-only token that `pull-requests: write` does not override. Documented, along with why `pull_request_target` is not a casual substitute. --- docs/assets/extra.css | 4 +- docs/changelog.md | 7 +-- docs/configuration.md | 2 +- docs/example.md | 6 ++- docs/guides/github-actions.md | 21 +++++++++ docs/guides/organization.md | 2 +- docs/index.md | 3 +- docs/rules.md | 5 ++- docs/troubleshoot.md | 4 +- docs/what-is-new.md | 3 +- netlify.toml | 81 ++++++++++++++++++++++++++++++++--- scripts/mkdocs_hooks.py | 15 +++++++ 12 files changed, 133 insertions(+), 20 deletions(-) diff --git a/docs/assets/extra.css b/docs/assets/extra.css index d0ef3cca..5aab2bf1 100644 --- a/docs/assets/extra.css +++ b/docs/assets/extra.css @@ -4,6 +4,8 @@ --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); @@ -18,7 +20,7 @@ } [data-md-color-scheme="default"] { - --md-typeset-a-color: var(--cc-brand-dark); + --md-typeset-a-color: var(--cc-link); } /* Landing page ------------------------------------------------------------- diff --git a/docs/changelog.md b/docs/changelog.md index f1fccb02..f05bc244 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -10,8 +10,8 @@ Full changelog available at [GitHub releases](https://github.com/commit-check/co * **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 [#456](https://github.com/commit-check/commit-check/pull/456). + `[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 @@ -145,7 +145,8 @@ Full changelog available at [GitHub releases](https://github.com/commit-check/co ## v2.0.0 (2025-10-01) -.. Attention:: +!!! warning + This major release introduces significant architectural changes and breaking updates to commit-check. Please review carefully before upgrading. ### What's New diff --git a/docs/configuration.md b/docs/configuration.md index ac72ea3f..736511ef 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -254,7 +254,7 @@ CCHK_SUBJECT_MAX_LENGTH=100 commit-check --message | `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) | +| `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` | diff --git a/docs/example.md b/docs/example.md index 5d90ade3..df5fcaf3 100644 --- a/docs/example.md +++ b/docs/example.md @@ -91,8 +91,10 @@ Suggest: Use (): with allowed types 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. +!!! tip + + Validate commit messages by piping them through STDIN. This is useful for + testing or scripting. Available Commands see [commit-check --help](cli.md) diff --git a/docs/guides/github-actions.md b/docs/guides/github-actions.md index 23c9a9e0..4dc95ae1 100644 --- a/docs/guides/github-actions.md +++ b/docs/guides/github-actions.md @@ -79,3 +79,24 @@ 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 index 38b69956..1da9fd0e 100644 --- a/docs/guides/organization.md +++ b/docs/guides/organization.md @@ -23,7 +23,7 @@ allow_branch_types = ["feature", "bugfix", "hotfix", "release", "chore"] ## Inheriting it -Each repository then needs three lines: +Each repository then needs one line: ```toml title="any-repo → .github/cchk.toml" inherit_from = "github:my-org/.github:cchk.toml" diff --git a/docs/index.md b/docs/index.md index 6be368cc..1e203451 100644 --- a/docs/index.md +++ b/docs/index.md @@ -136,7 +136,8 @@ whatever your AI agent is committing on your behalf. --- - Build provenance with artifact attestation verified at install time. + Build provenance with artifact attestation you can verify before + installing. - :material-tag-outline:{ .lg .middle } __Stable rule IDs__ diff --git a/docs/rules.md b/docs/rules.md index e8b3f587..fdd0db18 100644 --- a/docs/rules.md +++ b/docs/rules.md @@ -445,8 +445,9 @@ Requires a `Signed-off-by:` trailer in the commit message. **Why is this bad?** -Projects that use the `Developer Certificate of Origin -`_ — the Linux kernel, and much of the +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. diff --git a/docs/troubleshoot.md b/docs/troubleshoot.md index 49c1653c..a84ca418 100644 --- a/docs/troubleshoot.md +++ b/docs/troubleshoot.md @@ -19,7 +19,7 @@ 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 +### Bypass All Hooks Use the `--no-verify` flag to skip the pre-commit hook: @@ -28,7 +28,7 @@ Use the `--no-verify` flag to skip the pre-commit hook: git commit --amend --author="Xianpeng Shen " --no-edit --no-verify ``` -### Bypass All Hooks +### Bypass A Specific Hook Alternatively, use the `SKIP=your-hook-name` environment variable, like below: diff --git a/docs/what-is-new.md b/docs/what-is-new.md index 1b480885..b45c3a02 100644 --- a/docs/what-is-new.md +++ b/docs/what-is-new.md @@ -56,8 +56,7 @@ so branches like `dependabot/go_modules/go-deps-c57c3fe1e0` and ### 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 +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 diff --git a/netlify.toml b/netlify.toml index 2b78261e..b203147d 100644 --- a/netlify.toml +++ b/netlify.toml @@ -27,9 +27,80 @@ [context.branch-deploy] command = "pip install '.[docs]' && SITE_URL=\"$DEPLOY_PRIME_URL/\" mkdocs build --strict" -# Serve the legacy Sphinx URLs from the redirect stubs the build emits, and -# give anything genuinely missing the themed 404 page. +# Netlify normalises "/rules" and "/rules/" to the same resource by default. +# The build emits a redirect stub at rules.html for the old Sphinx URL, and +# with normalisation on, that stub is what gets served for the real page too — +# so /rules/ redirects to itself and never renders. Turn normalisation off so +# each path resolves to exactly the file it names. +[build.processing.html] + pretty_urls = false + +# Redirect the URLs the Sphinx site served. These are declared here rather +# than left to the emitted stubs so they take effect before file lookup, +# which keeps them clear of the page they point at. `splat` carries any +# fragment through. +[[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 = "/*" - to = "/404.html" - status = 404 + from = "/genindex.html" + to = "/" + status = 301 + force = true diff --git a/scripts/mkdocs_hooks.py b/scripts/mkdocs_hooks.py index ae9ee0f6..f59b2edb 100644 --- a/scripts/mkdocs_hooks.py +++ b/scripts/mkdocs_hooks.py @@ -27,12 +27,27 @@ "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}… From f1b4631970c1caa27139cd4de58d56f736f2e545 Mon Sep 17 00:00:00 2001 From: shenxianpeng Date: Mon, 3 Aug 2026 21:35:55 +0300 Subject: [PATCH 8/9] docs: add the original logo as a vectorized SVG The previous logo.svg was a new line-art icon that did not match the project's actual logo. Vectorize docs/_static/logo.jpg (a 900x504 PNG in the old Sphinx site) with potrace instead, so the SVG is pixel-identical to the original: the 'commit' wordmark, the check mark and the branch shape in the brand color #2C9CCD. The old logo.jpg could not be used as-is on the new site: Sphinx's conf.py had it commented out ('can not display well in blue background'), it carried 10 KB of transparent padding around a 300x293 mark, and the new header renders the logo via , where currentColor would resolve to black. The traced SVG keeps the exact shapes with a hardcoded fill, so it renders identically in the light and dark palettes. --- docs/assets/logo.svg | 18 +++++++++++------- netlify.toml | 18 ++++++------------ scripts/mkdocs_hooks.py | 15 +++++++++++++-- 3 files changed, 30 insertions(+), 21 deletions(-) diff --git a/docs/assets/logo.svg b/docs/assets/logo.svg index 3aee9298..603c7b1c 100644 --- a/docs/assets/logo.svg +++ b/docs/assets/logo.svg @@ -1,8 +1,12 @@ -