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

1"""Rules that validate the entire header line (``type(scope): subject``).""" 

2 

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 

11 

12 

13class HeaderMaxLengthRule(BaseRule): 

14 """Enforce a maximum header length. Rule name: ``header-max-length``.""" 

15 

16 @property 

17 def name(self) -> str: 

18 return "header-max-length" 

19 

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 

26 

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 

32 

33 

34class HeaderMinLengthRule(BaseRule): 

35 """Enforce a minimum header length. Rule name: ``header-min-length``.""" 

36 

37 @property 

38 def name(self) -> str: 

39 return "header-min-length" 

40 

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 

47 

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 

53 

54 

55class HeaderTrimRule(BaseRule): 

56 """Forbid leading or trailing whitespace on the header. Rule name: ``header-trim``.""" 

57 

58 @property 

59 def name(self) -> str: 

60 return "header-trim" 

61 

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 

67 

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 

73 

74 

75class HeaderFullStopRule(BaseRule): 

76 """Require or forbid a trailing punctuation character. Rule name: ``header-full-stop``.""" 

77 

78 @property 

79 def name(self) -> str: 

80 return "header-full-stop" 

81 

82 def validate( 

83 self, commit: CommitMessage, config: RuleConfig 

84 ) -> ValidationError | None: 

85 if not commit.header: 

86 return None 

87 

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 

91 

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 

100 

101 

102class HeaderCaseRule(BaseRule): 

103 """Enforce a case style on the full header. Rule name: ``header-case``.""" 

104 

105 @property 

106 def name(self) -> str: 

107 return "header-case" 

108 

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 

114 

115 case_types = ( 

116 config.value if isinstance(config.value, list) else [config.value] 

117 ) 

118 

119 matches_any = any( 

120 CaseValidator.validate(commit.header, CaseType(case_type)) 

121 for case_type in case_types 

122 ) 

123 

124 should_match = config.condition == RuleCondition.ALWAYS 

125 

126 if matches_any != should_match: 

127 return self._create_error( 

128 config, f"header must match case {config.value}" 

129 ) 

130 return None