Coverage for src / python_commitlint / rules / header_rules.py: 100%
63 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-04-28 02:54 +0000
« prev ^ index » next coverage.py v7.13.4, created at 2026-04-28 02:54 +0000
1"""Rules that validate the entire header line (``type(scope): subject``)."""
3from python_commitlint.core.enums import CaseType, RuleCondition
4from python_commitlint.core.models import (
5 CommitMessage,
6 RuleConfig,
7 ValidationError,
8)
9from python_commitlint.rules.base import BaseRule, config_value_or
10from python_commitlint.rules.case_validators import CaseValidator
13class HeaderMaxLengthRule(BaseRule):
14 """Enforce a maximum header length. Rule name: ``header-max-length``."""
16 @property
17 def name(self) -> str:
18 return "header-max-length"
20 def validate(
21 self, commit: CommitMessage, config: RuleConfig
22 ) -> ValidationError | None:
23 max_length = config_value_or(config, float("inf"))
24 is_valid = len(commit.header) <= max_length
25 should_be_valid = config.condition == RuleCondition.ALWAYS
27 if is_valid != should_be_valid:
28 return self._create_error(
29 config, f"header must be at most {max_length} characters"
30 )
31 return None
34class HeaderMinLengthRule(BaseRule):
35 """Enforce a minimum header length. Rule name: ``header-min-length``."""
37 @property
38 def name(self) -> str:
39 return "header-min-length"
41 def validate(
42 self, commit: CommitMessage, config: RuleConfig
43 ) -> ValidationError | None:
44 min_length = config_value_or(config, 0)
45 is_valid = len(commit.header) >= min_length
46 should_be_valid = config.condition == RuleCondition.ALWAYS
48 if is_valid != should_be_valid:
49 return self._create_error(
50 config, f"header must be at least {min_length} characters"
51 )
52 return None
55class HeaderTrimRule(BaseRule):
56 """Forbid leading or trailing whitespace on the header. Rule name: ``header-trim``."""
58 @property
59 def name(self) -> str:
60 return "header-trim"
62 def validate(
63 self, commit: CommitMessage, config: RuleConfig
64 ) -> ValidationError | None:
65 is_trimmed = commit.header == commit.header.strip()
66 should_be_trimmed = config.condition == RuleCondition.ALWAYS
68 if is_trimmed != should_be_trimmed:
69 return self._create_error(
70 config, "header must not have leading or trailing whitespace"
71 )
72 return None
75class HeaderFullStopRule(BaseRule):
76 """Require or forbid a trailing punctuation character. Rule name: ``header-full-stop``."""
78 @property
79 def name(self) -> str:
80 return "header-full-stop"
82 def validate(
83 self, commit: CommitMessage, config: RuleConfig
84 ) -> ValidationError | None:
85 if not commit.header:
86 return None
88 stop_char = config_value_or(config, ".")
89 ends_with_stop = commit.header.endswith(stop_char)
90 should_end_with_stop = config.condition == RuleCondition.ALWAYS
92 if ends_with_stop != should_end_with_stop:
93 msg = (
94 f"header must end with '{stop_char}'"
95 if should_end_with_stop
96 else f"header may not end with '{stop_char}'"
97 )
98 return self._create_error(config, msg)
99 return None
102class HeaderCaseRule(BaseRule):
103 """Enforce a case style on the full header. Rule name: ``header-case``."""
105 @property
106 def name(self) -> str:
107 return "header-case"
109 def validate(
110 self, commit: CommitMessage, config: RuleConfig
111 ) -> ValidationError | None:
112 if not commit.header or config.value is None:
113 return None
115 case_types = (
116 config.value if isinstance(config.value, list) else [config.value]
117 )
119 matches_any = any(
120 CaseValidator.validate(commit.header, CaseType(case_type))
121 for case_type in case_types
122 )
124 should_match = config.condition == RuleCondition.ALWAYS
126 if matches_any != should_match:
127 return self._create_error(
128 config, f"header must match case {config.value}"
129 )
130 return None