Implement github pipeline req of current-patchset

Require that the commit from the event is the latest commit in the pull
request.

Also fix a problem with faked github status grabs. Now we're sending an
event where the sha of the event isn't the head sha, and that was
tripping up our fakes.

Change-Id: I269c97d096e42f0a2d4a0f1b0e57eb238e0b7baf
diff --git a/tests/base.py b/tests/base.py
index c01e9c3..f41d783 100755
--- a/tests/base.py
+++ b/tests/base.py
@@ -996,8 +996,12 @@
         owner, proj = project.split('/')
         for pr in self.pull_requests:
             pr_owner, pr_project = pr.project.split('/')
+            # This is somewhat risky, if the same commit exists in multiple
+            # PRs, we might grab the wrong one that doesn't have a status
+            # that is expected to be there. Maybe re-work this so that there
+            # is a global registry of commit statuses like with github.
             if (pr_owner == owner and pr_project == proj and
-                pr.head_sha == sha):
+                sha in pr.statuses):
                 return pr.statuses[sha]
 
     def setCommitStatus(self, project, sha, state,
diff --git a/tests/fixtures/layouts/requirements-github.yaml b/tests/fixtures/layouts/requirements-github.yaml
index c7f1830..9933f27 100644
--- a/tests/fixtures/layouts/requirements-github.yaml
+++ b/tests/fixtures/layouts/requirements-github.yaml
@@ -154,6 +154,20 @@
       github:
         comment: true
 
+- pipeline:
+    name: require_current
+    manager: independent
+    require:
+      github:
+        current-patchset: true
+    trigger:
+      github:
+        - event: pull_request
+          action: changed
+    success:
+      github:
+        comment: true
+
 - job:
     name: project1-pipeline
 - job:
@@ -170,6 +184,8 @@
     name: project7-olderthan
 - job:
     name: project8-requireopen
+- job:
+    name: project9-requirecurrent
 
 - project:
     name: org/project1
@@ -221,3 +237,9 @@
     require_open:
       jobs:
         - project8-requireopen
+
+- project:
+    name: org/project9
+    require_current:
+      jobs:
+        - project9-requirecurrent
diff --git a/tests/unit/test_github_requirements.py b/tests/unit/test_github_requirements.py
index 3c77ff2..5dd6e80 100644
--- a/tests/unit/test_github_requirements.py
+++ b/tests/unit/test_github_requirements.py
@@ -307,3 +307,22 @@
         self.waitUntilSettled()
         # PR is closed, should not trigger
         self.assertEqual(len(self.history), 1)
+
+    @simple_layout('layouts/requirements-github.yaml', driver='github')
+    def test_require_current(self):
+
+        A = self.fake_github.openFakePullRequest('org/project9', 'master', 'A')
+        # A sync event that we will keep submitting to trigger
+        sync = A.getPullRequestSynchronizeEvent()
+        self.fake_github.emitEvent(sync)
+        self.waitUntilSettled()
+
+        # PR head is current should enqueue
+        self.assertEqual(len(self.history), 1)
+
+        # Add a commit to the PR, re-issue the original comment event
+        A.addCommit()
+        self.fake_github.emitEvent(sync)
+        self.waitUntilSettled()
+        # Event hash is not current, should not trigger
+        self.assertEqual(len(self.history), 1)
diff --git a/zuul/driver/github/githubconnection.py b/zuul/driver/github/githubconnection.py
index afb2d3e..27ece54 100644
--- a/zuul/driver/github/githubconnection.py
+++ b/zuul/driver/github/githubconnection.py
@@ -480,6 +480,9 @@
             change.reviews = self.getPullReviews(project, change.number)
             change.source_event = event
             change.open = self.getPullOpen(project, change.number)
+            change.is_current_patchset = self.getIsCurrent(project,
+                                                           change.number,
+                                                           event.patch_number)
         elif event.ref:
             change = Ref(project)
             change.ref = event.ref
@@ -721,6 +724,10 @@
         pr = self.getPull(project, number)
         return pr.get('state') == 'open'
 
+    def getIsCurrent(self, project, number, sha):
+        pr = self.getPull(project, number)
+        return pr.get('head').get('sha') == sha
+
     def _ghTimestampToDate(self, timestamp):
         return time.strptime(timestamp, '%Y-%m-%dT%H:%M:%SZ')
 
diff --git a/zuul/driver/github/githubmodel.py b/zuul/driver/github/githubmodel.py
index dafd989..3e25115 100644
--- a/zuul/driver/github/githubmodel.py
+++ b/zuul/driver/github/githubmodel.py
@@ -263,13 +263,15 @@
 
 
 class GithubRefFilter(RefFilter, GithubCommonFilter):
-    def __init__(self, statuses=[], required_reviews=[], open=None):
+    def __init__(self, statuses=[], required_reviews=[], open=None,
+                 current_patchset=None):
         RefFilter.__init__(self)
 
         GithubCommonFilter.__init__(self, required_reviews=required_reviews,
                                     required_statuses=statuses)
         self.statuses = statuses
         self.open = open
+        self.current_patchset = current_patchset
 
     def __repr__(self):
         ret = '<GithubRefFilter'
@@ -281,6 +283,8 @@
                     str(self.required_reviews))
         if self.open:
             ret += ' open: %s' % self.open
+        if self.current_patchset:
+            ret += ' current-patchset: %s' % self.current_patchset
 
         ret += '>'
 
@@ -294,6 +298,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
+
         # required reviews are ANDed
         if not self.matchesReviews(change):
             return False
diff --git a/zuul/driver/github/githubsource.py b/zuul/driver/github/githubsource.py
index bc168df..58ca2b9 100644
--- a/zuul/driver/github/githubsource.py
+++ b/zuul/driver/github/githubsource.py
@@ -99,6 +99,7 @@
             statuses=to_list(config.get('status')),
             required_reviews=to_list(config.get('review')),
             open=config.get('open'),
+            current_patchset=config.get('current-patchset'),
         )
         return [f]
 
@@ -118,7 +119,8 @@
 def getRequireSchema():
     require = {'status': scalar_or_list(str),
                'review': scalar_or_list(review),
-               'open': bool}
+               'open': bool,
+               'current-patchset': bool}
     return require