Merge "Factor out common code between cli utilities"
diff --git a/doc/source/zuul.rst b/doc/source/zuul.rst
index ef6259c..21d3bae 100644
--- a/doc/source/zuul.rst
+++ b/doc/source/zuul.rst
@@ -476,6 +476,10 @@
   A boolean value (``true`` or ``false``) that indicates whether the change
   must be open or closed in order to be enqueued.
 
+  **current-patchset**
+  A boolean value (``true`` or ``false``) that indicates whether the change
+  must be the current patchset in order to be enqueued.
+
   **status**
   A string value that corresponds with the status of the change
   reported by the trigger.  For example, when using the Gerrit
diff --git a/etc/status/public_html/index.html b/etc/status/public_html/index.html
index aac5024..8884069 100644
--- a/etc/status/public_html/index.html
+++ b/etc/status/public_html/index.html
@@ -21,64 +21,7 @@
     <title>Zuul Status</title>
     <link rel="stylesheet" href="bootstrap/css/bootstrap.min.css">
     <link rel="stylesheet" href="bootstrap/css/bootstrap-responsive.min.css">
-    <style>
-        .zuul-change {
-            margin-bottom: 10px;
-        }
-
-        .zuul-change-id {
-            float: right;
-        }
-
-        .zuul-job-result {
-            float: right;
-            width: 70px;
-            height: 15px;
-            margin: 2px 0 0 0;
-        }
-
-        .zuul-change-total-result {
-            height: 10px;
-            width: 100px;
-            margin: 5px 0 0 0;
-        }
-
-        .zuul-spinner,
-        .zuul-spinner:hover {
-            opacity: 0;
-            transition: opacity 0.5s ease-out;
-            cursor: default;
-            pointer-events: none;
-        }
-
-        .zuul-spinner-on,
-        .zuul-spinner-on:hover {
-            opacity: 1;
-            transition-duration: 0.2s;
-            cursor: progress;
-        }
-
-        .zuul-change-cell {
-            padding-left: 5px;
-        }
-
-        .zuul-change-job {
-            padding: 2px 8px;
-        }
-
-        .zuul-job-name {
-            font-size: small;
-        }
-
-        .zuul-non-voting-desc {
-            font-size: smaller;
-        }
-
-        .zuul-patchset-header {
-            font-size: small;
-            padding: 8px 12px;
-        }
-    </style>
+    <link rel="stylesheet" href="styles/zuul.css" />
 </head>
 <body>
     <div class="container">
diff --git a/etc/status/public_html/styles/zuul.css b/etc/status/public_html/styles/zuul.css
new file mode 100644
index 0000000..e833f4b
--- /dev/null
+++ b/etc/status/public_html/styles/zuul.css
@@ -0,0 +1,56 @@
+.zuul-change {
+    margin-bottom: 10px;
+}
+
+.zuul-change-id {
+    float: right;
+}
+
+.zuul-job-result {
+    float: right;
+    width: 70px;
+    height: 15px;
+    margin: 2px 0 0 0;
+}
+
+.zuul-change-total-result {
+    height: 10px;
+    width: 100px;
+    margin: 5px 0 0 0;
+}
+
+.zuul-spinner,
+.zuul-spinner:hover {
+    opacity: 0;
+    transition: opacity 0.5s ease-out;
+    cursor: default;
+    pointer-events: none;
+}
+
+.zuul-spinner-on,
+.zuul-spinner-on:hover {
+    opacity: 1;
+    transition-duration: 0.2s;
+    cursor: progress;
+}
+
+.zuul-change-cell {
+    padding-left: 5px;
+}
+
+.zuul-change-job {
+    padding: 2px 8px;
+}
+
+.zuul-job-name {
+    font-size: small;
+}
+
+.zuul-non-voting-desc {
+    font-size: smaller;
+}
+
+.zuul-patchset-header {
+    font-size: small;
+    padding: 8px 12px;
+}
\ No newline at end of file
diff --git a/tests/fixtures/layout-current-patchset.yaml b/tests/fixtures/layout-current-patchset.yaml
new file mode 100644
index 0000000..dc8f768
--- /dev/null
+++ b/tests/fixtures/layout-current-patchset.yaml
@@ -0,0 +1,24 @@
+includes:
+  - python-file: custom_functions.py
+
+pipelines:
+  - name: check
+    manager: IndependentPipelineManager
+    require:
+      current-patchset: True
+    trigger:
+      gerrit:
+        - event: patchset-created
+        - event: comment-added
+    success:
+      gerrit:
+        verified: 1
+    failure:
+      gerrit:
+        verified: -1
+
+projects:
+  - name: org/project
+    merge-mode: cherry-pick
+    check:
+      - project-check
diff --git a/tests/test_scheduler.py b/tests/test_scheduler.py
index f66c2fe..f5070bb 100755
--- a/tests/test_scheduler.py
+++ b/tests/test_scheduler.py
@@ -132,6 +132,7 @@
         self.upstream_root = upstream_root
         self.addPatchset()
         self.data['submitRecords'] = self.getSubmitRecords()
+        self.open = True
 
     def add_fake_change_to_repo(self, msg, fn, large):
         path = os.path.join(self.upstream_root, self.project)
@@ -221,6 +222,23 @@
                  "reason": ""}
         return event
 
+    def getChangeCommentEvent(self, patchset):
+        event = {"type": "comment-added",
+                 "change": {"project": self.project,
+                            "branch": self.branch,
+                            "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
+                            "number": str(self.number),
+                            "subject": self.subject,
+                            "owner": {"name": "User Name"},
+                            "url": "https://hostname/3"},
+                 "patchSet": self.patchsets[patchset - 1],
+                 "author": {"name": "User Name"},
+                 "approvals": [{"type": "Code-Review",
+                                "description": "Code-Review",
+                                "value": "0"}],
+                 "comment": "This is a comment"}
+        return event
+
     def addApproval(self, category, value, username='jenkins',
                     granted_on=None):
         if not granted_on:
@@ -4063,3 +4081,35 @@
             self.getJobFromHistory('experimental-project-test').result,
             'SUCCESS')
         self.assertEqual(A.reported, 1)
+
+    def test_old_patchset_doesnt_trigger(self):
+        "Test that jobs never run against old patchsets"
+        self.config.set('zuul', 'layout_config',
+                        'tests/fixtures/layout-current-patchset.yaml')
+        self.sched.reconfigure(self.config)
+        self.registerJobs()
+        # Create two patchsets and let their tests settle out. Then
+        # comment on first patchset and check that no additional
+        # jobs are run.
+        A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
+        # Added because the layout file really wants an approval but this
+        # doesn't match anyways.
+        self.fake_gerrit.addEvent(A.addApproval('CRVW', 1))
+        self.waitUntilSettled()
+        A.addPatchset()
+        self.fake_gerrit.addEvent(A.addApproval('CRVW', 1))
+        self.waitUntilSettled()
+
+        old_history_count = len(self.history)
+        self.assertEqual(old_history_count, 2)  # one job for each ps
+        self.fake_gerrit.addEvent(A.getChangeCommentEvent(1))
+        self.waitUntilSettled()
+
+        # Assert no new jobs ran after event for old patchset.
+        self.assertEqual(len(self.history), old_history_count)
+
+        # The last thing we did was add an event for a change then do
+        # nothing with a pipeline, so it will be in the cache;
+        # clean it up so it does not fail the test.
+        for pipeline in self.sched.layout.pipelines.values():
+            pipeline.trigger.maintainCache([])
diff --git a/zuul/layoutvalidator.py b/zuul/layoutvalidator.py
index 3e0a0ab..9a448a3 100644
--- a/zuul/layoutvalidator.py
+++ b/zuul/layoutvalidator.py
@@ -71,6 +71,7 @@
 
     require = {'approval': toList(require_approval),
                'open': bool,
+               'current-patchset': bool,
                'status': toList(str)}
 
     window = v.All(int, v.Range(min=0))
diff --git a/zuul/model.py b/zuul/model.py
index c5c5c7d..39d004d 100644
--- a/zuul/model.py
+++ b/zuul/model.py
@@ -1158,8 +1158,10 @@
 
 
 class ChangeishFilter(object):
-    def __init__(self, open=None, statuses=[], approvals=[]):
+    def __init__(self, open=None, current_patchset=None,
+                 statuses=[], approvals=[]):
         self.open = open
+        self.current_patchset = current_patchset
         self.statuses = statuses
         self.approvals = approvals
 
@@ -1176,10 +1178,12 @@
 
         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.approvals:
-            ret += ' approvals: %s' % ', '.join(str(self.approvals))
+            ret += ' approvals: %s' % str(self.approvals)
         ret += '>'
 
         return ret
@@ -1189,6 +1193,10 @@
             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
diff --git a/zuul/scheduler.py b/zuul/scheduler.py
index 55b1624..a7160c7 100644
--- a/zuul/scheduler.py
+++ b/zuul/scheduler.py
@@ -276,9 +276,11 @@
 
             if 'require' in conf_pipeline:
                 require = conf_pipeline['require']
-                f = ChangeishFilter(open=require.get('open'),
-                                    statuses=toList(require.get('status')),
-                                    approvals=toList(require.get('approval')))
+                f = ChangeishFilter(
+                    open=require.get('open'),
+                    current_patchset=require.get('current-patchset'),
+                    statuses=toList(require.get('status')),
+                    approvals=toList(require.get('approval')))
                 manager.changeish_filters.append(f)
 
             # TODO: move this into triggers (may require pluggable