11"""Methods for generating a release hint."""
22
3+ import copy
34import fnmatch
45import re
5- from typing import List , Optional , Sequence , Union
6+ from dataclasses import dataclass
7+ from typing import List , Optional , Sequence , Set , Union
8+
9+ from rich .table import Table
10+ from rich .text import Text
611
712from generate_changelog .configuration import RELEASE_TYPE_ORDER , Configuration
813from generate_changelog .context import CommitContext , VersionContext
14+ from generate_changelog .indented_logger import get_indented_logger
15+
16+ logger = get_indented_logger (__name__ )
917
1018
1119class InvalidRuleError (Exception ):
@@ -14,6 +22,24 @@ class InvalidRuleError(Exception):
1422 pass
1523
1624
25+ @dataclass
26+ class ReleaseRuleResult :
27+ """The result of evaluating a release rule."""
28+
29+ matches_grouping : bool
30+ matches_path : bool
31+ matches_branch : bool
32+ result : str
33+
34+ @property
35+ def matches_all (self ) -> bool :
36+ """All the commit criteria were met."""
37+ return all ([self .matches_grouping , self .matches_path , self .matches_branch ])
38+
39+ def __str__ (self ) -> str :
40+ return self .result or ""
41+
42+
1743class ReleaseRule :
1844 """
1945 A commit evaluation rule for hinting at the level of change.
@@ -35,7 +61,7 @@ def __init__(
3561 branch : Optional [str ] = None ,
3662 ):
3763 self .match_result = match_result
38- self .no_match_result = no_match_result
64+ self .no_match_result = no_match_result or "no-release"
3965 self .grouping = grouping if grouping != "*" else None
4066 normalized_path = path if path != "*" else None
4167 if isinstance (normalized_path , str ):
@@ -109,15 +135,22 @@ def matches_branch(self, current_branch: str) -> bool:
109135 """
110136 return bool (re .match (self .branch , current_branch )) if self .branch else True
111137
112- def __call__ (self , commit : CommitContext , current_branch : str ) -> Optional [ str ] :
138+ def __call__ (self , commit : CommitContext , current_branch : str ) -> ReleaseRuleResult :
113139 """Evaluate the commit using this rule."""
114140 if not self .is_valid :
115141 raise InvalidRuleError ()
116142
117143 matches_grouping = self .matches_grouping (commit )
118144 matches_path = self .matches_path (commit )
119145 matches_branch = self .matches_branch (current_branch )
120- return self .match_result if all ([matches_grouping , matches_path , matches_branch ]) else self .no_match_result
146+ matches_all = all ([matches_grouping , matches_path , matches_branch ])
147+ result = self .match_result if matches_all else self .no_match_result
148+ return ReleaseRuleResult (
149+ matches_grouping = matches_grouping ,
150+ matches_path = matches_path ,
151+ matches_branch = matches_branch ,
152+ result = result ,
153+ )
121154
122155
123156class RuleProcessor :
@@ -130,6 +163,7 @@ class RuleProcessor:
130163
131164 def __init__ (self , rule_list : List [dict ]):
132165 self .rules = [ReleaseRule (** kwargs ) for kwargs in rule_list ]
166+ self .results : List [ReleaseRuleResult ] = []
133167
134168 def __call__ (self , commit : CommitContext , current_branch : str ) -> Optional [str ]:
135169 """
@@ -142,13 +176,28 @@ def __call__(self, commit: CommitContext, current_branch: str) -> Optional[str]:
142176 Returns:
143177 The release hint
144178 """
145- suggestions = {rule (commit , current_branch ) for rule in self .rules }
179+ self .results = [rule (commit , current_branch ) for rule in self .rules ]
180+ suggestions : Set [str ] = {str (result .result ) for result in self .results }
146181 if unknown_suggestions := suggestions - set (RELEASE_TYPE_ORDER ):
147182 return unknown_suggestions .pop () # Return a random value from the unknowns
148183
149184 sorted_suggestions = sorted (suggestions , key = lambda s : RELEASE_TYPE_ORDER .index (s ))
150185 return sorted_suggestions [- 1 ]
151186
187+ def rule_string (self ) -> str :
188+ """Return a string representation of the rules."""
189+ output : List [str ] = []
190+ for i , rule in enumerate (self .rules ):
191+ output .extend (
192+ (
193+ f"Rule: { i } " ,
194+ f" Grouping: { rule .grouping } " ,
195+ f" Path: { rule .path } " ,
196+ f" Branch: { rule .branch } " ,
197+ )
198+ )
199+ return "\n " .join (output )
200+
152201
153202def suggest_release_type (current_branch : str , version_contexts : List [VersionContext ], config : Configuration ) -> str :
154203 """
@@ -160,20 +209,60 @@ def suggest_release_type(current_branch: str, version_contexts: List[VersionCont
160209 config: The current configuration
161210
162211 Returns:
163- The type of release based on the rules, or `` no-release` `
212+ The type of release based on the rules, or `no-release`
164213 """
214+ logger .info ("Processing commits to suggest release type..." )
215+ logger .indent ()
165216 rule_processor = RuleProcessor (rule_list = config .release_hint_rules )
217+ logger .debug (rule_processor .rule_string ())
166218
167219 # If the latest release is not "unreleased", there is no need for a release
168220 if version_contexts [0 ].label != config .unreleased_label :
221+ logger .info (f"The latest release is { version_contexts [0 ].label } . No release is suggested." )
222+ logger .dedent ()
169223 return "no-release"
170224
171225 suggestions = set ()
226+ results = {}
172227 for commit_group in version_contexts [0 ].grouped_commits :
173228 suggestions |= {rule_processor (commit , current_branch ) for commit in commit_group .commits }
229+ results ["Grouping: " + " " .join (commit_group .grouping )] = copy .deepcopy (rule_processor .results )
230+
231+ print_table (results )
174232
175233 if not suggestions :
234+ logger .info ("No suggestions found. No release is suggested." )
235+ logger .dedent ()
176236 return "no-release"
177237
178238 sorted_suggestions = sorted (suggestions , key = lambda s : RELEASE_TYPE_ORDER .index (s ))
179- return sorted_suggestions [- 1 ] or "no-release"
239+ result = sorted_suggestions [- 1 ] or "no-release"
240+ logger .info ("Suggested release type: %s" , result )
241+ logger .dedent ()
242+ return result
243+
244+
245+ def format_bool (bool_var : bool ) -> Text :
246+ """Format a boolean value as a string."""
247+ return Text ("X" , style = "bold green" ) if bool_var else Text ("-" , style = "bold red" )
248+
249+
250+ def print_table (results : dict ) -> None :
251+ """Print the test results as a table."""
252+ for group_name , result_list in results .items ():
253+ table = Table (title = group_name , title_justify = "left" )
254+ table .add_column ("Rule" , justify = "center" )
255+ table .add_column ("Group" , justify = "center" )
256+ table .add_column ("Path" , justify = "center" )
257+ table .add_column ("Branch" , justify = "center" )
258+ table .add_column ("Result" )
259+
260+ for i , result in enumerate (result_list ):
261+ table .add_row (
262+ str (i ),
263+ format_bool (result .matches_grouping ),
264+ format_bool (result .matches_path ),
265+ format_bool (result .matches_branch ),
266+ result .result ,
267+ )
268+ logger .debug (table )
0 commit comments