Add ability to filter patchset comments.

Zuul layouts can now specify that jobs get run when comments match
a filter. Trigger layouts would look like:
  trigger:
    - event: comment-added
      comment_filter:
        - reverify
        - recheck

This would trigger a job whenever comments containing the string
"reverify" or "recheck" are added to a change.

Change-Id: I3dd75abbf75686b3929dd2bb412b02740911d6ee
diff --git a/doc/source/zuul.rst b/doc/source/zuul.rst
index d962edb..0b06ffb 100644
--- a/doc/source/zuul.rst
+++ b/doc/source/zuul.rst
@@ -148,6 +148,14 @@
   ``code-review: 2`` matches a ``+2`` vote on the code review category.
   Multiple approvals may be listed.
 
+  *comment_filter*
+  This is only used for ``comment-added`` events.  It accepts a list of
+  regexes that are searched for in the comment string. If any of these
+  regexes matches a portion of the comment string the trigger is
+  matched. ``comment_filter: retrigger`` will match when comments
+  containing 'retrigger' somewhere in the comment text are added to a
+  change.
+
 **success**
   Describes what Zuul should do if all the jobs complete successfully.
   This section is optional; if it is omitted, Zuul will run jobs and
diff --git a/zuul/model.py b/zuul/model.py
index 9d49efb..68fbf2b 100644
--- a/zuul/model.py
+++ b/zuul/model.py
@@ -311,6 +311,7 @@
         self.refspec = None
         self.approvals = []
         self.branch = None
+        self.comment = None
         # ref-updated
         self.ref = None
         self.oldrev = None
@@ -332,13 +333,15 @@
 
 
 class EventFilter(object):
-    def __init__(self, types=[], branches=[], refs=[], approvals=[]):
+    def __init__(self, types=[], branches=[], refs=[], approvals=[],
+                                                comment_filters=[]):
         self._types = types
         self._branches = branches
         self._refs = refs
         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.comment_filters = [re.compile(x) for x in comment_filters]
         self.approvals = approvals
 
     def __repr__(self):
@@ -386,6 +389,15 @@
         if self.refs and not matches_ref:
             return False
 
+        # comment_filters are ORed
+        matches_comment_filter = False
+        for comment_filter in self.comment_filters:
+            if (event.comment is not None and
+                    comment_filter.search(event.comment)):
+                matches_comment_filter = True
+        if self.comment_filters and not matches_comment_filter:
+            return False
+
         # approvals are ANDed
         for category, value in self.approvals.items():
             matches_approval = False
diff --git a/zuul/scheduler.py b/zuul/scheduler.py
index 3582f5c..2a5d51e 100644
--- a/zuul/scheduler.py
+++ b/zuul/scheduler.py
@@ -72,7 +72,9 @@
                 f = EventFilter(types=toList(trigger['event']),
                                 branches=toList(trigger.get('branch')),
                                 refs=toList(trigger.get('ref')),
-                                approvals=approvals)
+                                approvals=approvals,
+                                comment_filters=toList(
+                                        trigger.get('comment_filter')))
                 manager.event_filters.append(f)
 
         for config_job in data['jobs']:
diff --git a/zuul/trigger/gerrit.py b/zuul/trigger/gerrit.py
index 2d2e778..accfc0c 100644
--- a/zuul/trigger/gerrit.py
+++ b/zuul/trigger/gerrit.py
@@ -43,6 +43,7 @@
                 event.patch_number = patchset.get('number')
                 event.refspec = patchset.get('ref')
             event.approvals = data.get('approvals', [])
+            event.comment = data.get('comment')
         refupdate = data.get('refUpdate')
         if refupdate:
             event.project_name = refupdate.get('project')