git-commit-guard 0.22.2__py3-none-any.whl → 0.23.0__py3-none-any.whl
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/__init__.py +42 -24
- {git_commit_guard-0.22.2.dist-info → git_commit_guard-0.23.0.dist-info}/METADATA +28 -12
- git_commit_guard-0.23.0.dist-info/RECORD +6 -0
- {git_commit_guard-0.22.2.dist-info → git_commit_guard-0.23.0.dist-info}/WHEEL +1 -1
- git_commit_guard-0.22.2.dist-info/RECORD +0 -6
- {git_commit_guard-0.22.2.dist-info → git_commit_guard-0.23.0.dist-info}/entry_points.txt +0 -0
- {git_commit_guard-0.22.2.dist-info → git_commit_guard-0.23.0.dist-info}/licenses/LICENSE +0 -0
git_commit_guard/__init__.py
CHANGED
|
@@ -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")
|
|
@@ -295,9 +299,9 @@ def check_required_trailers(message, required, result):
|
|
|
295
299
|
result.error(f"missing required trailer: {trailer}")
|
|
296
300
|
|
|
297
301
|
|
|
298
|
-
def
|
|
302
|
+
def _get_committer_email(rev):
|
|
299
303
|
return subprocess.check_output( # noqa: S603
|
|
300
|
-
["git", "log", "-1", "--format=%
|
|
304
|
+
["git", "log", "-1", "--format=%ce", rev], # noqa: S607
|
|
301
305
|
text=True,
|
|
302
306
|
stderr=subprocess.PIPE,
|
|
303
307
|
timeout=_git_timeout(),
|
|
@@ -320,7 +324,7 @@ def _get_github_remote_info():
|
|
|
320
324
|
return match.group("owner"), match.group("repo")
|
|
321
325
|
|
|
322
326
|
|
|
323
|
-
def
|
|
327
|
+
def _fetch_github_commit_committer(owner, repo, sha):
|
|
324
328
|
url = f"https://api.github.com/repos/{owner}/{repo}/commits/{sha}"
|
|
325
329
|
headers = {"Accept": "application/vnd.github+json"}
|
|
326
330
|
token = os.environ.get("GITHUB_TOKEN") or os.environ.get("GH_TOKEN")
|
|
@@ -329,8 +333,8 @@ def _fetch_github_commit_author(owner, repo, sha):
|
|
|
329
333
|
req = urllib.request.Request(url, headers=headers) # noqa: S310 Audit URL open for permitted schemes
|
|
330
334
|
with urllib.request.urlopen(req, timeout=_git_timeout()) as resp: # noqa: S310 Audit URL open for permitted schemes
|
|
331
335
|
data = json.loads(resp.read())
|
|
332
|
-
|
|
333
|
-
return
|
|
336
|
+
committer = data.get("committer")
|
|
337
|
+
return committer["login"] if committer else None
|
|
334
338
|
|
|
335
339
|
|
|
336
340
|
def _parse_noreply_username(email):
|
|
@@ -435,7 +439,7 @@ def _resolve_github_username(rev, email):
|
|
|
435
439
|
if remote:
|
|
436
440
|
owner, repo = remote
|
|
437
441
|
try:
|
|
438
|
-
username =
|
|
442
|
+
username = _fetch_github_commit_committer(owner, repo, rev)
|
|
439
443
|
except urllib.error.HTTPError as e:
|
|
440
444
|
if e.code == HTTPStatus.NOT_FOUND:
|
|
441
445
|
commits_api_404 = True
|
|
@@ -450,24 +454,24 @@ def _resolve_github_username(rev, email):
|
|
|
450
454
|
return username, commits_api_404
|
|
451
455
|
|
|
452
456
|
|
|
453
|
-
def
|
|
457
|
+
def _committer_not_found_message(commits_api_404):
|
|
454
458
|
had_token = bool(os.environ.get("GITHUB_TOKEN") or os.environ.get("GH_TOKEN"))
|
|
455
459
|
if commits_api_404 and not had_token:
|
|
456
460
|
return (
|
|
457
|
-
"
|
|
461
|
+
"committer not found on GitHub — if the repo is private, "
|
|
458
462
|
"set GITHUB_TOKEN in the workflow step "
|
|
459
463
|
"(env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }})"
|
|
460
464
|
)
|
|
461
|
-
return "
|
|
465
|
+
return "committer not found on GitHub — cannot verify signature"
|
|
462
466
|
|
|
463
467
|
|
|
464
468
|
def check_signature(rev, result):
|
|
465
469
|
try:
|
|
466
|
-
email =
|
|
470
|
+
email = _get_committer_email(rev)
|
|
467
471
|
username, commits_api_404 = _resolve_github_username(rev, email)
|
|
468
472
|
if username is None:
|
|
469
473
|
result.error(
|
|
470
|
-
|
|
474
|
+
_committer_not_found_message(commits_api_404),
|
|
471
475
|
check=Check.SIGNATURE,
|
|
472
476
|
)
|
|
473
477
|
return
|
|
@@ -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 (
|
|
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
|
|
915
|
-
|
|
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
|
-
|
|
931
|
-
return _report_jsonl(result, args.rev, subject)
|
|
932
|
-
return _report_text(result)
|
|
950
|
+
return _report_commit(args, result, args.rev, subject)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: git-commit-guard
|
|
3
|
-
Version: 0.
|
|
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
|
|
@@ -36,7 +36,7 @@ Opinionated conventional commit message linter with imperative mood detection.
|
|
|
36
36
|
verb, verified via nltk POS tagging — not a hand-coded regex of "bad"
|
|
37
37
|
words.
|
|
38
38
|
* **Signature verification without a local keyring.** Resolves the commit
|
|
39
|
-
|
|
39
|
+
committer via the GitHub API and verifies GPG/SSH against their published
|
|
40
40
|
`.gpg`/`.keys` — no per-runner key management.
|
|
41
41
|
* **Strict by default.** Subject format, body, trailers, `Signed-off-by`,
|
|
42
42
|
and signature all enforced out of the box; opt out with `--disable`.
|
|
@@ -242,7 +242,7 @@ independently of `--enable`/`--disable`.
|
|
|
242
242
|
The `signature` check verifies the commit without any local keyring setup:
|
|
243
243
|
|
|
244
244
|
1. If the repo has a GitHub remote, call the Commits API
|
|
245
|
-
(`GET /repos/{owner}/{repo}/commits/{sha}`) to resolve the
|
|
245
|
+
(`GET /repos/{owner}/{repo}/commits/{sha}`) to resolve the committer's GitHub
|
|
246
246
|
username — this works for corporate emails, noreply addresses, or any email
|
|
247
247
|
not listed publicly on a GitHub profile.
|
|
248
248
|
2. If the Commits API is unavailable (no GitHub remote, commit not yet pushed,
|
|
@@ -250,7 +250,7 @@ The `signature` check verifies the commit without any local keyring setup:
|
|
|
250
250
|
(`{id}+{username}@users.noreply.github.com` or
|
|
251
251
|
`{username}@users.noreply.github.com`) — no API call needed.
|
|
252
252
|
3. If neither of the above resolves a username, fall back to searching GitHub
|
|
253
|
-
by the commit
|
|
253
|
+
by the commit committer's email.
|
|
254
254
|
4. Fetch the resolved user's public keys from `github.com/{username}.gpg`
|
|
255
255
|
(GPG) and the `/users/{username}/ssh_signing_keys` API (SSH keys tagged
|
|
256
256
|
with the **Signing key** role). Auth-only SSH keys are deliberately not
|
|
@@ -261,7 +261,7 @@ The `signature` check verifies the commit without any local keyring setup:
|
|
|
261
261
|
`git verify-commit` with the SSH allowed-signers config.
|
|
262
262
|
7. If any key verifies, the check passes. If none do, it fails.
|
|
263
263
|
|
|
264
|
-
If the
|
|
264
|
+
If the committer cannot be resolved via either method, or the GitHub API is
|
|
265
265
|
unreachable, the check fails with a clear error.
|
|
266
266
|
|
|
267
267
|
For private repositories, set `GITHUB_TOKEN` or `GH_TOKEN` so the Commits API
|
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
497
|
+
rev: v0.23.0
|
|
482
498
|
hooks:
|
|
483
499
|
- id: commit-guard
|
|
484
500
|
- id: commit-guard-signature
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
git_commit_guard/__init__.py,sha256=wiXXUQPw2NWPgWUh83EREtjp26L62X7fPNFZ0GcPbfQ,30139
|
|
2
|
+
git_commit_guard-0.23.0.dist-info/METADATA,sha256=pGircQocHHj1TqurmthduMAdh4ovzZza9r056hlF6iY,16043
|
|
3
|
+
git_commit_guard-0.23.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
|
|
4
|
+
git_commit_guard-0.23.0.dist-info/entry_points.txt,sha256=24HK4TwCgn3tSQHOWnGE6zJK6nvg7EMAut7VHe9nuH4,55
|
|
5
|
+
git_commit_guard-0.23.0.dist-info/licenses/LICENSE,sha256=gXf5dRMhNSbfLPYYTY_5hsZ1r7UU1OaKQEAQUhuIBkM,18092
|
|
6
|
+
git_commit_guard-0.23.0.dist-info/RECORD,,
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
git_commit_guard/__init__.py,sha256=FrKpmqyW_YO2RgMj8Ff2m1TVEqQqBDhvm1pFtfem8rQ,29599
|
|
2
|
-
git_commit_guard-0.22.2.dist-info/METADATA,sha256=4pWaUYZ4ta6OwftzPFenE_KtBxzrFlmKcl1tKVi9qdE,15450
|
|
3
|
-
git_commit_guard-0.22.2.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
4
|
-
git_commit_guard-0.22.2.dist-info/entry_points.txt,sha256=24HK4TwCgn3tSQHOWnGE6zJK6nvg7EMAut7VHe9nuH4,55
|
|
5
|
-
git_commit_guard-0.22.2.dist-info/licenses/LICENSE,sha256=gXf5dRMhNSbfLPYYTY_5hsZ1r7UU1OaKQEAQUhuIBkM,18092
|
|
6
|
-
git_commit_guard-0.22.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|