zuul/zuul/change_matcher.py
James E. Blair 47591f086d Ignore /COMMIT_MSG in files matchers even more
We have mostly managed to ignore the /COMMIT_MSG in files matchers
(because it is unintuitive that it would be considered), but the recent
change to allow negated regexes in irrelevant-files exposed the fact
that it can still have an effect in that case.  The new feature hasn't
been used yet, so we could silently correct this, but a very similar
construction would be possible with the deprecated style of regex with
a negative lookahead.  Just in case someone wrote that, this change
includes a release note letting them know they can drop the /COMMIT_MSG
from their regex.

Change-Id: Ide04ed01224b5c0c48ab8d3c15ea7aef324cc42d
2024-04-30 16:09:41 -07:00

198 lines
5.3 KiB
Python

# Copyright 2015 Red Hat, Inc.
# Copyright 2023 Acme Gating, LLC
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
This module defines classes used in matching changes based on job
configuration.
"""
from zuul.lib.re2util import ZuulRegex
COMMIT_MSG = '/COMMIT_MSG'
class AbstractChangeMatcher(object):
"""An abstract class that matches change attributes against regular
expressions
:arg ZuulRegex regex: A Zuul regular expression object to match
"""
def __init__(self, regex):
self._regex = regex.pattern
self.regex = regex
def matches(self, change):
"""Return a boolean indication of whether change matches
implementation-specific criteria.
"""
raise NotImplementedError()
def copy(self):
return self.__class__(self.regex)
def __deepcopy__(self, memo):
return self.copy()
def __eq__(self, other):
return (self.__class__ == other.__class__ and
self.regex == other.regex)
def __ne__(self, other):
return not self.__eq__(other)
def __str__(self):
return '{%s:%s}' % (self.__class__.__name__, self._regex)
def __repr__(self):
return '<%s %s>' % (self.__class__.__name__, self._regex)
class ProjectMatcher(AbstractChangeMatcher):
def matches(self, change):
return self.regex.match(str(change.project))
class BranchMatcher(AbstractChangeMatcher):
exactmatch = False
def matches(self, change):
if hasattr(change, 'branch'):
# an implied branch matcher must do a fullmatch to work correctly
if self.exactmatch:
if self._regex == change.branch:
return True
else:
if self.regex.match(change.branch):
return True
return False
if self.regex.match(change.ref):
return True
if hasattr(change, 'containing_branches'):
for branch in change.containing_branches:
if self.exactmatch:
if self._regex == branch:
return True
else:
if self.regex.fullmatch(branch):
return True
return False
def serialize(self):
return {
"implied": self.exactmatch,
"regex": self.regex.serialize(),
}
@classmethod
def deserialize(cls, data):
regex = ZuulRegex.deserialize(data['regex'])
o = cls(regex)
return o
class ImpliedBranchMatcher(BranchMatcher):
exactmatch = True
class FileMatcher(AbstractChangeMatcher):
pass
class AbstractMatcherCollection(AbstractChangeMatcher):
def __init__(self, matchers):
self.matchers = matchers
def __eq__(self, other):
return str(self) == str(other)
def __str__(self):
return '{%s:%s}' % (self.__class__.__name__,
','.join([str(x) for x in self.matchers]))
def __repr__(self):
return '<%s>' % self.__class__.__name__
def copy(self):
return self.__class__(self.matchers[:])
class AbstractMatchFiles(AbstractMatcherCollection):
@property
def regexes(self):
for matcher in self.matchers:
yield matcher.regex
class MatchAllFiles(AbstractMatchFiles):
def matches(self, change):
# NOTE(yoctozepto): make irrelevant files matcher match when
# there are no files to check - return False (NB: reversed)
if not hasattr(change, 'files'):
return False
files = [c for c in change.files if c != COMMIT_MSG]
if not files:
return False
for file_ in files:
matched_file = False
for regex in self.regexes:
if regex.match(file_):
matched_file = True
break
if not matched_file:
return False
return True
class MatchAnyFiles(AbstractMatchFiles):
def matches(self, change):
# NOTE(yoctozepto): make files matcher match when
# there are no files to check - return True
if not hasattr(change, 'files'):
return True
files = [c for c in change.files if c != COMMIT_MSG]
if not files:
return True
for file_ in files:
for regex in self.regexes:
if regex.match(file_):
return True
return False
class MatchAll(AbstractMatcherCollection):
def matches(self, change):
for matcher in self.matchers:
if not matcher.matches(change):
return False
return True
class MatchAny(AbstractMatcherCollection):
def matches(self, change):
for matcher in self.matchers:
if matcher.matches(change):
return True
return False