commit-check 2.18.2__py3-none-any.whl → 2.18.3__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/api.py +0 -1
- commit_check/config.py +6 -5
- commit_check/config_merger.py +46 -10
- commit_check/engine.py +27 -27
- commit_check/main.py +66 -14
- commit_check/rule_builder.py +20 -19
- commit_check/rules_catalog.py +1 -0
- commit_check/util.py +76 -28
- {commit_check-2.18.2.dist-info → commit_check-2.18.3.dist-info}/METADATA +4 -2
- commit_check-2.18.3.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.18.3.dist-info}/WHEEL +0 -0
- {commit_check-2.18.2.dist-info → commit_check-2.18.3.dist-info}/entry_points.txt +0 -0
- {commit_check-2.18.2.dist-info → commit_check-2.18.3.dist-info}/licenses/LICENSE +0 -0
- {commit_check-2.18.2.dist-info → commit_check-2.18.3.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/api.py
CHANGED
|
@@ -60,7 +60,6 @@ from commit_check.engine import (
|
|
|
60
60
|
from commit_check.rule_builder import RuleBuilder
|
|
61
61
|
from commit_check.rules_catalog import BRANCH_CHECKS, MESSAGE_CHECKS
|
|
62
62
|
|
|
63
|
-
|
|
64
63
|
# ---------------------------------------------------------------------------
|
|
65
64
|
# Internal helpers
|
|
66
65
|
# ---------------------------------------------------------------------------
|
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:
|
|
@@ -108,11 +111,42 @@ def get_default_config() -> dict[str, Any]:
|
|
|
108
111
|
}
|
|
109
112
|
|
|
110
113
|
|
|
114
|
+
#: Read from the same file, but by other tools: ``warn`` by the rule builder,
|
|
115
|
+
#: ``[jira]`` and ``[pull_request]`` by the GitHub App.
|
|
116
|
+
_OTHER_KEYS = frozenset({"warn", "jira", "pull_request"})
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def unknown_setting_warnings(config: dict[str, Any]) -> list[str]:
|
|
120
|
+
"""One line per setting in a config file that nothing reads.
|
|
121
|
+
|
|
122
|
+
A misspelt key used to be ignored in silence, leaving its rule off.
|
|
123
|
+
"""
|
|
124
|
+
defaults = get_default_config()
|
|
125
|
+
names = []
|
|
126
|
+
for section, value in config.items():
|
|
127
|
+
if section in _OTHER_KEYS:
|
|
128
|
+
continue
|
|
129
|
+
if section not in defaults:
|
|
130
|
+
names.append((f"[{section}]", section, [*defaults, *_OTHER_KEYS]))
|
|
131
|
+
elif isinstance(value, dict):
|
|
132
|
+
names.extend(
|
|
133
|
+
(f"[{section}] {key}", key, defaults[section])
|
|
134
|
+
for key in value
|
|
135
|
+
if key not in defaults[section]
|
|
136
|
+
)
|
|
137
|
+
warnings = []
|
|
138
|
+
for shown, name, known in names:
|
|
139
|
+
match = difflib.get_close_matches(name, list(known), n=1)
|
|
140
|
+
hint = f"; did you mean {match[0]}?" if match else ""
|
|
141
|
+
warnings.append(f"⚠ unknown setting {shown} is ignored{hint}")
|
|
142
|
+
return warnings
|
|
143
|
+
|
|
144
|
+
|
|
111
145
|
class ConfigMerger:
|
|
112
146
|
"""Merges configurations from multiple sources with priority: CLI > Env > TOML > Defaults."""
|
|
113
147
|
|
|
114
148
|
# Mapping of environment variable names to config keys
|
|
115
|
-
ENV_VAR_MAPPING: dict[str, tuple[str, str, Callable[[Any], Any]]] = {
|
|
149
|
+
ENV_VAR_MAPPING: ClassVar[dict[str, tuple[str, str, Callable[[Any], Any]]]] = {
|
|
116
150
|
# Commit section
|
|
117
151
|
"CCHK_CONVENTIONAL_COMMITS": ("commit", "conventional_commits", parse_bool),
|
|
118
152
|
"CCHK_MESSAGE_PATTERN": ("commit", "message_pattern", str),
|
|
@@ -155,7 +189,7 @@ class ConfigMerger:
|
|
|
155
189
|
}
|
|
156
190
|
|
|
157
191
|
# Mapping of CLI argument names to config keys
|
|
158
|
-
CLI_ARG_MAPPING: dict[str, tuple[str, str]] = {
|
|
192
|
+
CLI_ARG_MAPPING: ClassVar[dict[str, tuple[str, str]]] = {
|
|
159
193
|
# Commit section
|
|
160
194
|
"conventional_commits": ("commit", "conventional_commits"),
|
|
161
195
|
"subject_capitalized": ("commit", "subject_capitalized"),
|
|
@@ -259,6 +293,8 @@ class ConfigMerger:
|
|
|
259
293
|
try:
|
|
260
294
|
toml_config = load_toml_config(config_path or "")
|
|
261
295
|
if toml_config:
|
|
296
|
+
for warning in unknown_setting_warnings(toml_config):
|
|
297
|
+
print(warning, file=sys.stderr)
|
|
262
298
|
deep_merge(config, toml_config)
|
|
263
299
|
except FileNotFoundError:
|
|
264
300
|
# 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):
|
|
@@ -210,7 +210,6 @@ class BaseValidator(ABC):
|
|
|
210
210
|
@abstractmethod
|
|
211
211
|
def validate(self, context: ValidationContext) -> ValidationResult:
|
|
212
212
|
"""Perform validation and return result."""
|
|
213
|
-
pass
|
|
214
213
|
|
|
215
214
|
@staticmethod
|
|
216
215
|
def _resolve_current_author(context: ValidationContext) -> str:
|
|
@@ -314,7 +313,7 @@ class BaseValidator(ABC):
|
|
|
314
313
|
try:
|
|
315
314
|
with open(context.commit_file, "r", encoding="utf-8") as f:
|
|
316
315
|
return f.read()
|
|
317
|
-
except
|
|
316
|
+
except OSError:
|
|
318
317
|
pass
|
|
319
318
|
if context.rev is not None:
|
|
320
319
|
return get_commit_info("b", context.rev)
|
|
@@ -1097,7 +1096,9 @@ class BodyValidator(BaseValidator):
|
|
|
1097
1096
|
return ValidationResult.PASS
|
|
1098
1097
|
|
|
1099
1098
|
# Check if there's content after the first line (even if separated by empty lines)
|
|
1100
|
-
|
|
1099
|
+
# Unreachable: the message is stripped, so two or more lines always
|
|
1100
|
+
# hold two non-empty ones and have passed above.
|
|
1101
|
+
if len(lines) > 1: # pragma: no cover
|
|
1101
1102
|
body_content = "\n".join(lines[1:]).strip()
|
|
1102
1103
|
if body_content:
|
|
1103
1104
|
return ValidationResult.PASS
|
|
@@ -1324,9 +1325,8 @@ class CommitTypeValidator(BaseValidator):
|
|
|
1324
1325
|
"""Check if WIP commits are allowed."""
|
|
1325
1326
|
upper_msg = message.upper()
|
|
1326
1327
|
is_wip = (
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
or upper_msg.startswith("WIP ") # WIP at start with space
|
|
1328
|
+
# wip: / [wip] / "wip " at the start, in any case
|
|
1329
|
+
upper_msg.startswith(("WIP:", "[WIP]", "WIP "))
|
|
1330
1330
|
or upper_msg == "WIP" # exact WIP
|
|
1331
1331
|
)
|
|
1332
1332
|
return not is_wip or self.rule.value
|
|
@@ -1510,7 +1510,7 @@ class AiAttributionValidator(BaseValidator):
|
|
|
1510
1510
|
class ValidationEngine:
|
|
1511
1511
|
"""Main validation engine that orchestrates all validations."""
|
|
1512
1512
|
|
|
1513
|
-
VALIDATOR_MAP: dict[str, type[BaseValidator]] = {
|
|
1513
|
+
VALIDATOR_MAP: ClassVar[dict[str, type[BaseValidator]]] = {
|
|
1514
1514
|
"message": CommitMessageValidator,
|
|
1515
1515
|
"subject_capitalized": SubjectCapitalizationValidator,
|
|
1516
1516
|
"subject_imperative": SubjectImperativeValidator,
|
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)",
|
|
@@ -520,6 +530,41 @@ def _get_parser() -> argparse.ArgumentParser:
|
|
|
520
530
|
return parser
|
|
521
531
|
|
|
522
532
|
|
|
533
|
+
def _fix_message_file(path: str, rules: list, config: dict) -> None:
|
|
534
|
+
"""Rewrite a commit message file when every failure in it has a fix.
|
|
535
|
+
|
|
536
|
+
A subject rule's fix is the corrected subject; every other rule's fix is
|
|
537
|
+
the corrected message. Fixes go in one at a time, re-checking after
|
|
538
|
+
each, and a failure no fix can reach leaves the file as it was.
|
|
539
|
+
"""
|
|
540
|
+
with open(path, encoding="utf-8") as f:
|
|
541
|
+
original = f.read()
|
|
542
|
+
message = original.strip()
|
|
543
|
+
engine = ValidationEngine([r for r in rules if r.check in MESSAGE_CHECKS])
|
|
544
|
+
fixed = []
|
|
545
|
+
for _ in range(len(engine.rules) + 1):
|
|
546
|
+
context = ValidationContext(
|
|
547
|
+
stdin_text=message, stdin_is_message=True, config=config
|
|
548
|
+
)
|
|
549
|
+
outcomes = engine.validate_all_detailed(context)
|
|
550
|
+
failed = [o for o in outcomes if o.status == "fail"]
|
|
551
|
+
fixable = [o for o in failed if o.fix]
|
|
552
|
+
if not fixable:
|
|
553
|
+
break
|
|
554
|
+
first = fixable[0]
|
|
555
|
+
subject = message.split("\n", 1)[0]
|
|
556
|
+
if first.value == subject:
|
|
557
|
+
message = first.fix + message[len(subject) :]
|
|
558
|
+
else:
|
|
559
|
+
message = first.fix
|
|
560
|
+
fixed.append(f"{first.rule_id} {first.check.replace('_', '-')}")
|
|
561
|
+
if failed or not fixed:
|
|
562
|
+
return
|
|
563
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
564
|
+
f.write(original.replace(original.strip(), message, 1))
|
|
565
|
+
print(f"✎ fixed the commit message: {', '.join(fixed)}", file=sys.stderr)
|
|
566
|
+
|
|
567
|
+
|
|
523
568
|
def _resolve_commit_message_source(
|
|
524
569
|
args: argparse.Namespace,
|
|
525
570
|
stdin_reader: StdinReader,
|
|
@@ -600,8 +645,10 @@ _UNCONFIGURED_HINTS = (
|
|
|
600
645
|
(
|
|
601
646
|
"branch",
|
|
602
647
|
("branch", "merge_base"),
|
|
603
|
-
|
|
604
|
-
|
|
648
|
+
(
|
|
649
|
+
"--branch requested but no branch rules are configured "
|
|
650
|
+
"(conventional_branch = false and no require_rebase_target)"
|
|
651
|
+
),
|
|
605
652
|
),
|
|
606
653
|
)
|
|
607
654
|
|
|
@@ -714,6 +761,11 @@ def main() -> int:
|
|
|
714
761
|
# Next to --message, piped stdin is the message; other checks read git.
|
|
715
762
|
stdin_is_message = args.message and stdin_content is not None
|
|
716
763
|
|
|
764
|
+
if args.fix and not commit_file_path:
|
|
765
|
+
parser.error("--fix rewrites a commit message file; pass one")
|
|
766
|
+
if args.fix and commit_file_path and not args.dry_run:
|
|
767
|
+
_fix_message_file(commit_file_path, filtered_rules, config_data)
|
|
768
|
+
|
|
717
769
|
# Reset banner state for this run
|
|
718
770
|
print_error_header.has_been_called = False
|
|
719
771
|
|
|
@@ -753,7 +805,7 @@ def main() -> int:
|
|
|
753
805
|
# Nothing was validated, so this is not a verdict on the commit.
|
|
754
806
|
print(f"Error: {e}", file=sys.stderr)
|
|
755
807
|
return EXIT_CONFIG_ERROR
|
|
756
|
-
except Exception as e:
|
|
808
|
+
except Exception as e: # noqa: BLE001 - last resort: report it, no traceback
|
|
757
809
|
print(f"Error: {e}", file=sys.stderr)
|
|
758
810
|
return EXIT_FAIL
|
|
759
811
|
|
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
|
|
@@ -732,7 +733,7 @@ class RuleBuilder:
|
|
|
732
733
|
# config allows "create-pull-request/.+" that way. Every alternative
|
|
733
734
|
# is anchored at both ends so a name has to match the whole branch
|
|
734
735
|
# name, not merely its start ("main-backup" is not "main").
|
|
735
|
-
names_pattern = "|
|
|
736
|
+
names_pattern = "master|main|HEAD|PR-.+"
|
|
736
737
|
if allowed_names:
|
|
737
738
|
names_pattern += "|" + "|".join(allowed_names)
|
|
738
739
|
return rf"^(?:{types_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,50 @@ 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
|
|
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.
|
|
97
|
+
"""
|
|
98
|
+
names: tuple[str, ...] = _CI_BRANCH_VARIABLES
|
|
99
|
+
if os.getenv("JENKINS_URL") and not os.getenv("TAG_NAME"):
|
|
100
|
+
names += _JENKINS_BRANCH_VARIABLES
|
|
101
|
+
for name in names:
|
|
102
|
+
value = (os.getenv(name) or "").strip()
|
|
103
|
+
if value.startswith("refs/heads/"):
|
|
104
|
+
value = value.removeprefix("refs/heads/")
|
|
105
|
+
elif value.startswith("refs/"):
|
|
106
|
+
continue
|
|
107
|
+
if value:
|
|
108
|
+
return value
|
|
109
|
+
return ""
|
|
110
|
+
|
|
111
|
+
|
|
66
112
|
def get_branch_name() -> str:
|
|
67
113
|
"""Identify current branch name.
|
|
68
114
|
.. note::
|
|
@@ -70,6 +116,11 @@ def get_branch_name() -> str:
|
|
|
70
116
|
Please open an issue at https://github.com/commit-check/commit-check/issues
|
|
71
117
|
if you encounter any issue.
|
|
72
118
|
|
|
119
|
+
On a detached HEAD — the usual CI checkout — git reports no branch, so
|
|
120
|
+
the name comes from the CI system's environment (GitHub Actions, GitLab
|
|
121
|
+
CI, Bitbucket Pipelines, Azure Pipelines, Jenkins), and is ``HEAD``
|
|
122
|
+
when none names one.
|
|
123
|
+
|
|
73
124
|
:returns: A `str` describing the current branch name.
|
|
74
125
|
"""
|
|
75
126
|
try:
|
|
@@ -80,10 +131,7 @@ def get_branch_name() -> str:
|
|
|
80
131
|
branch_name = ""
|
|
81
132
|
|
|
82
133
|
if not branch_name:
|
|
83
|
-
|
|
84
|
-
branch_name = (
|
|
85
|
-
os.getenv("GITHUB_HEAD_REF") or os.getenv("GITHUB_REF_NAME") or "HEAD"
|
|
86
|
-
)
|
|
134
|
+
branch_name = _ci_branch_name() or "HEAD"
|
|
87
135
|
return branch_name.strip()
|
|
88
136
|
|
|
89
137
|
|
|
@@ -102,8 +150,8 @@ def get_tags_at(rev: str = "HEAD") -> list[str]:
|
|
|
102
150
|
# and a git diagnostic must not be parsed as a tag name.
|
|
103
151
|
result = subprocess.run(
|
|
104
152
|
["git", "tag", "--points-at", rev],
|
|
105
|
-
|
|
106
|
-
|
|
153
|
+
capture_output=True,
|
|
154
|
+
check=False,
|
|
107
155
|
encoding="utf-8",
|
|
108
156
|
)
|
|
109
157
|
output = result.stdout if result.returncode == 0 else ""
|
|
@@ -250,8 +298,8 @@ def get_commit_files(rev: str = "HEAD") -> list[tuple[str, int]]:
|
|
|
250
298
|
# one, in a single call that also rejects an unresolvable revision.
|
|
251
299
|
lineage = subprocess.run(
|
|
252
300
|
["git", "rev-list", "--parents", "-n", "1", rev],
|
|
253
|
-
|
|
254
|
-
|
|
301
|
+
capture_output=True,
|
|
302
|
+
check=False,
|
|
255
303
|
encoding="utf-8",
|
|
256
304
|
)
|
|
257
305
|
if lineage.returncode != 0:
|
|
@@ -272,8 +320,8 @@ def get_commit_files(rev: str = "HEAD") -> list[tuple[str, int]]:
|
|
|
272
320
|
"-z",
|
|
273
321
|
*scope,
|
|
274
322
|
],
|
|
275
|
-
|
|
276
|
-
|
|
323
|
+
capture_output=True,
|
|
324
|
+
check=False,
|
|
277
325
|
encoding="utf-8",
|
|
278
326
|
)
|
|
279
327
|
if diff.returncode != 0:
|
|
@@ -287,8 +335,8 @@ def get_commit_files(rev: str = "HEAD") -> list[tuple[str, int]]:
|
|
|
287
335
|
try:
|
|
288
336
|
tree = subprocess.run(
|
|
289
337
|
["git", "ls-tree", "-r", "-l", "-z", "--full-tree", rev, "--", *batch],
|
|
290
|
-
|
|
291
|
-
|
|
338
|
+
capture_output=True,
|
|
339
|
+
check=False,
|
|
292
340
|
encoding="utf-8",
|
|
293
341
|
)
|
|
294
342
|
except OSError:
|
|
@@ -324,8 +372,8 @@ def get_push_commits(local_sha: str, remote_sha: str) -> list[str]:
|
|
|
324
372
|
args = [f"{remote_sha}..{local_sha}"]
|
|
325
373
|
result = subprocess.run(
|
|
326
374
|
["git", "rev-list", *args],
|
|
327
|
-
|
|
328
|
-
|
|
375
|
+
capture_output=True,
|
|
376
|
+
check=False,
|
|
329
377
|
encoding="utf-8",
|
|
330
378
|
)
|
|
331
379
|
if result.returncode != 0:
|
|
@@ -343,8 +391,8 @@ def get_upstream_branch() -> str:
|
|
|
343
391
|
"""
|
|
344
392
|
result = subprocess.run(
|
|
345
393
|
["git", "rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{upstream}"],
|
|
346
|
-
|
|
347
|
-
|
|
394
|
+
capture_output=True,
|
|
395
|
+
check=False,
|
|
348
396
|
encoding="utf-8",
|
|
349
397
|
)
|
|
350
398
|
if result.returncode == 0 and result.stdout:
|
|
@@ -378,8 +426,8 @@ def get_remote_branch_sha(remote_name: str, branch_name: str) -> str:
|
|
|
378
426
|
|
|
379
427
|
result = subprocess.run(
|
|
380
428
|
["git", "ls-remote", "--exit-code", remote_name, f"refs/heads/{branch_name}"],
|
|
381
|
-
|
|
382
|
-
|
|
429
|
+
capture_output=True,
|
|
430
|
+
check=False,
|
|
383
431
|
encoding="utf-8",
|
|
384
432
|
)
|
|
385
433
|
if result.returncode != 0 or not result.stdout:
|
|
@@ -401,8 +449,8 @@ def fetch_upstream_ref(upstream_ref: str) -> bool:
|
|
|
401
449
|
remote_name, branch_name = parts
|
|
402
450
|
result = subprocess.run(
|
|
403
451
|
["git", "fetch", "--quiet", "--no-tags", remote_name, branch_name],
|
|
404
|
-
|
|
405
|
-
|
|
452
|
+
capture_output=True,
|
|
453
|
+
check=False,
|
|
406
454
|
encoding="utf-8",
|
|
407
455
|
)
|
|
408
456
|
return result.returncode == 0
|
|
@@ -412,8 +460,8 @@ def get_git_remotes() -> list[str]:
|
|
|
412
460
|
"""Return configured git remote names."""
|
|
413
461
|
result = subprocess.run(
|
|
414
462
|
["git", "remote"],
|
|
415
|
-
|
|
416
|
-
|
|
463
|
+
capture_output=True,
|
|
464
|
+
check=False,
|
|
417
465
|
encoding="utf-8",
|
|
418
466
|
)
|
|
419
467
|
if result.returncode != 0 or not result.stdout:
|
|
@@ -433,8 +481,8 @@ def fetch_remote_ref(remote_name: str, remote_ref: str) -> bool:
|
|
|
433
481
|
|
|
434
482
|
result = subprocess.run(
|
|
435
483
|
["git", "fetch", "--quiet", "--no-tags", remote_name, remote_ref],
|
|
436
|
-
|
|
437
|
-
|
|
484
|
+
capture_output=True,
|
|
485
|
+
check=False,
|
|
438
486
|
encoding="utf-8",
|
|
439
487
|
)
|
|
440
488
|
return result.returncode == 0
|
|
@@ -551,7 +599,7 @@ def git_merge_base(target_branch: str, current_branch: str) -> int:
|
|
|
551
599
|
f"{current_branch}",
|
|
552
600
|
]
|
|
553
601
|
result = subprocess.run(
|
|
554
|
-
commands,
|
|
602
|
+
commands, capture_output=True, check=False, encoding="utf-8"
|
|
555
603
|
)
|
|
556
604
|
return result.returncode
|
|
557
605
|
except CalledProcessError:
|
|
@@ -565,7 +613,7 @@ def cmd_output(commands: list) -> str:
|
|
|
565
613
|
:returns: Get `str` output.
|
|
566
614
|
"""
|
|
567
615
|
result = subprocess.run(
|
|
568
|
-
commands,
|
|
616
|
+
commands, capture_output=True, check=False, encoding="utf-8"
|
|
569
617
|
)
|
|
570
618
|
if result.returncode == 0 and result.stdout is not None:
|
|
571
619
|
return result.stdout
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: commit-check
|
|
3
|
-
Version: 2.18.
|
|
3
|
+
Version: 2.18.3
|
|
4
4
|
Summary: Check commit message formatting, branch naming, commit author, email, and more.
|
|
5
5
|
Author-email: Xianpeng Shen <xianpeng.shen@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -83,12 +83,14 @@ Stop bad commits before they exist. In `.pre-commit-config.yaml`:
|
|
|
83
83
|
```yaml
|
|
84
84
|
repos:
|
|
85
85
|
- repo: https://github.com/commit-check/commit-check
|
|
86
|
-
rev: v2.18.
|
|
86
|
+
rev: v2.18.2
|
|
87
87
|
hooks:
|
|
88
88
|
- id: check-message
|
|
89
89
|
- id: check-branch
|
|
90
90
|
```
|
|
91
91
|
|
|
92
|
+
Add `args: [--fix]` to `check-message` and the hook corrects mechanical slips
|
|
93
|
+
itself — a type's case, a missing colon — instead of rejecting the commit.
|
|
92
94
|
The hooks for the other checks (`check-author-name`, `check-author-email`,
|
|
93
95
|
`check-no-force-push`, `check-tag`, `check-files`) are listed in the
|
|
94
96
|
[pre-commit guide](https://commit-check.com/guides/pre-commit/).
|
|
@@ -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=XTfHh8zD2EbHjuCBBbukQllTJWqO7EoehVc7bADMwQY,9437
|
|
4
|
+
commit_check/ai_signatures_data.py,sha256=33uiiAieL1wB6isgU-dgkz10IrQub7lVCGh8hf7XiN8,11213
|
|
5
|
+
commit_check/api.py,sha256=kzFlSvY7maIdyugFNcQo_2eKyQ-9mca1imFs_F7_LOo,12817
|
|
6
|
+
commit_check/config.py,sha256=Cy7DMWKrWbmVmYER1lUlpvFf9eHn5SfVUVWZ4zgit2w,9820
|
|
7
|
+
commit_check/config_merger.py,sha256=1KYqwNEzJ_fFSPOd45VGGHDNTKIRsShwVSIT3-gGLFw,13128
|
|
8
|
+
commit_check/engine.py,sha256=dQ8I5jh4qP7j6jd4n5HjyOFUOJFyLcmLscQZtwzNkKo,68694
|
|
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=8tVrtcPBMxGhPsuE_sFwOjbmt0NeWU-J83s1ji4IfQY,26925
|
|
12
|
+
commit_check/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
|
+
commit_check/rule_builder.py,sha256=9uoEXyc-PpfxQujv0s444Ow0mliPDvqRZi2YFzacHWI,29570
|
|
14
|
+
commit_check/rules_catalog.py,sha256=JspIhkGXUviLkMG4HhH_IK2x5YTfX3t1p9gI7PbFD7Q,11558
|
|
15
|
+
commit_check/util.py,sha256=XKSiheSa1Ia5lmPotM_Yz8k5T4TGgFLxf_ipXLmXAto,27550
|
|
16
|
+
commit_check-2.18.3.dist-info/licenses/LICENSE,sha256=VAJ9TE1ov8aUKmeoBRYqciMs0CXag1TeDCoLhwbeQmA,1095
|
|
17
|
+
commit_check-2.18.3.dist-info/METADATA,sha256=5tAjqdFPqQNMYQo5SB3mnrQZ7bBvMjXv6T1X2P-Dprc,15234
|
|
18
|
+
commit_check-2.18.3.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
19
|
+
commit_check-2.18.3.dist-info/entry_points.txt,sha256=YMrkFGeub-3-IanV1d9qnopXVaRQM7MA9foXKW9iZ88,86
|
|
20
|
+
commit_check-2.18.3.dist-info/top_level.txt,sha256=Wf46u-ooHBMJNHbhfrBNQw3wC5_m8wt-o_Lfbc4QpRg,13
|
|
21
|
+
commit_check-2.18.3.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
|