Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 44 additions & 1 deletion docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) |
Expand All @@ -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

Expand Down
2 changes: 1 addition & 1 deletion docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
74 changes: 69 additions & 5 deletions docs/example.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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]
Expand Down Expand Up @@ -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
Expand All @@ -178,14 +207,49 @@ 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
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
Expand Down
4 changes: 2 additions & 2 deletions docs/guides/integrations.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down