1# This file is part of Buildbot. Buildbot is free software: you can 2# redistribute it and/or modify it under the terms of the GNU General Public 3# License as published by the Free Software Foundation, version 2. 4# 5# This program is distributed in the hope that it will be useful, but WITHOUT 6# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 7# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 8# details. 9# 10# You should have received a copy of the GNU General Public License along with 11# this program; if not, write to the Free Software Foundation, Inc., 51 12# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 13# 14# Copyright Buildbot Team Members 15 16import re 17 18from buildbot.util import ComparableMixin 19from buildbot.util import NotABranch 20 21 22def is_re_pattern(obj): 23 # re.Pattern only exists in Python 3.7 24 return hasattr(obj, 'search') and hasattr(obj, 'match') 25 26 27def extract_filter_values(values, filter_name): 28 if not isinstance(values, (list, str)): 29 raise ValueError("Values of filter {} must be list of strings or a string".format( 30 filter_name)) 31 if isinstance(values, str): 32 values = [values] 33 else: 34 for value in values: 35 if not isinstance(value, str): 36 raise ValueError("Value of filter {} must be string".format(filter_name)) 37 return values 38 39 40def extract_filter_values_branch(values, filter_name): 41 if not isinstance(values, (list, str, type(None))): 42 raise ValueError("Values of filter {} must be list of strings, a string or None".format( 43 filter_name)) 44 if isinstance(values, (str, type(None))): 45 values = [values] 46 else: 47 for value in values: 48 if not isinstance(value, (str, type(None))): 49 raise ValueError("Value of filter {} must be string or None".format(filter_name)) 50 return values 51 52 53def extract_filter_values_regex(values, filter_name): 54 if not isinstance(values, (list, str)) and not is_re_pattern(values): 55 raise ValueError("Values of filter {} must be list of strings, a string or regex".format( 56 filter_name)) 57 if isinstance(values, str) or is_re_pattern(values): 58 values = [values] 59 else: 60 for value in values: 61 if not isinstance(value, str) and not is_re_pattern(value): 62 raise ValueError("Value of filter {} must be string or regex".format(filter_name)) 63 return values 64 65 66class _FilterExactMatch: 67 def __init__(self, values): 68 self.values = values 69 70 def is_matched(self, value): 71 return value in self.values 72 73 def describe(self, prop): 74 return '{} in {}'.format(prop, self.values) 75 76 77class _FilterExactMatchInverse: 78 def __init__(self, values): 79 self.values = values 80 81 def is_matched(self, value): 82 return value not in self.values 83 84 def describe(self, prop): 85 return '{} not in {}'.format(prop, self.values) 86 87 88class _FilterRegex: 89 def __init__(self, regexes): 90 self.regexes = [self._compile(regex) for regex in regexes] 91 92 def _compile(self, regex): 93 if is_re_pattern(regex): 94 return regex 95 return re.compile(regex) 96 97 def is_matched(self, value): 98 if value is None: 99 return False 100 for regex in self.regexes: 101 if regex.match(value) is not None: 102 return True 103 return False 104 105 def describe(self, prop): 106 return '{} matches {}'.format(prop, self.regexes) 107 108 109class _FilterRegexInverse: 110 def __init__(self, regexes): 111 self.regexes = [self._compile(regex) for regex in regexes] 112 113 def _compile(self, regex): 114 if is_re_pattern(regex): 115 return regex 116 return re.compile(regex) 117 118 def is_matched(self, value): 119 if value is None: 120 return True 121 for regex in self.regexes: 122 if regex.match(value) is not None: 123 return False 124 return True 125 126 def describe(self, prop): 127 return '{} does not match {}'.format(prop, self.regexes) 128 129 130class SourceStampFilter(ComparableMixin): 131 132 compare_attrs = ( 133 'filter_fn', 134 'project_filters', 135 'codebase_filters', 136 'repository_filters', 137 'branch_filters' 138 ) 139 140 def __init__(self, 141 # gets a SourceStamp dictionary, returns boolean 142 filter_fn=None, 143 144 project_eq=None, project_not_eq=None, project_re=None, project_not_re=None, 145 repository_eq=None, repository_not_eq=None, 146 repository_re=None, repository_not_re=None, 147 branch_eq=NotABranch, branch_not_eq=NotABranch, branch_re=None, branch_not_re=None, 148 codebase_eq=None, codebase_not_eq=None, codebase_re=None, codebase_not_re=None): 149 150 self.filter_fn = filter_fn 151 self.project_filters = self.create_filters(project_eq, project_not_eq, 152 project_re, project_not_re, 'project') 153 self.codebase_filters = self.create_filters(codebase_eq, codebase_not_eq, 154 codebase_re, codebase_not_re, 'codebase') 155 self.repository_filters = self.create_filters(repository_eq, repository_not_eq, 156 repository_re, repository_not_re, 157 'repository') 158 self.branch_filters = self.create_branch_filters(branch_eq, branch_not_eq, 159 branch_re, branch_not_re, 'branch') 160 161 def create_branch_filters(self, eq, not_eq, regex, not_regex, filter_name): 162 filters = [] 163 if eq is not NotABranch: 164 values = extract_filter_values_branch(eq, filter_name + '_eq') 165 filters.append(_FilterExactMatch(values)) 166 167 if not_eq is not NotABranch: 168 values = extract_filter_values_branch(not_eq, filter_name + '_not_eq') 169 filters.append(_FilterExactMatchInverse(values)) 170 171 if regex is not None: 172 values = extract_filter_values_regex(regex, filter_name + '_re') 173 filters.append(_FilterRegex(values)) 174 175 if not_regex is not None: 176 values = extract_filter_values_regex(not_regex, filter_name + '_re') 177 filters.append(_FilterRegexInverse(values)) 178 179 return filters 180 181 def create_filters(self, eq, not_eq, regex, not_regex, filter_name): 182 filters = [] 183 if eq is not None: 184 values = extract_filter_values(eq, filter_name + '_eq') 185 filters.append(_FilterExactMatch(values)) 186 187 if not_eq is not None: 188 values = extract_filter_values(not_eq, filter_name + '_not_eq') 189 filters.append(_FilterExactMatchInverse(values)) 190 191 if regex is not None: 192 values = extract_filter_values_regex(regex, filter_name + '_re') 193 filters.append(_FilterRegex(values)) 194 195 if not_regex is not None: 196 values = extract_filter_values_regex(not_regex, filter_name + '_re') 197 filters.append(_FilterRegexInverse(values)) 198 199 return filters 200 201 def do_prop_match(self, ss, prop, filters): 202 value = ss.get(prop, '') 203 for filter in filters: 204 if not filter.is_matched(value): 205 return False 206 return True 207 208 def is_matched(self, ss): 209 if self.filter_fn is not None and not self.filter_fn(ss): 210 return False 211 if self.project_filters and not self.do_prop_match(ss, 'project', self.project_filters): 212 return False 213 if self.codebase_filters and not self.do_prop_match(ss, 'codebase', self.codebase_filters): 214 return False 215 if self.repository_filters and \ 216 not self.do_prop_match(ss, 'repository', self.repository_filters): 217 return False 218 if self.branch_filters and not self.do_prop_match(ss, 'branch', self.branch_filters): 219 return False 220 return True 221 222 def is_matched_codebase(self, codebase): 223 for filter in self.codebase_filters: 224 if not filter.is_matched(codebase): 225 return False 226 return True 227 228 def _repr_filters(self, filters, prop): 229 return [filter.describe(prop) for filter in filters] 230 231 def __repr__(self): 232 filters = [] 233 if self.filter_fn is not None: 234 filters.append('{}()'.format(self.filter_fn.__name__)) 235 filters += self._repr_filters(self.project_filters, 'project') 236 filters += self._repr_filters(self.codebase_filters, 'codebase') 237 filters += self._repr_filters(self.repository_filters, 'repository') 238 filters += self._repr_filters(self.branch_filters, 'branch') 239 240 return "<{} on {}>".format(self.__class__.__name__, ' and '.join(filters)) 241