Skip to content

feat: add --rev, and stop the CLI hanging or passing in silence - #544

Merged
shenxianpeng merged 5 commits into
mainfrom
fix/cli-scripting-hazards
Aug 12, 2026
Merged

feat: add --rev, and stop the CLI hanging or passing in silence#544
shenxianpeng merged 5 commits into
mainfrom
fix/cli-scripting-hazards

Conversation

@shenxianpeng

@shenxianpeng shenxianpeng commented Aug 12, 2026

Copy link
Copy Markdown
Member

Three CLI behaviours that bit this project's own CI while #541 was being built, fixed as three commits. Together they make commit-check behave predictably when a script, a workflow, or an agent drives it rather than a human at a terminal.

1. commit-check --author-name could hang forever

stdin was read whenever it was not a tty — for every check type. On a pipe that is open but that nothing will ever write to or close (what stdin looks like under some CI runners and process managers), read() blocks indefinitely. In a workflow that is a stuck step, not a failed one, which is the worst way to fail.

The read is now gated with select(): genuinely piped input is already in the pipe buffer when the process starts (and EOF counts as readable), while an idle pipe is not readable and never will be. Piping still works — printf '%s' "$msg" | commit-check --message is unchanged — and Windows keeps the historic path, since select() only handles sockets there.

Regression tests use real pipes rather than mocks: idle-open pipe returns promptly, piped content still arrives, /dev/null reads as nothing.

2. There was no way to say which commit to check

Message checks read HEAD; author checks read the local git config before falling back to HEAD's author. Two consequences:

  • CI could not iterate a pull request's commits without checking each one out.
  • A malformed author on any commit passed, as long as whoever ran the check had a valid identity configured. Verified on a repo whose commit author is a with a clean config: exit 0.

New --rev REVISION names the commit under test. Message checks read that commit's message; author checks read that commit's author, never the config — an existing commit's identity is a fact about the commit, not about the operator. A revision that doesn't resolve is a clear one-line error up front. --rev plus a message file is rejected, and stdin is not consulted, since each would name a second subject for the same checks.

This is the revision input that came up in #541's review as an engine gap the workflow couldn't paper over. With it, iterating a PR becomes:

for sha in $(git rev-list HEAD^1..HEAD^2); do
  commit-check --message --author-name --author-email --rev "$sha"
done

3. Skipped checks were silent, which reads as passed

On a pull_request checkout HEAD is the synthetic merge commit. The subject rules bypass merge subjects — so a bare commit-check --message exited 0 having judged nothing it was asked about. That silence is what #541 had to engineer around in shell.

The merge/fixup bypasses now return SKIP (the status #537 introduced) instead of PASS, and text mode prints one stderr line naming every skipped check:

⊘ skipped (not validated): subject-max-length, subject-min-length

Exit codes are unchanged — a skip is still not a failure — and stdout is untouched, so nothing parsing it notices. ignore_authors keeps its PASS on an absent message: it judges the author and had already done so; a SKIP there would wrongly read as the author having been bypassed.

Verification

  • 580 passed; the one failure is the pre-existing config_test.py::test_load_config_file_permission_error, which fails on a clean main in this environment because the suite runs as root.
  • ruff clean, formatted, codespell clean.
  • Live checks besides the tests: the exact hang reproduced then eliminated; --rev HEAD^1 passes with a deliberately broken local config, proving the config is no longer consulted; the merge-HEAD false-green scenario now prints the skip notice.
  • All commit subjects pass this branch's own commit-check --message.

Follow-ups (not in this PR)

  • docs: --rev and the skip notice want entries on commit-check.com's configuration and rules pages, plus a changelog entry when this ships (v2.15.0 — the --rev addition makes it a minor).
  • commit-check-action can drop its checkout gymnastics once it can rely on --rev.

Summary by CodeRabbit

  • New Features

    • Added --rev to validate a specific Git commit revision.
    • Revision-based validation now uses that commit's message and author details.
    • Added clear handling for invalid revisions and conflicting command-line options.
  • Bug Fixes

    • Prevented validation from hanging on idle piped input.
    • Machine-generated, absent, or empty commit messages are now correctly marked as skipped.
    • Skipped checks are reported clearly in command-line output.

@shenxianpeng
shenxianpeng requested a review from a team as a code owner August 12, 2026 08:18
@github-actions github-actions Bot added the bug Something isn't working label Aug 12, 2026
@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds explicit commit revision support to validation, updates skip-state handling and reporting, and prevents blocking reads from idle POSIX stdin. The CLI validates revisions early and passes them into the validation context.

Changes

Revision validation flow

Layer / File(s) Summary
Revision-aware validation context
commit_check/engine.py
ValidationContext accepts rev. Commit message, subject, body, and author lookups use the selected revision.
Skipped validation outcomes
commit_check/engine.py, tests/engine_test.py
Merge, fixup, and absent-message checks return SKIP where applicable. validate_all reports skipped rule names to stderr.
CLI revision and stdin integration
commit_check/main.py, tests/main_test.py, pyproject.toml
The CLI adds --rev, validates revisions, rejects conflicting commit-file input, and avoids blocking on idle POSIX stdin. Tests cover revision validation, JSON output, and stdin readiness.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CLI
  participant Git
  participant ValidationEngine
  participant Output
  CLI->>Git: resolve --rev
  Git-->>CLI: return commit or error
  CLI->>ValidationEngine: validate revision context
  ValidationEngine-->>Output: return validation result and skipped checks
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 76.92% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main changes: adding --rev and preventing CLI hangs or silent handling of skipped checks.
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/cli-scripting-hazards

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@shenxianpeng shenxianpeng changed the title fix: make the CLI safe to script — no hangs, no silent passes, and a --rev to point it at feat: add --rev, and stop the CLI hanging or passing in silence Aug 12, 2026
@codecov

codecov Bot commented Aug 12, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
⚠️ Please upload report for BASE (main@711bd70). Learn more about missing BASE report.

Additional details and impacted files
@@           Coverage Diff           @@
##             main     #544   +/-   ##
=======================================
  Coverage        ?   98.13%           
=======================================
  Files           ?       12           
  Lines           ?     1337           
  Branches        ?        0           
=======================================
  Hits            ?     1312           
  Misses          ?       25           
  Partials        ?        0           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@commit_check/engine.py`:
- Around line 399-401: Update the subject classification checks in
commit_check/engine.py at lines 399-401, 437-439, and 490-492 to use one
consistent canonical Git merge-subject predicate, so ordinary subjects like
“merge settings” are validated. At lines 399-401 and 490-492, match only
canonical merge subjects; at lines 437-439, reuse that predicate and require the
exact “fixup!” prefix.
- Around line 1074-1084: Update the skipped-result reporting in the `if skipped`
block to avoid claiming that nothing was validated when other checks returned
PASS or FAIL. Use neutral wording such as “skipped checks:” for mixed outcomes,
or emit “nothing validated” only when all executed checks skipped.

In `@commit_check/main.py`:
- Around line 52-57: Update read_piped_input and its _has_pending_data readiness
flow so it does not call blocking sys.stdin.read() after select confirms input;
read only currently available bytes using a nonblocking descriptor or bounded
framed-input approach. Add a regression test that writes piped data while
keeping the write descriptor open and verifies the method returns without
blocking.
- Around line 519-535: Treat an empty --rev value as absent everywhere the CLI
currently checks args.rev, including revision validation and stdin suppression.
Update the conditions around the args.rev handling block and the related logic
near the ValidationContext setup so only a non-empty revision proceeds as
supplied, while preserving existing behavior for valid revisions and commit
message files.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 58601476-d0f4-43f8-a6f0-4d0960d8a547

📥 Commits

Reviewing files that changed from the base of the PR and between f8418e2 and cfb721f.

📒 Files selected for processing (5)
  • commit_check/engine.py
  • commit_check/main.py
  • pyproject.toml
  • tests/engine_test.py
  • tests/main_test.py

Comment thread commit_check/engine.py
Comment thread commit_check/engine.py
Comment thread commit_check/main.py
Comment thread commit_check/main.py Outdated
@github-actions github-actions Bot added the enhancement New feature or request label Aug 12, 2026
commit-check read stdin whenever it was not a tty, for every check type.
On a pipe that is open but that nothing will ever write to or close --
which is what stdin looks like under some CI runners and process
managers -- read() blocks forever, so 'commit-check --author-name'
became a stuck step rather than a failed one. Reproduced: it hangs
until killed.

Gate the read with select(): piped input is already in the pipe buffer
by the time this process starts (and EOF counts as readable), while an
idle pipe is not readable and never will be. Windows keeps the historic
blocking read, since select() only handles sockets there and the hang
has only been observed on POSIX runners.

Three regression tests use real pipes rather than mocks: the idle-open
pipe returns None promptly, piped content still arrives, /dev/null
reads as nothing. The existing tests that fake piped input by mocking
sys.stdin.read get an autouse fixture that opens the gate, restoring
the semantics those mocks assume.
There was no way to tell commit-check which commit to check. Message
checks read HEAD, and the author checks read the local git config
before falling back to HEAD's author -- so CI could not iterate a pull
request's commits, and a malformed author on any commit passed as long
as the identity of whoever ran the check was valid.

--rev REVISION names the commit under test. Message checks read that
commit's message. Author checks read that commit's author and never
the config: an existing commit's identity is a fact about the commit,
not about the operator. The revision is verified up front, so a typo
is a clear one-line error instead of a missing-message mystery deep in
a validator. stdin is not consulted when --rev is given, and combining
it with a message file is rejected -- both would name a second subject
for the same checks.

End-to-end tests run against a real two-commit repository where HEAD
is clean and its parent carries both a bad message and a bad author:
the verdict follows the revision, the author verdict flips even though
the config identity stays valid, and JSON mode reports the named
commit's values.
A silent skip is indistinguishable from a pass. That is how a merge
commit at HEAD -- which is what every pull_request checkout points at --
let a bare 'commit-check --message' report success having judged
nothing it was asked about.

The subject rules' merge and fixup bypasses now return SKIP rather than
PASS, matching what those bypasses mean: the rule declined to judge a
machine-written subject, it did not approve it. The same goes for a
message git never supplied. ignore_authors keeps its PASS there, since
it judges the author and had already done so; a SKIP would wrongly read
as the author having been bypassed.

validate_all then prints one stderr line naming every skipped check:

    ⊘ skipped (nothing validated): subject-max-length, subject-min-length

stderr so that nothing parsing stdout notices; exit codes are unchanged
because a skip is still not a failure. JSON consumers already saw skip
statuses; now the human running the text mode sees them too.
The subject rules bypassed anything starting with "merge" in any case,
so an author's own "merge the parser tables" escaped judgement. Git
writes "Merge " and "fixup! " exactly; only those forms are
machine-written, so only those are declined now.

--rev "" slipped past verification (an empty string is falsy) yet
reached the engine, where git's fatal message leaked into the checked
value with a green exit. Both rev sites now test against None, and the
empty string fails early with the same clear error as a bad revision.

Also rewords the skip notice to "not validated" and adds the tests
codecov flagged as uncovered.
@shenxianpeng
shenxianpeng force-pushed the fix/cli-scripting-hazards branch from 563e459 to 1483eb1 Compare August 12, 2026 08:31
BodyValidator reads the full message, so the earlier test never touched
_get_commit_body's rev branch; the attribution scan does.
@sonarqubecloud

sonarqubecloud Bot commented Aug 12, 2026

Copy link
Copy Markdown

Quality Gate Passed Quality Gate passed

Issues
0 New issues
2 Accepted issues

Measures
0 Security Hotspots
No data about Coverage
0.0% Duplication on New Code

See analysis details on SonarQube Cloud

@shenxianpeng shenxianpeng removed the bug Something isn't working label Aug 12, 2026
@codspeed-hq

codspeed-hq Bot commented Aug 12, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 489 untouched benchmarks
⏩ 121 skipped benchmarks1


Comparing fix/cli-scripting-hazards (629e243) with main (a90e8c8)2

Open in CodSpeed

Footnotes

  1. 121 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

  2. No successful run was found on main (711bd70) during the generation of this report, so a90e8c8 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

@shenxianpeng
shenxianpeng merged commit 423916a into main Aug 12, 2026
30 checks passed
@shenxianpeng
shenxianpeng deleted the fix/cli-scripting-hazards branch August 12, 2026 09:02
@shenxianpeng shenxianpeng added the minor A minor version bump label Aug 12, 2026
shenxianpeng added a commit to commit-check/commit-check.com that referenced this pull request Aug 12, 2026
Documentation for the engine changes merged in
commit-check/commit-check#544, ahead of the v2.15.0 release, plus the
release date for v2.14.0.

## Command-line recipes
([example.md](https://github.com/commit-check/commit-check.com/blob/docs/rev-and-skip-notice/docs/example.md))

- **A "From a revision" tab** under message checking: `--rev` takes
anything `git rev-parse` understands, errors up front on a revision that
does not resolve, and refuses to be combined with a message file or
stdin.
- **The author-check section now says whose identity is judged**:
without `--rev` it is the local git config (right for a hook, wrong for
CI); with `--rev` it is that commit's recorded author, and the config is
never consulted.
- **The range-checking recipe drops the stdin pipe** for `--rev`, which
also lets it include the author checks meaningfully, and shows `git
rev-list HEAD^1..HEAD^2` for covering exactly a PR's commits.
- **A new "When a check is skipped" section**: which rules decline merge
subjects and why only git's literal `Merge `/`fixup! ` prefixes qualify,
the one-line stderr notice, the JSON `"status": "skip"`, and a warning
box about the synthetic-merge-commit trap on `pull_request` checkouts.

Both console examples are pasted from real runs of the current engine,
not written by hand.

## Changelog

- New v2.15.0 (unreleased) entry: `--rev`, the stdin-hang fix, skip
visibility, and the tightened merge/fixup bypass.
- v2.14.0 stamped with its release date (published 2026-08-12).
- Highlights table row for 2.15.0.

`mkdocs build -s` passes (social cards disabled locally — the build
environment cannot reach fonts.google.com; nothing in this diff touches
that path).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request minor A minor version bump

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant