git-commit-guard 0.22.3__tar.gz → 0.23.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. {git_commit_guard-0.22.3 → git_commit_guard-0.23.0}/.github/workflows/lint-commits.yml +1 -1
  2. git_commit_guard-0.23.0/.github/workflows/security-audit.yml +69 -0
  3. {git_commit_guard-0.22.3 → git_commit_guard-0.23.0}/PKG-INFO +24 -8
  4. {git_commit_guard-0.22.3 → git_commit_guard-0.23.0}/README.md +23 -7
  5. {git_commit_guard-0.22.3 → git_commit_guard-0.23.0}/docs/index.html +13 -4
  6. {git_commit_guard-0.22.3 → git_commit_guard-0.23.0}/src/git_commit_guard/__init__.py +31 -13
  7. {git_commit_guard-0.22.3 → git_commit_guard-0.23.0}/tests/test_git_commit_guard.py +214 -0
  8. git_commit_guard-0.23.0/uv.lock +315 -0
  9. git_commit_guard-0.22.3/.github/workflows/security-audit.yml +0 -24
  10. git_commit_guard-0.22.3/uv.lock +0 -320
  11. {git_commit_guard-0.22.3 → git_commit_guard-0.23.0}/.editorconfig +0 -0
  12. {git_commit_guard-0.22.3 → git_commit_guard-0.23.0}/.github/workflows/coverage-baseline.yml +0 -0
  13. {git_commit_guard-0.22.3 → git_commit_guard-0.23.0}/.github/workflows/coverage-comment.yml +0 -0
  14. {git_commit_guard-0.22.3 → git_commit_guard-0.23.0}/.github/workflows/lint-md.yml +0 -0
  15. {git_commit_guard-0.22.3 → git_commit_guard-0.23.0}/.github/workflows/lint-python.yml +0 -0
  16. {git_commit_guard-0.22.3 → git_commit_guard-0.23.0}/.github/workflows/lint-workflows.yml +0 -0
  17. {git_commit_guard-0.22.3 → git_commit_guard-0.23.0}/.github/workflows/release.yml +0 -0
  18. {git_commit_guard-0.22.3 → git_commit_guard-0.23.0}/.github/workflows/test.yml +0 -0
  19. {git_commit_guard-0.22.3 → git_commit_guard-0.23.0}/.gitignore +0 -0
  20. {git_commit_guard-0.22.3 → git_commit_guard-0.23.0}/.markdownlint.json +0 -0
  21. {git_commit_guard-0.22.3 → git_commit_guard-0.23.0}/.pre-commit-hooks.yaml +0 -0
  22. {git_commit_guard-0.22.3 → git_commit_guard-0.23.0}/.python-version +0 -0
  23. {git_commit_guard-0.22.3 → git_commit_guard-0.23.0}/LICENSE +0 -0
  24. {git_commit_guard-0.22.3 → git_commit_guard-0.23.0}/action.yml +0 -0
  25. {git_commit_guard-0.22.3 → git_commit_guard-0.23.0}/cliff.toml +0 -0
  26. {git_commit_guard-0.22.3 → git_commit_guard-0.23.0}/docs/commit-guard-icon.svg +0 -0
  27. {git_commit_guard-0.22.3 → git_commit_guard-0.23.0}/pyproject.toml +0 -0
  28. {git_commit_guard-0.22.3 → git_commit_guard-0.23.0}/ruff.toml +0 -0
  29. {git_commit_guard-0.22.3 → git_commit_guard-0.23.0}/tests/__init__.py +0 -0
@@ -22,6 +22,6 @@ jobs:
22
22
  key: nltk-averaged-perceptron-tagger-punkt
23
23
  - name: Lint commits
24
24
  # yamllint disable-line rule:line-length
25
- uses: benner/commit-guard@e156ca25789cc1824f745d74209f9c2654135dca # v0.22.2
25
+ uses: benner/commit-guard@4f989e92ddf04153ec33040da42bc2f788c71584 # v0.22.3
26
26
  with:
27
27
  range: origin/${{ github.base_ref }}..HEAD
@@ -0,0 +1,69 @@
1
+ ---
2
+ name: Security Audit
3
+ on: # yamllint disable-line rule:truthy
4
+ pull_request:
5
+ schedule:
6
+ - cron: 13 3 * * *
7
+ permissions:
8
+ contents: read
9
+ jobs:
10
+ audit:
11
+ runs-on: ubuntu-latest
12
+ permissions:
13
+ contents: read
14
+ issues: write
15
+ steps:
16
+ - name: Checkout code
17
+ # yamllint disable-line rule:line-length
18
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
19
+ with:
20
+ persist-credentials: false
21
+ - name: Install uv
22
+ # yamllint disable-line rule:line-length
23
+ uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
24
+ - name: Audit dependencies
25
+ run: |
26
+ set -o pipefail
27
+ uv audit --frozen --preview-features audit-command 2>&1 |
28
+ tee audit.log
29
+ - name: Report failure in a tracking issue
30
+ if: failure() && github.event_name == 'schedule'
31
+ env:
32
+ GH_TOKEN: ${{ github.token }}
33
+ GH_REPO: ${{ github.repository }}
34
+ run: |
35
+ run_url="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY"
36
+ run_url="$run_url/actions/runs/$GITHUB_RUN_ID"
37
+ {
38
+ echo "Scheduled security audit failed: $run_url"
39
+ echo
40
+ echo '```'
41
+ cat audit.log
42
+ echo '```'
43
+ echo
44
+ echo '<!-- security-audit -->'
45
+ } > issue-body.md
46
+ issue=$(gh issue list --state open \
47
+ --search 'Security audit failing in:title' \
48
+ --json number --jq '.[0].number')
49
+ if [ -n "$issue" ]; then
50
+ gh issue comment "$issue" --body-file issue-body.md
51
+ else
52
+ gh issue create --title "Security audit failing" \
53
+ --body-file issue-body.md
54
+ fi
55
+ - name: Close the tracking issue
56
+ if: success() && github.event_name == 'schedule'
57
+ env:
58
+ GH_TOKEN: ${{ github.token }}
59
+ GH_REPO: ${{ github.repository }}
60
+ run: |-
61
+ run_url="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY"
62
+ run_url="$run_url/actions/runs/$GITHUB_RUN_ID"
63
+ issue=$(gh issue list --state open \
64
+ --search 'Security audit failing in:title' \
65
+ --json number --jq '.[0].number')
66
+ if [ -n "$issue" ]; then
67
+ gh issue close "$issue" \
68
+ --comment "Scheduled security audit is green again: $run_url"
69
+ fi
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: git-commit-guard
3
- Version: 0.22.3
3
+ Version: 0.23.0
4
4
  Summary: Opinionated conventional commit message linter with imperative mood detection
5
5
  Project-URL: Homepage, https://github.com/benner/commit-guard
6
6
  Project-URL: Repository, https://github.com/benner/commit-guard
@@ -316,7 +316,7 @@ COMMIT_GUARD_GIT_TIMEOUT=30 commit-guard --range origin/main..HEAD
316
316
  In GitHub Actions, set it at the step or job level:
317
317
 
318
318
  ```yaml
319
- - uses: benner/commit-guard@v0.22.3
319
+ - uses: benner/commit-guard@v0.23.0
320
320
  env:
321
321
  COMMIT_GUARD_GIT_TIMEOUT: 30
322
322
  with:
@@ -352,6 +352,22 @@ misconfigured range specs in CI. Use `--allow-empty` to exit 0 instead:
352
352
  commit-guard --range origin/main..HEAD --allow-empty
353
353
  ```
354
354
 
355
+ ### Quiet mode
356
+
357
+ Use `--quiet` (or `-q`) to suppress output for commits that have nothing to
358
+ report — only commits with errors or warnings are printed. On a long range
359
+ this leaves exactly the offending commits in the output; a fully compliant
360
+ range prints nothing and exits 0:
361
+
362
+ ```bash
363
+ commit-guard --range origin/main..HEAD --quiet
364
+ ```
365
+
366
+ Quiet mode applies to single-commit and range mode, in both text and
367
+ `--output jsonl` formats. Exit codes are unchanged, so it composes with CI
368
+ gating. `--output-file` is not affected — the file always receives the
369
+ complete record stream.
370
+
355
371
  ### Machine-readable output
356
372
 
357
373
  Use `--output jsonl` to emit one JSON line per commit to stdout instead of the
@@ -400,7 +416,7 @@ steps:
400
416
  - uses: actions/checkout@v4
401
417
  with:
402
418
  fetch-depth: 0
403
- - uses: benner/commit-guard@v0.22.3
419
+ - uses: benner/commit-guard@v0.23.0
404
420
  ```
405
421
 
406
422
  Check all commits in a pull request:
@@ -416,7 +432,7 @@ jobs:
416
432
  - uses: actions/checkout@v4
417
433
  with:
418
434
  fetch-depth: 0
419
- - uses: benner/commit-guard@v0.22.3
435
+ - uses: benner/commit-guard@v0.23.0
420
436
  with:
421
437
  range: ${{ env.PR_BASE }}..${{ env.PR_HEAD }}
422
438
  ```
@@ -424,7 +440,7 @@ jobs:
424
440
  Check a specific commit SHA (mirrors the positional CLI argument):
425
441
 
426
442
  ```yaml
427
- - uses: benner/commit-guard@v0.22.3
443
+ - uses: benner/commit-guard@v0.23.0
428
444
  with:
429
445
  rev: ${{ github.sha }}
430
446
  ```
@@ -442,7 +458,7 @@ jobs:
442
458
  - uses: actions/checkout@v4
443
459
  with:
444
460
  fetch-depth: 0
445
- - uses: benner/commit-guard@v0.22.3
461
+ - uses: benner/commit-guard@v0.23.0
446
462
  with:
447
463
  range: ${{ env.PR_BASE }}..${{ env.PR_HEAD }}
448
464
  disable: signed-off,signature
@@ -462,7 +478,7 @@ jobs:
462
478
  When `output-file` is set the action exposes the path as an output:
463
479
 
464
480
  ```yaml
465
- - uses: benner/commit-guard@v0.22.3
481
+ - uses: benner/commit-guard@v0.23.0
466
482
  id: cg
467
483
  with:
468
484
  range: ${{ env.PR_BASE }}..${{ env.PR_HEAD }}
@@ -478,7 +494,7 @@ Add to your `.pre-commit-config.yaml`:
478
494
  ---
479
495
  repos:
480
496
  - repo: https://github.com/benner/commit-guard
481
- rev: v0.22.3
497
+ rev: v0.23.0
482
498
  hooks:
483
499
  - id: commit-guard
484
500
  - id: commit-guard-signature
@@ -295,7 +295,7 @@ COMMIT_GUARD_GIT_TIMEOUT=30 commit-guard --range origin/main..HEAD
295
295
  In GitHub Actions, set it at the step or job level:
296
296
 
297
297
  ```yaml
298
- - uses: benner/commit-guard@v0.22.3
298
+ - uses: benner/commit-guard@v0.23.0
299
299
  env:
300
300
  COMMIT_GUARD_GIT_TIMEOUT: 30
301
301
  with:
@@ -331,6 +331,22 @@ misconfigured range specs in CI. Use `--allow-empty` to exit 0 instead:
331
331
  commit-guard --range origin/main..HEAD --allow-empty
332
332
  ```
333
333
 
334
+ ### Quiet mode
335
+
336
+ Use `--quiet` (or `-q`) to suppress output for commits that have nothing to
337
+ report — only commits with errors or warnings are printed. On a long range
338
+ this leaves exactly the offending commits in the output; a fully compliant
339
+ range prints nothing and exits 0:
340
+
341
+ ```bash
342
+ commit-guard --range origin/main..HEAD --quiet
343
+ ```
344
+
345
+ Quiet mode applies to single-commit and range mode, in both text and
346
+ `--output jsonl` formats. Exit codes are unchanged, so it composes with CI
347
+ gating. `--output-file` is not affected — the file always receives the
348
+ complete record stream.
349
+
334
350
  ### Machine-readable output
335
351
 
336
352
  Use `--output jsonl` to emit one JSON line per commit to stdout instead of the
@@ -379,7 +395,7 @@ steps:
379
395
  - uses: actions/checkout@v4
380
396
  with:
381
397
  fetch-depth: 0
382
- - uses: benner/commit-guard@v0.22.3
398
+ - uses: benner/commit-guard@v0.23.0
383
399
  ```
384
400
 
385
401
  Check all commits in a pull request:
@@ -395,7 +411,7 @@ jobs:
395
411
  - uses: actions/checkout@v4
396
412
  with:
397
413
  fetch-depth: 0
398
- - uses: benner/commit-guard@v0.22.3
414
+ - uses: benner/commit-guard@v0.23.0
399
415
  with:
400
416
  range: ${{ env.PR_BASE }}..${{ env.PR_HEAD }}
401
417
  ```
@@ -403,7 +419,7 @@ jobs:
403
419
  Check a specific commit SHA (mirrors the positional CLI argument):
404
420
 
405
421
  ```yaml
406
- - uses: benner/commit-guard@v0.22.3
422
+ - uses: benner/commit-guard@v0.23.0
407
423
  with:
408
424
  rev: ${{ github.sha }}
409
425
  ```
@@ -421,7 +437,7 @@ jobs:
421
437
  - uses: actions/checkout@v4
422
438
  with:
423
439
  fetch-depth: 0
424
- - uses: benner/commit-guard@v0.22.3
440
+ - uses: benner/commit-guard@v0.23.0
425
441
  with:
426
442
  range: ${{ env.PR_BASE }}..${{ env.PR_HEAD }}
427
443
  disable: signed-off,signature
@@ -441,7 +457,7 @@ jobs:
441
457
  When `output-file` is set the action exposes the path as an output:
442
458
 
443
459
  ```yaml
444
- - uses: benner/commit-guard@v0.22.3
460
+ - uses: benner/commit-guard@v0.23.0
445
461
  id: cg
446
462
  with:
447
463
  range: ${{ env.PR_BASE }}..${{ env.PR_HEAD }}
@@ -457,7 +473,7 @@ Add to your `.pre-commit-config.yaml`:
457
473
  ---
458
474
  repos:
459
475
  - repo: https://github.com/benner/commit-guard
460
- rev: v0.22.3
476
+ rev: v0.23.0
461
477
  hooks:
462
478
  - id: commit-guard
463
479
  - id: commit-guard-signature
@@ -551,6 +551,15 @@ require-subject-pattern = "[A-Z]+-[0-9]+"</code></pre>
551
551
  readable logs and structured results for downstream steps:
552
552
  </p>
553
553
  <pre><code class="language-bash">commit-guard --range origin/main..HEAD --output-file results.jsonl</code></pre>
554
+ <p>
555
+ Use <code>--quiet</code> (or <code>-q</code>) to suppress output for
556
+ commits that have nothing to report — only commits with errors or
557
+ warnings are printed, in both text and JSONL formats. A fully
558
+ compliant range prints nothing and exits 0. Exit codes are unchanged,
559
+ and <code>--output-file</code> always receives the complete record
560
+ stream:
561
+ </p>
562
+ <pre><code class="language-bash">commit-guard --range origin/main..HEAD --quiet</code></pre>
554
563
  </section>
555
564
 
556
565
  <section id="github-actions">
@@ -566,13 +575,13 @@ require-subject-pattern = "[A-Z]+-[0-9]+"</code></pre>
566
575
  - uses: actions/checkout@v4
567
576
  with:
568
577
  fetch-depth: 0
569
- - uses: benner/commit-guard@v0.22.3
578
+ - uses: benner/commit-guard@v0.23.0
570
579
  with:
571
580
  range: ${{ env.PR_BASE }}..${{ env.PR_HEAD }}
572
581
  disable: signed-off,signature</code></pre>
573
582
 
574
583
  <p>Check a specific commit SHA:</p>
575
- <pre><code class="language-yaml"> - uses: benner/commit-guard@v0.22.3
584
+ <pre><code class="language-yaml"> - uses: benner/commit-guard@v0.23.0
576
585
  with:
577
586
  rev: ${{ github.sha }}</code></pre>
578
587
 
@@ -591,7 +600,7 @@ require-subject-pattern = "[A-Z]+-[0-9]+"</code></pre>
591
600
  When <code>output-file</code> is set the action exposes the path as
592
601
  a step output, making JSONL results available to subsequent steps:
593
602
  </p>
594
- <pre><code class="language-yaml"> - uses: benner/commit-guard@v0.22.3
603
+ <pre><code class="language-yaml"> - uses: benner/commit-guard@v0.23.0
595
604
  id: cg
596
605
  with:
597
606
  range: ${{ env.PR_BASE }}..${{ env.PR_HEAD }}
@@ -604,7 +613,7 @@ require-subject-pattern = "[A-Z]+-[0-9]+"</code></pre>
604
613
  <p>Add to <code>.pre-commit-config.yaml</code>:</p>
605
614
  <pre><code class="language-yaml">repos:
606
615
  - repo: https://github.com/benner/commit-guard
607
- rev: v0.22.3
616
+ rev: v0.23.0
608
617
  hooks:
609
618
  - id: commit-guard
610
619
  - id: commit-guard-signature</code></pre>
@@ -146,6 +146,10 @@ class Result:
146
146
  def ok(self):
147
147
  return not any(lvl == Level.ERROR for _, lvl, _ in self.errors)
148
148
 
149
+ @property
150
+ def noteworthy(self):
151
+ return any(lvl in (Level.ERROR, Level.WARN) for _, lvl, _ in self.errors)
152
+
149
153
 
150
154
  def _ensure_nltk_data():
151
155
  _download_if_missing("taggers/averaged_perceptron_tagger_eng")
@@ -565,6 +569,7 @@ class Args:
565
569
  subject_pattern: re.Pattern | None
566
570
  output: OutputFormat
567
571
  output_file: Path | None
572
+ quiet: bool
568
573
 
569
574
 
570
575
  def _resolve_enabled(args, config, parser):
@@ -662,7 +667,7 @@ def _parse_checks(parser, value):
662
667
  parser.error(str(e))
663
668
 
664
669
 
665
- def _parse_args(): # noqa: PLR0915 Too many statements (59 > 50)
670
+ def _parse_args(): # noqa: PLR0915 Too many statements (60 > 50)
666
671
  checks_list = ",".join(sorted(Check))
667
672
  parser = ArgumentParser(description="conventional commit checker")
668
673
  parser.add_argument("rev", nargs="?", default=None)
@@ -761,6 +766,13 @@ def _parse_args(): # noqa: PLR0915 Too many statements (59 > 50)
761
766
  metavar="FILE",
762
767
  help="write JSONL results to FILE (text still goes to stdout)",
763
768
  )
769
+ parser.add_argument(
770
+ "-q",
771
+ "--quiet",
772
+ action="store_true",
773
+ default=False,
774
+ help="suppress output for commits without errors or warnings",
775
+ )
764
776
  args = parser.parse_args()
765
777
  config = _load_config()
766
778
  enabled = _resolve_enabled(args, config, parser)
@@ -823,6 +835,7 @@ def _parse_args(): # noqa: PLR0915 Too many statements (59 > 50)
823
835
  subject_pattern=subject_pattern,
824
836
  output=OutputFormat(args.output),
825
837
  output_file=args.output_file,
838
+ quiet=args.quiet,
826
839
  )
827
840
 
828
841
 
@@ -838,7 +851,9 @@ def _jsonl_record(result, sha, subject):
838
851
  }
839
852
 
840
853
 
841
- def _report_jsonl(result, sha, subject):
854
+ def _report_jsonl(result, sha, subject, *, quiet=False):
855
+ if quiet and not result.noteworthy:
856
+ return 0
842
857
  print(json.dumps(_jsonl_record(result, sha, subject)))
843
858
  return 0 if result.ok else 1
844
859
 
@@ -847,7 +862,9 @@ def _write_jsonl_record(result, sha, subject, file):
847
862
  file.write(json.dumps(_jsonl_record(result, sha, subject)) + "\n")
848
863
 
849
864
 
850
- def _report_text(result):
865
+ def _report_text(result, *, quiet=False):
866
+ if quiet and not result.noteworthy:
867
+ return 0
851
868
  color = sys.stdout.isatty()
852
869
  for check, level, msg in result.errors:
853
870
  prefix = f"[{check}] " if check else ""
@@ -892,6 +909,14 @@ def _run_checks(args, rev, message, result):
892
909
  check_signature(rev, result)
893
910
 
894
911
 
912
+ def _report_commit(args, result, sha, subject):
913
+ if args.output == OutputFormat.JSONL:
914
+ return _report_jsonl(result, sha, subject, quiet=args.quiet)
915
+ if args.rev_range and (not args.quiet or result.noteworthy):
916
+ print(f"{sha[:7]} {subject}")
917
+ return _report_text(result, quiet=args.quiet)
918
+
919
+
895
920
  def main():
896
921
  args = _parse_args()
897
922
 
@@ -911,13 +936,8 @@ def main():
911
936
  subject = message.split("\n")[0]
912
937
  result = Result()
913
938
  _run_checks(args, rev, message, result)
914
- if args.output == OutputFormat.JSONL:
915
- if _report_jsonl(result, rev, subject) != 0:
916
- failed = True
917
- else:
918
- print(f"{rev[:7]} {subject}")
919
- if _report_text(result) != 0:
920
- failed = True
939
+ if _report_commit(args, result, rev, subject) != 0:
940
+ failed = True
921
941
  if out_file:
922
942
  _write_jsonl_record(result, rev, subject, out_file)
923
943
  return 1 if failed else 0
@@ -927,6 +947,4 @@ def main():
927
947
  _run_checks(args, args.rev, args.message, result)
928
948
  if out_file:
929
949
  _write_jsonl_record(result, args.rev, subject, out_file)
930
- if args.output == OutputFormat.JSONL:
931
- return _report_jsonl(result, args.rev, subject)
932
- return _report_text(result)
950
+ return _report_commit(args, result, args.rev, subject)
@@ -12,6 +12,7 @@ from git_commit_guard import (
12
12
  GIT_TIMEOUT,
13
13
  MAX_SUBJECT_LEN,
14
14
  TYPES,
15
+ OutputFormat,
15
16
  Result,
16
17
  _download_if_missing,
17
18
  _ensure_nltk_data,
@@ -29,6 +30,7 @@ from git_commit_guard import (
29
30
  _parse_checks,
30
31
  _parse_config_checks,
31
32
  _parse_noreply_username,
33
+ _report_commit,
32
34
  _report_jsonl,
33
35
  _report_text,
34
36
  _resolve_max_subject_length,
@@ -1493,6 +1495,33 @@ class TestReport:
1493
1495
  _report_text(r)
1494
1496
  assert "\033[" not in capsys.readouterr().out
1495
1497
 
1498
+ def test_quiet_suppresses_clean_result(self, capsys):
1499
+ r = Result()
1500
+ ret = _report_text(r, quiet=True)
1501
+ assert ret == 0
1502
+ assert capsys.readouterr().out == ""
1503
+
1504
+ def test_quiet_suppresses_info_only_result(self, capsys):
1505
+ r = Result()
1506
+ r.info("signature type: SSH", check="signature")
1507
+ ret = _report_text(r, quiet=True)
1508
+ assert ret == 0
1509
+ assert capsys.readouterr().out == ""
1510
+
1511
+ def test_quiet_shows_warning(self, capsys):
1512
+ r = Result()
1513
+ r.warn("heads up")
1514
+ ret = _report_text(r, quiet=True)
1515
+ assert ret == 0
1516
+ assert "heads up" in capsys.readouterr().out
1517
+
1518
+ def test_quiet_shows_error(self, capsys):
1519
+ r = Result()
1520
+ r.error("something broke")
1521
+ ret = _report_text(r, quiet=True)
1522
+ assert ret == 1
1523
+ assert "something broke" in capsys.readouterr().out
1524
+
1496
1525
 
1497
1526
  class TestReportJsonl:
1498
1527
  def test_ok_commit(self, capsys):
@@ -1522,6 +1551,19 @@ class TestReportJsonl:
1522
1551
  "message": "missing body",
1523
1552
  }
1524
1553
 
1554
+ def test_quiet_suppresses_ok_commit(self, capsys):
1555
+ r = Result()
1556
+ ret = _report_jsonl(r, "abc1234567890", "fix: add thing", quiet=True)
1557
+ assert ret == 0
1558
+ assert capsys.readouterr().out == ""
1559
+
1560
+ def test_quiet_shows_failed_commit(self, capsys):
1561
+ r = Result()
1562
+ r.error("missing body", check="body")
1563
+ ret = _report_jsonl(r, "abc1234567890", "fix: add thing", quiet=True)
1564
+ assert ret == 1
1565
+ assert json.loads(capsys.readouterr().out)["ok"] is False
1566
+
1525
1567
  def test_null_sha(self, capsys):
1526
1568
  r = Result()
1527
1569
  ret = _report_jsonl(r, None, "fix: add thing")
@@ -1545,6 +1587,40 @@ class TestReportJsonl:
1545
1587
  assert out.count("\n") == 1
1546
1588
 
1547
1589
 
1590
+ class TestReportCommit:
1591
+ def test_range_text_prints_sha_header(self, capsys):
1592
+ args = Namespace(output=OutputFormat.TEXT, rev_range="a..b", quiet=False)
1593
+ ret = _report_commit(args, Result(), "abc1234567890", "fix: add thing")
1594
+ assert ret == 0
1595
+ out = capsys.readouterr().out
1596
+ assert out.startswith("abc1234 fix: add thing")
1597
+ assert "all checks passed" in out
1598
+
1599
+ def test_single_commit_text_has_no_header(self, capsys):
1600
+ args = Namespace(output=OutputFormat.TEXT, rev_range=None, quiet=False)
1601
+ ret = _report_commit(args, Result(), None, "fix: add thing")
1602
+ assert ret == 0
1603
+ out = capsys.readouterr().out
1604
+ assert "fix: add thing" not in out
1605
+ assert "all checks passed" in out
1606
+
1607
+ def test_jsonl_dispatch(self, capsys):
1608
+ args = Namespace(output=OutputFormat.JSONL, rev_range="a..b", quiet=False)
1609
+ r = Result()
1610
+ r.error("missing body", check="body")
1611
+ ret = _report_commit(args, r, "abc1234567890", "fix: add thing")
1612
+ assert ret == 1
1613
+ data = json.loads(capsys.readouterr().out)
1614
+ assert data["sha"] == "abc1234567890"
1615
+ assert data["ok"] is False
1616
+
1617
+ def test_quiet_range_suppresses_header_for_clean_commit(self, capsys):
1618
+ args = Namespace(output=OutputFormat.TEXT, rev_range="a..b", quiet=True)
1619
+ ret = _report_commit(args, Result(), "abc1234567890", "fix: add thing")
1620
+ assert ret == 0
1621
+ assert capsys.readouterr().out == ""
1622
+
1623
+
1548
1624
  _VALID_MSG = "fix: add thing\n\nbody text\n\nSigned-off-by: A User <a@b.com>"
1549
1625
 
1550
1626
 
@@ -2452,3 +2528,141 @@ class TestOutputFile:
2452
2528
  assert main() == 1
2453
2529
  data = json.loads(out_file.read_text())
2454
2530
  assert data["ok"] is False
2531
+
2532
+
2533
+ class TestQuiet:
2534
+ def test_range_text_shows_only_failing_commits(self, capsys):
2535
+ messages = {"aaa1234": _VALID_MSG, "bbb5678": "bad message"}
2536
+ with (
2537
+ patch(
2538
+ "sys.argv",
2539
+ [
2540
+ "cg",
2541
+ "--range",
2542
+ "HEAD~2..HEAD",
2543
+ "--disable",
2544
+ "signature,body,signed-off,imperative",
2545
+ "--quiet",
2546
+ ],
2547
+ ),
2548
+ patch(
2549
+ "git_commit_guard._get_range_revs",
2550
+ return_value=["aaa1234", "bbb5678"],
2551
+ ),
2552
+ patch(
2553
+ "git_commit_guard._get_message",
2554
+ side_effect=lambda rev: messages[rev],
2555
+ ),
2556
+ ):
2557
+ assert main() == 1
2558
+ out = capsys.readouterr().out
2559
+ assert "bbb5678" in out
2560
+ assert "aaa1234" not in out
2561
+
2562
+ def test_range_text_all_pass_prints_nothing(self, capsys):
2563
+ with (
2564
+ patch(
2565
+ "sys.argv",
2566
+ ["cg", "--range", "HEAD~2..HEAD", "--disable", "signature", "-q"],
2567
+ ),
2568
+ patch(
2569
+ "git_commit_guard._get_range_revs",
2570
+ return_value=["aaa1234", "bbb5678"],
2571
+ ),
2572
+ patch("git_commit_guard._get_message", return_value=_VALID_MSG),
2573
+ ):
2574
+ assert main() == 0
2575
+ assert capsys.readouterr().out == ""
2576
+
2577
+ def test_single_commit_pass_prints_nothing(self, capsys, tmp_path):
2578
+ msg_file = tmp_path / "msg"
2579
+ msg_file.write_text(_VALID_MSG)
2580
+ with patch(
2581
+ "sys.argv",
2582
+ [
2583
+ "cg",
2584
+ "--message-file",
2585
+ str(msg_file),
2586
+ "--disable",
2587
+ "signature,imperative",
2588
+ "--quiet",
2589
+ ],
2590
+ ):
2591
+ assert main() == 0
2592
+ assert capsys.readouterr().out == ""
2593
+
2594
+ def test_single_commit_failure_still_printed(self, capsys, tmp_path):
2595
+ msg_file = tmp_path / "msg"
2596
+ msg_file.write_text("fix: add thing")
2597
+ with patch(
2598
+ "sys.argv",
2599
+ [
2600
+ "cg",
2601
+ "--message-file",
2602
+ str(msg_file),
2603
+ "--disable",
2604
+ "signature,imperative",
2605
+ "--quiet",
2606
+ ],
2607
+ ):
2608
+ assert main() == 1
2609
+ assert "body" in capsys.readouterr().out
2610
+
2611
+ def test_range_jsonl_emits_only_failing_records(self, capsys):
2612
+ messages = {"aaa1234": _VALID_MSG, "bbb5678": "bad message"}
2613
+ with (
2614
+ patch(
2615
+ "sys.argv",
2616
+ [
2617
+ "cg",
2618
+ "--range",
2619
+ "HEAD~2..HEAD",
2620
+ "--disable",
2621
+ "signature,body,signed-off,imperative",
2622
+ "--output",
2623
+ "jsonl",
2624
+ "--quiet",
2625
+ ],
2626
+ ),
2627
+ patch(
2628
+ "git_commit_guard._get_range_revs",
2629
+ return_value=["aaa1234", "bbb5678"],
2630
+ ),
2631
+ patch(
2632
+ "git_commit_guard._get_message",
2633
+ side_effect=lambda rev: messages[rev],
2634
+ ),
2635
+ ):
2636
+ assert main() == 1
2637
+ lines = capsys.readouterr().out.strip().splitlines()
2638
+ assert len(lines) == 1
2639
+ data = json.loads(lines[0])
2640
+ assert data["sha"] == "bbb5678"
2641
+ assert data["ok"] is False
2642
+
2643
+ def test_output_file_still_receives_all_records(self, capsys, tmp_path):
2644
+ out_file = tmp_path / "results.jsonl"
2645
+ with (
2646
+ patch(
2647
+ "sys.argv",
2648
+ [
2649
+ "cg",
2650
+ "--range",
2651
+ "HEAD~2..HEAD",
2652
+ "--disable",
2653
+ "signature",
2654
+ "--quiet",
2655
+ "--output-file",
2656
+ str(out_file),
2657
+ ],
2658
+ ),
2659
+ patch(
2660
+ "git_commit_guard._get_range_revs",
2661
+ return_value=["aaa1234", "bbb5678"],
2662
+ ),
2663
+ patch("git_commit_guard._get_message", return_value=_VALID_MSG),
2664
+ ):
2665
+ assert main() == 0
2666
+ assert capsys.readouterr().out == ""
2667
+ lines = out_file.read_text().strip().splitlines()
2668
+ assert len(lines) == 2