Skip to content

fix: distinguish an absent commit message from an empty one - #534

Merged
shenxianpeng merged 3 commits into
mainfrom
claude/fix-empty-message-602anc
Aug 7, 2026
Merged

fix: distinguish an absent commit message from an empty one#534
shenxianpeng merged 3 commits into
mainfrom
claude/fix-empty-message-602anc

Conversation

@shenxianpeng

@shenxianpeng shenxianpeng commented Aug 7, 2026

Copy link
Copy Markdown
Member

Why this exists

main went red on the push run immediately after #532 merged, on test_empty_message_passes — a test that had been green on the pull request. Nothing regressed. The merge was simply the first time that test met a real commit.

CC013 ai-attribution check failed ==> Claude Code
AI-assisted commit is forbidden — detected tools: Claude Code

The bug

_get_commit_body tested stdin_text for truth, so an empty string read as not provided and the check fell through to get_commit_info("b") — the repository's HEAD commit. The test named "empty message passes" therefore never measured an empty message. Measured on a real checkout, its "empty" message resolved to 6694 characters.

That is why the timing looks strange, and it takes all three:

  1. On a pull_request run, HEAD is GitHub's synthetic merge commit, whose body is empty (git log -1 --format=%b → 1 character). Empty body → early return PASSthe test passed for the wrong reason.
  2. On main, HEAD became the squashed commit, which carries a Co-authored-by: trailer.
  3. CC013 detected it and the test failed.

The commit that set it off was #532's own AI attribution.

This is the same shape as the merge-base defect #532 fixed: a test that looks isolated but reads ambient git state, in a repository where the PR checkout and the main checkout are structurally different commits. #532 cleaned up three such tests in MergeBaseValidator; the pattern was also sitting in AiAttributionValidator.

What changed

One line makes main green_get_commit_body splits on None, matching _should_skip_validation and _resolve_current_author, which already did.

The same looseness was in four more readers, so they follow the same rule now: _get_commit_message, _get_subject, _get_author_value, and BranchValidator. The intent was already recorded upstream — api.validate_author distinguishes with name is not None — only these readers had not followed it.

ForcePushValidator deliberately keeps its truth test, and says so in a comment: its stdin_text carries a list of refs, where empty genuinely means nothing to check rather than a value to judge.

That surfaced a rule that could never fire. _is_empty_commit_allowed exists to reject an empty message under allow_empty_commits = false, but CommitTypeValidator returned PASS on a falsy message before ever reaching it — the rejecting branch was dead code. A supplied message now reaches the rule even when empty; one git never gave us still returns early:

validate_message("")                            -> pass   (default: empty allowed)
validate_message("", allow_empty_commits=false) -> fail   CC008

The other validators keep their early return. BodyValidator documents whitespace-only input as "no commit message at all" and has tests asserting it; allow_empty_commits is the rule that owns that judgement, so the rest defer to it.

Impact beyond the red build

The public API was answering the wrong question. validate_message("") reported on the last commit rather than on the empty message it was handed — so commit-check-mcp's validate_commit_message("") validated whatever the server's working directory had committed last. It now reports value=''.

The CLI is unaffected either way: _resolve_commit_message_source already normalises empty stdin to None.

Verification

  • 514 passed (python -m pytest tests/), ruff clean and formatted.
  • Three new tests, each patching get_commit_info and asserting it is never consulted, so the verdict provably comes from the supplied message and not from the repository's HEAD. The pre-existing test_empty_message_passes only holds while the checkout's own HEAD carries no AI trailers — which is exactly what made it fragile — so the replacements pin the behaviour instead of the environment.
  • Non-vacuity checked by restoring each early return and confirming the matching test goes red.
  • test_empty_message_returns_fail in api_test.py was vacuous for the same reason: it patched get_commit_info to "test-user" and validated that string. It now genuinely exercises the empty message.

🤖 Generated with Claude Code

https://claude.ai/code/session_01U9zFxq8V4qxG4aMzJhGBFn


Generated by Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Explicitly supplied empty commit messages, subjects, authors, and branches are now validated correctly instead of being treated as missing.
    • Empty commit messages can now trigger rules that disallow them.
    • Validation no longer falls back to the latest repository commit when an empty message is explicitly provided.
    • Commit-type validation now correctly skips only when no message is available.

shenxianpeng and others added 2 commits August 7, 2026 07:10
main went red on the push run right after #532 merged, on a test that
had been green on the pull request. Nothing regressed -- the merge was
the first time the test met a real commit.

_get_commit_body tested stdin_text for truth, so an empty string read
as "not provided" and the check fell through to get_commit_info("b"),
the repository's HEAD commit. test_empty_message_passes therefore never
measured an empty message: on a pull_request run HEAD is GitHub's
synthetic merge commit, whose body is empty, so it passed for the wrong
reason; on main HEAD became the squashed commit carrying a
Co-authored-by trailer, CC013 detected it, and the test failed. Measured
on this checkout, the "empty" message resolved to 6694 characters.

The same looseness reaches the public API: validate_message("") answers
about the last commit rather than the empty message it was given.

The skip logic in this file already draws the line at None
(_should_skip_validation, _resolve_current_author); _get_commit_body now
follows it. The CLI is unaffected -- _resolve_commit_message_source
already normalises empty stdin to None.

Adds a hermetic regression test: the existing one only holds while the
checkout's own HEAD carries no AI trailers, which is what made it fragile
in the first place. The new one patches get_commit_info and asserts it is
never consulted.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U9zFxq8V4qxG4aMzJhGBFn
Follows the one-line fix on _get_commit_body by applying the same rule
to the readers that were still testing stdin_text for truth, so an
empty string is no longer read as "the caller said nothing".

_get_commit_message, _get_subject, _get_author_value and BranchValidator
now split on None, matching _should_skip_validation and
_resolve_current_author, which already did. api.validate_author draws
the same line with `name is not None`, so the intent was there; only
these readers had not followed it. ForcePushValidator deliberately keeps
a truth test: its stdin_text carries a *list* of refs, where empty
genuinely means nothing to check rather than a value to judge.

That surfaced a rule that could never fire. _is_empty_commit_allowed
exists to reject an empty message under allow_empty_commits = false, but
CommitTypeValidator returned PASS on a falsy message before ever
reaching it, so the rejecting branch was dead code. A supplied message
now reaches the rule even when empty; one git never gave us still
returns early. Measured after the change:

    validate_message("")                          -> pass  (default)
    validate_message("", allow_empty_commits=off) -> fail  CC008

The other validators keep their early return: BodyValidator documents
whitespace-only input as "no commit message at all", and
allow_empty_commits is the rule that owns that judgement.

Adds two tests pinning both directions, each patching get_commit_info to
prove the verdict comes from the supplied message rather than the
repository's HEAD. Restoring the early return fails them.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U9zFxq8V4qxG4aMzJhGBFn
@shenxianpeng
shenxianpeng requested a review from a team as a code owner August 7, 2026 07:24
@github-actions github-actions Bot added the bug Something isn't working label Aug 7, 2026
@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@shenxianpeng, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 50 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 0a83c2e0-b0b6-4db3-a1fb-ffcb97d2b5f5

📥 Commits

Reviewing files that changed from the base of the PR and between 7d31a5d and 2377b45.

📒 Files selected for processing (1)
  • commit_check/engine.py
📝 Walkthrough

Walkthrough

The engine now distinguishes explicitly supplied empty values from absent inputs. Commit, body, subject, author, and branch validation preserve empty stdin values. Commit-type and AI-attribution validation use these values without reading repository data.

Changes

Empty Input Validation

Layer / File(s) Summary
Input source handling
commit_check/engine.py
Message, body, subject, author, and branch extraction preserve explicitly supplied empty stdin values. Force-push handling continues to treat empty stdin as no ref lines.
Empty message rule validation
commit_check/engine.py, tests/engine_test.py
Commit-type validation evaluates supplied empty messages. Tests cover rejection of empty messages, skipping absent messages, and avoiding repository lookups during AI-attribution validation.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

Suggested labels: tests

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: distinguishing absent commit messages from supplied empty messages.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/fix-empty-message-602anc

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.

@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: 1

🤖 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 141-149: Update commit_check/engine.py at lines 141-149 in
_message_was_supplied to base the supplied flag on successful message
resolution, so an unreadable commit_file is not treated as explicit input;
update lines 769-774 to use that resolved-source flag when evaluating empty
messages, preserving the no-CC008 behavior for a missing commit file with an
empty Git message, and add a regression test covering that case.
🪄 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: 1ade9a11-e7c2-47a0-bb3d-3b0b7a032096

📥 Commits

Reviewing files that changed from the base of the PR and between 98fb97c and 7d31a5d.

📒 Files selected for processing (2)
  • commit_check/engine.py
  • tests/engine_test.py

Comment thread commit_check/engine.py
@codecov

codecov Bot commented Aug 7, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 97.72%. Comparing base (7315edf) to head (2377b45).
⚠️ Report is 2 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #534      +/-   ##
==========================================
+ Coverage   97.61%   97.72%   +0.10%     
==========================================
  Files          12       12              
  Lines        1258     1272      +14     
==========================================
+ Hits         1228     1243      +15     
+ Misses         30       29       -1     

☔ 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.

Review asked whether _message_was_supplied should drop to False when a
commit_file cannot be read, since the text then comes from git. Measured
the only reachable case: a HEAD commit whose message is genuinely empty,
where allow_empty_commits = false makes CC008 the correct verdict.
Deriving the flag from successful resolution would restore the miss this
branch fixes, so the behaviour stands and the docstring now says why.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U9zFxq8V4qxG4aMzJhGBFn
@sonarqubecloud

sonarqubecloud Bot commented Aug 7, 2026

Copy link
Copy Markdown

@codspeed-hq

codspeed-hq Bot commented Aug 7, 2026

Copy link
Copy Markdown

Merging this PR will regress 1 benchmark

⚡ 3 improved benchmarks
❌ 1 regressed benchmark
✅ 435 untouched benchmarks
⏩ 121 skipped benchmarks1

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Benchmark BASE HEAD Efficiency
test_merge_base_validator_valid 3 ms 4.5 ms -34.53%
test_empty_message_passes 9,269.4 µs 340.9 µs ×27
test_empty_message_returns_fail 2.8 ms 2.1 ms +33.3%
test_merge_base_validator_invalid 5.5 ms 4.5 ms +20.75%

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing claude/fix-empty-message-602anc (2377b45) with main (7315edf)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 (98fb97c) during the generation of this report, so 7315edf was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

Copy link
Copy Markdown
Member Author

Not acting on the CodSpeed regression — the evidence says it is measurement noise, not this branch.

The regressed benchmark exercises code this branch does not touch. The diff is 44 lines in commit_check/engine.py, none of them in the merge-base path:

$ git diff origin/main...HEAD -- commit_check/engine.py | grep -iE "^[+-].*(merge_base|MergeBase)"
(no matches)

Its twin moved the opposite way on that same untouched path. test_merge_base_validator_invalid improved +20.06% while test_merge_base_validator_valid regressed -34.76%. One code path cannot get both faster and slower in one commit; at 3–5 ms, dominated by subprocess spawning, that spread is the noise floor.

The comparison base is known-bad. CodSpeed's own footnote says it: "No successful run was found on main (98fb97c) during the generation of this report, so 7315edf was used instead. There might be some changes unrelated to this pull request in this report." That missing run is the red main this PR exists to fix — so the report is measuring against the wrong baseline by its own admission.

The two benchmarks that are on the changed path moved the way the change predicts: test_empty_message_passes ×27 faster (9,269 µs → 342 µs) and test_empty_message_returns_fail +33%, both from skipping repository lookups when the message came from stdin.

Also worth noting the run was on 7d31a5d; head is now 2377b45. Once main is green again the baseline regenerates and this comparison stops being meaningful. Happy to acknowledge it on CodSpeed rather than chase it.


Generated by Claude Code

@shenxianpeng
shenxianpeng merged commit 08d14c1 into main Aug 7, 2026
27 of 28 checks passed
@shenxianpeng
shenxianpeng deleted the claude/fix-empty-message-602anc branch August 7, 2026 09:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant