git-commit-guard 0.15.1__tar.gz → 0.16.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 (22) hide show
  1. {git_commit_guard-0.15.1 → git_commit_guard-0.16.0}/.github/workflows/lint-commits.yml +1 -1
  2. {git_commit_guard-0.15.1 → git_commit_guard-0.16.0}/.github/workflows/test.yml +9 -1
  3. {git_commit_guard-0.15.1 → git_commit_guard-0.16.0}/PKG-INFO +22 -8
  4. {git_commit_guard-0.15.1 → git_commit_guard-0.16.0}/README.md +21 -7
  5. {git_commit_guard-0.15.1 → git_commit_guard-0.16.0}/docs/index.html +101 -14
  6. {git_commit_guard-0.15.1 → git_commit_guard-0.16.0}/src/git_commit_guard/__init__.py +5 -1
  7. {git_commit_guard-0.15.1 → git_commit_guard-0.16.0}/tests/test_git_commit_guard.py +6 -0
  8. {git_commit_guard-0.15.1 → git_commit_guard-0.16.0}/.editorconfig +0 -0
  9. {git_commit_guard-0.15.1 → git_commit_guard-0.16.0}/.github/workflows/lint-md.yml +0 -0
  10. {git_commit_guard-0.15.1 → git_commit_guard-0.16.0}/.github/workflows/lint-python.yml +0 -0
  11. {git_commit_guard-0.15.1 → git_commit_guard-0.16.0}/.github/workflows/release.yml +0 -0
  12. {git_commit_guard-0.15.1 → git_commit_guard-0.16.0}/.gitignore +0 -0
  13. {git_commit_guard-0.15.1 → git_commit_guard-0.16.0}/.markdownlint.json +0 -0
  14. {git_commit_guard-0.15.1 → git_commit_guard-0.16.0}/.pre-commit-hooks.yaml +0 -0
  15. {git_commit_guard-0.15.1 → git_commit_guard-0.16.0}/.python-version +0 -0
  16. {git_commit_guard-0.15.1 → git_commit_guard-0.16.0}/LICENSE +0 -0
  17. {git_commit_guard-0.15.1 → git_commit_guard-0.16.0}/action.yml +0 -0
  18. {git_commit_guard-0.15.1 → git_commit_guard-0.16.0}/docs/commit-guard-icon.svg +0 -0
  19. {git_commit_guard-0.15.1 → git_commit_guard-0.16.0}/pyproject.toml +0 -0
  20. {git_commit_guard-0.15.1 → git_commit_guard-0.16.0}/ruff.toml +0 -0
  21. {git_commit_guard-0.15.1 → git_commit_guard-0.16.0}/tests/__init__.py +0 -0
  22. {git_commit_guard-0.15.1 → git_commit_guard-0.16.0}/uv.lock +0 -0
@@ -22,7 +22,7 @@ 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@c3ca496e477d64051d06fdb3aac44d8ffec7e852 # v0.14.1
25
+ uses: benner/commit-guard@064132b8b0cf5ec26083de7193f4e78d37a615d4 # v0.15.1
26
26
  with:
27
27
  range: origin/${{ github.base_ref }}..HEAD
28
28
  disable: signature
@@ -4,6 +4,7 @@ on: # yamllint disable-line rule:truthy
4
4
  pull_request:
5
5
  permissions:
6
6
  contents: read
7
+ pull-requests: write
7
8
  jobs:
8
9
  test:
9
10
  runs-on: ubuntu-latest
@@ -24,4 +25,11 @@ jobs:
24
25
  - name: Run tests with coverage
25
26
  run: |-
26
27
  uv run --dev pytest \
27
- tests/ --cov=git_commit_guard --cov-report=term-missing
28
+ tests/ --cov=git_commit_guard --cov-report=term-missing \
29
+ --cov-report=xml
30
+ - name: Post coverage comment
31
+ # yamllint disable-line rule:line-length
32
+ uses: MishaKav/pytest-coverage-comment@dd5b80bde6d16941f336518e92929e89069d8451 # v1.7.2
33
+ with:
34
+ pytest-xml-coverage-path: coverage.xml
35
+ unique-id-for-comment: coverage
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: git-commit-guard
3
- Version: 0.15.1
3
+ Version: 0.16.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
@@ -96,7 +96,7 @@ Available checks:
96
96
  * `subject` - Format matches `type(scope): description`, valid type,
97
97
  lowercase start, no trailing period, max 72 chars
98
98
  * `imperative` - First word is an imperative verb (for example `add` not `added`)
99
- * `body` - Body is present after a blank line
99
+ * `body` - Blank line separates subject from body, and body is non-empty
100
100
  * `signed-off` - `Signed-off-by:` trailer exists
101
101
  * `signature` - Verify GPG or SSH signature
102
102
 
@@ -207,7 +207,7 @@ COMMIT_GUARD_GIT_TIMEOUT=30 commit-guard --range origin/main..HEAD
207
207
  In GitHub Actions, set it at the step or job level:
208
208
 
209
209
  ```yaml
210
- - uses: benner/commit-guard@v0.15.0
210
+ - uses: benner/commit-guard@v0.16.0
211
211
  env:
212
212
  COMMIT_GUARD_GIT_TIMEOUT: 30
213
213
  with:
@@ -280,6 +280,10 @@ commit-guard --range origin/main..HEAD --output-file results.jsonl
280
280
  `--output-file` is independent of `--output`: combining both writes JSONL to
281
281
  both stdout and the file.
282
282
 
283
+ In GitHub Actions, `output-file` is the recommended way to get machine-readable
284
+ results — text stays in the CI log and the file is accessible to subsequent steps
285
+ via `steps.<id>.outputs.output-file`.
286
+
283
287
  ### GitHub Actions
284
288
 
285
289
  ```yaml
@@ -287,7 +291,7 @@ steps:
287
291
  - uses: actions/checkout@v4
288
292
  with:
289
293
  fetch-depth: 0
290
- - uses: benner/commit-guard@v0.15.0
294
+ - uses: benner/commit-guard@v0.16.0
291
295
  ```
292
296
 
293
297
  Check all commits in a pull request:
@@ -303,11 +307,19 @@ jobs:
303
307
  - uses: actions/checkout@v4
304
308
  with:
305
309
  fetch-depth: 0
306
- - uses: benner/commit-guard@v0.15.0
310
+ - uses: benner/commit-guard@v0.16.0
307
311
  with:
308
312
  range: ${{ env.PR_BASE }}..${{ env.PR_HEAD }}
309
313
  ```
310
314
 
315
+ Check a specific commit SHA (mirrors the positional CLI argument):
316
+
317
+ ```yaml
318
+ - uses: benner/commit-guard@v0.16.0
319
+ with:
320
+ rev: ${{ github.sha }}
321
+ ```
322
+
311
323
  All inputs are optional and mirror the CLI flags:
312
324
 
313
325
  ```yaml
@@ -321,7 +333,7 @@ jobs:
321
333
  - uses: actions/checkout@v4
322
334
  with:
323
335
  fetch-depth: 0
324
- - uses: benner/commit-guard@v0.15.0
336
+ - uses: benner/commit-guard@v0.16.0
325
337
  with:
326
338
  range: ${{ env.PR_BASE }}..${{ env.PR_HEAD }}
327
339
  disable: signed-off,signature
@@ -330,13 +342,15 @@ jobs:
330
342
  require-trailer: 'Closes,Reviewed-by'
331
343
  max-subject-length: '100'
332
344
  min-description-length: '10'
345
+ allow-empty: 'true'
346
+ include-merges: 'true'
333
347
  output-file: results.jsonl
334
348
  ```
335
349
 
336
350
  When `output-file` is set the action exposes the path as an output:
337
351
 
338
352
  ```yaml
339
- - uses: benner/commit-guard@v0.15.0
353
+ - uses: benner/commit-guard@v0.16.0
340
354
  id: cg
341
355
  with:
342
356
  range: ${{ env.PR_BASE }}..${{ env.PR_HEAD }}
@@ -352,7 +366,7 @@ Add to your `.pre-commit-config.yaml`:
352
366
  ---
353
367
  repos:
354
368
  - repo: https://github.com/benner/commit-guard
355
- rev: v0.15.0
369
+ rev: v0.16.0
356
370
  hooks:
357
371
  - id: commit-guard
358
372
  - id: commit-guard-signature
@@ -75,7 +75,7 @@ Available checks:
75
75
  * `subject` - Format matches `type(scope): description`, valid type,
76
76
  lowercase start, no trailing period, max 72 chars
77
77
  * `imperative` - First word is an imperative verb (for example `add` not `added`)
78
- * `body` - Body is present after a blank line
78
+ * `body` - Blank line separates subject from body, and body is non-empty
79
79
  * `signed-off` - `Signed-off-by:` trailer exists
80
80
  * `signature` - Verify GPG or SSH signature
81
81
 
@@ -186,7 +186,7 @@ COMMIT_GUARD_GIT_TIMEOUT=30 commit-guard --range origin/main..HEAD
186
186
  In GitHub Actions, set it at the step or job level:
187
187
 
188
188
  ```yaml
189
- - uses: benner/commit-guard@v0.15.0
189
+ - uses: benner/commit-guard@v0.16.0
190
190
  env:
191
191
  COMMIT_GUARD_GIT_TIMEOUT: 30
192
192
  with:
@@ -259,6 +259,10 @@ commit-guard --range origin/main..HEAD --output-file results.jsonl
259
259
  `--output-file` is independent of `--output`: combining both writes JSONL to
260
260
  both stdout and the file.
261
261
 
262
+ In GitHub Actions, `output-file` is the recommended way to get machine-readable
263
+ results — text stays in the CI log and the file is accessible to subsequent steps
264
+ via `steps.<id>.outputs.output-file`.
265
+
262
266
  ### GitHub Actions
263
267
 
264
268
  ```yaml
@@ -266,7 +270,7 @@ steps:
266
270
  - uses: actions/checkout@v4
267
271
  with:
268
272
  fetch-depth: 0
269
- - uses: benner/commit-guard@v0.15.0
273
+ - uses: benner/commit-guard@v0.16.0
270
274
  ```
271
275
 
272
276
  Check all commits in a pull request:
@@ -282,11 +286,19 @@ jobs:
282
286
  - uses: actions/checkout@v4
283
287
  with:
284
288
  fetch-depth: 0
285
- - uses: benner/commit-guard@v0.15.0
289
+ - uses: benner/commit-guard@v0.16.0
286
290
  with:
287
291
  range: ${{ env.PR_BASE }}..${{ env.PR_HEAD }}
288
292
  ```
289
293
 
294
+ Check a specific commit SHA (mirrors the positional CLI argument):
295
+
296
+ ```yaml
297
+ - uses: benner/commit-guard@v0.16.0
298
+ with:
299
+ rev: ${{ github.sha }}
300
+ ```
301
+
290
302
  All inputs are optional and mirror the CLI flags:
291
303
 
292
304
  ```yaml
@@ -300,7 +312,7 @@ jobs:
300
312
  - uses: actions/checkout@v4
301
313
  with:
302
314
  fetch-depth: 0
303
- - uses: benner/commit-guard@v0.15.0
315
+ - uses: benner/commit-guard@v0.16.0
304
316
  with:
305
317
  range: ${{ env.PR_BASE }}..${{ env.PR_HEAD }}
306
318
  disable: signed-off,signature
@@ -309,13 +321,15 @@ jobs:
309
321
  require-trailer: 'Closes,Reviewed-by'
310
322
  max-subject-length: '100'
311
323
  min-description-length: '10'
324
+ allow-empty: 'true'
325
+ include-merges: 'true'
312
326
  output-file: results.jsonl
313
327
  ```
314
328
 
315
329
  When `output-file` is set the action exposes the path as an output:
316
330
 
317
331
  ```yaml
318
- - uses: benner/commit-guard@v0.15.0
332
+ - uses: benner/commit-guard@v0.16.0
319
333
  id: cg
320
334
  with:
321
335
  range: ${{ env.PR_BASE }}..${{ env.PR_HEAD }}
@@ -331,7 +345,7 @@ Add to your `.pre-commit-config.yaml`:
331
345
  ---
332
346
  repos:
333
347
  - repo: https://github.com/benner/commit-guard
334
- rev: v0.15.0
348
+ rev: v0.16.0
335
349
  hooks:
336
350
  - id: commit-guard
337
351
  - id: commit-guard-signature
@@ -332,7 +332,10 @@ $ commit-guard abc1234
332
332
  $ commit-guard --range origin/main..HEAD
333
333
 
334
334
  # read from a file (for git hooks)
335
- $ commit-guard --message-file .git/COMMIT_EDITMSG</code></pre>
335
+ $ commit-guard --message-file .git/COMMIT_EDITMSG
336
+
337
+ # pipe message via stdin
338
+ $ echo "fix(auth): add token refresh" | commit-guard</code></pre>
336
339
 
337
340
  <h3>Checks</h3>
338
341
  <p>
@@ -351,8 +354,11 @@ $ commit-guard --message-file .git/COMMIT_EDITMSG</code></pre>
351
354
  <tr>
352
355
  <td><code>subject</code></td>
353
356
  <td>
354
- Format matches <code>type(scope): description</code>, valid
355
- type, lowercase start, no trailing period, max 72 chars
357
+ <a href="https://www.conventionalcommits.org/" target="_blank" rel="noopener">Conventional Commits</a>
358
+ format: valid type, lowercase start, no trailing period,
359
+ max length (default 72). All limits are configurable. Use
360
+ <code>!</code> before the colon for breaking changes:
361
+ <code>feat!: remove endpoint</code>
356
362
  </td>
357
363
  </tr>
358
364
  <tr>
@@ -363,7 +369,7 @@ $ commit-guard --message-file .git/COMMIT_EDITMSG</code></pre>
363
369
  </tr>
364
370
  <tr>
365
371
  <td><code>body</code></td>
366
- <td>Body is present after a blank line</td>
372
+ <td>Blank line separates subject from body, and body is non-empty</td>
367
373
  </tr>
368
374
  <tr>
369
375
  <td><code>signed-off</code></td>
@@ -381,8 +387,9 @@ $ commit-guard --message-file .git/COMMIT_EDITMSG</code></pre>
381
387
  <section id="configuration">
382
388
  <h2>Configuration <a href="#configuration" class="anchor">#</a></h2>
383
389
  <p>
384
- Place <code>.commit-guard.toml</code> in your project root. CLI flags
385
- always take precedence over the config file.
390
+ Place <code>.commit-guard.toml</code> in your project root or any
391
+ parent directory commit-guard searches upward and uses the first
392
+ file found. CLI flags always take precedence.
386
393
  </p>
387
394
  <pre><code class="language-toml"># .commit-guard.toml
388
395
  disable = ["signature", "body"]
@@ -392,6 +399,69 @@ types = ["feat", "fix", "chore", "wip"]
392
399
  max-subject-length = 100
393
400
  min-description-length = 10
394
401
  require-trailers = ["Closes", "Reviewed-by"]</code></pre>
402
+
403
+ <h3>Required trailers</h3>
404
+ <p>
405
+ Require arbitrary trailers in the commit message. Accepts a
406
+ comma-separated list; matching is case-sensitive and requires a
407
+ non-empty value after the colon (e.g. <code>Closes: #42</code>):
408
+ </p>
409
+ <pre><code class="language-bash">commit-guard --require-trailer "Closes,Reviewed-by"</code></pre>
410
+
411
+ <h3>Range options</h3>
412
+ <p>
413
+ When using <code>--range</code>, merge commits are excluded by
414
+ default. Use <code>--include-merges</code> to check them. An empty
415
+ range exits non-zero by default — use <code>--allow-empty</code> to
416
+ exit 0 instead:
417
+ </p>
418
+ <pre><code class="language-bash">commit-guard --range origin/main..HEAD --include-merges --allow-empty</code></pre>
419
+
420
+ <h3>Environment variables</h3>
421
+ <figure>
422
+ <table>
423
+ <thead>
424
+ <tr>
425
+ <th>Variable</th>
426
+ <th>Default</th>
427
+ <th>Description</th>
428
+ </tr>
429
+ </thead>
430
+ <tbody>
431
+ <tr>
432
+ <td><code>COMMIT_GUARD_GIT_TIMEOUT</code></td>
433
+ <td><code>10</code></td>
434
+ <td>Timeout in seconds for git subprocess calls</td>
435
+ </tr>
436
+ </tbody>
437
+ </table>
438
+ </figure>
439
+ </section>
440
+
441
+ <section id="output">
442
+ <h2>Output <a href="#output" class="anchor">#</a></h2>
443
+ <p>
444
+ Use <code>--output jsonl</code> to emit one JSON line per commit to
445
+ stdout instead of the default human-readable text:
446
+ </p>
447
+ <pre><code class="language-bash">commit-guard --range origin/main..HEAD --output jsonl | jq 'select(.ok == false)'</code></pre>
448
+ <p>Each line is a JSON object:</p>
449
+ <pre><code>{
450
+ "sha": "abc1234...",
451
+ "subject": "feat: add thing",
452
+ "ok": false,
453
+ "results": [{"check": "body", "level": "error", "message": "missing body"}]
454
+ }</code></pre>
455
+ <p>
456
+ <code>sha</code> is <code>null</code> when reading from a file or
457
+ stdin. <code>results</code> is empty when all checks pass.
458
+ </p>
459
+ <p>
460
+ Use <code>--output-file FILE</code> to write JSONL to a file while
461
+ keeping human-readable text on stdout — useful in CI where you want
462
+ readable logs and structured results for downstream steps:
463
+ </p>
464
+ <pre><code class="language-bash">commit-guard --range origin/main..HEAD --output-file results.jsonl</code></pre>
395
465
  </section>
396
466
 
397
467
  <section id="github-actions">
@@ -407,18 +477,35 @@ require-trailers = ["Closes", "Reviewed-by"]</code></pre>
407
477
  - uses: actions/checkout@v4
408
478
  with:
409
479
  fetch-depth: 0
410
- - uses: benner/commit-guard@v0.14.1
480
+ - uses: benner/commit-guard@v0.16.0
411
481
  with:
412
482
  range: ${{ env.PR_BASE }}..${{ env.PR_HEAD }}
413
483
  disable: signed-off,signature</code></pre>
414
484
 
485
+ <p>Check a specific commit SHA:</p>
486
+ <pre><code class="language-yaml"> - uses: benner/commit-guard@v0.16.0
487
+ with:
488
+ rev: ${{ github.sha }}</code></pre>
489
+
490
+ <p>
491
+ All inputs mirror the CLI flags: <code>rev</code>,
492
+ <code>range</code>, <code>enable</code>, <code>disable</code>,
493
+ <code>scopes</code>, <code>require-scope</code>, <code>types</code>,
494
+ <code>max-subject-length</code>, <code>min-description-length</code>,
495
+ <code>require-trailer</code>, <code>allow-empty</code>,
496
+ <code>include-merges</code>, <code>output-file</code>.
497
+ </p>
498
+
415
499
  <p>
416
- All inputs mirror the CLI flags: <code>enable</code>,
417
- <code>disable</code>, <code>scopes</code>, <code>require-scope</code>,
418
- <code>types</code>, <code>max-subject-length</code>,
419
- <code>min-description-length</code>, <code>require-trailer</code>,
420
- <code>output-file</code>.
500
+ When <code>output-file</code> is set the action exposes the path as
501
+ a step output, making JSONL results available to subsequent steps:
421
502
  </p>
503
+ <pre><code class="language-yaml"> - uses: benner/commit-guard@v0.16.0
504
+ id: cg
505
+ with:
506
+ range: ${{ env.PR_BASE }}..${{ env.PR_HEAD }}
507
+ output-file: results.jsonl
508
+ - run: jq 'select(.ok == false)' "${{ steps.cg.outputs.output-file }}"</code></pre>
422
509
  </section>
423
510
 
424
511
  <section id="pre-commit">
@@ -426,7 +513,7 @@ require-trailers = ["Closes", "Reviewed-by"]</code></pre>
426
513
  <p>Add to <code>.pre-commit-config.yaml</code>:</p>
427
514
  <pre><code class="language-yaml">repos:
428
515
  - repo: https://github.com/benner/commit-guard
429
- rev: v0.14.1
516
+ rev: v0.16.0
430
517
  hooks:
431
518
  - id: commit-guard
432
519
  - id: commit-guard-signature</code></pre>
@@ -596,7 +683,7 @@ require-trailers = ["Closes", "Reviewed-by"]</code></pre>
596
683
  });
597
684
 
598
685
  document.querySelectorAll(
599
- "#configuration pre code, #github-actions pre code, #pre-commit pre code"
686
+ "#configuration pre code, #output pre code, #github-actions pre code, #pre-commit pre code"
600
687
  ).forEach((code) => {
601
688
  const text = code.innerText.trim();
602
689
  const isConfig = code.className.includes("language-");
@@ -220,11 +220,15 @@ def check_imperative(desc, result):
220
220
 
221
221
 
222
222
  def check_body(lines, result):
223
- if len(lines) < 3: # noqa: PLR2004
223
+ if len(lines) == 1:
224
224
  result.error("missing body", check=Check.BODY)
225
225
  return
226
226
  if lines[1].strip():
227
227
  result.error("missing blank line between subject and body", check=Check.BODY)
228
+ return
229
+ if len(lines) == 2: # noqa: PLR2004 Magic value used in comparison, consider replacing 2 with a constant variable
230
+ result.error("missing body", check=Check.BODY)
231
+ return
228
232
  body_lines = [ln for ln in lines[2:] if not _TRAILER_RE.match(ln)]
229
233
  if not any(ln.strip() for ln in body_lines):
230
234
  result.error("missing body", check=Check.BODY)
@@ -237,6 +237,12 @@ class TestCheckBody:
237
237
  check_body(["fix: add thing", "body text", "more"], r)
238
238
  assert not r.ok
239
239
 
240
+ def test_missing_blank_line_two_lines(self):
241
+ r = Result()
242
+ check_body(["fix: add thing", "body text"], r)
243
+ assert not r.ok
244
+ assert any("blank line" in msg for _, _, msg in r.errors)
245
+
240
246
  def test_blank_body_content(self):
241
247
  r = Result()
242
248
  check_body(["fix: add thing", "", " "], r)