Merge "Add driver-specific pipeline requirements" into feature/zuulv3
diff --git a/doc/source/developer/datamodel.rst b/doc/source/developer/datamodel.rst
index 2996ff4..acb8612 100644
--- a/doc/source/developer/datamodel.rst
+++ b/doc/source/developer/datamodel.rst
@@ -54,7 +54,7 @@
 Filters
 ~~~~~~~
 
-.. autoclass:: zuul.model.ChangeishFilter
+.. autoclass:: zuul.model.RefFilter
 .. autoclass:: zuul.model.EventFilter
 
 
diff --git a/tests/fixtures/config/requirements/email/git/common-config/zuul.yaml b/tests/fixtures/config/requirements/email/git/common-config/zuul.yaml
index 1a5baed..efc3b32 100644
--- a/tests/fixtures/config/requirements/email/git/common-config/zuul.yaml
+++ b/tests/fixtures/config/requirements/email/git/common-config/zuul.yaml
@@ -11,8 +11,9 @@
       gerrit:
         verified: -1
     require:
-      approval:
-        - email: jenkins@example.com
+      gerrit:
+        approval:
+          - email: jenkins@example.com
 
 - pipeline:
     name: trigger
diff --git a/tests/fixtures/config/requirements/newer-than/git/common-config/zuul.yaml b/tests/fixtures/config/requirements/newer-than/git/common-config/zuul.yaml
index fa230de..6f0601d 100644
--- a/tests/fixtures/config/requirements/newer-than/git/common-config/zuul.yaml
+++ b/tests/fixtures/config/requirements/newer-than/git/common-config/zuul.yaml
@@ -11,9 +11,10 @@
       gerrit:
         verified: -1
     require:
-      approval:
-        - username: jenkins
-          newer-than: 48h
+      gerrit:
+        approval:
+          - username: jenkins
+            newer-than: 48h
 
 - pipeline:
     name: trigger
diff --git a/tests/fixtures/config/requirements/older-than/git/common-config/zuul.yaml b/tests/fixtures/config/requirements/older-than/git/common-config/zuul.yaml
index 14541b6..77ee388 100644
--- a/tests/fixtures/config/requirements/older-than/git/common-config/zuul.yaml
+++ b/tests/fixtures/config/requirements/older-than/git/common-config/zuul.yaml
@@ -11,9 +11,10 @@
       gerrit:
         verified: -1
     require:
-      approval:
-        - username: jenkins
-          older-than: 48h
+      gerrit:
+        approval:
+          - username: jenkins
+            older-than: 48h
 
 - pipeline:
     name: trigger
diff --git a/tests/fixtures/config/requirements/reject-username/git/common-config/zuul.yaml b/tests/fixtures/config/requirements/reject-username/git/common-config/zuul.yaml
index 61f3819..9e9d000 100644
--- a/tests/fixtures/config/requirements/reject-username/git/common-config/zuul.yaml
+++ b/tests/fixtures/config/requirements/reject-username/git/common-config/zuul.yaml
@@ -2,8 +2,9 @@
     name: pipeline
     manager: independent
     reject:
-      approval:
-        - username: jenkins
+      gerrit:
+        approval:
+          - username: jenkins
     trigger:
       gerrit:
         - event: comment-added
diff --git a/tests/fixtures/config/requirements/reject/git/common-config/zuul.yaml b/tests/fixtures/config/requirements/reject/git/common-config/zuul.yaml
index 32a7582..b08a105 100644
--- a/tests/fixtures/config/requirements/reject/git/common-config/zuul.yaml
+++ b/tests/fixtures/config/requirements/reject/git/common-config/zuul.yaml
@@ -2,16 +2,18 @@
     name: pipeline
     manager: independent
     require:
-      approval:
-        - username: jenkins
-          verified:
-            - 1
-            - 2
+      gerrit:
+        approval:
+          - username: jenkins
+            verified:
+              - 1
+              - 2
     reject:
-      approval:
-        - verified:
-            - -1
-            - -2
+      gerrit:
+        approval:
+          - verified:
+              - -1
+              - -2
     trigger:
       gerrit:
         - event: comment-added
diff --git a/tests/fixtures/config/requirements/state/git/common-config/zuul.yaml b/tests/fixtures/config/requirements/state/git/common-config/zuul.yaml
index ffc3453..bd9dc8f 100644
--- a/tests/fixtures/config/requirements/state/git/common-config/zuul.yaml
+++ b/tests/fixtures/config/requirements/state/git/common-config/zuul.yaml
@@ -2,7 +2,8 @@
     name: current-check
     manager: independent
     require:
-      current-patchset: true
+      gerrit:
+        current-patchset: true
     trigger:
       gerrit:
         - event: patchset-created
@@ -18,7 +19,8 @@
     name: open-check
     manager: independent
     require:
-      open: true
+      gerrit:
+        open: true
     trigger:
       gerrit:
         - event: patchset-created
@@ -34,7 +36,8 @@
     name: status-check
     manager: independent
     require:
-      status: NEW
+      gerrit:
+        status: NEW
     trigger:
       gerrit:
         - event: patchset-created
diff --git a/tests/fixtures/config/requirements/username/git/common-config/zuul.yaml b/tests/fixtures/config/requirements/username/git/common-config/zuul.yaml
index bc1083a..455d9de 100644
--- a/tests/fixtures/config/requirements/username/git/common-config/zuul.yaml
+++ b/tests/fixtures/config/requirements/username/git/common-config/zuul.yaml
@@ -11,8 +11,9 @@
       gerrit:
         verified: -1
     require:
-      approval:
-        - username: ^(jenkins|zuul)$
+      gerrit:
+        approval:
+          - username: ^(jenkins|zuul)$
 
 - pipeline:
     name: trigger
diff --git a/tests/fixtures/config/requirements/vote1/git/common-config/zuul.yaml b/tests/fixtures/config/requirements/vote1/git/common-config/zuul.yaml
index 7d9164d..799282d 100644
--- a/tests/fixtures/config/requirements/vote1/git/common-config/zuul.yaml
+++ b/tests/fixtures/config/requirements/vote1/git/common-config/zuul.yaml
@@ -2,9 +2,10 @@
     name: pipeline
     manager: independent
     require:
-      approval:
-        - username: jenkins
-          verified: 1
+      gerrit:
+        approval:
+          - username: jenkins
+            verified: 1
     trigger:
       gerrit:
         - event: comment-added
diff --git a/tests/fixtures/config/requirements/vote2/git/common-config/zuul.yaml b/tests/fixtures/config/requirements/vote2/git/common-config/zuul.yaml
index 7308c8a..f337371 100644
--- a/tests/fixtures/config/requirements/vote2/git/common-config/zuul.yaml
+++ b/tests/fixtures/config/requirements/vote2/git/common-config/zuul.yaml
@@ -2,11 +2,12 @@
     name: pipeline
     manager: independent
     require:
-      approval:
-        - username: jenkins
-          verified:
-            - 1
-            - 2
+      gerrit:
+        approval:
+          - username: jenkins
+            verified:
+              - 1
+              - 2
     trigger:
       gerrit:
         - event: comment-added
diff --git a/tests/fixtures/config/zuultrigger/parent-change-enqueued/git/common-config/zuul.yaml b/tests/fixtures/config/zuultrigger/parent-change-enqueued/git/common-config/zuul.yaml
index 2b21c9b..351092c 100644
--- a/tests/fixtures/config/zuultrigger/parent-change-enqueued/git/common-config/zuul.yaml
+++ b/tests/fixtures/config/zuultrigger/parent-change-enqueued/git/common-config/zuul.yaml
@@ -2,8 +2,9 @@
     name: check
     manager: independent
     require:
-      approval:
-        - verified: -1
+      gerrit:
+        approval:
+          - verified: -1
     trigger:
       gerrit:
         - event: patchset-created
@@ -21,8 +22,9 @@
     name: gate
     manager: dependent
     require:
-      approval:
-        - verified: 1
+      gerrit:
+        approval:
+          - verified: 1
     trigger:
       gerrit:
         - event: comment-added
diff --git a/zuul/configloader.py b/zuul/configloader.py
index 90440f7..1374e9b 100644
--- a/zuul/configloader.py
+++ b/zuul/configloader.py
@@ -604,6 +604,8 @@
         methods = {
             'trigger': 'getTriggerSchema',
             'reporter': 'getReporterSchema',
+            'require': 'getRequireSchema',
+            'reject': 'getRejectSchema',
         }
 
         schema = {}
@@ -665,6 +667,10 @@
                     '_source_context': model.SourceContext,
                     '_start_mark': yaml.Mark,
                     }
+        pipeline['require'] = PipelineParser.getDriverSchema('require',
+                                                             connections)
+        pipeline['reject'] = PipelineParser.getDriverSchema('reject',
+                                                            connections)
         pipeline['trigger'] = vs.Required(
             PipelineParser.getDriverSchema('trigger', connections))
         for action in ['start', 'success', 'failure', 'merge-failure',
@@ -741,24 +747,21 @@
         pipeline.setManager(manager)
         layout.pipelines[conf['name']] = pipeline
 
-        if 'require' in conf or 'reject' in conf:
-            require = conf.get('require', {})
-            reject = conf.get('reject', {})
-            f = model.ChangeishFilter(
-                open=require.get('open'),
-                current_patchset=require.get('current-patchset'),
-                statuses=as_list(require.get('status')),
-                required_approvals=as_list(require.get('approval')),
-                reject_approvals=as_list(reject.get('approval'))
-            )
-            manager.changeish_filters.append(f)
+        for source_name, require_config in conf.get('require', {}).items():
+            source = connections.getSource(source_name)
+            manager.changeish_filters.extend(
+                source.getRequireFilters(require_config))
+
+        for source_name, reject_config in conf.get('reject', {}).items():
+            source = connections.getSource(source_name)
+            manager.changeish_filters.extend(
+                source.getRejectFilters(reject_config))
 
         for trigger_name, trigger_config in conf.get('trigger').items():
             trigger = connections.getTrigger(trigger_name, trigger_config)
             pipeline.triggers.append(trigger)
-
-            manager.event_filters += trigger.getEventFilters(
-                conf['trigger'][trigger_name])
+            manager.event_filters.extend(
+                trigger.getEventFilters(conf['trigger'][trigger_name]))
 
         return pipeline
 
diff --git a/zuul/driver/__init__.py b/zuul/driver/__init__.py
index 57b5cf9..671996a 100644
--- a/zuul/driver/__init__.py
+++ b/zuul/driver/__init__.py
@@ -191,6 +191,30 @@
         """
         pass
 
+    @abc.abstractmethod
+    def getRequireSchema(self):
+        """Get the schema for this driver's pipeline requirement filter.
+
+        This method is required by the interface.
+
+        :returns: A voluptuous schema.
+        :rtype: dict or Schema
+
+        """
+        pass
+
+    @abc.abstractmethod
+    def getRejectSchema(self):
+        """Get the schema for this driver's pipeline reject filter.
+
+        This method is required by the interface.
+
+        :returns: A voluptuous schema.
+        :rtype: dict or Schema
+
+        """
+        pass
+
 
 @six.add_metaclass(abc.ABCMeta)
 class ReporterInterface(object):
diff --git a/zuul/driver/gerrit/__init__.py b/zuul/driver/gerrit/__init__.py
index a36e912..76ab5b7 100644
--- a/zuul/driver/gerrit/__init__.py
+++ b/zuul/driver/gerrit/__init__.py
@@ -41,3 +41,9 @@
 
     def getReporterSchema(self):
         return gerritreporter.getSchema()
+
+    def getRequireSchema(self):
+        return gerritsource.getRequireSchema()
+
+    def getRejectSchema(self):
+        return gerritsource.getRejectSchema()
diff --git a/zuul/driver/gerrit/gerritconnection.py b/zuul/driver/gerrit/gerritconnection.py
index 25cce42..dcbc172 100644
--- a/zuul/driver/gerrit/gerritconnection.py
+++ b/zuul/driver/gerrit/gerritconnection.py
@@ -27,8 +27,9 @@
 import voluptuous as v
 
 from zuul.connection import BaseConnection
-from zuul.model import TriggerEvent, Change, Ref
+from zuul.model import Ref
 from zuul import exceptions
+from zuul.driver.gerrit.gerritmodel import GerritChange, GerritTriggerEvent
 
 
 # Walk the change dependency tree to find a cycle
@@ -73,7 +74,7 @@
         # should always be a constant number of seconds behind Gerrit.
         now = time.time()
         time.sleep(max((ts + self.delay) - now, 0.0))
-        event = TriggerEvent()
+        event = GerritTriggerEvent()
         event.type = data.get('type')
         event.trigger_name = 'gerrit'
         change = data.get('change')
@@ -321,7 +322,7 @@
         if change and not refresh:
             return change
         if not change:
-            change = Change(None)
+            change = GerritChange(None)
             change.number = number
             change.patchset = patchset
         key = '%s,%s' % (change.number, change.patchset)
diff --git a/zuul/driver/gerrit/gerritmodel.py b/zuul/driver/gerrit/gerritmodel.py
new file mode 100644
index 0000000..009a723
--- /dev/null
+++ b/zuul/driver/gerrit/gerritmodel.py
@@ -0,0 +1,343 @@
+# Copyright 2017 Red Hat, Inc.
+#
+# 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.
+
+import copy
+import re
+import time
+
+from zuul.model import EventFilter, RefFilter
+from zuul.model import Change, TriggerEvent
+from zuul.driver.util import time_to_seconds
+
+
+EMPTY_GIT_REF = '0' * 40  # git sha of all zeros, used during creates/deletes
+
+
+def normalize_category(name):
+    name = name.lower()
+    return re.sub(' ', '-', name)
+
+
+class GerritChange(Change):
+    def __init__(self, project):
+        super(GerritChange, self).__init__(project)
+        self.approvals = []
+
+
+class GerritTriggerEvent(TriggerEvent):
+    """Incoming event from an external system."""
+    def __init__(self):
+        super(GerritTriggerEvent, self).__init__()
+        self.approvals = []
+
+    def __repr__(self):
+        ret = '<GerritTriggerEvent %s %s' % (self.type,
+                                             self.canonical_project_name)
+
+        if self.branch:
+            ret += " %s" % self.branch
+        if self.change_number:
+            ret += " %s,%s" % (self.change_number, self.patch_number)
+        if self.approvals:
+            ret += ' ' + ', '.join(
+                ['%s:%s' % (a['type'], a['value']) for a in self.approvals])
+        ret += '>'
+
+        return ret
+
+    def isPatchsetCreated(self):
+        return 'patchset-created' == self.type
+
+    def isChangeAbandoned(self):
+        return 'change-abandoned' == self.type
+
+
+class GerritApprovalFilter(object):
+    def __init__(self, required_approvals=[], reject_approvals=[]):
+        self._required_approvals = copy.deepcopy(required_approvals)
+        self.required_approvals = self._tidy_approvals(required_approvals)
+        self._reject_approvals = copy.deepcopy(reject_approvals)
+        self.reject_approvals = self._tidy_approvals(reject_approvals)
+
+    def _tidy_approvals(self, approvals):
+        for a in approvals:
+            for k, v in a.items():
+                if k == 'username':
+                    a['username'] = re.compile(v)
+                elif k in ['email', 'email-filter']:
+                    a['email'] = re.compile(v)
+                elif k == 'newer-than':
+                    a[k] = time_to_seconds(v)
+                elif k == 'older-than':
+                    a[k] = time_to_seconds(v)
+            if 'email-filter' in a:
+                del a['email-filter']
+        return approvals
+
+    def _match_approval_required_approval(self, rapproval, approval):
+        # Check if the required approval and approval match
+        if 'description' not in approval:
+            return False
+        now = time.time()
+        by = approval.get('by', {})
+        for k, v in rapproval.items():
+            if k == 'username':
+                if (not v.search(by.get('username', ''))):
+                        return False
+            elif k == 'email':
+                if (not v.search(by.get('email', ''))):
+                        return False
+            elif k == 'newer-than':
+                t = now - v
+                if (approval['grantedOn'] < t):
+                        return False
+            elif k == 'older-than':
+                t = now - v
+                if (approval['grantedOn'] >= t):
+                    return False
+            else:
+                if not isinstance(v, list):
+                    v = [v]
+                if (normalize_category(approval['description']) != k or
+                        int(approval['value']) not in v):
+                    return False
+        return True
+
+    def matchesApprovals(self, change):
+        if (self.required_approvals and not change.approvals
+                or self.reject_approvals and not change.approvals):
+            # A change with no approvals can not match
+            return False
+
+        # TODO(jhesketh): If we wanted to optimise this slightly we could
+        # analyse both the REQUIRE and REJECT filters by looping over the
+        # approvals on the change and keeping track of what we have checked
+        # rather than needing to loop on the change approvals twice
+        return (self.matchesRequiredApprovals(change) and
+                self.matchesNoRejectApprovals(change))
+
+    def matchesRequiredApprovals(self, change):
+        # Check if any approvals match the requirements
+        for rapproval in self.required_approvals:
+            matches_rapproval = False
+            for approval in change.approvals:
+                if self._match_approval_required_approval(rapproval, approval):
+                    # We have a matching approval so this requirement is
+                    # fulfilled
+                    matches_rapproval = True
+                    break
+            if not matches_rapproval:
+                return False
+        return True
+
+    def matchesNoRejectApprovals(self, change):
+        # Check to make sure no approvals match a reject criteria
+        for rapproval in self.reject_approvals:
+            for approval in change.approvals:
+                if self._match_approval_required_approval(rapproval, approval):
+                    # A reject approval has been matched, so we reject
+                    # immediately
+                    return False
+        # To get here no rejects can have been matched so we should be good to
+        # queue
+        return True
+
+
+class GerritEventFilter(EventFilter, GerritApprovalFilter):
+    def __init__(self, trigger, types=[], branches=[], refs=[],
+                 event_approvals={}, comments=[], emails=[], usernames=[],
+                 required_approvals=[], reject_approvals=[],
+                 ignore_deletes=True):
+
+        EventFilter.__init__(self, trigger)
+
+        GerritApprovalFilter.__init__(self,
+                                      required_approvals=required_approvals,
+                                      reject_approvals=reject_approvals)
+
+        self._types = types
+        self._branches = branches
+        self._refs = refs
+        self._comments = comments
+        self._emails = emails
+        self._usernames = usernames
+        self.types = [re.compile(x) for x in types]
+        self.branches = [re.compile(x) for x in branches]
+        self.refs = [re.compile(x) for x in refs]
+        self.comments = [re.compile(x) for x in comments]
+        self.emails = [re.compile(x) for x in emails]
+        self.usernames = [re.compile(x) for x in usernames]
+        self.event_approvals = event_approvals
+        self.ignore_deletes = ignore_deletes
+
+    def __repr__(self):
+        ret = '<GerritEventFilter'
+
+        if self._types:
+            ret += ' types: %s' % ', '.join(self._types)
+        if self._branches:
+            ret += ' branches: %s' % ', '.join(self._branches)
+        if self._refs:
+            ret += ' refs: %s' % ', '.join(self._refs)
+        if self.ignore_deletes:
+            ret += ' ignore_deletes: %s' % self.ignore_deletes
+        if self.event_approvals:
+            ret += ' event_approvals: %s' % ', '.join(
+                ['%s:%s' % a for a in self.event_approvals.items()])
+        if self.required_approvals:
+            ret += ' required_approvals: %s' % ', '.join(
+                ['%s' % a for a in self._required_approvals])
+        if self.reject_approvals:
+            ret += ' reject_approvals: %s' % ', '.join(
+                ['%s' % a for a in self._reject_approvals])
+        if self._comments:
+            ret += ' comments: %s' % ', '.join(self._comments)
+        if self._emails:
+            ret += ' emails: %s' % ', '.join(self._emails)
+        if self._usernames:
+            ret += ' usernames: %s' % ', '.join(self._usernames)
+        ret += '>'
+
+        return ret
+
+    def matches(self, event, change):
+        # event types are ORed
+        matches_type = False
+        for etype in self.types:
+            if etype.match(event.type):
+                matches_type = True
+        if self.types and not matches_type:
+            return False
+
+        # branches are ORed
+        matches_branch = False
+        for branch in self.branches:
+            if branch.match(event.branch):
+                matches_branch = True
+        if self.branches and not matches_branch:
+            return False
+
+        # refs are ORed
+        matches_ref = False
+        if event.ref is not None:
+            for ref in self.refs:
+                if ref.match(event.ref):
+                    matches_ref = True
+        if self.refs and not matches_ref:
+            return False
+        if self.ignore_deletes and event.newrev == EMPTY_GIT_REF:
+            # If the updated ref has an empty git sha (all 0s),
+            # then the ref is being deleted
+            return False
+
+        # comments are ORed
+        matches_comment_re = False
+        for comment_re in self.comments:
+            if (event.comment is not None and
+                comment_re.search(event.comment)):
+                matches_comment_re = True
+        if self.comments and not matches_comment_re:
+            return False
+
+        # We better have an account provided by Gerrit to do
+        # email filtering.
+        if event.account is not None:
+            account_email = event.account.get('email')
+            # emails are ORed
+            matches_email_re = False
+            for email_re in self.emails:
+                if (account_email is not None and
+                        email_re.search(account_email)):
+                    matches_email_re = True
+            if self.emails and not matches_email_re:
+                return False
+
+            # usernames are ORed
+            account_username = event.account.get('username')
+            matches_username_re = False
+            for username_re in self.usernames:
+                if (account_username is not None and
+                    username_re.search(account_username)):
+                    matches_username_re = True
+            if self.usernames and not matches_username_re:
+                return False
+
+        # approvals are ANDed
+        for category, value in self.event_approvals.items():
+            matches_approval = False
+            for eapp in event.approvals:
+                if (normalize_category(eapp['description']) == category and
+                    int(eapp['value']) == int(value)):
+                    matches_approval = True
+            if not matches_approval:
+                return False
+
+        # required approvals are ANDed (reject approvals are ORed)
+        if not self.matchesApprovals(change):
+            return False
+
+        return True
+
+
+class GerritRefFilter(RefFilter, GerritApprovalFilter):
+    def __init__(self, open=None, current_patchset=None,
+                 statuses=[], required_approvals=[],
+                 reject_approvals=[]):
+        RefFilter.__init__(self)
+
+        GerritApprovalFilter.__init__(self,
+                                      required_approvals=required_approvals,
+                                      reject_approvals=reject_approvals)
+
+        self.open = open
+        self.current_patchset = current_patchset
+        self.statuses = statuses
+
+    def __repr__(self):
+        ret = '<GerritRefFilter'
+
+        if self.open is not None:
+            ret += ' open: %s' % self.open
+        if self.current_patchset is not None:
+            ret += ' current-patchset: %s' % self.current_patchset
+        if self.statuses:
+            ret += ' statuses: %s' % ', '.join(self.statuses)
+        if self.required_approvals:
+            ret += (' required-approvals: %s' %
+                    str(self.required_approvals))
+        if self.reject_approvals:
+            ret += (' reject-approvals: %s' %
+                    str(self.reject_approvals))
+        ret += '>'
+
+        return ret
+
+    def matches(self, change):
+        if self.open is not None:
+            if self.open != change.open:
+                return False
+
+        if self.current_patchset is not None:
+            if self.current_patchset != change.is_current_patchset:
+                return False
+
+        if self.statuses:
+            if change.status not in self.statuses:
+                return False
+
+        # required approvals are ANDed (reject approvals are ORed)
+        if not self.matchesApprovals(change):
+            return False
+
+        return True
diff --git a/zuul/driver/gerrit/gerritsource.py b/zuul/driver/gerrit/gerritsource.py
index e6230df..6cb0c39 100644
--- a/zuul/driver/gerrit/gerritsource.py
+++ b/zuul/driver/gerrit/gerritsource.py
@@ -13,8 +13,11 @@
 # under the License.
 
 import logging
+import voluptuous as vs
 from zuul.source import BaseSource
 from zuul.model import Project
+from zuul.driver.gerrit.gerritmodel import GerritRefFilter
+from zuul.driver.util import scalar_or_list, to_list
 
 
 class GerritSource(BaseSource):
@@ -59,3 +62,41 @@
 
     def _getGitwebUrl(self, project, sha=None):
         return self.connection._getGitwebUrl(project, sha)
+
+    def getRequireFilters(self, config):
+        f = GerritRefFilter(
+            open=config.get('open'),
+            current_patchset=config.get('current-patchset'),
+            statuses=to_list(config.get('status')),
+            required_approvals=to_list(config.get('approval')),
+        )
+        return [f]
+
+    def getRejectFilters(self, config):
+        f = GerritRefFilter(
+            reject_approvals=to_list(config.get('approval')),
+        )
+        return [f]
+
+
+approval = vs.Schema({'username': str,
+                      'email-filter': str,
+                      'email': str,
+                      'older-than': str,
+                      'newer-than': str,
+                      }, extra=vs.ALLOW_EXTRA)
+
+
+def getRequireSchema():
+    require = {'approval': scalar_or_list(approval),
+               'open': bool,
+               'current-patchset': bool,
+               'status': scalar_or_list(str)}
+
+    return require
+
+
+def getRejectSchema():
+    reject = {'approval': scalar_or_list(approval)}
+
+    return reject
diff --git a/zuul/driver/gerrit/gerrittrigger.py b/zuul/driver/gerrit/gerrittrigger.py
index 70c65fd..706b7df 100644
--- a/zuul/driver/gerrit/gerrittrigger.py
+++ b/zuul/driver/gerrit/gerrittrigger.py
@@ -14,8 +14,9 @@
 
 import logging
 import voluptuous as v
-from zuul.model import EventFilter
 from zuul.trigger import BaseTrigger
+from zuul.driver.gerrit.gerritmodel import GerritEventFilter
+from zuul.driver.util import scalar_or_list, to_list
 
 
 class GerritTrigger(BaseTrigger):
@@ -23,43 +24,36 @@
     log = logging.getLogger("zuul.GerritTrigger")
 
     def getEventFilters(self, trigger_conf):
-        def toList(item):
-            if not item:
-                return []
-            if isinstance(item, list):
-                return item
-            return [item]
-
         efilters = []
-        for trigger in toList(trigger_conf):
+        for trigger in to_list(trigger_conf):
             approvals = {}
-            for approval_dict in toList(trigger.get('approval')):
+            for approval_dict in to_list(trigger.get('approval')):
                 for key, val in approval_dict.items():
                     approvals[key] = val
             # Backwards compat for *_filter versions of these args
-            comments = toList(trigger.get('comment'))
+            comments = to_list(trigger.get('comment'))
             if not comments:
-                comments = toList(trigger.get('comment_filter'))
-            emails = toList(trigger.get('email'))
+                comments = to_list(trigger.get('comment_filter'))
+            emails = to_list(trigger.get('email'))
             if not emails:
-                emails = toList(trigger.get('email_filter'))
-            usernames = toList(trigger.get('username'))
+                emails = to_list(trigger.get('email_filter'))
+            usernames = to_list(trigger.get('username'))
             if not usernames:
-                usernames = toList(trigger.get('username_filter'))
+                usernames = to_list(trigger.get('username_filter'))
             ignore_deletes = trigger.get('ignore-deletes', True)
-            f = EventFilter(
+            f = GerritEventFilter(
                 trigger=self,
-                types=toList(trigger['event']),
-                branches=toList(trigger.get('branch')),
-                refs=toList(trigger.get('ref')),
+                types=to_list(trigger['event']),
+                branches=to_list(trigger.get('branch')),
+                refs=to_list(trigger.get('ref')),
                 event_approvals=approvals,
                 comments=comments,
                 emails=emails,
                 usernames=usernames,
                 required_approvals=(
-                    toList(trigger.get('require-approval'))
+                    to_list(trigger.get('require-approval'))
                 ),
-                reject_approvals=toList(
+                reject_approvals=to_list(
                     trigger.get('reject-approval')
                 ),
                 ignore_deletes=ignore_deletes
@@ -80,8 +74,6 @@
 
 
 def getSchema():
-    def toList(x):
-        return v.Any([x], x)
     variable_dict = v.Schema(dict)
 
     approval = v.Schema({'username': str,
@@ -93,25 +85,25 @@
 
     gerrit_trigger = {
         v.Required('event'):
-            toList(v.Any('patchset-created',
-                         'draft-published',
-                         'change-abandoned',
-                         'change-restored',
-                         'change-merged',
-                         'comment-added',
-                         'ref-updated')),
-        'comment_filter': toList(str),
-        'comment': toList(str),
-        'email_filter': toList(str),
-        'email': toList(str),
-        'username_filter': toList(str),
-        'username': toList(str),
-        'branch': toList(str),
-        'ref': toList(str),
+            scalar_or_list(v.Any('patchset-created',
+                                 'draft-published',
+                                 'change-abandoned',
+                                 'change-restored',
+                                 'change-merged',
+                                 'comment-added',
+                                 'ref-updated')),
+        'comment_filter': scalar_or_list(str),
+        'comment': scalar_or_list(str),
+        'email_filter': scalar_or_list(str),
+        'email': scalar_or_list(str),
+        'username_filter': scalar_or_list(str),
+        'username': scalar_or_list(str),
+        'branch': scalar_or_list(str),
+        'ref': scalar_or_list(str),
         'ignore-deletes': bool,
-        'approval': toList(variable_dict),
-        'require-approval': toList(approval),
-        'reject-approval': toList(approval),
+        'approval': scalar_or_list(variable_dict),
+        'require-approval': scalar_or_list(approval),
+        'reject-approval': scalar_or_list(approval),
     }
 
     return gerrit_trigger
diff --git a/zuul/driver/git/__init__.py b/zuul/driver/git/__init__.py
index 5ebedac..0faa036 100644
--- a/zuul/driver/git/__init__.py
+++ b/zuul/driver/git/__init__.py
@@ -25,3 +25,9 @@
 
     def getSource(self, connection):
         return gitsource.GitSource(self, connection)
+
+    def getRequireSchema(self):
+        return {}
+
+    def getRejectSchema(self):
+        return {}
diff --git a/zuul/driver/git/gitsource.py b/zuul/driver/git/gitsource.py
index 485a6e4..61a328e 100644
--- a/zuul/driver/git/gitsource.py
+++ b/zuul/driver/git/gitsource.py
@@ -53,3 +53,9 @@
 
     def getProjectOpenChanges(self, project):
         raise NotImplemented()
+
+    def getRequireFilters(self, config):
+        return []
+
+    def getRejectFilters(self, config):
+        return []
diff --git a/zuul/driver/github/__init__.py b/zuul/driver/github/__init__.py
index e59dc58..f75e907 100644
--- a/zuul/driver/github/__init__.py
+++ b/zuul/driver/github/__init__.py
@@ -41,3 +41,9 @@
 
     def getReporterSchema(self):
         return githubreporter.getSchema()
+
+    def getRequireSchema(self):
+        return githubsource.getRequireSchema()
+
+    def getRejectSchema(self):
+        return githubsource.getRejectSchema()
diff --git a/zuul/driver/github/githubconnection.py b/zuul/driver/github/githubconnection.py
index b7fb05d..4b945a5 100644
--- a/zuul/driver/github/githubconnection.py
+++ b/zuul/driver/github/githubconnection.py
@@ -25,8 +25,9 @@
 from github3.exceptions import MethodNotAllowed
 
 from zuul.connection import BaseConnection
-from zuul.model import PullRequest, Ref, GithubTriggerEvent
+from zuul.model import Ref
 from zuul.exceptions import MergeFailure
+from zuul.driver.github.githubmodel import PullRequest, GithubTriggerEvent
 
 
 class GithubWebhookListener():
diff --git a/zuul/driver/github/githubmodel.py b/zuul/driver/github/githubmodel.py
new file mode 100644
index 0000000..0d77cae
--- /dev/null
+++ b/zuul/driver/github/githubmodel.py
@@ -0,0 +1,163 @@
+# Copyright 2015 Hewlett-Packard Development Company, L.P.
+# Copyright 2017 IBM Corp.
+# Copyright 2017 Red Hat, Inc.
+#
+# 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.
+
+import re
+
+from zuul.model import Change, TriggerEvent, EventFilter
+
+
+EMPTY_GIT_REF = '0' * 40  # git sha of all zeros, used during creates/deletes
+
+
+class PullRequest(Change):
+    def __init__(self, project):
+        super(PullRequest, self).__init__(project)
+        self.updated_at = None
+        self.title = None
+
+    def isUpdateOf(self, other):
+        if (hasattr(other, 'number') and self.number == other.number and
+            hasattr(other, 'patchset') and self.patchset != other.patchset and
+            hasattr(other, 'updated_at') and
+            self.updated_at > other.updated_at):
+            return True
+        return False
+
+
+class GithubTriggerEvent(TriggerEvent):
+    def __init__(self):
+        super(GithubTriggerEvent, self).__init__()
+        self.title = None
+        self.label = None
+        self.unlabel = None
+
+    def isPatchsetCreated(self):
+        if self.type == 'pull_request':
+            return self.action in ['opened', 'changed']
+        return False
+
+    def isChangeAbandoned(self):
+        if self.type == 'pull_request':
+            return 'closed' == self.action
+        return False
+
+
+class GithubEventFilter(EventFilter):
+    def __init__(self, trigger, types=[], branches=[], refs=[],
+                 comments=[], actions=[], labels=[], unlabels=[],
+                 states=[], ignore_deletes=True):
+
+        EventFilter.__init__(self, trigger)
+
+        self._types = types
+        self._branches = branches
+        self._refs = refs
+        self._comments = comments
+        self.types = [re.compile(x) for x in types]
+        self.branches = [re.compile(x) for x in branches]
+        self.refs = [re.compile(x) for x in refs]
+        self.comments = [re.compile(x) for x in comments]
+        self.actions = actions
+        self.labels = labels
+        self.unlabels = unlabels
+        self.states = states
+        self.ignore_deletes = ignore_deletes
+
+    def __repr__(self):
+        ret = '<GithubEventFilter'
+
+        if self._types:
+            ret += ' types: %s' % ', '.join(self._types)
+        if self._branches:
+            ret += ' branches: %s' % ', '.join(self._branches)
+        if self._refs:
+            ret += ' refs: %s' % ', '.join(self._refs)
+        if self.ignore_deletes:
+            ret += ' ignore_deletes: %s' % self.ignore_deletes
+        if self._comments:
+            ret += ' comments: %s' % ', '.join(self._comments)
+        if self.actions:
+            ret += ' actions: %s' % ', '.join(self.actions)
+        if self.labels:
+            ret += ' labels: %s' % ', '.join(self.labels)
+        if self.unlabels:
+            ret += ' unlabels: %s' % ', '.join(self.unlabels)
+        if self.states:
+            ret += ' states: %s' % ', '.join(self.states)
+        ret += '>'
+
+        return ret
+
+    def matches(self, event, change):
+        # event types are ORed
+        matches_type = False
+        for etype in self.types:
+            if etype.match(event.type):
+                matches_type = True
+        if self.types and not matches_type:
+            return False
+
+        # branches are ORed
+        matches_branch = False
+        for branch in self.branches:
+            if branch.match(event.branch):
+                matches_branch = True
+        if self.branches and not matches_branch:
+            return False
+
+        # refs are ORed
+        matches_ref = False
+        if event.ref is not None:
+            for ref in self.refs:
+                if ref.match(event.ref):
+                    matches_ref = True
+        if self.refs and not matches_ref:
+            return False
+        if self.ignore_deletes and event.newrev == EMPTY_GIT_REF:
+            # If the updated ref has an empty git sha (all 0s),
+            # then the ref is being deleted
+            return False
+
+        # comments are ORed
+        matches_comment_re = False
+        for comment_re in self.comments:
+            if (event.comment is not None and
+                comment_re.search(event.comment)):
+                matches_comment_re = True
+        if self.comments and not matches_comment_re:
+            return False
+
+        # actions are ORed
+        matches_action = False
+        for action in self.actions:
+            if (event.action == action):
+                matches_action = True
+        if self.actions and not matches_action:
+            return False
+
+        # labels are ORed
+        if self.labels and event.label not in self.labels:
+            return False
+
+        # unlabels are ORed
+        if self.unlabels and event.unlabel not in self.unlabels:
+            return False
+
+        # states are ORed
+        if self.states and event.state not in self.states:
+            return False
+
+        return True
diff --git a/zuul/driver/github/githubsource.py b/zuul/driver/github/githubsource.py
index 312ee87..e7d19ac 100644
--- a/zuul/driver/github/githubsource.py
+++ b/zuul/driver/github/githubsource.py
@@ -90,3 +90,17 @@
 
     def _ghTimestampToDate(self, timestamp):
         return time.strptime(timestamp, '%Y-%m-%dT%H:%M:%SZ')
+
+    def getRequireFilters(self, config):
+        return []
+
+    def getRejectFilters(self, config):
+        return []
+
+
+def getRequireSchema():
+    return {}
+
+
+def getRejectSchema():
+    return {}
diff --git a/zuul/driver/github/githubtrigger.py b/zuul/driver/github/githubtrigger.py
index b9c1026..f0bd2f4 100644
--- a/zuul/driver/github/githubtrigger.py
+++ b/zuul/driver/github/githubtrigger.py
@@ -14,8 +14,8 @@
 
 import logging
 import voluptuous as v
-from zuul.model import EventFilter
 from zuul.trigger import BaseTrigger
+from zuul.driver.github.githubmodel import GithubEventFilter
 
 
 class GithubTrigger(BaseTrigger):
@@ -32,7 +32,7 @@
 
         efilters = []
         for trigger in toList(trigger_config):
-            f = EventFilter(
+            f = GithubEventFilter(
                 trigger=self,
                 types=toList(trigger['event']),
                 actions=toList(trigger.get('action')),
diff --git a/zuul/driver/timer/__init__.py b/zuul/driver/timer/__init__.py
index bca91a1..cdaea74 100644
--- a/zuul/driver/timer/__init__.py
+++ b/zuul/driver/timer/__init__.py
@@ -20,8 +20,8 @@
 from apscheduler.triggers.cron import CronTrigger
 
 from zuul.driver import Driver, TriggerInterface
-from zuul.model import TriggerEvent
 from zuul.driver.timer import timertrigger
+from zuul.driver.timer.timermodel import TimerTriggerEvent
 
 
 class TimerDriver(Driver, TriggerInterface):
@@ -81,7 +81,7 @@
     def _onTrigger(self, tenant, pipeline_name, timespec):
         for project_name in tenant.layout.project_configs.keys():
             project_hostname, project_name = project_name.split('/', 1)
-            event = TriggerEvent()
+            event = TimerTriggerEvent()
             event.type = 'timer'
             event.timespec = timespec
             event.forced_pipeline = pipeline_name
diff --git a/zuul/driver/timer/timermodel.py b/zuul/driver/timer/timermodel.py
new file mode 100644
index 0000000..d6f1415
--- /dev/null
+++ b/zuul/driver/timer/timermodel.py
@@ -0,0 +1,62 @@
+# Copyright 2017 Red Hat, Inc.
+#
+# 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.
+
+import re
+
+from zuul.model import EventFilter, TriggerEvent
+
+
+class TimerEventFilter(EventFilter):
+    def __init__(self, trigger, types=[], timespecs=[]):
+        EventFilter.__init__(self, trigger)
+
+        self._types = types
+        self.types = [re.compile(x) for x in types]
+        self.timespecs = timespecs
+
+    def __repr__(self):
+        ret = '<TimerEventFilter'
+
+        if self._types:
+            ret += ' types: %s' % ', '.join(self._types)
+        if self.timespecs:
+            ret += ' timespecs: %s' % ', '.join(self.timespecs)
+        ret += '>'
+
+        return ret
+
+    def matches(self, event, change):
+        # event types are ORed
+        matches_type = False
+        for etype in self.types:
+            if etype.match(event.type):
+                matches_type = True
+        if self.types and not matches_type:
+            return False
+
+        # timespecs are ORed
+        matches_timespec = False
+        for timespec in self.timespecs:
+            if (event.timespec == timespec):
+                matches_timespec = True
+        if self.timespecs and not matches_timespec:
+            return False
+
+        return True
+
+
+class TimerTriggerEvent(TriggerEvent):
+    def __init__(self):
+        super(TimerTriggerEvent, self).__init__()
+        self.timespec = None
diff --git a/zuul/driver/timer/timertrigger.py b/zuul/driver/timer/timertrigger.py
index b0f282c..81b41a1 100644
--- a/zuul/driver/timer/timertrigger.py
+++ b/zuul/driver/timer/timertrigger.py
@@ -15,26 +15,20 @@
 
 import voluptuous as v
 
-from zuul.model import EventFilter
 from zuul.trigger import BaseTrigger
+from zuul.driver.timer.timermodel import TimerEventFilter
+from zuul.driver.util import to_list
 
 
 class TimerTrigger(BaseTrigger):
     name = 'timer'
 
     def getEventFilters(self, trigger_conf):
-        def toList(item):
-            if not item:
-                return []
-            if isinstance(item, list):
-                return item
-            return [item]
-
         efilters = []
-        for trigger in toList(trigger_conf):
-            f = EventFilter(trigger=self,
-                            types=['timer'],
-                            timespecs=toList(trigger['time']))
+        for trigger in to_list(trigger_conf):
+            f = TimerEventFilter(trigger=self,
+                                 types=['timer'],
+                                 timespecs=to_list(trigger['time']))
 
             efilters.append(f)
 
diff --git a/zuul/driver/util.py b/zuul/driver/util.py
new file mode 100644
index 0000000..902ce76
--- /dev/null
+++ b/zuul/driver/util.py
@@ -0,0 +1,43 @@
+# Copyright 2017 Red Hat, Inc.
+#
+# 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.
+
+# Utility methods to promote consistent configuration among drivers.
+
+import voluptuous as vs
+
+
+def time_to_seconds(s):
+    if s.endswith('s'):
+        return int(s[:-1])
+    if s.endswith('m'):
+        return int(s[:-1]) * 60
+    if s.endswith('h'):
+        return int(s[:-1]) * 60 * 60
+    if s.endswith('d'):
+        return int(s[:-1]) * 24 * 60 * 60
+    if s.endswith('w'):
+        return int(s[:-1]) * 7 * 24 * 60 * 60
+    raise Exception("Unable to parse time value: %s" % s)
+
+
+def scalar_or_list(x):
+    return vs.Any([x], x)
+
+
+def to_list(item):
+    if not item:
+        return []
+    if isinstance(item, list):
+        return item
+    return [item]
diff --git a/zuul/driver/zuul/__init__.py b/zuul/driver/zuul/__init__.py
index 4c3be3d..08612dc 100644
--- a/zuul/driver/zuul/__init__.py
+++ b/zuul/driver/zuul/__init__.py
@@ -15,7 +15,7 @@
 import logging
 
 from zuul.driver import Driver, TriggerInterface
-from zuul.model import TriggerEvent
+from zuul.driver.zuul.zuulmodel import ZuulTriggerEvent
 
 from zuul.driver.zuul import zuultrigger
 
@@ -73,7 +73,7 @@
             self._createProjectChangeMergedEvent(open_change)
 
     def _createProjectChangeMergedEvent(self, change):
-        event = TriggerEvent()
+        event = ZuulTriggerEvent()
         event.type = PROJECT_CHANGE_MERGED
         event.trigger_name = self.name
         event.project_hostname = change.project.canonical_hostname
@@ -94,7 +94,7 @@
             self._createParentChangeEnqueuedEvent(needs, pipeline)
 
     def _createParentChangeEnqueuedEvent(self, change, pipeline):
-        event = TriggerEvent()
+        event = ZuulTriggerEvent()
         event.type = PARENT_CHANGE_ENQUEUED
         event.trigger_name = self.name
         event.pipeline_name = pipeline.name
diff --git a/zuul/driver/zuul/zuulmodel.py b/zuul/driver/zuul/zuulmodel.py
new file mode 100644
index 0000000..036f6d2
--- /dev/null
+++ b/zuul/driver/zuul/zuulmodel.py
@@ -0,0 +1,63 @@
+# Copyright 2017 Red Hat, Inc.
+#
+# 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.
+
+import re
+
+from zuul.model import EventFilter, TriggerEvent
+
+
+class ZuulEventFilter(EventFilter):
+    def __init__(self, trigger, types=[], pipelines=[]):
+        EventFilter.__init__(self, trigger)
+
+        self._types = types
+        self._pipelines = pipelines
+        self.types = [re.compile(x) for x in types]
+        self.pipelines = [re.compile(x) for x in pipelines]
+
+    def __repr__(self):
+        ret = '<ZuulEventFilter'
+
+        if self._types:
+            ret += ' types: %s' % ', '.join(self._types)
+        if self._pipelines:
+            ret += ' pipelines: %s' % ', '.join(self._pipelines)
+        ret += '>'
+
+        return ret
+
+    def matches(self, event, change):
+        # event types are ORed
+        matches_type = False
+        for etype in self.types:
+            if etype.match(event.type):
+                matches_type = True
+        if self.types and not matches_type:
+            return False
+
+        # pipelines are ORed
+        matches_pipeline = False
+        for epipe in self.pipelines:
+            if epipe.match(event.pipeline_name):
+                matches_pipeline = True
+        if self.pipelines and not matches_pipeline:
+            return False
+
+        return True
+
+
+class ZuulTriggerEvent(TriggerEvent):
+    def __init__(self):
+        super(ZuulTriggerEvent, self).__init__()
+        self.pipeline_name = None
diff --git a/zuul/driver/zuul/zuultrigger.py b/zuul/driver/zuul/zuultrigger.py
index c0c2fb3..628687e 100644
--- a/zuul/driver/zuul/zuultrigger.py
+++ b/zuul/driver/zuul/zuultrigger.py
@@ -15,8 +15,9 @@
 
 import logging
 import voluptuous as v
-from zuul.model import EventFilter
 from zuul.trigger import BaseTrigger
+from zuul.driver.zuul.zuulmodel import ZuulEventFilter
+from zuul.driver.util import scalar_or_list, to_list
 
 
 class ZuulTrigger(BaseTrigger):
@@ -29,25 +30,12 @@
         self._handle_project_change_merged_events = False
 
     def getEventFilters(self, trigger_conf):
-        def toList(item):
-            if not item:
-                return []
-            if isinstance(item, list):
-                return item
-            return [item]
-
         efilters = []
-        for trigger in toList(trigger_conf):
-            f = EventFilter(
+        for trigger in to_list(trigger_conf):
+            f = ZuulEventFilter(
                 trigger=self,
-                types=toList(trigger['event']),
-                pipelines=toList(trigger.get('pipeline')),
-                required_approvals=(
-                    toList(trigger.get('require-approval'))
-                ),
-                reject_approvals=toList(
-                    trigger.get('reject-approval')
-                ),
+                types=to_list(trigger['event']),
+                pipelines=to_list(trigger.get('pipeline')),
             )
             efilters.append(f)
 
@@ -55,9 +43,6 @@
 
 
 def getSchema():
-    def toList(x):
-        return v.Any([x], x)
-
     approval = v.Schema({'username': str,
                          'email-filter': str,
                          'email': str,
@@ -67,11 +52,11 @@
 
     zuul_trigger = {
         v.Required('event'):
-        toList(v.Any('parent-change-enqueued',
-                     'project-change-merged')),
-        'pipeline': toList(str),
-        'require-approval': toList(approval),
-        'reject-approval': toList(approval),
+        scalar_or_list(v.Any('parent-change-enqueued',
+                             'project-change-merged')),
+        'pipeline': scalar_or_list(str),
+        'require-approval': scalar_or_list(approval),
+        'reject-approval': scalar_or_list(approval),
     }
 
     return zuul_trigger
diff --git a/zuul/model.py b/zuul/model.py
index 7f6223b..4ae6f9a 100644
--- a/zuul/model.py
+++ b/zuul/model.py
@@ -16,7 +16,6 @@
 import copy
 import logging
 import os
-import re
 import struct
 import time
 from uuid import uuid4
@@ -28,8 +27,6 @@
                                   'ordereddict.OrderedDict'])
 
 
-EMPTY_GIT_REF = '0' * 40  # git sha of all zeros, used during creates/deletes
-
 MERGER_MERGE = 1          # "git merge"
 MERGER_MERGE_RESOLVE = 2  # "git merge -s resolve"
 MERGER_CHERRY_PICK = 3    # "git cherry-pick"
@@ -78,25 +75,6 @@
                    STATE_DELETING])
 
 
-def time_to_seconds(s):
-    if s.endswith('s'):
-        return int(s[:-1])
-    if s.endswith('m'):
-        return int(s[:-1]) * 60
-    if s.endswith('h'):
-        return int(s[:-1]) * 60 * 60
-    if s.endswith('d'):
-        return int(s[:-1]) * 24 * 60 * 60
-    if s.endswith('w'):
-        return int(s[:-1]) * 7 * 24 * 60 * 60
-    raise Exception("Unable to parse time value: %s" % s)
-
-
-def normalizeCategory(name):
-    name = name.lower()
-    return re.sub(' ', '-', name)
-
-
 class Attributes(object):
     """A class to hold attributes for string formatting."""
 
@@ -1810,7 +1788,6 @@
         self.can_merge = False
         self.is_merged = False
         self.failed_to_merge = False
-        self.approvals = []
         self.open = None
         self.status = None
         self.owner = None
@@ -1863,24 +1840,10 @@
                           patchset=self.patchset)
 
 
-class PullRequest(Change):
-    def __init__(self, project):
-        super(PullRequest, self).__init__(project)
-        self.updated_at = None
-        self.title = None
-
-    def isUpdateOf(self, other):
-        if (hasattr(other, 'number') and self.number == other.number and
-            hasattr(other, 'patchset') and self.patchset != other.patchset and
-            hasattr(other, 'updated_at') and
-            self.updated_at > other.updated_at):
-            return True
-        return False
-
-
 class TriggerEvent(object):
     """Incoming event from an external system."""
     def __init__(self):
+        # TODO(jeblair): further reduce this list
         self.data = None
         # common
         self.type = None
@@ -1896,20 +1859,13 @@
         self.change_url = None
         self.patch_number = None
         self.refspec = None
-        self.approvals = []
         self.branch = None
         self.comment = None
-        self.label = None
-        self.unlabel = None
         self.state = None
         # ref-updated
         self.ref = None
         self.oldrev = None
         self.newrev = None
-        # timer
-        self.timespec = None
-        # zuultrigger
-        self.pipeline_name = None
         # For events that arrive with a destination pipeline (eg, from
         # an admin command, etc):
         self.forced_pipeline = None
@@ -1918,374 +1874,35 @@
     def canonical_project_name(self):
         return self.project_hostname + '/' + self.project_name
 
-    def __repr__(self):
-        ret = '<TriggerEvent %s %s' % (self.type, self.canonical_project_name)
-
-        if self.branch:
-            ret += " %s" % self.branch
-        if self.change_number:
-            ret += " %s,%s" % (self.change_number, self.patch_number)
-        if self.approvals:
-            ret += ' ' + ', '.join(
-                ['%s:%s' % (a['type'], a['value']) for a in self.approvals])
-        ret += '>'
-
-        return ret
-
     def isPatchsetCreated(self):
-        return 'patchset-created' == self.type
-
-    def isChangeAbandoned(self):
-        return 'change-abandoned' == self.type
-
-
-class GithubTriggerEvent(TriggerEvent):
-
-    def __init__(self):
-        super(GithubTriggerEvent, self).__init__()
-        self.title = None
-
-    def isPatchsetCreated(self):
-        if self.type == 'pull_request':
-            return self.action in ['opened', 'changed']
         return False
 
     def isChangeAbandoned(self):
-        if self.type == 'pull_request':
-            return 'closed' == self.action
         return False
 
 
 class BaseFilter(object):
     """Base Class for filtering which Changes and Events to process."""
-    def __init__(self, required_approvals=[], reject_approvals=[]):
-        self._required_approvals = copy.deepcopy(required_approvals)
-        self.required_approvals = self._tidy_approvals(required_approvals)
-        self._reject_approvals = copy.deepcopy(reject_approvals)
-        self.reject_approvals = self._tidy_approvals(reject_approvals)
-
-    def _tidy_approvals(self, approvals):
-        for a in approvals:
-            for k, v in a.items():
-                if k == 'username':
-                    a['username'] = re.compile(v)
-                elif k in ['email', 'email-filter']:
-                    a['email'] = re.compile(v)
-                elif k == 'newer-than':
-                    a[k] = time_to_seconds(v)
-                elif k == 'older-than':
-                    a[k] = time_to_seconds(v)
-            if 'email-filter' in a:
-                del a['email-filter']
-        return approvals
-
-    def _match_approval_required_approval(self, rapproval, approval):
-        # Check if the required approval and approval match
-        if 'description' not in approval:
-            return False
-        now = time.time()
-        by = approval.get('by', {})
-        for k, v in rapproval.items():
-            if k == 'username':
-                if (not v.search(by.get('username', ''))):
-                        return False
-            elif k == 'email':
-                if (not v.search(by.get('email', ''))):
-                        return False
-            elif k == 'newer-than':
-                t = now - v
-                if (approval['grantedOn'] < t):
-                        return False
-            elif k == 'older-than':
-                t = now - v
-                if (approval['grantedOn'] >= t):
-                    return False
-            else:
-                if not isinstance(v, list):
-                    v = [v]
-                if (normalizeCategory(approval['description']) != k or
-                        int(approval['value']) not in v):
-                    return False
-        return True
-
-    def matchesApprovals(self, change):
-        if (self.required_approvals and not change.approvals
-                or self.reject_approvals and not change.approvals):
-            # A change with no approvals can not match
-            return False
-
-        # TODO(jhesketh): If we wanted to optimise this slightly we could
-        # analyse both the REQUIRE and REJECT filters by looping over the
-        # approvals on the change and keeping track of what we have checked
-        # rather than needing to loop on the change approvals twice
-        return (self.matchesRequiredApprovals(change) and
-                self.matchesNoRejectApprovals(change))
-
-    def matchesRequiredApprovals(self, change):
-        # Check if any approvals match the requirements
-        for rapproval in self.required_approvals:
-            matches_rapproval = False
-            for approval in change.approvals:
-                if self._match_approval_required_approval(rapproval, approval):
-                    # We have a matching approval so this requirement is
-                    # fulfilled
-                    matches_rapproval = True
-                    break
-            if not matches_rapproval:
-                return False
-        return True
-
-    def matchesNoRejectApprovals(self, change):
-        # Check to make sure no approvals match a reject criteria
-        for rapproval in self.reject_approvals:
-            for approval in change.approvals:
-                if self._match_approval_required_approval(rapproval, approval):
-                    # A reject approval has been matched, so we reject
-                    # immediately
-                    return False
-        # To get here no rejects can have been matched so we should be good to
-        # queue
-        return True
+    pass
 
 
 class EventFilter(BaseFilter):
     """Allows a Pipeline to only respond to certain events."""
-    def __init__(self, trigger, types=[], branches=[], refs=[],
-                 event_approvals={}, comments=[], emails=[], usernames=[],
-                 timespecs=[], required_approvals=[], reject_approvals=[],
-                 pipelines=[], actions=[], labels=[], unlabels=[], states=[],
-                 ignore_deletes=True):
-        super(EventFilter, self).__init__(
-            required_approvals=required_approvals,
-            reject_approvals=reject_approvals)
+    def __init__(self, trigger):
+        super(EventFilter, self).__init__()
         self.trigger = trigger
-        self._types = types
-        self._branches = branches
-        self._refs = refs
-        self._comments = comments
-        self._emails = emails
-        self._usernames = usernames
-        self._pipelines = pipelines
-        self.types = [re.compile(x) for x in types]
-        self.branches = [re.compile(x) for x in branches]
-        self.refs = [re.compile(x) for x in refs]
-        self.comments = [re.compile(x) for x in comments]
-        self.emails = [re.compile(x) for x in emails]
-        self.usernames = [re.compile(x) for x in usernames]
-        self.pipelines = [re.compile(x) for x in pipelines]
-        self.actions = actions
-        self.event_approvals = event_approvals
-        self.timespecs = timespecs
-        self.labels = labels
-        self.unlabels = unlabels
-        self.states = states
-        self.ignore_deletes = ignore_deletes
 
-    def __repr__(self):
-        ret = '<EventFilter'
-
-        if self._types:
-            ret += ' types: %s' % ', '.join(self._types)
-        if self._pipelines:
-            ret += ' pipelines: %s' % ', '.join(self._pipelines)
-        if self._branches:
-            ret += ' branches: %s' % ', '.join(self._branches)
-        if self._refs:
-            ret += ' refs: %s' % ', '.join(self._refs)
-        if self.ignore_deletes:
-            ret += ' ignore_deletes: %s' % self.ignore_deletes
-        if self.event_approvals:
-            ret += ' event_approvals: %s' % ', '.join(
-                ['%s:%s' % a for a in self.event_approvals.items()])
-        if self.required_approvals:
-            ret += ' required_approvals: %s' % ', '.join(
-                ['%s' % a for a in self._required_approvals])
-        if self.reject_approvals:
-            ret += ' reject_approvals: %s' % ', '.join(
-                ['%s' % a for a in self._reject_approvals])
-        if self._comments:
-            ret += ' comments: %s' % ', '.join(self._comments)
-        if self._emails:
-            ret += ' emails: %s' % ', '.join(self._emails)
-        if self._usernames:
-            ret += ' username_filters: %s' % ', '.join(self._usernames)
-        if self.timespecs:
-            ret += ' timespecs: %s' % ', '.join(self.timespecs)
-        if self.actions:
-            ret += ' actions: %s' % ', '.join(self.actions)
-        if self.labels:
-            ret += ' labels: %s' % ', '.join(self.labels)
-        if self.unlabels:
-            ret += ' unlabels: %s' % ', '.join(self.unlabels)
-        if self.states:
-            ret += ' states: %s' % ', '.join(self.states)
-        ret += '>'
-
-        return ret
-
-    def matches(self, event, change):
-        # event types are ORed
-        matches_type = False
-        for etype in self.types:
-            if etype.match(event.type):
-                matches_type = True
-        if self.types and not matches_type:
-            return False
-
-        # pipelines are ORed
-        matches_pipeline = False
-        for epipe in self.pipelines:
-            if epipe.match(event.pipeline_name):
-                matches_pipeline = True
-        if self.pipelines and not matches_pipeline:
-            return False
-
-        # branches are ORed
-        matches_branch = False
-        for branch in self.branches:
-            if branch.match(event.branch):
-                matches_branch = True
-        if self.branches and not matches_branch:
-            return False
-
-        # refs are ORed
-        matches_ref = False
-        if event.ref is not None:
-            for ref in self.refs:
-                if ref.match(event.ref):
-                    matches_ref = True
-        if self.refs and not matches_ref:
-            return False
-        if self.ignore_deletes and event.newrev == EMPTY_GIT_REF:
-            # If the updated ref has an empty git sha (all 0s),
-            # then the ref is being deleted
-            return False
-
-        # comments are ORed
-        matches_comment_re = False
-        for comment_re in self.comments:
-            if (event.comment is not None and
-                comment_re.search(event.comment)):
-                matches_comment_re = True
-        if self.comments and not matches_comment_re:
-            return False
-
-        # We better have an account provided by Gerrit to do
-        # email filtering.
-        if event.account is not None:
-            account_email = event.account.get('email')
-            # emails are ORed
-            matches_email_re = False
-            for email_re in self.emails:
-                if (account_email is not None and
-                        email_re.search(account_email)):
-                    matches_email_re = True
-            if self.emails and not matches_email_re:
-                return False
-
-            # usernames are ORed
-            account_username = event.account.get('username')
-            matches_username_re = False
-            for username_re in self.usernames:
-                if (account_username is not None and
-                    username_re.search(account_username)):
-                    matches_username_re = True
-            if self.usernames and not matches_username_re:
-                return False
-
-        # approvals are ANDed
-        for category, value in self.event_approvals.items():
-            matches_approval = False
-            for eapproval in event.approvals:
-                if (normalizeCategory(eapproval['description']) == category and
-                    int(eapproval['value']) == int(value)):
-                    matches_approval = True
-            if not matches_approval:
-                return False
-
-        # required approvals are ANDed (reject approvals are ORed)
-        if not self.matchesApprovals(change):
-            return False
-
-        # timespecs are ORed
-        matches_timespec = False
-        for timespec in self.timespecs:
-            if (event.timespec == timespec):
-                matches_timespec = True
-        if self.timespecs and not matches_timespec:
-            return False
-
-        # actions are ORed
-        matches_action = False
-        for action in self.actions:
-            if (event.action == action):
-                matches_action = True
-        if self.actions and not matches_action:
-            return False
-
-        # labels are ORed
-        if self.labels and event.label not in self.labels:
-            return False
-
-        # unlabels are ORed
-        if self.unlabels and event.unlabel not in self.unlabels:
-            return False
-
-        # states are ORed
-        if self.states and event.state not in self.states:
-            return False
-
+    def matches(self, event, ref):
+        # TODO(jeblair): consider removing ref argument
         return True
 
 
-class ChangeishFilter(BaseFilter):
+class RefFilter(BaseFilter):
     """Allows a Manager to only enqueue Changes that meet certain criteria."""
-    def __init__(self, open=None, current_patchset=None,
-                 statuses=[], required_approvals=[],
-                 reject_approvals=[]):
-        super(ChangeishFilter, self).__init__(
-            required_approvals=required_approvals,
-            reject_approvals=reject_approvals)
-        self.open = open
-        self.current_patchset = current_patchset
-        self.statuses = statuses
-
-    def __repr__(self):
-        ret = '<ChangeishFilter'
-
-        if self.open is not None:
-            ret += ' open: %s' % self.open
-        if self.current_patchset is not None:
-            ret += ' current-patchset: %s' % self.current_patchset
-        if self.statuses:
-            ret += ' statuses: %s' % ', '.join(self.statuses)
-        if self.required_approvals:
-            ret += (' required_approvals: %s' %
-                    str(self.required_approvals))
-        if self.reject_approvals:
-            ret += (' reject_approvals: %s' %
-                    str(self.reject_approvals))
-        ret += '>'
-
-        return ret
+    def __init__(self):
+        super(RefFilter, self).__init__()
 
     def matches(self, change):
-        if self.open is not None:
-            if self.open != change.open:
-                return False
-
-        if self.current_patchset is not None:
-            if self.current_patchset != change.is_current_patchset:
-                return False
-
-        if self.statuses:
-            if change.status not in self.statuses:
-                return False
-
-        # required approvals are ANDed (reject approvals are ORed)
-        if not self.matchesApprovals(change):
-            return False
-
         return True
 
 
diff --git a/zuul/source/__init__.py b/zuul/source/__init__.py
index f0eeba6..68baf0e 100644
--- a/zuul/source/__init__.py
+++ b/zuul/source/__init__.py
@@ -69,3 +69,13 @@
     @abc.abstractmethod
     def getProjectBranches(self, project):
         """Get branches for a project"""
+
+    @abc.abstractmethod
+    def getRequireFilters(self, config):
+        """Return a list of ChangeFilters for the scheduler to match against.
+        """
+
+    @abc.abstractmethod
+    def getRejectFilters(self, config):
+        """Return a list of ChangeFilters for the scheduler to match against.
+        """