1111
1212import argparse
1313import copy
14+ import inspect
1415import itertools
1516import json
1617import logging
@@ -319,6 +320,15 @@ class RedmineUpkeep:
319320 GITHUB_RATE_LIMITED = False
320321 MAX_UPKEEP_FAILURES = 5
321322
323+ class Filter :
324+ @staticmethod
325+ def get_filters ():
326+ raise NotImplementedError ("NI" )
327+
328+ @staticmethod
329+ def requires_github_api ():
330+ raise NotImplementedError ("NI" )
331+
322332 def __init__ (self , args ):
323333 self .G = git .Repo (args .git )
324334 self .R = self ._redmine_connect ()
@@ -355,15 +365,15 @@ def __init__(self, args):
355365 self .transform_methods .sort (key = lambda x : x .__name__ )
356366 log .debug (f"Sorted transformation methods: { [m .__name__ for m in self .transform_methods ]} " )
357367
358- # Discover filter methods based on prefix
359- self .filter_methods = []
360- for name in dir ( self ):
361- if name . startswith ( '_filter_' ) and callable ( getattr ( self , name )) :
362- self . filter_methods . append ( getattr ( self , name ) )
363- log . debug ( f"Discovered filter methods: { [ f . __name__ for f in self .filter_methods ] } " )
364-
365- random . shuffle ( self . filter_methods )
366- log .debug (f"Shuffled filter methods for processing order : { [f .__name__ for f in self .filter_methods ]} " )
368+ # Discover filters based on prefix
369+ self .filters = []
370+ for name , v in RedmineUpkeep . __dict__ . items ( ):
371+ if inspect . isclass ( v ) and issubclass ( v , self . Filter ) and v != self . Filter :
372+ log . debug ( "discovered %s" , v . NAME )
373+ self .filters . append ( v )
374+ random . shuffle ( self . filters ) # to shuffle equivalent PRIORITY
375+ self . filters . sort ( key = lambda filter : filter . PRIORITY , reverse = True )
376+ log .debug (f"Discovered filters : { [f .__name__ for f in self .filters ]} " )
367377
368378 def _redmine_connect (self ):
369379 log .info ("Connecting to %s" , REDMINE_ENDPOINT )
@@ -373,15 +383,28 @@ def _redmine_connect(self):
373383
374384 # Transformations:
375385
376- def _filter_merged (self , filters ):
377- log .debug ("Applying _filter_merged criteria." )
378- filters [f"cf_{ REDMINE_CUSTOM_FIELD_ID_PULL_REQUEST_ID } " ] = '>=0'
379- filters [f"cf_{ REDMINE_CUSTOM_FIELD_ID_MERGE_COMMIT } " ] = '!*'
380- filters ["status_id" ] = [
381- REDMINE_STATUS_ID_PENDING_BACKPORT ,
382- REDMINE_STATUS_ID_RESOLVED
383- ]
384- return True # needs github API
386+ class FilterMerged (Filter ):
387+ """
388+ Filter issues that are closed but no merge commit is set.
389+ """
390+
391+ PRIORITY = 1000
392+ NAME = "Merged"
393+
394+ @staticmethod
395+ def get_filters ():
396+ return {
397+ f"cf_{ REDMINE_CUSTOM_FIELD_ID_PULL_REQUEST_ID } " : '>=0' ,
398+ f"cf_{ REDMINE_CUSTOM_FIELD_ID_MERGE_COMMIT } " : '!*' ,
399+ "status_id" : [
400+ REDMINE_STATUS_ID_PENDING_BACKPORT ,
401+ REDMINE_STATUS_ID_RESOLVED ,
402+ ],
403+ }
404+
405+ @staticmethod
406+ def requires_github_api ():
407+ return True
385408
386409 def _transform_merged (self , issue_update ):
387410 """
@@ -464,11 +487,23 @@ def _transform_backport_resolved(self, issue_update):
464487 issue_update .logger .info ("Issue is already in 'Resolved' status. No change needed." )
465488 return False
466489
467- def _filter_released (self , filters ):
468- log .debug ("Applying _filter_released criteria." )
469- filters [f"cf_{ REDMINE_CUSTOM_FIELD_ID_MERGE_COMMIT } " ] = '*'
470- filters [f"cf_{ REDMINE_CUSTOM_FIELD_ID_RELEASED_IN } " ] = '!*'
471- return False
490+ class FilterReleased (Filter ):
491+ """
492+ Filter for issues that are merged but not yet released.
493+ """
494+
495+ PRIORITY = 10
496+ NAME = "Released"
497+
498+ @staticmethod
499+ def get_filters ():
500+ return {
501+ "status_id" : REDMINE_STATUS_ID_PENDING_BACKPORT ,
502+ }
503+
504+ @staticmethod
505+ def requires_github_api ():
506+ return False
472507
473508 def _transform_released (self , issue_update ):
474509 """
@@ -497,15 +532,27 @@ def _transform_released(self, issue_update):
497532 issue_update .logger .info (f"Commit { commit } not yet in a release. 'Released In' field will not be updated." )
498533 return False
499534
500- def _filter_issues_pending_backport (self , filters ):
535+
536+ class FilterPendingBackport (Filter ):
501537 """
502538 Filter for issues that are in 'Pending Backport' status. The
503539 transformation will then check if they are non-backport trackers and if
504540 all their 'Copied to' backports are resolved.
505541 """
506- log .debug ("Applying _filter_issues_pending_backport criteria." )
507- filters ["status_id" ] = REDMINE_STATUS_ID_PENDING_BACKPORT
508- return False
542+
543+ PRIORITY = 10
544+ NAME = "Pending Backport"
545+
546+ @staticmethod
547+ def get_filters ():
548+ return {
549+ "status_id" : REDMINE_STATUS_ID_PENDING_BACKPORT ,
550+ }
551+
552+ @staticmethod
553+ def requires_github_api ():
554+ return False
555+
509556
510557 def _transform_resolve_main_issue_from_backports (self , issue_update ):
511558 """
@@ -914,35 +961,35 @@ def _execute_filters(self):
914961 # This reduces Redmine API calls for filtering
915962 common_filters = {
916963 "project_id" : self .project_id ,
917- "limit" : limit ,
918964 "sort" : f'cf_{ REDMINE_CUSTOM_FIELD_ID_UPKEEP_TIMESTAMP } ' ,
919965 "status_id" : "*" ,
920966 f"cf_{ REDMINE_CUSTOM_FIELD_ID_TAGS } " : "!upkeep-failed" ,
921967 }
922968 #f"cf_{REDMINE_CUSTOM_FIELD_ID_UPKEEP_TIMESTAMP}": f"<={cutoff_date}", # Not updated recently
923- log .info ("Beginning to loop through shuffled filters." )
924- for filter_method in self .filter_methods :
969+
970+ log .info ("Beginning to loop through filters." )
971+ for f in self .filters :
925972 if limit <= 0 :
926973 log .info ("Issue processing limit reached. Stopping filter execution." )
927974 break
928- common_filters [ 'limit' ] = limit
929- filters = copy . deepcopy ( common_filters )
930- needs_github_api = filter_method ( filters )
975+ issue_filter = { ** common_filters , ** f . get_filters ()}
976+ issue_filter [ 'limit' ] = limit
977+ needs_github_api = f . requires_github_api ( )
931978 try :
932- log .info (f"Running filter { filter_method . __name__ } with criteria: { filters } " )
933- issues = self .R .issue .filter (** filters )
979+ log .info (f"Running filter { f . NAME } with criteria: { issue_filter } " )
980+ issues = self .R .issue .filter (** issue_filter )
934981 issue_count = len (issues )
935- log .info (f"Filter { filter_method . __name__ } returned { issue_count } issue(s)." )
982+ log .info (f"Filter { f . NAME } returned { issue_count } issue(s)." )
936983 for issue in issues :
937984 if needs_github_api and self .GITHUB_RATE_LIMITED :
938- log .warning (f"Stopping filter { filter_method . __name__ } due to Github rate limits." )
985+ log .warning (f"Stopping filter { f . NAME } due to Github rate limits." )
939986 break
940987 limit = limit - 1
941988 self ._process_issue_transformations (issue )
942989 if limit <= 0 :
943990 break
944991 except redminelib .exceptions .ResourceAttrError as e :
945- log .warning (f"Redmine API error with filter { filters } : { e } " )
992+ log .warning (f"Redmine API error with filter { issue_filter } : { e } " )
946993
947994def main ():
948995 parser = argparse .ArgumentParser (description = "Ceph redmine upkeep tool" )
0 commit comments