git-commit-guard 0.15.1__tar.gz → 0.17.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.
- git_commit_guard-0.17.0/.github/workflows/coverage-baseline.yml +33 -0
- git_commit_guard-0.17.0/.github/workflows/coverage-comment.yml +97 -0
- {git_commit_guard-0.15.1 → git_commit_guard-0.17.0}/.github/workflows/lint-commits.yml +1 -1
- {git_commit_guard-0.15.1 → git_commit_guard-0.17.0}/.github/workflows/release.yml +1 -1
- {git_commit_guard-0.15.1 → git_commit_guard-0.17.0}/.github/workflows/test.yml +13 -1
- {git_commit_guard-0.15.1 → git_commit_guard-0.17.0}/PKG-INFO +65 -10
- {git_commit_guard-0.15.1 → git_commit_guard-0.17.0}/README.md +64 -9
- {git_commit_guard-0.15.1 → git_commit_guard-0.17.0}/action.yml +11 -0
- {git_commit_guard-0.15.1 → git_commit_guard-0.17.0}/docs/index.html +104 -13
- {git_commit_guard-0.15.1 → git_commit_guard-0.17.0}/src/git_commit_guard/__init__.py +48 -5
- {git_commit_guard-0.15.1 → git_commit_guard-0.17.0}/tests/test_git_commit_guard.py +36 -0
- {git_commit_guard-0.15.1 → git_commit_guard-0.17.0}/.editorconfig +0 -0
- {git_commit_guard-0.15.1 → git_commit_guard-0.17.0}/.github/workflows/lint-md.yml +0 -0
- {git_commit_guard-0.15.1 → git_commit_guard-0.17.0}/.github/workflows/lint-python.yml +0 -0
- {git_commit_guard-0.15.1 → git_commit_guard-0.17.0}/.gitignore +0 -0
- {git_commit_guard-0.15.1 → git_commit_guard-0.17.0}/.markdownlint.json +0 -0
- {git_commit_guard-0.15.1 → git_commit_guard-0.17.0}/.pre-commit-hooks.yaml +0 -0
- {git_commit_guard-0.15.1 → git_commit_guard-0.17.0}/.python-version +0 -0
- {git_commit_guard-0.15.1 → git_commit_guard-0.17.0}/LICENSE +0 -0
- {git_commit_guard-0.15.1 → git_commit_guard-0.17.0}/docs/commit-guard-icon.svg +0 -0
- {git_commit_guard-0.15.1 → git_commit_guard-0.17.0}/pyproject.toml +0 -0
- {git_commit_guard-0.15.1 → git_commit_guard-0.17.0}/ruff.toml +0 -0
- {git_commit_guard-0.15.1 → git_commit_guard-0.17.0}/tests/__init__.py +0 -0
- {git_commit_guard-0.15.1 → git_commit_guard-0.17.0}/uv.lock +0 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Coverage Baseline
|
|
3
|
+
on: # yamllint disable-line rule:truthy
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
permissions:
|
|
7
|
+
contents: read
|
|
8
|
+
jobs:
|
|
9
|
+
baseline:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- name: Checkout code
|
|
13
|
+
# yamllint disable-line rule:line-length
|
|
14
|
+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # ratchet:actions/checkout@v6
|
|
15
|
+
with:
|
|
16
|
+
persist-credentials: false
|
|
17
|
+
- name: Install uv
|
|
18
|
+
# yamllint disable-line rule:line-length
|
|
19
|
+
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # ratchet:astral-sh/setup-uv@v7
|
|
20
|
+
- name: Cache NLTK data
|
|
21
|
+
# yamllint disable-line rule:line-length
|
|
22
|
+
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # ratchet:actions/cache@v5
|
|
23
|
+
with:
|
|
24
|
+
path: ~/nltk_data
|
|
25
|
+
key: nltk-averaged-perceptron-tagger-punkt
|
|
26
|
+
- name: Run tests with coverage
|
|
27
|
+
run: uv run --dev pytest tests/ --cov=git_commit_guard --cov-report=xml
|
|
28
|
+
- name: Upload coverage baseline
|
|
29
|
+
# yamllint disable-line rule:line-length
|
|
30
|
+
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # ratchet:actions/upload-artifact@v4
|
|
31
|
+
with:
|
|
32
|
+
name: main-coverage
|
|
33
|
+
path: coverage.xml
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Coverage Comment
|
|
3
|
+
on: # yamllint disable-line rule:truthy # zizmor: ignore[dangerous-triggers]
|
|
4
|
+
workflow_run:
|
|
5
|
+
workflows: [Test]
|
|
6
|
+
types: [completed]
|
|
7
|
+
permissions:
|
|
8
|
+
actions: read
|
|
9
|
+
pull-requests: write
|
|
10
|
+
jobs:
|
|
11
|
+
comment:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
if: >-
|
|
14
|
+
github.event.workflow_run.event == 'pull_request' &&
|
|
15
|
+
github.event.workflow_run.conclusion == 'success'
|
|
16
|
+
steps:
|
|
17
|
+
- name: Download artifact
|
|
18
|
+
# yamllint disable-line rule:line-length
|
|
19
|
+
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # ratchet:actions/download-artifact@v4
|
|
20
|
+
with:
|
|
21
|
+
run-id: ${{ github.event.workflow_run.id }}
|
|
22
|
+
name: pr
|
|
23
|
+
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
24
|
+
- name: Read PR number
|
|
25
|
+
id: pr
|
|
26
|
+
run: |
|
|
27
|
+
pr_number=$(cat NR)
|
|
28
|
+
if ! [[ "$pr_number" =~ ^[0-9]+$ ]]; then
|
|
29
|
+
echo "Invalid PR number: $pr_number" >&2
|
|
30
|
+
exit 1
|
|
31
|
+
fi
|
|
32
|
+
echo "number=$pr_number" >> "$GITHUB_OUTPUT"
|
|
33
|
+
- name: Find baseline run
|
|
34
|
+
id: baseline
|
|
35
|
+
env:
|
|
36
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
37
|
+
run: |
|
|
38
|
+
python3 - <<'PYEOF'
|
|
39
|
+
import json, os, urllib.request
|
|
40
|
+
token = os.environ['GITHUB_TOKEN']
|
|
41
|
+
repo = os.environ['GITHUB_REPOSITORY']
|
|
42
|
+
url = (
|
|
43
|
+
f'https://api.github.com/repos/{repo}/actions/workflows'
|
|
44
|
+
f'/coverage-baseline.yml/runs'
|
|
45
|
+
f'?branch=main&status=success&per_page=1'
|
|
46
|
+
)
|
|
47
|
+
req = urllib.request.Request(url, headers={
|
|
48
|
+
'Authorization': f'Bearer {token}',
|
|
49
|
+
'Accept': 'application/vnd.github+json',
|
|
50
|
+
})
|
|
51
|
+
with urllib.request.urlopen(req) as r:
|
|
52
|
+
runs = json.loads(r.read()).get('workflow_runs', [])
|
|
53
|
+
run_id = str(runs[0]['id']) if runs else ''
|
|
54
|
+
with open(os.environ['GITHUB_OUTPUT'], 'a') as f:
|
|
55
|
+
f.write(f'run-id={run_id}\n')
|
|
56
|
+
PYEOF
|
|
57
|
+
- name: Download baseline coverage
|
|
58
|
+
if: steps.baseline.outputs.run-id != ''
|
|
59
|
+
# yamllint disable-line rule:line-length
|
|
60
|
+
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # ratchet:actions/download-artifact@v4
|
|
61
|
+
with:
|
|
62
|
+
run-id: ${{ steps.baseline.outputs.run-id }}
|
|
63
|
+
name: main-coverage
|
|
64
|
+
path: main/
|
|
65
|
+
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
66
|
+
- name: Compute coverage delta
|
|
67
|
+
id: delta
|
|
68
|
+
if: steps.baseline.outputs.run-id != ''
|
|
69
|
+
run: |
|
|
70
|
+
python3 - <<'PYEOF'
|
|
71
|
+
import xml.etree.ElementTree as ET
|
|
72
|
+
import os, sys
|
|
73
|
+
try:
|
|
74
|
+
pr_rate = ET.parse('coverage.xml').getroot().get('line-rate')
|
|
75
|
+
main_xml = ET.parse('main/coverage.xml').getroot()
|
|
76
|
+
main_rate = main_xml.get('line-rate')
|
|
77
|
+
pr = float(pr_rate) * 100
|
|
78
|
+
main = float(main_rate) * 100
|
|
79
|
+
delta = pr - main
|
|
80
|
+
if not (-100 <= delta <= 100):
|
|
81
|
+
raise ValueError(f"delta out of range: {delta}")
|
|
82
|
+
sign = '+' if delta >= 0 else ''
|
|
83
|
+
title = f"Coverage Report (Δ {sign}{delta:.1f}%)"
|
|
84
|
+
except Exception as e:
|
|
85
|
+
print(f"Warning: could not compute delta: {e}", file=sys.stderr)
|
|
86
|
+
title = "Coverage Report"
|
|
87
|
+
with open(os.environ['GITHUB_OUTPUT'], 'a') as f:
|
|
88
|
+
f.write(f"title={title}\n")
|
|
89
|
+
PYEOF
|
|
90
|
+
- name: Post coverage comment
|
|
91
|
+
# yamllint disable-line rule:line-length
|
|
92
|
+
uses: MishaKav/pytest-coverage-comment@dd5b80bde6d16941f336518e92929e89069d8451 # ratchet:MishaKav/pytest-coverage-comment@v1.7.2
|
|
93
|
+
with:
|
|
94
|
+
pytest-xml-coverage-path: coverage.xml
|
|
95
|
+
unique-id-for-comment: coverage
|
|
96
|
+
issue-number: ${{ steps.pr.outputs.number }}
|
|
97
|
+
title: ${{ steps.delta.outputs.title || 'Coverage Report' }}
|
|
@@ -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@
|
|
25
|
+
uses: benner/commit-guard@064132b8b0cf5ec26083de7193f4e78d37a615d4 # v0.15.1
|
|
26
26
|
with:
|
|
27
27
|
range: origin/${{ github.base_ref }}..HEAD
|
|
28
28
|
disable: signature
|
|
@@ -33,7 +33,7 @@ jobs:
|
|
|
33
33
|
- name: Generate release notes
|
|
34
34
|
id: git-cliff
|
|
35
35
|
# yamllint disable-line rule:line-length
|
|
36
|
-
uses: orhun/git-cliff-action@
|
|
36
|
+
uses: orhun/git-cliff-action@f50e11560dce63f7c33227798f90b924471a88b5 # ratchet:orhun/git-cliff-action@v4.8.0
|
|
37
37
|
with:
|
|
38
38
|
args: --current
|
|
39
39
|
- name: Create GitHub Release
|
|
@@ -24,4 +24,16 @@ jobs:
|
|
|
24
24
|
- name: Run tests with coverage
|
|
25
25
|
run: |-
|
|
26
26
|
uv run --dev pytest \
|
|
27
|
-
tests/ --cov=git_commit_guard --cov-report=term-missing
|
|
27
|
+
tests/ --cov=git_commit_guard --cov-report=term-missing \
|
|
28
|
+
--cov-report=xml
|
|
29
|
+
- name: Save coverage artifact
|
|
30
|
+
run: |
|
|
31
|
+
mkdir -p ./pr
|
|
32
|
+
echo ${{ github.event.number }} > ./pr/NR
|
|
33
|
+
cp coverage.xml ./pr/
|
|
34
|
+
- name: Upload PR artifact
|
|
35
|
+
# yamllint disable-line rule:line-length
|
|
36
|
+
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # ratchet:actions/upload-artifact@v4
|
|
37
|
+
with:
|
|
38
|
+
name: pr
|
|
39
|
+
path: pr/
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: git-commit-guard
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.17.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` -
|
|
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
|
|
|
@@ -116,6 +116,41 @@ By default there is no minimum description length. Enforce one with
|
|
|
116
116
|
commit-guard --min-description-length 10
|
|
117
117
|
```
|
|
118
118
|
|
|
119
|
+
### Subject format
|
|
120
|
+
|
|
121
|
+
By default the description must start with a lowercase letter. To allow
|
|
122
|
+
uppercase descriptions:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
commit-guard --no-require-lowercase
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
In `.commit-guard.toml`:
|
|
129
|
+
|
|
130
|
+
```toml
|
|
131
|
+
require-lowercase = false
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
By default trailing `.` is forbidden. To change the set of forbidden trailing
|
|
135
|
+
characters (any character is valid, including space):
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
commit-guard --no-trailing-chars ".,"
|
|
139
|
+
commit-guard --no-trailing-chars ".,!"
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
In `.commit-guard.toml`:
|
|
143
|
+
|
|
144
|
+
```toml
|
|
145
|
+
no-trailing-chars = [".", "!"]
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Pass an empty list to disable the check entirely:
|
|
149
|
+
|
|
150
|
+
```toml
|
|
151
|
+
no-trailing-chars = []
|
|
152
|
+
```
|
|
153
|
+
|
|
119
154
|
### Type validation
|
|
120
155
|
|
|
121
156
|
By default the standard conventional commit types are accepted. Use `--types`
|
|
@@ -170,7 +205,8 @@ independently of `--enable`/`--disable`.
|
|
|
170
205
|
|
|
171
206
|
Place `.commit-guard.toml` in your project root (or any parent directory) to
|
|
172
207
|
set defaults for `enable`, `disable`, `scopes`, `require-scope`, `types`,
|
|
173
|
-
`max-subject-length`, `min-description-length`,
|
|
208
|
+
`max-subject-length`, `min-description-length`, `require-lowercase`,
|
|
209
|
+
`no-trailing-chars`, and `require-trailers`.
|
|
174
210
|
commit-guard searches upward from the working directory and uses the first
|
|
175
211
|
file found.
|
|
176
212
|
|
|
@@ -182,6 +218,8 @@ require-scope = true
|
|
|
182
218
|
types = ["feat", "fix", "chore", "wip"]
|
|
183
219
|
max-subject-length = 100
|
|
184
220
|
min-description-length = 10
|
|
221
|
+
require-lowercase = false
|
|
222
|
+
no-trailing-chars = [".", "!"]
|
|
185
223
|
require-trailers = ["Closes", "Reviewed-by"]
|
|
186
224
|
```
|
|
187
225
|
|
|
@@ -191,7 +229,8 @@ enable = ["subject", "imperative"]
|
|
|
191
229
|
```
|
|
192
230
|
|
|
193
231
|
CLI flags (`--enable`, `--disable`, `--scopes`, `--require-scope`, `--types`,
|
|
194
|
-
`--max-subject-length`, `--min-description-length`, `--require-
|
|
232
|
+
`--max-subject-length`, `--min-description-length`, `--no-require-lowercase`,
|
|
233
|
+
`--no-trailing-chars`, `--require-trailer`) take
|
|
195
234
|
full precedence and ignore config file values when provided.
|
|
196
235
|
|
|
197
236
|
### Environment variables
|
|
@@ -207,7 +246,7 @@ COMMIT_GUARD_GIT_TIMEOUT=30 commit-guard --range origin/main..HEAD
|
|
|
207
246
|
In GitHub Actions, set it at the step or job level:
|
|
208
247
|
|
|
209
248
|
```yaml
|
|
210
|
-
- uses: benner/commit-guard@v0.
|
|
249
|
+
- uses: benner/commit-guard@v0.17.0
|
|
211
250
|
env:
|
|
212
251
|
COMMIT_GUARD_GIT_TIMEOUT: 30
|
|
213
252
|
with:
|
|
@@ -280,6 +319,10 @@ commit-guard --range origin/main..HEAD --output-file results.jsonl
|
|
|
280
319
|
`--output-file` is independent of `--output`: combining both writes JSONL to
|
|
281
320
|
both stdout and the file.
|
|
282
321
|
|
|
322
|
+
In GitHub Actions, `output-file` is the recommended way to get machine-readable
|
|
323
|
+
results — text stays in the CI log and the file is accessible to subsequent steps
|
|
324
|
+
via `steps.<id>.outputs.output-file`.
|
|
325
|
+
|
|
283
326
|
### GitHub Actions
|
|
284
327
|
|
|
285
328
|
```yaml
|
|
@@ -287,7 +330,7 @@ steps:
|
|
|
287
330
|
- uses: actions/checkout@v4
|
|
288
331
|
with:
|
|
289
332
|
fetch-depth: 0
|
|
290
|
-
- uses: benner/commit-guard@v0.
|
|
333
|
+
- uses: benner/commit-guard@v0.17.0
|
|
291
334
|
```
|
|
292
335
|
|
|
293
336
|
Check all commits in a pull request:
|
|
@@ -303,11 +346,19 @@ jobs:
|
|
|
303
346
|
- uses: actions/checkout@v4
|
|
304
347
|
with:
|
|
305
348
|
fetch-depth: 0
|
|
306
|
-
- uses: benner/commit-guard@v0.
|
|
349
|
+
- uses: benner/commit-guard@v0.17.0
|
|
307
350
|
with:
|
|
308
351
|
range: ${{ env.PR_BASE }}..${{ env.PR_HEAD }}
|
|
309
352
|
```
|
|
310
353
|
|
|
354
|
+
Check a specific commit SHA (mirrors the positional CLI argument):
|
|
355
|
+
|
|
356
|
+
```yaml
|
|
357
|
+
- uses: benner/commit-guard@v0.17.0
|
|
358
|
+
with:
|
|
359
|
+
rev: ${{ github.sha }}
|
|
360
|
+
```
|
|
361
|
+
|
|
311
362
|
All inputs are optional and mirror the CLI flags:
|
|
312
363
|
|
|
313
364
|
```yaml
|
|
@@ -321,7 +372,7 @@ jobs:
|
|
|
321
372
|
- uses: actions/checkout@v4
|
|
322
373
|
with:
|
|
323
374
|
fetch-depth: 0
|
|
324
|
-
- uses: benner/commit-guard@v0.
|
|
375
|
+
- uses: benner/commit-guard@v0.17.0
|
|
325
376
|
with:
|
|
326
377
|
range: ${{ env.PR_BASE }}..${{ env.PR_HEAD }}
|
|
327
378
|
disable: signed-off,signature
|
|
@@ -330,13 +381,17 @@ jobs:
|
|
|
330
381
|
require-trailer: 'Closes,Reviewed-by'
|
|
331
382
|
max-subject-length: '100'
|
|
332
383
|
min-description-length: '10'
|
|
384
|
+
no-require-lowercase: 'true'
|
|
385
|
+
no-trailing-chars: '.,!'
|
|
386
|
+
allow-empty: 'true'
|
|
387
|
+
include-merges: 'true'
|
|
333
388
|
output-file: results.jsonl
|
|
334
389
|
```
|
|
335
390
|
|
|
336
391
|
When `output-file` is set the action exposes the path as an output:
|
|
337
392
|
|
|
338
393
|
```yaml
|
|
339
|
-
- uses: benner/commit-guard@v0.
|
|
394
|
+
- uses: benner/commit-guard@v0.17.0
|
|
340
395
|
id: cg
|
|
341
396
|
with:
|
|
342
397
|
range: ${{ env.PR_BASE }}..${{ env.PR_HEAD }}
|
|
@@ -352,7 +407,7 @@ Add to your `.pre-commit-config.yaml`:
|
|
|
352
407
|
---
|
|
353
408
|
repos:
|
|
354
409
|
- repo: https://github.com/benner/commit-guard
|
|
355
|
-
rev: v0.
|
|
410
|
+
rev: v0.17.0
|
|
356
411
|
hooks:
|
|
357
412
|
- id: commit-guard
|
|
358
413
|
- 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` -
|
|
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
|
|
|
@@ -95,6 +95,41 @@ By default there is no minimum description length. Enforce one with
|
|
|
95
95
|
commit-guard --min-description-length 10
|
|
96
96
|
```
|
|
97
97
|
|
|
98
|
+
### Subject format
|
|
99
|
+
|
|
100
|
+
By default the description must start with a lowercase letter. To allow
|
|
101
|
+
uppercase descriptions:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
commit-guard --no-require-lowercase
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
In `.commit-guard.toml`:
|
|
108
|
+
|
|
109
|
+
```toml
|
|
110
|
+
require-lowercase = false
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
By default trailing `.` is forbidden. To change the set of forbidden trailing
|
|
114
|
+
characters (any character is valid, including space):
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
commit-guard --no-trailing-chars ".,"
|
|
118
|
+
commit-guard --no-trailing-chars ".,!"
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
In `.commit-guard.toml`:
|
|
122
|
+
|
|
123
|
+
```toml
|
|
124
|
+
no-trailing-chars = [".", "!"]
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Pass an empty list to disable the check entirely:
|
|
128
|
+
|
|
129
|
+
```toml
|
|
130
|
+
no-trailing-chars = []
|
|
131
|
+
```
|
|
132
|
+
|
|
98
133
|
### Type validation
|
|
99
134
|
|
|
100
135
|
By default the standard conventional commit types are accepted. Use `--types`
|
|
@@ -149,7 +184,8 @@ independently of `--enable`/`--disable`.
|
|
|
149
184
|
|
|
150
185
|
Place `.commit-guard.toml` in your project root (or any parent directory) to
|
|
151
186
|
set defaults for `enable`, `disable`, `scopes`, `require-scope`, `types`,
|
|
152
|
-
`max-subject-length`, `min-description-length`,
|
|
187
|
+
`max-subject-length`, `min-description-length`, `require-lowercase`,
|
|
188
|
+
`no-trailing-chars`, and `require-trailers`.
|
|
153
189
|
commit-guard searches upward from the working directory and uses the first
|
|
154
190
|
file found.
|
|
155
191
|
|
|
@@ -161,6 +197,8 @@ require-scope = true
|
|
|
161
197
|
types = ["feat", "fix", "chore", "wip"]
|
|
162
198
|
max-subject-length = 100
|
|
163
199
|
min-description-length = 10
|
|
200
|
+
require-lowercase = false
|
|
201
|
+
no-trailing-chars = [".", "!"]
|
|
164
202
|
require-trailers = ["Closes", "Reviewed-by"]
|
|
165
203
|
```
|
|
166
204
|
|
|
@@ -170,7 +208,8 @@ enable = ["subject", "imperative"]
|
|
|
170
208
|
```
|
|
171
209
|
|
|
172
210
|
CLI flags (`--enable`, `--disable`, `--scopes`, `--require-scope`, `--types`,
|
|
173
|
-
`--max-subject-length`, `--min-description-length`, `--require-
|
|
211
|
+
`--max-subject-length`, `--min-description-length`, `--no-require-lowercase`,
|
|
212
|
+
`--no-trailing-chars`, `--require-trailer`) take
|
|
174
213
|
full precedence and ignore config file values when provided.
|
|
175
214
|
|
|
176
215
|
### Environment variables
|
|
@@ -186,7 +225,7 @@ COMMIT_GUARD_GIT_TIMEOUT=30 commit-guard --range origin/main..HEAD
|
|
|
186
225
|
In GitHub Actions, set it at the step or job level:
|
|
187
226
|
|
|
188
227
|
```yaml
|
|
189
|
-
- uses: benner/commit-guard@v0.
|
|
228
|
+
- uses: benner/commit-guard@v0.17.0
|
|
190
229
|
env:
|
|
191
230
|
COMMIT_GUARD_GIT_TIMEOUT: 30
|
|
192
231
|
with:
|
|
@@ -259,6 +298,10 @@ commit-guard --range origin/main..HEAD --output-file results.jsonl
|
|
|
259
298
|
`--output-file` is independent of `--output`: combining both writes JSONL to
|
|
260
299
|
both stdout and the file.
|
|
261
300
|
|
|
301
|
+
In GitHub Actions, `output-file` is the recommended way to get machine-readable
|
|
302
|
+
results — text stays in the CI log and the file is accessible to subsequent steps
|
|
303
|
+
via `steps.<id>.outputs.output-file`.
|
|
304
|
+
|
|
262
305
|
### GitHub Actions
|
|
263
306
|
|
|
264
307
|
```yaml
|
|
@@ -266,7 +309,7 @@ steps:
|
|
|
266
309
|
- uses: actions/checkout@v4
|
|
267
310
|
with:
|
|
268
311
|
fetch-depth: 0
|
|
269
|
-
- uses: benner/commit-guard@v0.
|
|
312
|
+
- uses: benner/commit-guard@v0.17.0
|
|
270
313
|
```
|
|
271
314
|
|
|
272
315
|
Check all commits in a pull request:
|
|
@@ -282,11 +325,19 @@ jobs:
|
|
|
282
325
|
- uses: actions/checkout@v4
|
|
283
326
|
with:
|
|
284
327
|
fetch-depth: 0
|
|
285
|
-
- uses: benner/commit-guard@v0.
|
|
328
|
+
- uses: benner/commit-guard@v0.17.0
|
|
286
329
|
with:
|
|
287
330
|
range: ${{ env.PR_BASE }}..${{ env.PR_HEAD }}
|
|
288
331
|
```
|
|
289
332
|
|
|
333
|
+
Check a specific commit SHA (mirrors the positional CLI argument):
|
|
334
|
+
|
|
335
|
+
```yaml
|
|
336
|
+
- uses: benner/commit-guard@v0.17.0
|
|
337
|
+
with:
|
|
338
|
+
rev: ${{ github.sha }}
|
|
339
|
+
```
|
|
340
|
+
|
|
290
341
|
All inputs are optional and mirror the CLI flags:
|
|
291
342
|
|
|
292
343
|
```yaml
|
|
@@ -300,7 +351,7 @@ jobs:
|
|
|
300
351
|
- uses: actions/checkout@v4
|
|
301
352
|
with:
|
|
302
353
|
fetch-depth: 0
|
|
303
|
-
- uses: benner/commit-guard@v0.
|
|
354
|
+
- uses: benner/commit-guard@v0.17.0
|
|
304
355
|
with:
|
|
305
356
|
range: ${{ env.PR_BASE }}..${{ env.PR_HEAD }}
|
|
306
357
|
disable: signed-off,signature
|
|
@@ -309,13 +360,17 @@ jobs:
|
|
|
309
360
|
require-trailer: 'Closes,Reviewed-by'
|
|
310
361
|
max-subject-length: '100'
|
|
311
362
|
min-description-length: '10'
|
|
363
|
+
no-require-lowercase: 'true'
|
|
364
|
+
no-trailing-chars: '.,!'
|
|
365
|
+
allow-empty: 'true'
|
|
366
|
+
include-merges: 'true'
|
|
312
367
|
output-file: results.jsonl
|
|
313
368
|
```
|
|
314
369
|
|
|
315
370
|
When `output-file` is set the action exposes the path as an output:
|
|
316
371
|
|
|
317
372
|
```yaml
|
|
318
|
-
- uses: benner/commit-guard@v0.
|
|
373
|
+
- uses: benner/commit-guard@v0.17.0
|
|
319
374
|
id: cg
|
|
320
375
|
with:
|
|
321
376
|
range: ${{ env.PR_BASE }}..${{ env.PR_HEAD }}
|
|
@@ -331,7 +386,7 @@ Add to your `.pre-commit-config.yaml`:
|
|
|
331
386
|
---
|
|
332
387
|
repos:
|
|
333
388
|
- repo: https://github.com/benner/commit-guard
|
|
334
|
-
rev: v0.
|
|
389
|
+
rev: v0.17.0
|
|
335
390
|
hooks:
|
|
336
391
|
- id: commit-guard
|
|
337
392
|
- id: commit-guard-signature
|
|
@@ -34,6 +34,13 @@ inputs:
|
|
|
34
34
|
min-description-length:
|
|
35
35
|
description: Minimum description length in characters (default 0, off)
|
|
36
36
|
required: false
|
|
37
|
+
no-require-lowercase:
|
|
38
|
+
description: Allow description to start with uppercase (default false)
|
|
39
|
+
required: false
|
|
40
|
+
default: 'false'
|
|
41
|
+
no-trailing-chars:
|
|
42
|
+
description: Forbidden trailing characters, comma-separated (default '.')
|
|
43
|
+
required: false
|
|
37
44
|
allow-empty:
|
|
38
45
|
description: Exit 0 when --range yields no commits
|
|
39
46
|
required: false
|
|
@@ -75,6 +82,8 @@ runs:
|
|
|
75
82
|
CG_TYPES: ${{ inputs.types }}
|
|
76
83
|
CG_MAX_SUBJECT_LENGTH: ${{ inputs.max-subject-length }}
|
|
77
84
|
CG_MIN_DESCRIPTION_LENGTH: ${{ inputs.min-description-length }}
|
|
85
|
+
CG_NO_REQUIRE_LOWERCASE: ${{ inputs.no-require-lowercase }}
|
|
86
|
+
CG_NO_TRAILING_CHARS: ${{ inputs.no-trailing-chars }}
|
|
78
87
|
CG_ALLOW_EMPTY: ${{ inputs.allow-empty }}
|
|
79
88
|
CG_INCLUDE_MERGES: ${{ inputs.include-merges }}
|
|
80
89
|
CG_REQUIRE_TRAILER: ${{ inputs.require-trailer }}
|
|
@@ -92,6 +101,8 @@ runs:
|
|
|
92
101
|
ARGS+=(--max-subject-length "$CG_MAX_SUBJECT_LENGTH")
|
|
93
102
|
[[ -n "$CG_MIN_DESCRIPTION_LENGTH" ]] && \
|
|
94
103
|
ARGS+=(--min-description-length "$CG_MIN_DESCRIPTION_LENGTH")
|
|
104
|
+
[[ "$CG_NO_REQUIRE_LOWERCASE" == "true" ]] && ARGS+=(--no-require-lowercase)
|
|
105
|
+
[[ -n "$CG_NO_TRAILING_CHARS" ]] && ARGS+=(--no-trailing-chars "$CG_NO_TRAILING_CHARS")
|
|
95
106
|
[[ "$CG_ALLOW_EMPTY" == "true" ]] && ARGS+=(--allow-empty)
|
|
96
107
|
[[ "$CG_INCLUDE_MERGES" == "true" ]] && ARGS+=(--include-merges)
|
|
97
108
|
[[ -n "$CG_REQUIRE_TRAILER" ]] && ARGS+=(--require-trailer "$CG_REQUIRE_TRAILER")
|
|
@@ -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
|
|
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
|
-
|
|
355
|
-
type, lowercase start, no trailing period,
|
|
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>
|
|
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
|
|
385
|
-
|
|
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"]
|
|
@@ -391,7 +398,72 @@ require-scope = true
|
|
|
391
398
|
types = ["feat", "fix", "chore", "wip"]
|
|
392
399
|
max-subject-length = 100
|
|
393
400
|
min-description-length = 10
|
|
401
|
+
require-lowercase = false
|
|
402
|
+
no-trailing-chars = [".", "!"]
|
|
394
403
|
require-trailers = ["Closes", "Reviewed-by"]</code></pre>
|
|
404
|
+
|
|
405
|
+
<h3>Required trailers</h3>
|
|
406
|
+
<p>
|
|
407
|
+
Require arbitrary trailers in the commit message. Accepts a
|
|
408
|
+
comma-separated list; matching is case-sensitive and requires a
|
|
409
|
+
non-empty value after the colon (e.g. <code>Closes: #42</code>):
|
|
410
|
+
</p>
|
|
411
|
+
<pre><code class="language-bash">commit-guard --require-trailer "Closes,Reviewed-by"</code></pre>
|
|
412
|
+
|
|
413
|
+
<h3>Range options</h3>
|
|
414
|
+
<p>
|
|
415
|
+
When using <code>--range</code>, merge commits are excluded by
|
|
416
|
+
default. Use <code>--include-merges</code> to check them. An empty
|
|
417
|
+
range exits non-zero by default — use <code>--allow-empty</code> to
|
|
418
|
+
exit 0 instead:
|
|
419
|
+
</p>
|
|
420
|
+
<pre><code class="language-bash">commit-guard --range origin/main..HEAD --include-merges --allow-empty</code></pre>
|
|
421
|
+
|
|
422
|
+
<h3>Environment variables</h3>
|
|
423
|
+
<figure>
|
|
424
|
+
<table>
|
|
425
|
+
<thead>
|
|
426
|
+
<tr>
|
|
427
|
+
<th>Variable</th>
|
|
428
|
+
<th>Default</th>
|
|
429
|
+
<th>Description</th>
|
|
430
|
+
</tr>
|
|
431
|
+
</thead>
|
|
432
|
+
<tbody>
|
|
433
|
+
<tr>
|
|
434
|
+
<td><code>COMMIT_GUARD_GIT_TIMEOUT</code></td>
|
|
435
|
+
<td><code>10</code></td>
|
|
436
|
+
<td>Timeout in seconds for git subprocess calls</td>
|
|
437
|
+
</tr>
|
|
438
|
+
</tbody>
|
|
439
|
+
</table>
|
|
440
|
+
</figure>
|
|
441
|
+
</section>
|
|
442
|
+
|
|
443
|
+
<section id="output">
|
|
444
|
+
<h2>Output <a href="#output" class="anchor">#</a></h2>
|
|
445
|
+
<p>
|
|
446
|
+
Use <code>--output jsonl</code> to emit one JSON line per commit to
|
|
447
|
+
stdout instead of the default human-readable text:
|
|
448
|
+
</p>
|
|
449
|
+
<pre><code class="language-bash">commit-guard --range origin/main..HEAD --output jsonl | jq 'select(.ok == false)'</code></pre>
|
|
450
|
+
<p>Each line is a JSON object:</p>
|
|
451
|
+
<pre><code>{
|
|
452
|
+
"sha": "abc1234...",
|
|
453
|
+
"subject": "feat: add thing",
|
|
454
|
+
"ok": false,
|
|
455
|
+
"results": [{"check": "body", "level": "error", "message": "missing body"}]
|
|
456
|
+
}</code></pre>
|
|
457
|
+
<p>
|
|
458
|
+
<code>sha</code> is <code>null</code> when reading from a file or
|
|
459
|
+
stdin. <code>results</code> is empty when all checks pass.
|
|
460
|
+
</p>
|
|
461
|
+
<p>
|
|
462
|
+
Use <code>--output-file FILE</code> to write JSONL to a file while
|
|
463
|
+
keeping human-readable text on stdout — useful in CI where you want
|
|
464
|
+
readable logs and structured results for downstream steps:
|
|
465
|
+
</p>
|
|
466
|
+
<pre><code class="language-bash">commit-guard --range origin/main..HEAD --output-file results.jsonl</code></pre>
|
|
395
467
|
</section>
|
|
396
468
|
|
|
397
469
|
<section id="github-actions">
|
|
@@ -407,18 +479,37 @@ require-trailers = ["Closes", "Reviewed-by"]</code></pre>
|
|
|
407
479
|
- uses: actions/checkout@v4
|
|
408
480
|
with:
|
|
409
481
|
fetch-depth: 0
|
|
410
|
-
- uses: benner/commit-guard@v0.
|
|
482
|
+
- uses: benner/commit-guard@v0.17.0
|
|
411
483
|
with:
|
|
412
484
|
range: ${{ env.PR_BASE }}..${{ env.PR_HEAD }}
|
|
413
485
|
disable: signed-off,signature</code></pre>
|
|
414
486
|
|
|
487
|
+
<p>Check a specific commit SHA:</p>
|
|
488
|
+
<pre><code class="language-yaml"> - uses: benner/commit-guard@v0.17.0
|
|
489
|
+
with:
|
|
490
|
+
rev: ${{ github.sha }}</code></pre>
|
|
491
|
+
|
|
415
492
|
<p>
|
|
416
|
-
All inputs mirror the CLI flags: <code>
|
|
417
|
-
<code>
|
|
418
|
-
<code>
|
|
419
|
-
<code>
|
|
493
|
+
All inputs mirror the CLI flags: <code>rev</code>,
|
|
494
|
+
<code>range</code>, <code>enable</code>, <code>disable</code>,
|
|
495
|
+
<code>scopes</code>, <code>require-scope</code>, <code>types</code>,
|
|
496
|
+
<code>max-subject-length</code>, <code>min-description-length</code>,
|
|
497
|
+
<code>no-require-lowercase</code>, <code>no-trailing-chars</code>,
|
|
498
|
+
<code>require-trailer</code>,
|
|
499
|
+
<code>allow-empty</code>, <code>include-merges</code>,
|
|
420
500
|
<code>output-file</code>.
|
|
421
501
|
</p>
|
|
502
|
+
|
|
503
|
+
<p>
|
|
504
|
+
When <code>output-file</code> is set the action exposes the path as
|
|
505
|
+
a step output, making JSONL results available to subsequent steps:
|
|
506
|
+
</p>
|
|
507
|
+
<pre><code class="language-yaml"> - uses: benner/commit-guard@v0.17.0
|
|
508
|
+
id: cg
|
|
509
|
+
with:
|
|
510
|
+
range: ${{ env.PR_BASE }}..${{ env.PR_HEAD }}
|
|
511
|
+
output-file: results.jsonl
|
|
512
|
+
- run: jq 'select(.ok == false)' "${{ steps.cg.outputs.output-file }}"</code></pre>
|
|
422
513
|
</section>
|
|
423
514
|
|
|
424
515
|
<section id="pre-commit">
|
|
@@ -426,7 +517,7 @@ require-trailers = ["Closes", "Reviewed-by"]</code></pre>
|
|
|
426
517
|
<p>Add to <code>.pre-commit-config.yaml</code>:</p>
|
|
427
518
|
<pre><code class="language-yaml">repos:
|
|
428
519
|
- repo: https://github.com/benner/commit-guard
|
|
429
|
-
rev: v0.
|
|
520
|
+
rev: v0.17.0
|
|
430
521
|
hooks:
|
|
431
522
|
- id: commit-guard
|
|
432
523
|
- id: commit-guard-signature</code></pre>
|
|
@@ -596,7 +687,7 @@ require-trailers = ["Closes", "Reviewed-by"]</code></pre>
|
|
|
596
687
|
});
|
|
597
688
|
|
|
598
689
|
document.querySelectorAll(
|
|
599
|
-
"#configuration pre code, #github-actions pre code, #pre-commit pre code"
|
|
690
|
+
"#configuration pre code, #output pre code, #github-actions pre code, #pre-commit pre code"
|
|
600
691
|
).forEach((code) => {
|
|
601
692
|
const text = code.innerText.trim();
|
|
602
693
|
const isConfig = code.className.includes("language-");
|
|
@@ -146,15 +146,17 @@ def _strip_comments(message):
|
|
|
146
146
|
)
|
|
147
147
|
|
|
148
148
|
|
|
149
|
-
def check_subject( # noqa: PLR0913 Too many arguments in function definition (
|
|
149
|
+
def check_subject( # noqa: PLR0913 Too many arguments in function definition (9 > 5)
|
|
150
150
|
line,
|
|
151
151
|
result,
|
|
152
152
|
allowed_scopes=frozenset(),
|
|
153
153
|
allowed_types=TYPES,
|
|
154
154
|
max_subject_length=MAX_SUBJECT_LEN,
|
|
155
155
|
min_description_length=0,
|
|
156
|
+
no_trailing_chars=frozenset("."),
|
|
156
157
|
*,
|
|
157
158
|
require_scope=False,
|
|
159
|
+
require_lowercase=True,
|
|
158
160
|
):
|
|
159
161
|
m = SUBJECT_RE.match(line)
|
|
160
162
|
if not m:
|
|
@@ -174,10 +176,10 @@ def check_subject( # noqa: PLR0913 Too many arguments in function definition (7
|
|
|
174
176
|
result.error(f"unknown scope: {scope}", check=Check.SUBJECT)
|
|
175
177
|
|
|
176
178
|
desc = m.group("desc")
|
|
177
|
-
if desc[0].isupper():
|
|
179
|
+
if require_lowercase and desc[0].isupper():
|
|
178
180
|
result.error("description must not start with uppercase", check=Check.SUBJECT)
|
|
179
|
-
if desc
|
|
180
|
-
result.error("description must not end with
|
|
181
|
+
if no_trailing_chars and desc[-1] in no_trailing_chars:
|
|
182
|
+
result.error(f"description must not end with {desc[-1]!r}", check=Check.SUBJECT)
|
|
181
183
|
if len(line) > max_subject_length:
|
|
182
184
|
result.error(
|
|
183
185
|
f"subject too long: {len(line)} > {max_subject_length}", check=Check.SUBJECT
|
|
@@ -220,11 +222,15 @@ def check_imperative(desc, result):
|
|
|
220
222
|
|
|
221
223
|
|
|
222
224
|
def check_body(lines, result):
|
|
223
|
-
if len(lines)
|
|
225
|
+
if len(lines) == 1:
|
|
224
226
|
result.error("missing body", check=Check.BODY)
|
|
225
227
|
return
|
|
226
228
|
if lines[1].strip():
|
|
227
229
|
result.error("missing blank line between subject and body", check=Check.BODY)
|
|
230
|
+
return
|
|
231
|
+
if len(lines) == 2: # noqa: PLR2004 Magic value used in comparison, consider replacing 2 with a constant variable
|
|
232
|
+
result.error("missing body", check=Check.BODY)
|
|
233
|
+
return
|
|
228
234
|
body_lines = [ln for ln in lines[2:] if not _TRAILER_RE.match(ln)]
|
|
229
235
|
if not any(ln.strip() for ln in body_lines):
|
|
230
236
|
result.error("missing body", check=Check.BODY)
|
|
@@ -301,6 +307,8 @@ class Args:
|
|
|
301
307
|
allowed_types: frozenset
|
|
302
308
|
max_subject_length: int
|
|
303
309
|
min_description_length: int
|
|
310
|
+
require_lowercase: bool
|
|
311
|
+
no_trailing_chars: frozenset
|
|
304
312
|
rev_range: str | None
|
|
305
313
|
allow_empty: bool
|
|
306
314
|
include_merges: bool
|
|
@@ -341,6 +349,22 @@ def _resolve_min_description_length(args, config):
|
|
|
341
349
|
return 0
|
|
342
350
|
|
|
343
351
|
|
|
352
|
+
def _resolve_require_lowercase(args, config):
|
|
353
|
+
if args.require_lowercase is not None:
|
|
354
|
+
return args.require_lowercase
|
|
355
|
+
if "require-lowercase" in config:
|
|
356
|
+
return config["require-lowercase"]
|
|
357
|
+
return True
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def _resolve_no_trailing_chars(args, config):
|
|
361
|
+
if args.no_trailing_chars is not None:
|
|
362
|
+
return frozenset(c for c in args.no_trailing_chars.split(",") if c)
|
|
363
|
+
if "no-trailing-chars" in config:
|
|
364
|
+
return frozenset(config["no-trailing-chars"])
|
|
365
|
+
return frozenset(".")
|
|
366
|
+
|
|
367
|
+
|
|
344
368
|
def _resolve_required_trailers(args, config):
|
|
345
369
|
if args.require_trailer:
|
|
346
370
|
return [t.strip() for t in args.require_trailer.split(",")]
|
|
@@ -427,6 +451,19 @@ def _parse_args():
|
|
|
427
451
|
metavar="N",
|
|
428
452
|
help="minimum description length in characters (default: 0, off)",
|
|
429
453
|
)
|
|
454
|
+
parser.add_argument(
|
|
455
|
+
"--no-require-lowercase",
|
|
456
|
+
dest="require_lowercase",
|
|
457
|
+
action="store_false",
|
|
458
|
+
default=None,
|
|
459
|
+
help="allow description to start with uppercase (default: disallowed)",
|
|
460
|
+
)
|
|
461
|
+
parser.add_argument(
|
|
462
|
+
"--no-trailing-chars",
|
|
463
|
+
default=None,
|
|
464
|
+
metavar="CHAR[,CHAR,...]",
|
|
465
|
+
help="forbidden trailing characters in description (default: '.')",
|
|
466
|
+
)
|
|
430
467
|
parser.add_argument(
|
|
431
468
|
"--range",
|
|
432
469
|
dest="rev_range",
|
|
@@ -469,6 +506,8 @@ def _parse_args():
|
|
|
469
506
|
allowed_types = _resolve_types(args, config)
|
|
470
507
|
max_subject_length = _resolve_max_subject_length(args, config)
|
|
471
508
|
min_description_length = _resolve_min_description_length(args, config)
|
|
509
|
+
require_lowercase = _resolve_require_lowercase(args, config)
|
|
510
|
+
no_trailing_chars = _resolve_no_trailing_chars(args, config)
|
|
472
511
|
required_trailers = _resolve_required_trailers(args, config)
|
|
473
512
|
|
|
474
513
|
if args.allow_empty and not args.rev_range:
|
|
@@ -503,6 +542,8 @@ def _parse_args():
|
|
|
503
542
|
allowed_types=allowed_types,
|
|
504
543
|
max_subject_length=max_subject_length,
|
|
505
544
|
min_description_length=min_description_length,
|
|
545
|
+
require_lowercase=require_lowercase,
|
|
546
|
+
no_trailing_chars=no_trailing_chars,
|
|
506
547
|
rev_range=args.rev_range,
|
|
507
548
|
allow_empty=args.allow_empty,
|
|
508
549
|
include_merges=args.include_merges,
|
|
@@ -556,7 +597,9 @@ def _run_checks(args, rev, message, result):
|
|
|
556
597
|
args.allowed_types,
|
|
557
598
|
args.max_subject_length,
|
|
558
599
|
args.min_description_length,
|
|
600
|
+
args.no_trailing_chars,
|
|
559
601
|
require_scope=args.require_scope,
|
|
602
|
+
require_lowercase=args.require_lowercase,
|
|
560
603
|
)
|
|
561
604
|
if Check.IMPERATIVE in args.enabled:
|
|
562
605
|
if desc is None:
|
|
@@ -113,11 +113,41 @@ class TestCheckSubject:
|
|
|
113
113
|
check_subject("fix: Add token", r)
|
|
114
114
|
assert not r.ok
|
|
115
115
|
|
|
116
|
+
def test_uppercase_description_allowed(self):
|
|
117
|
+
r = Result()
|
|
118
|
+
check_subject("fix: Add token", r, require_lowercase=False)
|
|
119
|
+
assert r.ok
|
|
120
|
+
|
|
121
|
+
def test_lowercase_required_by_default(self):
|
|
122
|
+
r = Result()
|
|
123
|
+
check_subject("fix: add token", r)
|
|
124
|
+
assert r.ok
|
|
125
|
+
|
|
116
126
|
def test_trailing_period(self):
|
|
117
127
|
r = Result()
|
|
118
128
|
check_subject("fix: add token.", r)
|
|
119
129
|
assert not r.ok
|
|
120
130
|
|
|
131
|
+
def test_trailing_char_custom(self):
|
|
132
|
+
r = Result()
|
|
133
|
+
check_subject("fix: add token!", r, no_trailing_chars=frozenset("!"))
|
|
134
|
+
assert not r.ok
|
|
135
|
+
|
|
136
|
+
def test_trailing_char_space(self):
|
|
137
|
+
r = Result()
|
|
138
|
+
check_subject("fix: add token ", r, no_trailing_chars=frozenset(". "))
|
|
139
|
+
assert not r.ok
|
|
140
|
+
|
|
141
|
+
def test_trailing_chars_empty_disables_check(self):
|
|
142
|
+
r = Result()
|
|
143
|
+
check_subject("fix: add token.", r, no_trailing_chars=frozenset())
|
|
144
|
+
assert r.ok
|
|
145
|
+
|
|
146
|
+
def test_trailing_chars_multiple(self):
|
|
147
|
+
r = Result()
|
|
148
|
+
check_subject("fix: add token!", r, no_trailing_chars=frozenset(".!"))
|
|
149
|
+
assert not r.ok
|
|
150
|
+
|
|
121
151
|
def test_subject_too_long(self):
|
|
122
152
|
r = Result()
|
|
123
153
|
check_subject("fix: " + "a" * 68, r) # 73 chars total
|
|
@@ -237,6 +267,12 @@ class TestCheckBody:
|
|
|
237
267
|
check_body(["fix: add thing", "body text", "more"], r)
|
|
238
268
|
assert not r.ok
|
|
239
269
|
|
|
270
|
+
def test_missing_blank_line_two_lines(self):
|
|
271
|
+
r = Result()
|
|
272
|
+
check_body(["fix: add thing", "body text"], r)
|
|
273
|
+
assert not r.ok
|
|
274
|
+
assert any("blank line" in msg for _, _, msg in r.errors)
|
|
275
|
+
|
|
240
276
|
def test_blank_body_content(self):
|
|
241
277
|
r = Result()
|
|
242
278
|
check_body(["fix: add thing", "", " "], r)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|