commit-check 2.18.2__py3-none-any.whl → 2.19.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.
- commit_check/__init__.py +2 -4
- commit_check/ai_signatures.py +8 -5
- commit_check/ai_signatures_data.py +57 -0
- commit_check/api.py +1 -1
- commit_check/config.py +6 -5
- commit_check/config_merger.py +53 -10
- commit_check/engine.py +90 -32
- commit_check/main.py +92 -22
- commit_check/rule_builder.py +37 -21
- commit_check/rules_catalog.py +1 -0
- commit_check/util.py +80 -28
- {commit_check-2.18.2.dist-info → commit_check-2.19.0.dist-info}/METADATA +17 -9
- commit_check-2.19.0.dist-info/RECORD +21 -0
- commit_check-2.18.2.dist-info/RECORD +0 -21
- {commit_check-2.18.2.dist-info → commit_check-2.19.0.dist-info}/WHEEL +0 -0
- {commit_check-2.18.2.dist-info → commit_check-2.19.0.dist-info}/entry_points.txt +0 -0
- {commit_check-2.18.2.dist-info → commit_check-2.19.0.dist-info}/licenses/LICENSE +0 -0
- {commit_check-2.18.2.dist-info → commit_check-2.19.0.dist-info}/top_level.txt +0 -0
commit_check/__init__.py
CHANGED
|
@@ -8,7 +8,7 @@ Exports:
|
|
|
8
8
|
|
|
9
9
|
import os
|
|
10
10
|
import sys
|
|
11
|
-
from importlib.metadata import
|
|
11
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
12
12
|
|
|
13
13
|
# Exit codes used across the package
|
|
14
14
|
PASS = 0
|
|
@@ -40,9 +40,7 @@ def supports_color() -> bool:
|
|
|
40
40
|
return False
|
|
41
41
|
if not sys.stdout.isatty():
|
|
42
42
|
return False
|
|
43
|
-
|
|
44
|
-
return False
|
|
45
|
-
return True
|
|
43
|
+
return os.environ.get("TERM") not in ("", "dumb")
|
|
46
44
|
|
|
47
45
|
|
|
48
46
|
# ANSI color codes used for CLI output, empty when stdout cannot render color.
|
commit_check/ai_signatures.py
CHANGED
|
@@ -52,10 +52,10 @@ class _Group:
|
|
|
52
52
|
"""One compiled scan, and what each of its alternatives means.
|
|
53
53
|
|
|
54
54
|
Every pattern that reads the same trailer key is compiled into a single
|
|
55
|
-
alternation whose branches
|
|
56
|
-
order, so a trailer line is read once however many tools the
|
|
57
|
-
knows and the most specific pattern still wins. A body marker
|
|
58
|
-
key, and its group is the pattern by itself.
|
|
55
|
+
alternation whose branches each end in a numbered capturing group, in
|
|
56
|
+
catalog order, so a trailer line is read once however many tools the
|
|
57
|
+
catalog knows and the most specific pattern still wins. A body marker
|
|
58
|
+
has no key, and its group is the pattern by itself.
|
|
59
59
|
"""
|
|
60
60
|
|
|
61
61
|
keys: frozenset[str]
|
|
@@ -91,7 +91,10 @@ def _build_groups() -> list[_Group]:
|
|
|
91
91
|
groups = []
|
|
92
92
|
for keys, (values, members) in keyed.items():
|
|
93
93
|
alternation = "|".join(re.escape(key) for key in sorted(keys))
|
|
94
|
-
|
|
94
|
+
# The group is empty and closes the branch. Wrapped around the value
|
|
95
|
+
# instead, it is entered on every line the branch rejects, at a cost
|
|
96
|
+
# that grows with each tool added to the catalog.
|
|
97
|
+
branches = "|".join(f"(?:{value})(?P<s{i}>)" for i, value in enumerate(values))
|
|
95
98
|
regex = re.compile(
|
|
96
99
|
rf"^(?:{alternation}):[ \t]*(?:{branches})[ \t]*$",
|
|
97
100
|
re.IGNORECASE | re.MULTILINE,
|
|
@@ -265,6 +265,59 @@ TABBY = KnownAiTool(
|
|
|
265
265
|
],
|
|
266
266
|
)
|
|
267
267
|
|
|
268
|
+
# --- Amp (Sourcegraph) ---
|
|
269
|
+
# Both trailers are on by default and switched off in Amp's settings:
|
|
270
|
+
# https://ampcode.com/docs/cli/settings
|
|
271
|
+
AMP = KnownAiTool(
|
|
272
|
+
name="Amp",
|
|
273
|
+
name_pattern=_names(r"amp|ampcode"),
|
|
274
|
+
patterns=[
|
|
275
|
+
# "Amp" is a short word, so when an address is present it must be
|
|
276
|
+
# Amp's own.
|
|
277
|
+
*_identity(r"Amp(?:[ \t]*<amp@ampcode\.com>)?", "Amp <amp@ampcode.com>"),
|
|
278
|
+
_stamp_trailer("Amp-Thread-ID", "``Amp-Thread-ID:`` trailer"),
|
|
279
|
+
],
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
# --- OpenHands (All Hands AI) ---
|
|
283
|
+
# Commits as openhands <openhands@all-hands.dev> by default, and stays on as
|
|
284
|
+
# a co-author when the user sets their own identity:
|
|
285
|
+
# https://docs.openhands.dev/openhands/usage/settings/application-settings
|
|
286
|
+
OPENHANDS = KnownAiTool(
|
|
287
|
+
name="OpenHands",
|
|
288
|
+
name_pattern=_names(r"openhands|all-hands"),
|
|
289
|
+
patterns=[
|
|
290
|
+
# By name only: the people who work at All Hands share the agent's
|
|
291
|
+
# all-hands.dev domain.
|
|
292
|
+
*_identity(r"OpenHands[ \t]*(?:<[^>\n]*>)?", "OpenHands"),
|
|
293
|
+
],
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
# --- Factory Droid ---
|
|
297
|
+
# Adds itself as a co-author by default (includeCoAuthoredByDroid):
|
|
298
|
+
# https://docs.factory.com/droid-cli/settings.md
|
|
299
|
+
FACTORY_DROID = KnownAiTool(
|
|
300
|
+
name="Factory Droid",
|
|
301
|
+
name_pattern=_names(r"factory-droid|droid"),
|
|
302
|
+
patterns=[
|
|
303
|
+
*_identity(r"factory-droid\[bot\][ \t]*(?:<[^>\n]*>)?", "factory-droid[bot]"),
|
|
304
|
+
],
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
# --- Jules (Google) ---
|
|
308
|
+
# Commits as the google-labs-jules[bot] app, or co-authors with the user
|
|
309
|
+
# when Commit Authoring says so: https://jules.google/docs/changelog/2026-02-19
|
|
310
|
+
# "Jules" alone is a person's name, so only the app's name counts.
|
|
311
|
+
JULES = KnownAiTool(
|
|
312
|
+
name="Jules",
|
|
313
|
+
name_pattern=_names(r"google-labs-jules|jules"),
|
|
314
|
+
patterns=[
|
|
315
|
+
*_identity(
|
|
316
|
+
r"google-labs-jules\[bot\][ \t]*(?:<[^>\n]*>)?", "google-labs-jules[bot]"
|
|
317
|
+
),
|
|
318
|
+
],
|
|
319
|
+
)
|
|
320
|
+
|
|
268
321
|
# --- Generic / catch-all AI patterns ---
|
|
269
322
|
GENERIC_AI = KnownAiTool(
|
|
270
323
|
name="Generic AI",
|
|
@@ -321,5 +374,9 @@ ALL_KNOWN_TOOLS: list[KnownAiTool] = [
|
|
|
321
374
|
AIDER,
|
|
322
375
|
WINDSURF,
|
|
323
376
|
TABBY,
|
|
377
|
+
AMP,
|
|
378
|
+
OPENHANDS,
|
|
379
|
+
FACTORY_DROID,
|
|
380
|
+
JULES,
|
|
324
381
|
GENERIC_AI,
|
|
325
382
|
]
|
commit_check/api.py
CHANGED
|
@@ -29,6 +29,7 @@ Return-value schema (all functions)::
|
|
|
29
29
|
"error": "<error description>",
|
|
30
30
|
"suggest": "<how to fix>",
|
|
31
31
|
"fix": "<the corrected value when the fix is unambiguous; else empty>",
|
|
32
|
+
"reason": "<why a skipped check did not run, when known; else empty>",
|
|
32
33
|
},
|
|
33
34
|
...
|
|
34
35
|
]
|
|
@@ -60,7 +61,6 @@ from commit_check.engine import (
|
|
|
60
61
|
from commit_check.rule_builder import RuleBuilder
|
|
61
62
|
from commit_check.rules_catalog import BRANCH_CHECKS, MESSAGE_CHECKS
|
|
62
63
|
|
|
63
|
-
|
|
64
64
|
# ---------------------------------------------------------------------------
|
|
65
65
|
# Internal helpers
|
|
66
66
|
# ---------------------------------------------------------------------------
|
commit_check/config.py
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
"""TOML config loader and schema for commit-check."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
from pathlib import Path
|
|
4
|
+
|
|
6
5
|
import copy
|
|
7
6
|
import io
|
|
8
7
|
import sys
|
|
9
|
-
import urllib.request
|
|
10
8
|
import urllib.error
|
|
9
|
+
import urllib.request
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
11
12
|
|
|
12
13
|
try:
|
|
13
14
|
import tomllib
|
|
@@ -125,7 +126,7 @@ class _LazyOpener:
|
|
|
125
126
|
|
|
126
127
|
@property
|
|
127
128
|
def handlers(self) -> list[urllib.request.BaseHandler]:
|
|
128
|
-
return list(
|
|
129
|
+
return list(self._get().handlers) # type: ignore[attr-defined]
|
|
129
130
|
|
|
130
131
|
def _get(self) -> urllib.request.OpenerDirector:
|
|
131
132
|
if self._opener is None:
|
|
@@ -151,7 +152,7 @@ def _load_from_url(url: str) -> dict[str, Any]:
|
|
|
151
152
|
"""
|
|
152
153
|
if not url.startswith("https://"):
|
|
153
154
|
raise ValueError("only https:// URLs are accepted")
|
|
154
|
-
with _opener.open(url, timeout=10) as response:
|
|
155
|
+
with _opener.open(url, timeout=10) as response:
|
|
155
156
|
data = response.read()
|
|
156
157
|
return toml_load(io.BytesIO(data))
|
|
157
158
|
|
commit_check/config_merger.py
CHANGED
|
@@ -1,24 +1,27 @@
|
|
|
1
1
|
"""Configuration merger that combines CLI args, env vars, TOML config, and defaults."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import difflib
|
|
4
7
|
import os
|
|
5
8
|
import sys
|
|
6
|
-
import argparse
|
|
7
9
|
from collections.abc import Callable
|
|
8
|
-
from typing import Any
|
|
10
|
+
from typing import Any, ClassVar
|
|
9
11
|
|
|
10
|
-
from commit_check.config import deep_merge, load_config as load_toml_config
|
|
11
12
|
from commit_check import (
|
|
12
|
-
DEFAULT_COMMIT_TYPES,
|
|
13
|
-
DEFAULT_BRANCH_TYPES,
|
|
14
|
-
DEFAULT_BRANCH_NAMES,
|
|
15
|
-
DEFAULT_BOOLEAN_RULES,
|
|
16
|
-
DEFAULT_PUSH_RULES,
|
|
17
13
|
DEFAULT_AI_ATTRIBUTION,
|
|
18
14
|
DEFAULT_AI_DISCLOSURE_PATTERN,
|
|
19
15
|
DEFAULT_AI_DISCLOSURE_TRAILERS,
|
|
16
|
+
DEFAULT_BOOLEAN_RULES,
|
|
17
|
+
DEFAULT_BRANCH_NAMES,
|
|
18
|
+
DEFAULT_BRANCH_TYPES,
|
|
19
|
+
DEFAULT_COMMIT_TYPES,
|
|
20
|
+
DEFAULT_PUSH_RULES,
|
|
20
21
|
DEFAULT_TAG_REGEX,
|
|
21
22
|
)
|
|
23
|
+
from commit_check.config import deep_merge
|
|
24
|
+
from commit_check.config import load_config as load_toml_config
|
|
22
25
|
|
|
23
26
|
|
|
24
27
|
def parse_bool(value: Any) -> bool:
|
|
@@ -89,6 +92,7 @@ def get_default_config() -> dict[str, Any]:
|
|
|
89
92
|
},
|
|
90
93
|
"branch": {
|
|
91
94
|
"conventional_branch": True,
|
|
95
|
+
"require_description_grammar": False,
|
|
92
96
|
"allow_branch_types": DEFAULT_BRANCH_TYPES.copy(),
|
|
93
97
|
"allow_branch_names": DEFAULT_BRANCH_NAMES.copy(),
|
|
94
98
|
"require_rebase_target": "",
|
|
@@ -108,11 +112,42 @@ def get_default_config() -> dict[str, Any]:
|
|
|
108
112
|
}
|
|
109
113
|
|
|
110
114
|
|
|
115
|
+
#: Read from the same file, but by other tools: ``warn`` by the rule builder,
|
|
116
|
+
#: ``[jira]`` and ``[pull_request]`` by the GitHub App.
|
|
117
|
+
_OTHER_KEYS = frozenset({"warn", "jira", "pull_request"})
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def unknown_setting_warnings(config: dict[str, Any]) -> list[str]:
|
|
121
|
+
"""One line per setting in a config file that nothing reads.
|
|
122
|
+
|
|
123
|
+
A misspelt key used to be ignored in silence, leaving its rule off.
|
|
124
|
+
"""
|
|
125
|
+
defaults = get_default_config()
|
|
126
|
+
names = []
|
|
127
|
+
for section, value in config.items():
|
|
128
|
+
if section in _OTHER_KEYS:
|
|
129
|
+
continue
|
|
130
|
+
if section not in defaults:
|
|
131
|
+
names.append((f"[{section}]", section, [*defaults, *_OTHER_KEYS]))
|
|
132
|
+
elif isinstance(value, dict):
|
|
133
|
+
names.extend(
|
|
134
|
+
(f"[{section}] {key}", key, defaults[section])
|
|
135
|
+
for key in value
|
|
136
|
+
if key not in defaults[section]
|
|
137
|
+
)
|
|
138
|
+
warnings = []
|
|
139
|
+
for shown, name, known in names:
|
|
140
|
+
match = difflib.get_close_matches(name, list(known), n=1)
|
|
141
|
+
hint = f"; did you mean {match[0]}?" if match else ""
|
|
142
|
+
warnings.append(f"⚠ unknown setting {shown} is ignored{hint}")
|
|
143
|
+
return warnings
|
|
144
|
+
|
|
145
|
+
|
|
111
146
|
class ConfigMerger:
|
|
112
147
|
"""Merges configurations from multiple sources with priority: CLI > Env > TOML > Defaults."""
|
|
113
148
|
|
|
114
149
|
# Mapping of environment variable names to config keys
|
|
115
|
-
ENV_VAR_MAPPING: dict[str, tuple[str, str, Callable[[Any], Any]]] = {
|
|
150
|
+
ENV_VAR_MAPPING: ClassVar[dict[str, tuple[str, str, Callable[[Any], Any]]]] = {
|
|
116
151
|
# Commit section
|
|
117
152
|
"CCHK_CONVENTIONAL_COMMITS": ("commit", "conventional_commits", parse_bool),
|
|
118
153
|
"CCHK_MESSAGE_PATTERN": ("commit", "message_pattern", str),
|
|
@@ -140,6 +175,11 @@ class ConfigMerger:
|
|
|
140
175
|
"CCHK_AUTHOR_NAME_PATTERN": ("commit", "author_name_pattern", str),
|
|
141
176
|
# Branch section
|
|
142
177
|
"CCHK_CONVENTIONAL_BRANCH": ("branch", "conventional_branch", parse_bool),
|
|
178
|
+
"CCHK_REQUIRE_DESCRIPTION_GRAMMAR": (
|
|
179
|
+
"branch",
|
|
180
|
+
"require_description_grammar",
|
|
181
|
+
parse_bool,
|
|
182
|
+
),
|
|
143
183
|
"CCHK_ALLOW_BRANCH_TYPES": ("branch", "allow_branch_types", parse_list),
|
|
144
184
|
"CCHK_ALLOW_BRANCH_NAMES": ("branch", "allow_branch_names", parse_list),
|
|
145
185
|
"CCHK_REQUIRE_REBASE_TARGET": ("branch", "require_rebase_target", str),
|
|
@@ -155,7 +195,7 @@ class ConfigMerger:
|
|
|
155
195
|
}
|
|
156
196
|
|
|
157
197
|
# Mapping of CLI argument names to config keys
|
|
158
|
-
CLI_ARG_MAPPING: dict[str, tuple[str, str]] = {
|
|
198
|
+
CLI_ARG_MAPPING: ClassVar[dict[str, tuple[str, str]]] = {
|
|
159
199
|
# Commit section
|
|
160
200
|
"conventional_commits": ("commit", "conventional_commits"),
|
|
161
201
|
"subject_capitalized": ("commit", "subject_capitalized"),
|
|
@@ -178,6 +218,7 @@ class ConfigMerger:
|
|
|
178
218
|
"author_name_pattern": ("commit", "author_name_pattern"),
|
|
179
219
|
# Branch section
|
|
180
220
|
"conventional_branch": ("branch", "conventional_branch"),
|
|
221
|
+
"require_description_grammar": ("branch", "require_description_grammar"),
|
|
181
222
|
"allow_branch_types": ("branch", "allow_branch_types"),
|
|
182
223
|
"allow_branch_names": ("branch", "allow_branch_names"),
|
|
183
224
|
"require_rebase_target": ("branch", "require_rebase_target"),
|
|
@@ -259,6 +300,8 @@ class ConfigMerger:
|
|
|
259
300
|
try:
|
|
260
301
|
toml_config = load_toml_config(config_path or "")
|
|
261
302
|
if toml_config:
|
|
303
|
+
for warning in unknown_setting_warnings(toml_config):
|
|
304
|
+
print(warning, file=sys.stderr)
|
|
262
305
|
deep_merge(config, toml_config)
|
|
263
306
|
except FileNotFoundError:
|
|
264
307
|
# If a specific path was provided and not found, this error is already raised
|
commit_check/engine.py
CHANGED
|
@@ -1,54 +1,54 @@
|
|
|
1
1
|
"""Clean validation engine following SOLID principles."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
|
+
|
|
4
5
|
import re
|
|
5
6
|
import shlex
|
|
6
7
|
import subprocess
|
|
7
8
|
import sys
|
|
8
9
|
from abc import ABC, abstractmethod
|
|
9
10
|
from collections.abc import Iterable
|
|
10
|
-
from dataclasses import dataclass
|
|
11
|
+
from dataclasses import dataclass, field
|
|
11
12
|
from enum import IntEnum
|
|
12
|
-
from dataclasses import field
|
|
13
13
|
from fnmatch import fnmatchcase
|
|
14
|
+
from typing import ClassVar
|
|
14
15
|
|
|
15
|
-
from commit_check import DEFAULT_AI_DISCLOSURE_TRAILERS
|
|
16
|
-
from commit_check import ai_policy
|
|
17
|
-
from commit_check.rule_builder import ValidationRule
|
|
16
|
+
from commit_check import DEFAULT_AI_DISCLOSURE_TRAILERS, ai_policy
|
|
18
17
|
from commit_check.ai_signatures import (
|
|
19
18
|
detect_ai_signatures,
|
|
20
19
|
)
|
|
20
|
+
from commit_check.fixes import (
|
|
21
|
+
fix_branch_type,
|
|
22
|
+
fix_conventional_header,
|
|
23
|
+
fix_subject_case,
|
|
24
|
+
fix_wip,
|
|
25
|
+
signoff_trailer,
|
|
26
|
+
strip_lines_containing,
|
|
27
|
+
)
|
|
28
|
+
from commit_check.imperatives import IMPERATIVES, NON_IMPERATIVE_LOOKALIKES
|
|
29
|
+
from commit_check.rule_builder import ValidationRule
|
|
21
30
|
from commit_check.util import (
|
|
22
31
|
_print_failure,
|
|
23
32
|
fetch_remote_ref,
|
|
24
33
|
fetch_upstream_ref,
|
|
34
|
+
format_size,
|
|
35
|
+
get_branch_name,
|
|
25
36
|
get_commit_author_identity,
|
|
37
|
+
get_commit_files,
|
|
26
38
|
get_commit_info,
|
|
27
|
-
get_git_user_identity,
|
|
28
39
|
get_git_config_value,
|
|
29
|
-
get_branch_name,
|
|
30
|
-
get_commit_files,
|
|
31
|
-
get_push_commits,
|
|
32
40
|
get_git_remotes,
|
|
41
|
+
get_git_user_identity,
|
|
42
|
+
get_push_commits,
|
|
33
43
|
get_tags_at,
|
|
34
|
-
format_size,
|
|
35
44
|
get_upstream_branch,
|
|
36
45
|
get_upstream_remote_sha,
|
|
37
|
-
has_commits,
|
|
38
46
|
git_merge_base,
|
|
39
47
|
git_rev_parse_verify,
|
|
48
|
+
has_commits,
|
|
40
49
|
print_error_header,
|
|
41
50
|
rejection_headline,
|
|
42
51
|
)
|
|
43
|
-
from commit_check.imperatives import IMPERATIVES, NON_IMPERATIVE_LOOKALIKES
|
|
44
|
-
from commit_check.fixes import (
|
|
45
|
-
fix_branch_type,
|
|
46
|
-
fix_conventional_header,
|
|
47
|
-
fix_subject_case,
|
|
48
|
-
fix_wip,
|
|
49
|
-
signoff_trailer,
|
|
50
|
-
strip_lines_containing,
|
|
51
|
-
)
|
|
52
52
|
|
|
53
53
|
|
|
54
54
|
class ValidationResult(IntEnum):
|
|
@@ -96,12 +96,27 @@ class ValidationContext:
|
|
|
96
96
|
# Set when stdin_text is a piped commit message: checks with a value of
|
|
97
97
|
# their own (branch, author, tags, push refs) then read git instead.
|
|
98
98
|
stdin_is_message: bool = False
|
|
99
|
+
# Set when stdin_text is the pre-push line rebuilt from pre-commit's
|
|
100
|
+
# environment. pre-commit names one ref per push, the first that carries
|
|
101
|
+
# commits the remote lacks, so a ref pushed alongside it -- a tag that
|
|
102
|
+
# ``git push --follow-tags`` sends after a branch -- is not in the line.
|
|
103
|
+
push_from_pre_commit: bool = False
|
|
99
104
|
|
|
100
105
|
@property
|
|
101
106
|
def piped_value(self) -> str | None:
|
|
102
107
|
"""stdin_text as a check's own value, or None when it is the message."""
|
|
103
108
|
return None if self.stdin_is_message else self.stdin_text
|
|
104
109
|
|
|
110
|
+
@property
|
|
111
|
+
def piped_name(self) -> str | None:
|
|
112
|
+
"""piped_value as the one name a branch or author check reads.
|
|
113
|
+
|
|
114
|
+
None as well when stdin_text is the pre-push line rebuilt from
|
|
115
|
+
pre-commit's environment: that names refs for the push checks run
|
|
116
|
+
alongside, not a branch or an author, so these read git instead.
|
|
117
|
+
"""
|
|
118
|
+
return None if self.push_from_pre_commit else self.piped_value
|
|
119
|
+
|
|
105
120
|
|
|
106
121
|
@dataclass
|
|
107
122
|
class CheckOutcome:
|
|
@@ -132,6 +147,9 @@ class CheckOutcome:
|
|
|
132
147
|
fix: str = ""
|
|
133
148
|
rule_id: str = ""
|
|
134
149
|
docs_url: str = ""
|
|
150
|
+
# Why a skipped check did not run, when commit-check knows (the author
|
|
151
|
+
# is on an ``ignore_authors`` list); empty otherwise.
|
|
152
|
+
reason: str = ""
|
|
135
153
|
|
|
136
154
|
def to_dict(self) -> dict[str, str]:
|
|
137
155
|
"""Serialise to a plain dict (suitable for JSON encoding)."""
|
|
@@ -144,6 +162,7 @@ class CheckOutcome:
|
|
|
144
162
|
"suggest": self.suggest,
|
|
145
163
|
"fix": self.fix,
|
|
146
164
|
"docs_url": self.docs_url,
|
|
165
|
+
"reason": self.reason,
|
|
147
166
|
}
|
|
148
167
|
|
|
149
168
|
|
|
@@ -206,11 +225,14 @@ class BaseValidator(ABC):
|
|
|
206
225
|
# collection. Text-mode validation skips the extra lookups (e.g. a
|
|
207
226
|
# git subprocess for the branch name) and keeps values empty.
|
|
208
227
|
self._collect_value: bool = False
|
|
228
|
+
# Why the rule declined to run, when the reason is something the
|
|
229
|
+
# user configured (an ignored author). A bare "skipped" leaves the
|
|
230
|
+
# reader guessing whether the policy was bypassed on purpose.
|
|
231
|
+
self._skip_reason: str = ""
|
|
209
232
|
|
|
210
233
|
@abstractmethod
|
|
211
234
|
def validate(self, context: ValidationContext) -> ValidationResult:
|
|
212
235
|
"""Perform validation and return result."""
|
|
213
|
-
pass
|
|
214
236
|
|
|
215
237
|
@staticmethod
|
|
216
238
|
def _resolve_current_author(context: ValidationContext) -> str:
|
|
@@ -286,6 +308,7 @@ class BaseValidator(ABC):
|
|
|
286
308
|
|
|
287
309
|
current_author = self._resolve_current_author(context)
|
|
288
310
|
if current_author and current_author in ignore_authors:
|
|
311
|
+
self._skip_reason = f"author {current_author} is in [commit].ignore_authors"
|
|
289
312
|
return True
|
|
290
313
|
|
|
291
314
|
# Check co-authors from the commit message body
|
|
@@ -298,7 +321,13 @@ class BaseValidator(ABC):
|
|
|
298
321
|
message,
|
|
299
322
|
re.MULTILINE,
|
|
300
323
|
)
|
|
301
|
-
|
|
324
|
+
for co_author in co_authors:
|
|
325
|
+
if co_author.strip() in ignore_authors:
|
|
326
|
+
self._skip_reason = (
|
|
327
|
+
f"co-author {co_author.strip()} is in [commit].ignore_authors"
|
|
328
|
+
)
|
|
329
|
+
return True
|
|
330
|
+
return False
|
|
302
331
|
|
|
303
332
|
@staticmethod
|
|
304
333
|
def _get_commit_body(context: ValidationContext) -> str:
|
|
@@ -314,7 +343,7 @@ class BaseValidator(ABC):
|
|
|
314
343
|
try:
|
|
315
344
|
with open(context.commit_file, "r", encoding="utf-8") as f:
|
|
316
345
|
return f.read()
|
|
317
|
-
except
|
|
346
|
+
except OSError:
|
|
318
347
|
pass
|
|
319
348
|
if context.rev is not None:
|
|
320
349
|
return get_commit_info("b", context.rev)
|
|
@@ -348,8 +377,11 @@ class BaseValidator(ABC):
|
|
|
348
377
|
if ignore_authors:
|
|
349
378
|
current_author = self._resolve_current_author(context)
|
|
350
379
|
if current_author and current_author in ignore_authors:
|
|
380
|
+
self._skip_reason = (
|
|
381
|
+
f"author {current_author} is in [branch].ignore_authors"
|
|
382
|
+
)
|
|
351
383
|
return True
|
|
352
|
-
return context.
|
|
384
|
+
return context.piped_name is None and not has_commits()
|
|
353
385
|
|
|
354
386
|
def _print_failure(
|
|
355
387
|
self,
|
|
@@ -622,7 +654,7 @@ class AuthorValidator(BaseValidator):
|
|
|
622
654
|
Checks git config first (for pre-commit validation of the configured identity),
|
|
623
655
|
then falls back to the last commit's author info.
|
|
624
656
|
"""
|
|
625
|
-
supplied = context.
|
|
657
|
+
supplied = context.piped_name
|
|
626
658
|
if supplied is not None:
|
|
627
659
|
return supplied.strip()
|
|
628
660
|
|
|
@@ -678,7 +710,7 @@ class BranchValidator(BaseValidator):
|
|
|
678
710
|
def validate(self, context: ValidationContext) -> ValidationResult:
|
|
679
711
|
if self._should_skip_branch_validation(context):
|
|
680
712
|
return ValidationResult.SKIP
|
|
681
|
-
supplied = context.
|
|
713
|
+
supplied = context.piped_name
|
|
682
714
|
branch_name = supplied.strip() if supplied is not None else get_branch_name()
|
|
683
715
|
self._checked_value = branch_name
|
|
684
716
|
|
|
@@ -706,7 +738,9 @@ class TagValidator(BaseValidator):
|
|
|
706
738
|
``HEAD``). A commit with no tag is a skip, not a failure: the rule
|
|
707
739
|
validates how tags are named, and the absence of one is not a naming
|
|
708
740
|
violation. Piped input (or an API-supplied value) names the tags to check
|
|
709
|
-
directly, one per line, without consulting git.
|
|
741
|
+
directly, one per line, without consulting git. In a pre-push hook the
|
|
742
|
+
tags under push are checked instead; under pre-commit, which names only
|
|
743
|
+
one ref of a push, a branch push checks the tags on the commits it pushes.
|
|
710
744
|
"""
|
|
711
745
|
|
|
712
746
|
@staticmethod
|
|
@@ -735,10 +769,32 @@ class TagValidator(BaseValidator):
|
|
|
735
769
|
return list(dict.fromkeys(tags))
|
|
736
770
|
return lines
|
|
737
771
|
|
|
772
|
+
@staticmethod
|
|
773
|
+
def _tags_on_pushed_commits(text: str) -> list[str]:
|
|
774
|
+
"""The tags pointing at any commit pre-push lines carry.
|
|
775
|
+
|
|
776
|
+
Every commit of the push counts, not only the tip: a release commit
|
|
777
|
+
is often followed by another before the push, and
|
|
778
|
+
``git push --follow-tags`` still sends the tag on the earlier one.
|
|
779
|
+
"""
|
|
780
|
+
tags = []
|
|
781
|
+
for ln in text.splitlines():
|
|
782
|
+
fields = ln.split()
|
|
783
|
+
if len(fields) == 4 and set(fields[1]) != {"0"}:
|
|
784
|
+
for rev in get_push_commits(fields[1], fields[3]):
|
|
785
|
+
tags.extend(get_tags_at(rev))
|
|
786
|
+
return list(dict.fromkeys(tags))
|
|
787
|
+
|
|
738
788
|
def validate(self, context: ValidationContext) -> ValidationResult:
|
|
739
789
|
supplied = context.piped_value
|
|
740
790
|
if supplied is not None:
|
|
741
791
|
tags = self._tags_from_stdin(supplied)
|
|
792
|
+
if not tags and context.push_from_pre_commit:
|
|
793
|
+
# A branch push, as pre-commit tells it: any tag pushed with
|
|
794
|
+
# the branch is missing from the line, so the tags on the
|
|
795
|
+
# pushed commits stand in for it. Reading HEAD instead would
|
|
796
|
+
# judge whatever is checked out, which need not be pushed.
|
|
797
|
+
tags = self._tags_on_pushed_commits(supplied)
|
|
742
798
|
else:
|
|
743
799
|
tags = get_tags_at(context.rev or "HEAD")
|
|
744
800
|
|
|
@@ -1097,7 +1153,9 @@ class BodyValidator(BaseValidator):
|
|
|
1097
1153
|
return ValidationResult.PASS
|
|
1098
1154
|
|
|
1099
1155
|
# Check if there's content after the first line (even if separated by empty lines)
|
|
1100
|
-
|
|
1156
|
+
# Unreachable: the message is stripped, so two or more lines always
|
|
1157
|
+
# hold two non-empty ones and have passed above.
|
|
1158
|
+
if len(lines) > 1: # pragma: no cover
|
|
1101
1159
|
body_content = "\n".join(lines[1:]).strip()
|
|
1102
1160
|
if body_content:
|
|
1103
1161
|
return ValidationResult.PASS
|
|
@@ -1324,9 +1382,8 @@ class CommitTypeValidator(BaseValidator):
|
|
|
1324
1382
|
"""Check if WIP commits are allowed."""
|
|
1325
1383
|
upper_msg = message.upper()
|
|
1326
1384
|
is_wip = (
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
or upper_msg.startswith("WIP ") # WIP at start with space
|
|
1385
|
+
# wip: / [wip] / "wip " at the start, in any case
|
|
1386
|
+
upper_msg.startswith(("WIP:", "[WIP]", "WIP "))
|
|
1330
1387
|
or upper_msg == "WIP" # exact WIP
|
|
1331
1388
|
)
|
|
1332
1389
|
return not is_wip or self.rule.value
|
|
@@ -1510,7 +1567,7 @@ class AiAttributionValidator(BaseValidator):
|
|
|
1510
1567
|
class ValidationEngine:
|
|
1511
1568
|
"""Main validation engine that orchestrates all validations."""
|
|
1512
1569
|
|
|
1513
|
-
VALIDATOR_MAP: dict[str, type[BaseValidator]] = {
|
|
1570
|
+
VALIDATOR_MAP: ClassVar[dict[str, type[BaseValidator]]] = {
|
|
1514
1571
|
"message": CommitMessageValidator,
|
|
1515
1572
|
"subject_capitalized": SubjectCapitalizationValidator,
|
|
1516
1573
|
"subject_imperative": SubjectImperativeValidator,
|
|
@@ -1688,6 +1745,7 @@ class ValidationEngine:
|
|
|
1688
1745
|
value="" if skipped else (validator._checked_value or ""),
|
|
1689
1746
|
rule_id=rule.rule_id or "",
|
|
1690
1747
|
docs_url=rule.docs_url or "",
|
|
1748
|
+
reason=validator._skip_reason if skipped else "",
|
|
1691
1749
|
)
|
|
1692
1750
|
)
|
|
1693
1751
|
|
commit_check/main.py
CHANGED
|
@@ -1,14 +1,23 @@
|
|
|
1
1
|
"""Modern commit-check CLI with clean architecture and TOML support."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
4
6
|
import json
|
|
5
7
|
import os
|
|
6
8
|
import select
|
|
7
9
|
import sys
|
|
8
|
-
import argparse
|
|
9
10
|
|
|
10
11
|
from commit_check.config import ConfigError, find_config_path
|
|
11
|
-
from commit_check.config_merger import ConfigMerger, parse_bool,
|
|
12
|
+
from commit_check.config_merger import ConfigMerger, parse_bool, parse_int, parse_list
|
|
13
|
+
from commit_check.engine import (
|
|
14
|
+
CheckOutcome,
|
|
15
|
+
ValidationContext,
|
|
16
|
+
ValidationEngine,
|
|
17
|
+
ValidationResult,
|
|
18
|
+
count_warnings,
|
|
19
|
+
overall_status,
|
|
20
|
+
)
|
|
12
21
|
from commit_check.rule_builder import RuleBuilder
|
|
13
22
|
from commit_check.rules_catalog import BRANCH_CHECKS, FILES_CHECKS, MESSAGE_CHECKS
|
|
14
23
|
from commit_check.util import (
|
|
@@ -16,14 +25,7 @@ from commit_check.util import (
|
|
|
16
25
|
git_rev_parse_verify,
|
|
17
26
|
print_error_header,
|
|
18
27
|
)
|
|
19
|
-
|
|
20
|
-
ValidationEngine,
|
|
21
|
-
ValidationContext,
|
|
22
|
-
ValidationResult,
|
|
23
|
-
CheckOutcome,
|
|
24
|
-
count_warnings,
|
|
25
|
-
overall_status,
|
|
26
|
-
)
|
|
28
|
+
|
|
27
29
|
from . import AI_ATTRIBUTION_POLICIES, __version__
|
|
28
30
|
|
|
29
31
|
# Exit codes. ``1`` is a verdict on the commit; ``2`` means the run could not
|
|
@@ -71,7 +73,7 @@ class StdinReader:
|
|
|
71
73
|
if not sys.stdin.isatty() and cls._has_pending_data(timeout=0.1):
|
|
72
74
|
data = sys.stdin.read()
|
|
73
75
|
return data.strip() if data else None
|
|
74
|
-
except
|
|
76
|
+
except OSError:
|
|
75
77
|
return None
|
|
76
78
|
return None
|
|
77
79
|
|
|
@@ -232,6 +234,14 @@ def _get_parser() -> argparse.ArgumentParser:
|
|
|
232
234
|
required=False,
|
|
233
235
|
)
|
|
234
236
|
|
|
237
|
+
parser.add_argument(
|
|
238
|
+
"--fix",
|
|
239
|
+
help="rewrite the commit message file when every failed message check "
|
|
240
|
+
"has a mechanical fix, such as a type's case or a missing colon",
|
|
241
|
+
action="store_true",
|
|
242
|
+
required=False,
|
|
243
|
+
)
|
|
244
|
+
|
|
235
245
|
check_group.add_argument(
|
|
236
246
|
"--no-force-push",
|
|
237
247
|
help="check that no force push is being performed (uses pre-push hook stdin when available, otherwise checks the current branch against its upstream)",
|
|
@@ -439,6 +449,15 @@ def _get_parser() -> argparse.ArgumentParser:
|
|
|
439
449
|
help="enforce conventional branch naming (true/false)",
|
|
440
450
|
)
|
|
441
451
|
|
|
452
|
+
branch_group.add_argument(
|
|
453
|
+
"--require-description-grammar",
|
|
454
|
+
type=parse_bool,
|
|
455
|
+
default=None,
|
|
456
|
+
metavar="BOOL",
|
|
457
|
+
help="hold the description after '/' to the Conventional Branch grammar "
|
|
458
|
+
"(true/false)",
|
|
459
|
+
)
|
|
460
|
+
|
|
442
461
|
branch_group.add_argument(
|
|
443
462
|
"--allow-branch-types",
|
|
444
463
|
type=parse_list,
|
|
@@ -520,6 +539,41 @@ def _get_parser() -> argparse.ArgumentParser:
|
|
|
520
539
|
return parser
|
|
521
540
|
|
|
522
541
|
|
|
542
|
+
def _fix_message_file(path: str, rules: list, config: dict) -> None:
|
|
543
|
+
"""Rewrite a commit message file when every failure in it has a fix.
|
|
544
|
+
|
|
545
|
+
A subject rule's fix is the corrected subject; every other rule's fix is
|
|
546
|
+
the corrected message. Fixes go in one at a time, re-checking after
|
|
547
|
+
each, and a failure no fix can reach leaves the file as it was.
|
|
548
|
+
"""
|
|
549
|
+
with open(path, encoding="utf-8") as f:
|
|
550
|
+
original = f.read()
|
|
551
|
+
message = original.strip()
|
|
552
|
+
engine = ValidationEngine([r for r in rules if r.check in MESSAGE_CHECKS])
|
|
553
|
+
fixed = []
|
|
554
|
+
for _ in range(len(engine.rules) + 1):
|
|
555
|
+
context = ValidationContext(
|
|
556
|
+
stdin_text=message, stdin_is_message=True, config=config
|
|
557
|
+
)
|
|
558
|
+
outcomes = engine.validate_all_detailed(context)
|
|
559
|
+
failed = [o for o in outcomes if o.status == "fail"]
|
|
560
|
+
fixable = [o for o in failed if o.fix]
|
|
561
|
+
if not fixable:
|
|
562
|
+
break
|
|
563
|
+
first = fixable[0]
|
|
564
|
+
subject = message.split("\n", 1)[0]
|
|
565
|
+
if first.value == subject:
|
|
566
|
+
message = first.fix + message[len(subject) :]
|
|
567
|
+
else:
|
|
568
|
+
message = first.fix
|
|
569
|
+
fixed.append(f"{first.rule_id} {first.check.replace('_', '-')}")
|
|
570
|
+
if failed or not fixed:
|
|
571
|
+
return
|
|
572
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
573
|
+
f.write(original.replace(original.strip(), message, 1))
|
|
574
|
+
print(f"✎ fixed the commit message: {', '.join(fixed)}", file=sys.stderr)
|
|
575
|
+
|
|
576
|
+
|
|
523
577
|
def _resolve_commit_message_source(
|
|
524
578
|
args: argparse.Namespace,
|
|
525
579
|
stdin_reader: StdinReader,
|
|
@@ -540,8 +594,12 @@ def _resolve_commit_message_source(
|
|
|
540
594
|
|
|
541
595
|
def _resolve_stdin_for_non_message(
|
|
542
596
|
args: argparse.Namespace, stdin_reader: StdinReader
|
|
543
|
-
) -> str | None:
|
|
544
|
-
"""Resolve stdin content for non-message validation types.
|
|
597
|
+
) -> tuple[str | None, bool]:
|
|
598
|
+
"""Resolve stdin content for non-message validation types.
|
|
599
|
+
|
|
600
|
+
Returns the content, and whether it is the pre-push line rebuilt from
|
|
601
|
+
pre-commit's environment rather than anything piped.
|
|
602
|
+
"""
|
|
545
603
|
has_non_message_check = any(
|
|
546
604
|
[
|
|
547
605
|
args.branch,
|
|
@@ -553,15 +611,16 @@ def _resolve_stdin_for_non_message(
|
|
|
553
611
|
]
|
|
554
612
|
)
|
|
555
613
|
if not has_non_message_check:
|
|
556
|
-
return None
|
|
614
|
+
return None, False
|
|
557
615
|
|
|
558
616
|
stdin_content = stdin_reader.read_piped_input()
|
|
559
|
-
#
|
|
617
|
+
# The push-shaped checks need the pre-push ref lines. Under the
|
|
560
618
|
# pre-commit framework git's native stdin is consumed, so the same data
|
|
561
619
|
# is rebuilt from the PRE_COMMIT_* environment.
|
|
562
|
-
if (args.no_force_push or args.files) and stdin_content is None:
|
|
563
|
-
|
|
564
|
-
|
|
620
|
+
if (args.no_force_push or args.files or args.tag) and stdin_content is None:
|
|
621
|
+
push_input = _build_pre_commit_push_input()
|
|
622
|
+
return push_input, push_input is not None
|
|
623
|
+
return stdin_content, False
|
|
565
624
|
|
|
566
625
|
|
|
567
626
|
def _get_requested_checks(args: argparse.Namespace) -> set[str]:
|
|
@@ -600,8 +659,10 @@ _UNCONFIGURED_HINTS = (
|
|
|
600
659
|
(
|
|
601
660
|
"branch",
|
|
602
661
|
("branch", "merge_base"),
|
|
603
|
-
|
|
604
|
-
|
|
662
|
+
(
|
|
663
|
+
"--branch requested but no branch rules are configured "
|
|
664
|
+
"(conventional_branch = false and no require_rebase_target)"
|
|
665
|
+
),
|
|
605
666
|
),
|
|
606
667
|
)
|
|
607
668
|
|
|
@@ -702,6 +763,7 @@ def main() -> int:
|
|
|
702
763
|
# Resolve validation context inputs. With --rev the commit itself is
|
|
703
764
|
# the thing under test, so stdin is never consulted: piping and a
|
|
704
765
|
# revision would name two different subjects for the same checks.
|
|
766
|
+
push_from_pre_commit = False
|
|
705
767
|
if args.rev is not None:
|
|
706
768
|
stdin_content, commit_file_path = None, None
|
|
707
769
|
else:
|
|
@@ -709,11 +771,18 @@ def main() -> int:
|
|
|
709
771
|
args, stdin_reader
|
|
710
772
|
)
|
|
711
773
|
if not args.message:
|
|
712
|
-
stdin_content = _resolve_stdin_for_non_message(
|
|
774
|
+
stdin_content, push_from_pre_commit = _resolve_stdin_for_non_message(
|
|
775
|
+
args, stdin_reader
|
|
776
|
+
)
|
|
713
777
|
|
|
714
778
|
# Next to --message, piped stdin is the message; other checks read git.
|
|
715
779
|
stdin_is_message = args.message and stdin_content is not None
|
|
716
780
|
|
|
781
|
+
if args.fix and not commit_file_path:
|
|
782
|
+
parser.error("--fix rewrites a commit message file; pass one")
|
|
783
|
+
if args.fix and commit_file_path and not args.dry_run:
|
|
784
|
+
_fix_message_file(commit_file_path, filtered_rules, config_data)
|
|
785
|
+
|
|
717
786
|
# Reset banner state for this run
|
|
718
787
|
print_error_header.has_been_called = False
|
|
719
788
|
|
|
@@ -727,6 +796,7 @@ def main() -> int:
|
|
|
727
796
|
push_upstream_fallback=args.no_force_push
|
|
728
797
|
and (stdin_content is None or stdin_is_message),
|
|
729
798
|
stdin_is_message=stdin_is_message,
|
|
799
|
+
push_from_pre_commit=push_from_pre_commit,
|
|
730
800
|
)
|
|
731
801
|
|
|
732
802
|
# Run validation – choose output mode based on --format
|
|
@@ -753,7 +823,7 @@ def main() -> int:
|
|
|
753
823
|
# Nothing was validated, so this is not a verdict on the commit.
|
|
754
824
|
print(f"Error: {e}", file=sys.stderr)
|
|
755
825
|
return EXIT_CONFIG_ERROR
|
|
756
|
-
except Exception as e:
|
|
826
|
+
except Exception as e: # noqa: BLE001 - last resort: report it, no traceback
|
|
757
827
|
print(f"Error: {e}", file=sys.stderr)
|
|
758
828
|
return EXIT_FAIL
|
|
759
829
|
|
commit_check/rule_builder.py
CHANGED
|
@@ -1,34 +1,35 @@
|
|
|
1
1
|
"""Rule builder that creates validation rules from config and catalog."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
|
+
|
|
4
5
|
import re
|
|
5
6
|
import sys
|
|
6
|
-
from typing import Any
|
|
7
7
|
from dataclasses import dataclass, replace
|
|
8
|
-
from
|
|
9
|
-
|
|
10
|
-
from commit_check.rules_catalog import (
|
|
11
|
-
COMMIT_RULES,
|
|
12
|
-
BRANCH_RULES,
|
|
13
|
-
PUSH_RULES,
|
|
14
|
-
FILES_RULES,
|
|
15
|
-
TAG_RULES,
|
|
16
|
-
RULES_BY_CHECK,
|
|
17
|
-
RuleCatalogEntry,
|
|
18
|
-
)
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
19
10
|
from commit_check import (
|
|
20
11
|
AI_ATTRIBUTION_POLICIES,
|
|
21
|
-
DEFAULT_COMMIT_TYPES,
|
|
22
|
-
DEFAULT_BRANCH_TYPES,
|
|
23
|
-
DEFAULT_BRANCH_NAMES,
|
|
24
|
-
DEFAULT_BOOLEAN_RULES,
|
|
25
|
-
DEFAULT_PUSH_RULES,
|
|
26
12
|
DEFAULT_AI_ATTRIBUTION,
|
|
27
13
|
DEFAULT_AI_DISCLOSURE_PATTERN,
|
|
28
14
|
DEFAULT_AI_DISCLOSURE_TRAILERS,
|
|
15
|
+
DEFAULT_BOOLEAN_RULES,
|
|
16
|
+
DEFAULT_BRANCH_NAMES,
|
|
17
|
+
DEFAULT_BRANCH_TYPES,
|
|
18
|
+
DEFAULT_COMMIT_TYPES,
|
|
19
|
+
DEFAULT_PUSH_RULES,
|
|
29
20
|
DEFAULT_TAG_REGEX,
|
|
30
21
|
)
|
|
31
|
-
|
|
22
|
+
from commit_check.config import ConfigError
|
|
23
|
+
from commit_check.rules_catalog import (
|
|
24
|
+
BRANCH_RULES,
|
|
25
|
+
COMMIT_RULES,
|
|
26
|
+
FILES_RULES,
|
|
27
|
+
PUSH_RULES,
|
|
28
|
+
RULES_BY_CHECK,
|
|
29
|
+
TAG_RULES,
|
|
30
|
+
RuleCatalogEntry,
|
|
31
|
+
)
|
|
32
|
+
from commit_check.util import format_size, parse_size
|
|
32
33
|
|
|
33
34
|
#: The checks each ``ai_attribution`` policy builds. ``forbid`` rejects every
|
|
34
35
|
#: AI signature under one rule; ``disclose`` asks for three things, one rule
|
|
@@ -43,6 +44,15 @@ _AI_CHECKS: frozenset[str] = frozenset().union(*_AI_CHECKS_BY_POLICY.values())
|
|
|
43
44
|
#: A git trailer token: letters, digits and hyphens, and no colon.
|
|
44
45
|
_TRAILER_KEY = re.compile(r"^[A-Za-z][A-Za-z0-9-]*$")
|
|
45
46
|
|
|
47
|
+
#: The description grammar from conventionalbranch.org's spec.json, used when
|
|
48
|
+
#: [branch] require_description_grammar is on; otherwise the description is ".+".
|
|
49
|
+
_STRICT_BRANCH_DESCRIPTION = r"[a-z0-9]+(?:\.[a-z0-9]+)*(?:-[a-z0-9]+(?:\.[a-z0-9]+)*)*"
|
|
50
|
+
_STRICT_BRANCH_SUGGEST = (
|
|
51
|
+
"Use <type>/<description> with an allowed type and a lowercase, "
|
|
52
|
+
"hyphen-separated description (e.g. feature/add-login), "
|
|
53
|
+
"or add the branch to allow_branch_names in config"
|
|
54
|
+
)
|
|
55
|
+
|
|
46
56
|
|
|
47
57
|
# Lookup tables for the top-level ``warn`` list, built once at import: a
|
|
48
58
|
# check name or rule ID in any case maps to the catalog's check name.
|
|
@@ -481,12 +491,13 @@ class RuleBuilder:
|
|
|
481
491
|
allowed_types = self._get_allowed_branch_types()
|
|
482
492
|
allowed_names = self._get_allowed_branch_names()
|
|
483
493
|
regex = self._build_conventional_branch_regex(allowed_types, allowed_names)
|
|
494
|
+
strict = self.branch_config.get("require_description_grammar", False)
|
|
484
495
|
|
|
485
496
|
return ValidationRule(
|
|
486
497
|
check=catalog_entry.check,
|
|
487
498
|
regex=regex,
|
|
488
499
|
error=catalog_entry.error,
|
|
489
|
-
suggest=catalog_entry.suggest,
|
|
500
|
+
suggest=_STRICT_BRANCH_SUGGEST if strict else catalog_entry.suggest,
|
|
490
501
|
allowed=allowed_types,
|
|
491
502
|
)
|
|
492
503
|
|
|
@@ -732,7 +743,12 @@ class RuleBuilder:
|
|
|
732
743
|
# config allows "create-pull-request/.+" that way. Every alternative
|
|
733
744
|
# is anchored at both ends so a name has to match the whole branch
|
|
734
745
|
# name, not merely its start ("main-backup" is not "main").
|
|
735
|
-
names_pattern = "|
|
|
746
|
+
names_pattern = "master|main|HEAD|PR-.+"
|
|
736
747
|
if allowed_names:
|
|
737
748
|
names_pattern += "|" + "|".join(allowed_names)
|
|
738
|
-
|
|
749
|
+
description_pattern = (
|
|
750
|
+
_STRICT_BRANCH_DESCRIPTION
|
|
751
|
+
if self.branch_config.get("require_description_grammar", False)
|
|
752
|
+
else ".+"
|
|
753
|
+
)
|
|
754
|
+
return rf"^(?:{types_pattern})/{description_pattern}$|^(?:{names_pattern})$"
|
commit_check/rules_catalog.py
CHANGED
commit_check/util.py
CHANGED
|
@@ -6,12 +6,14 @@ A module containing utility functions.
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
from __future__ import annotations
|
|
9
|
+
|
|
9
10
|
import os
|
|
10
11
|
import subprocess
|
|
11
12
|
import sys
|
|
12
|
-
from subprocess import CalledProcessError
|
|
13
|
-
from commit_check import RED, GREEN, YELLOW, RESET_COLOR
|
|
14
13
|
from collections.abc import Iterable
|
|
14
|
+
from subprocess import CalledProcessError
|
|
15
|
+
|
|
16
|
+
from commit_check import GREEN, RED, RESET_COLOR, YELLOW
|
|
15
17
|
from commit_check.rules_catalog import display_name
|
|
16
18
|
|
|
17
19
|
|
|
@@ -63,6 +65,54 @@ def _print_failure(
|
|
|
63
65
|
print()
|
|
64
66
|
|
|
65
67
|
|
|
68
|
+
# Where CI systems name the branch under test. Their checkouts are usually a
|
|
69
|
+
# detached HEAD, where git has no branch to report. Tried in order: within
|
|
70
|
+
# one system, a pull or merge request's source branch comes first, because
|
|
71
|
+
# the plain branch variable of such a build names something else.
|
|
72
|
+
_CI_BRANCH_VARIABLES = (
|
|
73
|
+
"GITHUB_HEAD_REF", # GitHub Actions, pull request
|
|
74
|
+
"GITHUB_REF_NAME", # GitHub Actions, push; the tag, on a tag push
|
|
75
|
+
"CI_MERGE_REQUEST_SOURCE_BRANCH_NAME", # GitLab CI, merge request pipeline
|
|
76
|
+
"CI_COMMIT_BRANCH", # GitLab CI, branch pipeline
|
|
77
|
+
"BITBUCKET_BRANCH", # Bitbucket Pipelines
|
|
78
|
+
"SYSTEM_PULLREQUEST_SOURCEBRANCH", # Azure Pipelines, pull request
|
|
79
|
+
"BUILD_SOURCEBRANCH", # Azure Pipelines, branch build
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
# Jenkins multibranch names are generic enough to be set by hand elsewhere,
|
|
83
|
+
# so they are read only inside a Jenkins build, and not in a tag build, where
|
|
84
|
+
# BRANCH_NAME is the tag (TAG_NAME is set too).
|
|
85
|
+
_JENKINS_BRANCH_VARIABLES = (
|
|
86
|
+
"CHANGE_BRANCH", # pull request source branch
|
|
87
|
+
"BRANCH_NAME", # branch build; "PR-24" on a pull request
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _ci_branch_name() -> str:
|
|
92
|
+
"""The branch a CI system says it is building, or ``""``.
|
|
93
|
+
|
|
94
|
+
Azure Pipelines gives full refs (``refs/heads/feature/tools``); the branch
|
|
95
|
+
is what follows ``refs/heads/``. Any other ref — a tag, a pull request's
|
|
96
|
+
merge ref — names no branch and is passed over. So is ``GITHUB_REF_NAME``
|
|
97
|
+
in a GitHub Actions tag build (``GITHUB_REF_TYPE=tag``), where it holds
|
|
98
|
+
the tag.
|
|
99
|
+
"""
|
|
100
|
+
names: tuple[str, ...] = _CI_BRANCH_VARIABLES
|
|
101
|
+
if os.getenv("GITHUB_REF_TYPE") == "tag":
|
|
102
|
+
names = tuple(name for name in names if name != "GITHUB_REF_NAME")
|
|
103
|
+
if os.getenv("JENKINS_URL") and not os.getenv("TAG_NAME"):
|
|
104
|
+
names += _JENKINS_BRANCH_VARIABLES
|
|
105
|
+
for name in names:
|
|
106
|
+
value = (os.getenv(name) or "").strip()
|
|
107
|
+
if value.startswith("refs/heads/"):
|
|
108
|
+
value = value.removeprefix("refs/heads/")
|
|
109
|
+
elif value.startswith("refs/"):
|
|
110
|
+
continue
|
|
111
|
+
if value:
|
|
112
|
+
return value
|
|
113
|
+
return ""
|
|
114
|
+
|
|
115
|
+
|
|
66
116
|
def get_branch_name() -> str:
|
|
67
117
|
"""Identify current branch name.
|
|
68
118
|
.. note::
|
|
@@ -70,6 +120,11 @@ def get_branch_name() -> str:
|
|
|
70
120
|
Please open an issue at https://github.com/commit-check/commit-check/issues
|
|
71
121
|
if you encounter any issue.
|
|
72
122
|
|
|
123
|
+
On a detached HEAD — the usual CI checkout — git reports no branch, so
|
|
124
|
+
the name comes from the CI system's environment (GitHub Actions, GitLab
|
|
125
|
+
CI, Bitbucket Pipelines, Azure Pipelines, Jenkins), and is ``HEAD``
|
|
126
|
+
when none names one.
|
|
127
|
+
|
|
73
128
|
:returns: A `str` describing the current branch name.
|
|
74
129
|
"""
|
|
75
130
|
try:
|
|
@@ -80,10 +135,7 @@ def get_branch_name() -> str:
|
|
|
80
135
|
branch_name = ""
|
|
81
136
|
|
|
82
137
|
if not branch_name:
|
|
83
|
-
|
|
84
|
-
branch_name = (
|
|
85
|
-
os.getenv("GITHUB_HEAD_REF") or os.getenv("GITHUB_REF_NAME") or "HEAD"
|
|
86
|
-
)
|
|
138
|
+
branch_name = _ci_branch_name() or "HEAD"
|
|
87
139
|
return branch_name.strip()
|
|
88
140
|
|
|
89
141
|
|
|
@@ -102,8 +154,8 @@ def get_tags_at(rev: str = "HEAD") -> list[str]:
|
|
|
102
154
|
# and a git diagnostic must not be parsed as a tag name.
|
|
103
155
|
result = subprocess.run(
|
|
104
156
|
["git", "tag", "--points-at", rev],
|
|
105
|
-
|
|
106
|
-
|
|
157
|
+
capture_output=True,
|
|
158
|
+
check=False,
|
|
107
159
|
encoding="utf-8",
|
|
108
160
|
)
|
|
109
161
|
output = result.stdout if result.returncode == 0 else ""
|
|
@@ -250,8 +302,8 @@ def get_commit_files(rev: str = "HEAD") -> list[tuple[str, int]]:
|
|
|
250
302
|
# one, in a single call that also rejects an unresolvable revision.
|
|
251
303
|
lineage = subprocess.run(
|
|
252
304
|
["git", "rev-list", "--parents", "-n", "1", rev],
|
|
253
|
-
|
|
254
|
-
|
|
305
|
+
capture_output=True,
|
|
306
|
+
check=False,
|
|
255
307
|
encoding="utf-8",
|
|
256
308
|
)
|
|
257
309
|
if lineage.returncode != 0:
|
|
@@ -272,8 +324,8 @@ def get_commit_files(rev: str = "HEAD") -> list[tuple[str, int]]:
|
|
|
272
324
|
"-z",
|
|
273
325
|
*scope,
|
|
274
326
|
],
|
|
275
|
-
|
|
276
|
-
|
|
327
|
+
capture_output=True,
|
|
328
|
+
check=False,
|
|
277
329
|
encoding="utf-8",
|
|
278
330
|
)
|
|
279
331
|
if diff.returncode != 0:
|
|
@@ -287,8 +339,8 @@ def get_commit_files(rev: str = "HEAD") -> list[tuple[str, int]]:
|
|
|
287
339
|
try:
|
|
288
340
|
tree = subprocess.run(
|
|
289
341
|
["git", "ls-tree", "-r", "-l", "-z", "--full-tree", rev, "--", *batch],
|
|
290
|
-
|
|
291
|
-
|
|
342
|
+
capture_output=True,
|
|
343
|
+
check=False,
|
|
292
344
|
encoding="utf-8",
|
|
293
345
|
)
|
|
294
346
|
except OSError:
|
|
@@ -324,8 +376,8 @@ def get_push_commits(local_sha: str, remote_sha: str) -> list[str]:
|
|
|
324
376
|
args = [f"{remote_sha}..{local_sha}"]
|
|
325
377
|
result = subprocess.run(
|
|
326
378
|
["git", "rev-list", *args],
|
|
327
|
-
|
|
328
|
-
|
|
379
|
+
capture_output=True,
|
|
380
|
+
check=False,
|
|
329
381
|
encoding="utf-8",
|
|
330
382
|
)
|
|
331
383
|
if result.returncode != 0:
|
|
@@ -343,8 +395,8 @@ def get_upstream_branch() -> str:
|
|
|
343
395
|
"""
|
|
344
396
|
result = subprocess.run(
|
|
345
397
|
["git", "rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{upstream}"],
|
|
346
|
-
|
|
347
|
-
|
|
398
|
+
capture_output=True,
|
|
399
|
+
check=False,
|
|
348
400
|
encoding="utf-8",
|
|
349
401
|
)
|
|
350
402
|
if result.returncode == 0 and result.stdout:
|
|
@@ -378,8 +430,8 @@ def get_remote_branch_sha(remote_name: str, branch_name: str) -> str:
|
|
|
378
430
|
|
|
379
431
|
result = subprocess.run(
|
|
380
432
|
["git", "ls-remote", "--exit-code", remote_name, f"refs/heads/{branch_name}"],
|
|
381
|
-
|
|
382
|
-
|
|
433
|
+
capture_output=True,
|
|
434
|
+
check=False,
|
|
383
435
|
encoding="utf-8",
|
|
384
436
|
)
|
|
385
437
|
if result.returncode != 0 or not result.stdout:
|
|
@@ -401,8 +453,8 @@ def fetch_upstream_ref(upstream_ref: str) -> bool:
|
|
|
401
453
|
remote_name, branch_name = parts
|
|
402
454
|
result = subprocess.run(
|
|
403
455
|
["git", "fetch", "--quiet", "--no-tags", remote_name, branch_name],
|
|
404
|
-
|
|
405
|
-
|
|
456
|
+
capture_output=True,
|
|
457
|
+
check=False,
|
|
406
458
|
encoding="utf-8",
|
|
407
459
|
)
|
|
408
460
|
return result.returncode == 0
|
|
@@ -412,8 +464,8 @@ def get_git_remotes() -> list[str]:
|
|
|
412
464
|
"""Return configured git remote names."""
|
|
413
465
|
result = subprocess.run(
|
|
414
466
|
["git", "remote"],
|
|
415
|
-
|
|
416
|
-
|
|
467
|
+
capture_output=True,
|
|
468
|
+
check=False,
|
|
417
469
|
encoding="utf-8",
|
|
418
470
|
)
|
|
419
471
|
if result.returncode != 0 or not result.stdout:
|
|
@@ -433,8 +485,8 @@ def fetch_remote_ref(remote_name: str, remote_ref: str) -> bool:
|
|
|
433
485
|
|
|
434
486
|
result = subprocess.run(
|
|
435
487
|
["git", "fetch", "--quiet", "--no-tags", remote_name, remote_ref],
|
|
436
|
-
|
|
437
|
-
|
|
488
|
+
capture_output=True,
|
|
489
|
+
check=False,
|
|
438
490
|
encoding="utf-8",
|
|
439
491
|
)
|
|
440
492
|
return result.returncode == 0
|
|
@@ -551,7 +603,7 @@ def git_merge_base(target_branch: str, current_branch: str) -> int:
|
|
|
551
603
|
f"{current_branch}",
|
|
552
604
|
]
|
|
553
605
|
result = subprocess.run(
|
|
554
|
-
commands,
|
|
606
|
+
commands, capture_output=True, check=False, encoding="utf-8"
|
|
555
607
|
)
|
|
556
608
|
return result.returncode
|
|
557
609
|
except CalledProcessError:
|
|
@@ -565,7 +617,7 @@ def cmd_output(commands: list) -> str:
|
|
|
565
617
|
:returns: Get `str` output.
|
|
566
618
|
"""
|
|
567
619
|
result = subprocess.run(
|
|
568
|
-
commands,
|
|
620
|
+
commands, capture_output=True, check=False, encoding="utf-8"
|
|
569
621
|
)
|
|
570
622
|
if result.returncode == 0 and result.stdout is not None:
|
|
571
623
|
return result.stdout
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: commit-check
|
|
3
|
-
Version: 2.
|
|
4
|
-
Summary:
|
|
3
|
+
Version: 2.19.0
|
|
4
|
+
Summary: Commit policy as code for teams and AI agents: check commit messages, branch names, authors, tags, files and AI attribution.
|
|
5
5
|
Author-email: Xianpeng Shen <xianpeng.shen@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
7
7
|
Project-URL: documentation, https://commit-check.com/
|
|
8
8
|
Project-URL: source, https://github.com/commit-check/commit-check
|
|
9
9
|
Project-URL: tracker, https://github.com/commit-check/commit-check/issues
|
|
10
|
-
Keywords: commit conventions,conventional commits,conventional branch,branch naming,commit-check,message,lint message,devops
|
|
10
|
+
Keywords: commit conventions,conventional commits,conventional branch,branch naming,commit-check,message,lint message,devops,policy as code,ai agents
|
|
11
11
|
Classifier: Development Status :: 5 - Production/Stable
|
|
12
12
|
Classifier: Intended Audience :: Developers
|
|
13
13
|
Classifier: Natural Language :: English
|
|
@@ -59,10 +59,11 @@ Dynamic: license-file
|
|
|
59
59
|
|
|
60
60
|
</div>
|
|
61
61
|
|
|
62
|
-
**Commit Check**
|
|
63
|
-
tags, committed files and AI
|
|
64
|
-
commit hook, in CI, on every
|
|
65
|
-
fix is obvious, it hands you
|
|
62
|
+
**Commit Check** is commit policy as code for teams and AI agents. It validates
|
|
63
|
+
commit messages, branch names, authors, sign-offs, tags, committed files and AI
|
|
64
|
+
attribution against one `cchk.toml` — in your commit hook, in CI, on every
|
|
65
|
+
pull request and inside your AI agent. When the fix is obvious, it hands you
|
|
66
|
+
the line.
|
|
66
67
|
|
|
67
68
|

|
|
68
69
|
|
|
@@ -83,12 +84,14 @@ Stop bad commits before they exist. In `.pre-commit-config.yaml`:
|
|
|
83
84
|
```yaml
|
|
84
85
|
repos:
|
|
85
86
|
- repo: https://github.com/commit-check/commit-check
|
|
86
|
-
rev: v2.
|
|
87
|
+
rev: v2.19.0
|
|
87
88
|
hooks:
|
|
88
89
|
- id: check-message
|
|
89
90
|
- id: check-branch
|
|
90
91
|
```
|
|
91
92
|
|
|
93
|
+
Add `args: [--fix]` to `check-message` and the hook corrects mechanical slips
|
|
94
|
+
itself — a type's case, a missing colon — instead of rejecting the commit.
|
|
92
95
|
The hooks for the other checks (`check-author-name`, `check-author-email`,
|
|
93
96
|
`check-no-force-push`, `check-tag`, `check-files`) are listed in the
|
|
94
97
|
[pre-commit guide](https://commit-check.com/guides/pre-commit/).
|
|
@@ -167,6 +170,7 @@ Priority: CLI > env > TOML > default. Booleans accept `true/false`, `yes/no`,
|
|
|
167
170
|
| `commit.author_email_pattern` | `"^.+@.+$"` | `--author-email-pattern` | `CCHK_AUTHOR_EMAIL_PATTERN` | Regex the author email must match (CC102, with `--author-email`) |
|
|
168
171
|
| `commit.author_name_pattern` | `""` | `--author-name-pattern` | `CCHK_AUTHOR_NAME_PATTERN` | Regex the author name must match (CC101, with `--author-name`) |
|
|
169
172
|
| `branch.conventional_branch` | `true` | `--conventional-branch` | `CCHK_CONVENTIONAL_BRANCH` | Enforce `<type>/<description>` branch names (CC201) |
|
|
173
|
+
| `branch.require_description_grammar` | `false` | `--require-description-grammar` | `CCHK_REQUIRE_DESCRIPTION_GRAMMAR` | Also hold the description after `<type>/` to the Conventional Branch grammar: lowercase alphanumeric words joined by single hyphens, dots allowed inside a word (CC201). Off by default, since bot branches such as `dependabot/npm_and_yarn/lodash-4.17.21` do not follow it |
|
|
170
174
|
| `branch.allow_branch_types` | `feature, bugfix, hotfix, release, chore, feat, fix, build, ci, docs, perf, refactor, test, style, ai, claude, codex, copilot, cursor, dependabot, renovate` | `--allow-branch-types` | `CCHK_ALLOW_BRANCH_TYPES` | Allowed branch `<type>` prefixes; each entry is a regex matched against the whole type |
|
|
171
175
|
| `branch.allow_branch_names` | `[]` | `--allow-branch-names` | `CCHK_ALLOW_BRANCH_NAMES` | Extra branch names allowed besides `main`, `master`, `HEAD`, `PR-.+`; each entry is a regex matched against the whole branch name, so `create-pull-request/.+` allows a family of them |
|
|
172
176
|
| `branch.require_rebase_target` | `""` | `--require-rebase-target` | `CCHK_REQUIRE_REBASE_TARGET` | Branch the current branch must be rebased onto (CC202); empty disables |
|
|
@@ -204,10 +208,14 @@ The failing entry of its `checks` array:
|
|
|
204
208
|
"error": "The commit message should follow Conventional Commits. See https://www.conventionalcommits.org",
|
|
205
209
|
"suggest": "Use \"fix: add streaming support\"",
|
|
206
210
|
"fix": "fix: add streaming support",
|
|
207
|
-
"docs_url": "https://commit-check.com/rules/#cc001"
|
|
211
|
+
"docs_url": "https://commit-check.com/rules/#cc001",
|
|
212
|
+
"reason": ""
|
|
208
213
|
}
|
|
209
214
|
```
|
|
210
215
|
|
|
216
|
+
A check that did not run has status `skip`, and `reason` says why when the
|
|
217
|
+
author is on an `ignore_authors` list: `"author dependabot[bot] is in [commit].ignore_authors"`.
|
|
218
|
+
|
|
211
219
|
The same result, without a subprocess:
|
|
212
220
|
|
|
213
221
|
```python
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
commit_check/__init__.py,sha256=QTpfP0b6G9TNS4McgunE6Oozwt3CkUaN4xkqEaR4570,4390
|
|
2
|
+
commit_check/ai_policy.py,sha256=gYC3ROLgGL1TDyhPkFXkV-NiYbvMt80Qt0MFrJcbJY4,8131
|
|
3
|
+
commit_check/ai_signatures.py,sha256=sPef5yb9KZ1FxN19XjLTAM2rzb4YdOSktF8HYUnzh5I,9662
|
|
4
|
+
commit_check/ai_signatures_data.py,sha256=3WZIV1fibvr9f1s7MGSA-mNt_na2t11GIgVeS7Uw8Wc,13125
|
|
5
|
+
commit_check/api.py,sha256=07bvBtV8PrhZIOwCDHiHfu_7CXsX_4U2_K2xMAucd6c,12905
|
|
6
|
+
commit_check/config.py,sha256=Cy7DMWKrWbmVmYER1lUlpvFf9eHn5SfVUVWZ4zgit2w,9820
|
|
7
|
+
commit_check/config_merger.py,sha256=umsjoTcWG7WXufxn7dIMlHrek0a8VWmqjynJR-A4LYQ,13406
|
|
8
|
+
commit_check/engine.py,sha256=BEjQ-70SV5j3a0WvLyjEWwpUXUsVIFYynBMWK_dwWs4,71662
|
|
9
|
+
commit_check/fixes.py,sha256=AsE9ChMXoOCV61pgxbZZNkAzmQS9_ARTWvvox4HkgEg,7088
|
|
10
|
+
commit_check/imperatives.py,sha256=dEd_ssu4G0j6Cann6jqPdB97lw5Adua43K0_Odg_FyA,9518
|
|
11
|
+
commit_check/main.py,sha256=14DkzTVO3dXgzMH0ZyEUob1fuPN8lBJiKJfnvlJx9Dg,27564
|
|
12
|
+
commit_check/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
|
+
commit_check/rule_builder.py,sha256=LU619d0Ndd76oiiszQ07ZHuZ1V-wKQRnUJBP35SiPHE,30351
|
|
14
|
+
commit_check/rules_catalog.py,sha256=JspIhkGXUviLkMG4HhH_IK2x5YTfX3t1p9gI7PbFD7Q,11558
|
|
15
|
+
commit_check/util.py,sha256=XRt9xc6yLjIYY0xwpcZZOAWLc1dL_MPiJNu-64Fi7jk,27810
|
|
16
|
+
commit_check-2.19.0.dist-info/licenses/LICENSE,sha256=VAJ9TE1ov8aUKmeoBRYqciMs0CXag1TeDCoLhwbeQmA,1095
|
|
17
|
+
commit_check-2.19.0.dist-info/METADATA,sha256=0vshw4iiOm7lPfdNUmgwrhMqLv2AIO7_i27AFW9sJIA,15940
|
|
18
|
+
commit_check-2.19.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
19
|
+
commit_check-2.19.0.dist-info/entry_points.txt,sha256=YMrkFGeub-3-IanV1d9qnopXVaRQM7MA9foXKW9iZ88,86
|
|
20
|
+
commit_check-2.19.0.dist-info/top_level.txt,sha256=Wf46u-ooHBMJNHbhfrBNQw3wC5_m8wt-o_Lfbc4QpRg,13
|
|
21
|
+
commit_check-2.19.0.dist-info/RECORD,,
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
commit_check/__init__.py,sha256=--TSQBRscNxJtSNl74Mm6OGxjX0SDh_Gqicuu2AoEYU,4420
|
|
2
|
-
commit_check/ai_policy.py,sha256=gYC3ROLgGL1TDyhPkFXkV-NiYbvMt80Qt0MFrJcbJY4,8131
|
|
3
|
-
commit_check/ai_signatures.py,sha256=XTfHh8zD2EbHjuCBBbukQllTJWqO7EoehVc7bADMwQY,9437
|
|
4
|
-
commit_check/ai_signatures_data.py,sha256=33uiiAieL1wB6isgU-dgkz10IrQub7lVCGh8hf7XiN8,11213
|
|
5
|
-
commit_check/api.py,sha256=RlLGWfpEu9iW-7zIWJ7xDHTYHdH7w4a-0AE7yJH6jU0,12818
|
|
6
|
-
commit_check/config.py,sha256=3KAoCU3sREyT0llePdKE6YrM79tmGka3a2j8LqF27WA,9815
|
|
7
|
-
commit_check/config_merger.py,sha256=k3heMtKEMCSca-LtAntfyvNdPaB18iusfPjt_BOe6j8,11758
|
|
8
|
-
commit_check/engine.py,sha256=vjMcrKpjQR9e_HZoHYkmCUJqcmOw8iOckxpCBqUUvXk,68640
|
|
9
|
-
commit_check/fixes.py,sha256=AsE9ChMXoOCV61pgxbZZNkAzmQS9_ARTWvvox4HkgEg,7088
|
|
10
|
-
commit_check/imperatives.py,sha256=dEd_ssu4G0j6Cann6jqPdB97lw5Adua43K0_Odg_FyA,9518
|
|
11
|
-
commit_check/main.py,sha256=dyvEHptwOdGZA01E3nxbNwNmUvxfgT-vIgfXzvLtqBM,24881
|
|
12
|
-
commit_check/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
|
-
commit_check/rule_builder.py,sha256=f42vN3X_hYU47YgJ0xCniwKW9CwlDwt-4Q_WC7_Y-6E,29591
|
|
14
|
-
commit_check/rules_catalog.py,sha256=Sa4jsyxWJ8g6fX2qJTdIMnd6513in5GmSxHYL5nTwws,11557
|
|
15
|
-
commit_check/util.py,sha256=o0PuiiRhSgIVBmBF_20VytiHj74VlKb3nCMJ8KxB2Ds,25749
|
|
16
|
-
commit_check-2.18.2.dist-info/licenses/LICENSE,sha256=VAJ9TE1ov8aUKmeoBRYqciMs0CXag1TeDCoLhwbeQmA,1095
|
|
17
|
-
commit_check-2.18.2.dist-info/METADATA,sha256=9LtV66iFvvvt72VtMZo07FUtJCB78s5hBUfqRt-YsNU,15077
|
|
18
|
-
commit_check-2.18.2.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
19
|
-
commit_check-2.18.2.dist-info/entry_points.txt,sha256=YMrkFGeub-3-IanV1d9qnopXVaRQM7MA9foXKW9iZ88,86
|
|
20
|
-
commit_check-2.18.2.dist-info/top_level.txt,sha256=Wf46u-ooHBMJNHbhfrBNQw3wC5_m8wt-o_Lfbc4QpRg,13
|
|
21
|
-
commit_check-2.18.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|