diff --git a/docs/changelog.md b/docs/changelog.md index 5801395..c9fdcea 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -11,6 +11,7 @@ below and to the page that documents the feature properly. | Version | What changed | Documented in | |---|---|---| +| [2.15.0](#v2150) | `--rev` names the commit under test; skipped checks are named on stderr | [Command-line recipes](example.md#checking-a-range-of-commits) | | [2.14.0](#v2140) | CC003 judges imperative mood by a word's form, not by a list of verbs | [CC003](rules.md#cc003) | | [2.13.1](#v2131) | JSON output reports the checked value for passing checks | [Output for scripts and CI](example.md#output-for-scripts-and-ci) | | [2.13.0](#v2130) | Stable rule IDs in terminal output and JSON | [Rules reference](rules.md) | @@ -24,7 +25,49 @@ below and to the page that documents the feature properly. | [2.5.0](#v250) | Organization-wide config with `inherit_from` | [Integrations](guides/integrations.md#across-an-organization) | | [2.0.0](#v200) | Configuration moved from YAML to TOML — breaking | [Migrating from v1](migration.md) | -## v2.14.0 (unreleased) { #v2140 } +## v2.15.0 (unreleased) { #v2150 } + +### Added + +* **`--rev REVISION` names the commit under test** — anything `git rev-parse` + understands: a SHA, `HEAD~2`, `HEAD^2`. Message checks read that commit's + message, and the author checks read **that commit's recorded author, never + the local git config** — an existing commit's identity is a fact about the + commit, not about whoever runs the check. Before `--rev`, CI could not + iterate a pull request's commits without checking each one out, and a + malformed author on any commit passed as long as the operator's own config + was valid. A revision that does not resolve is a one-line error before any + check runs; combining `--rev` with a message file or stdin is rejected, + since each would name a second subject for the same checks. See + [Checking a range of commits](example.md#checking-a-range-of-commits). + +### Fixed + +* **The CLI no longer hangs on an open, idle stdin** — stdin was read + whenever it was not a terminal, for every check type. Under CI runners and + process managers that hand the process a pipe nothing ever writes to or + closes, `commit-check --author-name` blocked forever: a stuck step, not a + failed one. The read is now gated on data actually being available, and + genuinely piped input still works unchanged. + +### Changed + +* **Skipped checks are named on stderr instead of passing in silence** — a + check that had nothing to judge (a merge subject under the subject rules, + an absent message) reports a skip, and text mode prints one line naming + every skipped check: `⊘ skipped (not validated): subject-max-length, + subject-min-length`. Exit codes are unchanged and stdout is untouched. The + case that motivated it: on a `pull_request` checkout `HEAD` is the + synthetic merge commit, so a bare `commit-check -m` used to exit `0` + having validated nothing it was asked about. See + [When a check is skipped](example.md#when-a-check-is-skipped). + +* **Only git's literal merge and fixup prefixes bypass the subject rules** — + the bypass matched any subject starting with the word "merge" in any case, + so an author's own `merge the parser tables` escaped judgement. Now only + the machine-written forms qualify: `Merge ` (and `fixup! ` for CC003). + +## v2.14.0 (2026-08-12) { #v2140 } ### Changed diff --git a/docs/configuration.md b/docs/configuration.md index 6d5fe20..6b03041 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -167,7 +167,7 @@ Used from a hook definition, with no config file anywhere in the repository: ```yaml title=".pre-commit-config.yaml" repos: - repo: https://github.com/commit-check/commit-check - rev: v2.13.4 + rev: v2.14.0 hooks: - id: check-message args: diff --git a/docs/example.md b/docs/example.md index c58d8b0..fc443cb 100644 --- a/docs/example.md +++ b/docs/example.md @@ -9,7 +9,8 @@ in [Configuration](configuration.md). ## Checking a commit message -The message can come from the repository, a file, or standard input. +The message can come from the repository, a file, standard input, or a named +revision. === "From the repository" @@ -33,6 +34,21 @@ The message can come from the repository, a file, or standard input. $ echo "feat(auth): add OAuth2 login" | commit-check -m ``` +=== "From a revision" + + `--rev` names the commit under test — anything `git rev-parse` + understands. A revision that does not resolve is a one-line error before + any check runs. + + ```console + $ commit-check -m --rev HEAD~1 + $ commit-check -m --rev 1a2b3c4 + ``` + + A revision and a message file would name two different subjects for the + same checks, so passing both is rejected; stdin is likewise not consulted + while `--rev` is set. + ### Trying a message before you write it ```console @@ -91,6 +107,17 @@ Either flag works alone. [CC101](rules.md#cc101) and [CC102](rules.md#cc102) describe what the built-in patterns accept and how to tighten them. +Without `--rev`, these validate the *local git config* — whoever is about to +commit — falling back to `HEAD`'s author only when no identity is configured. +That is the right subject for a hook and the wrong one for CI: an existing +commit's identity is a fact about the commit, not about the operator running +the check. Add `--rev` and both checks read that commit's recorded author, and +the config is never consulted: + +```console +$ commit-check --author-name --author-email --rev HEAD +``` + ## Blocking force pushes ```console @@ -104,7 +131,7 @@ pushed: ```yaml title=".pre-commit-config.yaml" repos: - repo: https://github.com/commit-check/commit-check - rev: v2.13.4 + rev: v2.14.0 hooks: - id: check-no-force-push stages: [pre-push] @@ -165,11 +192,13 @@ and how CLI, environment and file settings override each other. ### Checking a range of commits -Nothing built in, but the exit code makes it a one-liner: +`--rev` makes each commit addressable without checking it out or piping its +message, and it is the only way the author checks apply to the commit rather +than to the local config: ```bash title="check-recent.sh" #!/usr/bin/env bash -# Check the last N commit messages; exits non-zero if any fail. +# Check the last N commits; exits non-zero if any fail. # Resolved before the loop rather than inside it: an unreadable range or a # directory that is not a repository would otherwise expand to nothing, and @@ -178,7 +207,7 @@ shas=$(git rev-list -n "${1:-10}" HEAD) || exit 1 status=0 for sha in $shas; do - if ! git log -1 --format=%B "$sha" | commit-check -m --compact; then + if ! commit-check -m --author-name --author-email --rev "$sha" --compact; then echo " ↑ $sha" status=1 fi @@ -186,6 +215,41 @@ done exit $status ``` +On a `pull_request` checkout the same loop covers exactly the commits the PR +adds — `HEAD` is GitHub's synthetic merge commit, whose first parent is the +base branch and second the PR branch: + +```console +$ git rev-list HEAD^1..HEAD^2 +``` + +### When a check is skipped + +A check that had nothing to judge reports a **skip**, not a pass. The common +case is a merge subject: `Merge branch 'x'` is git's writing, so +[CC002](rules.md#cc002), [CC003](rules.md#cc003), [CC004](rules.md#cc004) and +[CC005](rules.md#cc005) decline it rather than grade prose the author never +wrote. Only git's literal `Merge ` prefix qualifies (plus `fixup! ` for +CC003); a subject that merely starts with the lowercase word is judged like +any other. + +Text mode names every skipped check in one line on stderr, leaving stdout and +the exit code untouched — a skip is still not a failure: + +```console +$ echo "Merge branch 'main' into topic" | commit-check -m --no-banner +⊘ skipped (not validated): subject-max-length, subject-min-length +``` + +In JSON each skipped check carries `"status": "skip"`, distinct from `"pass"`. + +!!! warning "A green run can still have validated nothing" + + On a `pull_request` checkout, `HEAD` is the synthetic merge commit — so a + bare `commit-check -m` exits `0` with every subject rule skipped. The + notice makes that visible; the fix is to check what you actually mean: + the PR title piped on stdin, or each branch commit via `--rev` as above. + ### Reading the JSON ```console diff --git a/docs/guides/integrations.md b/docs/guides/integrations.md index 308c19d..e706f57 100644 --- a/docs/guides/integrations.md +++ b/docs/guides/integrations.md @@ -23,7 +23,7 @@ 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.13.4 + rev: v2.14.0 hooks: - id: check-message - id: check-branch @@ -70,7 +70,7 @@ 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.13.4 + rev: v2.14.0 hooks: - id: check-message args: diff --git a/docs/index.md b/docs/index.md index a721bae..eb5e267 100644 --- a/docs/index.md +++ b/docs/index.md @@ -36,7 +36,7 @@ whatever your AI agent is committing on your behalf. ```yaml title=".pre-commit-config.yaml" repos: - repo: https://github.com/commit-check/commit-check - rev: v2.13.4 + rev: v2.14.0 hooks: - id: check-message - id: check-branch