Encapsulate determining the event purpose

Github webhook events are pretty detailed, so it's able to distinguish
eg. between opening a new pull request and pushing into already existing
pull request. As this does not map exactly onto gerrit events, provide
a level of abstraction for places where certain kind of events have to
be distinguished.

This causes that dequeue mechanism starts to work with github pull
requests.

Change-Id: I90ef72ccf2d4e669b7e1304e5b9eb351ca9b5b62
diff --git a/tests/fixtures/layouts/dequeue-github.yaml b/tests/fixtures/layouts/dequeue-github.yaml
new file mode 100644
index 0000000..25e92c9
--- /dev/null
+++ b/tests/fixtures/layouts/dequeue-github.yaml
@@ -0,0 +1,18 @@
+- pipeline:
+    name: check
+    manager: independent
+    trigger:
+      github:
+        - event: pull_request
+          action:
+            - opened
+            - changed
+
+- job:
+    name: one-job-project-merge
+
+- project:
+    name: org/one-job-project
+    check:
+      jobs:
+        - one-job-project-merge
diff --git a/tests/unit/test_github_driver.py b/tests/unit/test_github_driver.py
index c7c5f3a..97a0066 100644
--- a/tests/unit/test_github_driver.py
+++ b/tests/unit/test_github_driver.py
@@ -15,6 +15,7 @@
 import logging
 import re
 from testtools.matchers import MatchesRegex
+import time
 
 from tests.base import ZuulTestCase, simple_layout, random_sha1
 
@@ -138,6 +139,47 @@
         self.assertEqual(2, len(self.history))
         self.assertEqual(['other label'], C.labels)
 
+    @simple_layout('layouts/dequeue-github.yaml', driver='github')
+    def test_dequeue_pull_synchronized(self):
+        self.executor_server.hold_jobs_in_build = True
+
+        pr = self.fake_github.openFakePullRequest(
+            'org/one-job-project', 'master')
+        self.fake_github.emitEvent(pr.getPullRequestOpenedEvent())
+        self.waitUntilSettled()
+
+        # event update stamp has resolution one second, wait so the latter
+        # one has newer timestamp
+        time.sleep(1)
+        pr.addCommit()
+        self.fake_github.emitEvent(pr.getPullRequestSynchronizeEvent())
+        self.waitUntilSettled()
+
+        self.executor_server.hold_jobs_in_build = False
+        self.executor_server.release()
+        self.waitUntilSettled()
+
+        self.assertEqual(2, len(self.history))
+        self.assertEqual(1, self.countJobResults(self.history, 'ABORTED'))
+
+    @simple_layout('layouts/dequeue-github.yaml', driver='github')
+    def test_dequeue_pull_abandoned(self):
+        self.executor_server.hold_jobs_in_build = True
+
+        pr = self.fake_github.openFakePullRequest(
+            'org/one-job-project', 'master')
+        self.fake_github.emitEvent(pr.getPullRequestOpenedEvent())
+        self.waitUntilSettled()
+        self.fake_github.emitEvent(pr.getPullRequestClosedEvent())
+        self.waitUntilSettled()
+
+        self.executor_server.hold_jobs_in_build = False
+        self.executor_server.release()
+        self.waitUntilSettled()
+
+        self.assertEqual(1, len(self.history))
+        self.assertEqual(1, self.countJobResults(self.history, 'ABORTED'))
+
     @simple_layout('layouts/basic-github.yaml', driver='github')
     def test_git_https_url(self):
         """Test that git_ssh option gives git url with ssh"""
diff --git a/zuul/driver/github/githubconnection.py b/zuul/driver/github/githubconnection.py
index c73b88e..ecf72a7 100644
--- a/zuul/driver/github/githubconnection.py
+++ b/zuul/driver/github/githubconnection.py
@@ -24,7 +24,7 @@
 from github3.exceptions import MethodNotAllowed
 
 from zuul.connection import BaseConnection
-from zuul.model import PullRequest, Ref, TriggerEvent
+from zuul.model import PullRequest, Ref, GithubTriggerEvent
 from zuul.exceptions import MergeFailure
 
 
@@ -77,7 +77,7 @@
         body = request.json_body
         base_repo = body.get('repository')
 
-        event = TriggerEvent()
+        event = GithubTriggerEvent()
         event.trigger_name = 'github'
         event.project_name = base_repo.get('full_name')
         event.type = 'push'
@@ -175,7 +175,7 @@
         return True
 
     def _pull_request_to_event(self, pr_body):
-        event = TriggerEvent()
+        event = GithubTriggerEvent()
         event.trigger_name = 'github'
 
         base = pr_body.get('base')
diff --git a/zuul/model.py b/zuul/model.py
index a86fbbd..7ed8339 100644
--- a/zuul/model.py
+++ b/zuul/model.py
@@ -1926,6 +1926,25 @@
 
         return ret
 
+    def isPatchsetCreated(self):
+        return 'patchset-created' == self.type
+
+    def isChangeAbandoned(self):
+        return 'change-abandoned' == self.type
+
+
+class GithubTriggerEvent(TriggerEvent):
+
+    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."""
diff --git a/zuul/scheduler.py b/zuul/scheduler.py
index 53ca4c1..2e9bef2 100644
--- a/zuul/scheduler.py
+++ b/zuul/scheduler.py
@@ -759,9 +759,9 @@
                     change.project.unparsed_config = None
                     self.reconfigureTenant(tenant)
                 for pipeline in tenant.layout.pipelines.values():
-                    if event.type == 'patchset-created':
+                    if event.isPatchsetCreated():
                         pipeline.manager.removeOldVersionsOfChange(change)
-                    elif event.type == 'change-abandoned':
+                    elif event.isChangeAbandoned():
                         pipeline.manager.removeAbandonedChange(change)
                     if pipeline.manager.eventMatches(event, change):
                         pipeline.manager.addChange(change)