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 CHANGED
@@ -8,7 +8,7 @@ Exports:
8
8
 
9
9
  import os
10
10
  import sys
11
- from importlib.metadata import version, PackageNotFoundError
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
- if os.environ.get("TERM") in ("", "dumb"):
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
- from typing import Any
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(getattr(self._get(), "handlers"))
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: # noqa: S310
155
+ with _opener.open(url, timeout=10) as response:
155
156
  data = response.read()
156
157
  return toml_load(io.BytesIO(data))
157
158
 
@@ -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 (OSError, IOError):
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
- if len(lines) > 1:
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
- upper_msg.startswith("WIP:") # wip: / WIP:
1328
- or upper_msg.startswith("[WIP]") # [wip] / [WIP]
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, parse_list, parse_int
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
- from commit_check.engine import (
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 (OSError, IOError):
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
- "--branch requested but no branch rules are configured "
604
- "(conventional_branch = false and no require_rebase_target)",
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
 
@@ -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 commit_check.config import ConfigError
9
- from commit_check.util import format_size, parse_size
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 = "|".join(["master", "main", "HEAD", r"PR-.+"])
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})$"
@@ -19,6 +19,7 @@ Internal bookkeeping entries that never produce a diagnostic (such as
19
19
  """
20
20
 
21
21
  from __future__ import annotations
22
+
22
23
  from dataclasses import dataclass
23
24
 
24
25
  #: Base URL of the rules reference documentation.
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
- # Fallback to environment variables (GitHub Actions)
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
- stdout=subprocess.PIPE,
106
- stderr=subprocess.PIPE,
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
- stdout=subprocess.PIPE,
254
- stderr=subprocess.PIPE,
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
- stdout=subprocess.PIPE,
276
- stderr=subprocess.PIPE,
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
- stdout=subprocess.PIPE,
291
- stderr=subprocess.PIPE,
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
- stdout=subprocess.PIPE,
328
- stderr=subprocess.PIPE,
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
- stdout=subprocess.PIPE,
347
- stderr=subprocess.PIPE,
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
- stdout=subprocess.PIPE,
382
- stderr=subprocess.PIPE,
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
- stdout=subprocess.PIPE,
405
- stderr=subprocess.PIPE,
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
- stdout=subprocess.PIPE,
416
- stderr=subprocess.PIPE,
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
- stdout=subprocess.PIPE,
437
- stderr=subprocess.PIPE,
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, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding="utf-8"
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, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding="utf-8"
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.2
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.1
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,,