Merge "Merge pull requests from github reporter" into feature/zuulv3
diff --git a/doc/source/reporters.rst b/doc/source/reporters.rst
index cf4f6e4..ced3b78 100644
--- a/doc/source/reporters.rst
+++ b/doc/source/reporters.rst
@@ -50,6 +50,11 @@
   to ``true``.
   ``comment: false``
 
+  **merge**
+  Boolean value (``true`` or ``false``) that determines if the reporter should
+  merge the pull reqeust. Defaults to ``false``.
+  ``merge=true``
+
 SMTP
 ----
 
diff --git a/requirements.txt b/requirements.txt
index 44cef95..2fe6963 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,6 +1,6 @@
 pbr>=1.1.0
 
-Github3.py
+Github3.py==1.0.0a2
 PyYAML>=3.1.0
 Paste
 WebOb>=1.2.3
diff --git a/tests/base.py b/tests/base.py
index 0ad1ec1..d7bf467 100755
--- a/tests/base.py
+++ b/tests/base.py
@@ -70,6 +70,7 @@
 import zuul.merger.server
 import zuul.nodepool
 import zuul.zk
+from zuul.exceptions import MergeFailure
 
 FIXTURE_DIR = os.path.join(os.path.dirname(__file__),
                            'fixtures')
@@ -559,6 +560,7 @@
         self.statuses = {}
         self.updated_at = None
         self.head_sha = None
+        self.is_merged = False
         self._createPRRef()
         self._addCommitToRepo()
         self._updateTimeStamp()
@@ -693,6 +695,8 @@
         self.pr_number = 0
         self.pull_requests = []
         self.upstream_root = upstream_root
+        self.merge_failure = False
+        self.merge_not_allowed_count = 0
 
     def openFakePullRequest(self, project, branch):
         self.pr_number += 1
@@ -762,6 +766,16 @@
         pull_request = self.pull_requests[pr_number - 1]
         pull_request.addComment(message)
 
+    def mergePull(self, project, pr_number, sha=None):
+        pull_request = self.pull_requests[pr_number - 1]
+        if self.merge_failure:
+            raise Exception('Pull request was not merged')
+        if self.merge_not_allowed_count > 0:
+            self.merge_not_allowed_count -= 1
+            raise MergeFailure('Merge was not successful due to mergeability'
+                               ' conflict')
+        pull_request.is_merged = True
+
     def setCommitStatus(self, project, sha, state,
                         url='', description='', context=''):
         owner, proj = project.split('/')
diff --git a/tests/fixtures/layouts/merging-github.yaml b/tests/fixtures/layouts/merging-github.yaml
new file mode 100644
index 0000000..4e13063
--- /dev/null
+++ b/tests/fixtures/layouts/merging-github.yaml
@@ -0,0 +1,19 @@
+- pipeline:
+    name: merge
+    description: Pipeline for merging the pull request
+    manager: independent
+    trigger:
+      github:
+        - event: pull_request
+          action: comment
+          comment: 'merge me'
+    success:
+      github:
+        merge: true
+        comment: false
+
+- project:
+    name: org/project
+    merge:
+      jobs:
+        - noop
diff --git a/tests/unit/test_github_driver.py b/tests/unit/test_github_driver.py
index 9017ce9..409d966 100644
--- a/tests/unit/test_github_driver.py
+++ b/tests/unit/test_github_driver.py
@@ -165,3 +165,35 @@
         self.waitUntilSettled()
         self.assertNotIn('reporting', pr.statuses)
         self.assertEqual(2, len(pr.comments))
+
+    @simple_layout('layouts/merging-github.yaml', driver='github')
+    def test_report_pull_merge(self):
+        # pipeline merges the pull request on success
+        A = self.fake_github.openFakePullRequest('org/project', 'master')
+        self.fake_github.emitEvent(A.getCommentAddedEvent('merge me'))
+        self.waitUntilSettled()
+        self.assertTrue(A.is_merged)
+
+        # pipeline merges the pull request on success after failure
+        self.fake_github.merge_failure = True
+        B = self.fake_github.openFakePullRequest('org/project', 'master')
+        self.fake_github.emitEvent(B.getCommentAddedEvent('merge me'))
+        self.waitUntilSettled()
+        self.assertFalse(B.is_merged)
+        self.fake_github.merge_failure = False
+
+        # pipeline merges the pull request on second run of merge
+        # first merge failed on 405 Method Not Allowed error
+        self.fake_github.merge_not_allowed_count = 1
+        C = self.fake_github.openFakePullRequest('org/project', 'master')
+        self.fake_github.emitEvent(C.getCommentAddedEvent('merge me'))
+        self.waitUntilSettled()
+        self.assertTrue(C.is_merged)
+
+        # pipeline does not merge the pull request
+        # merge failed on 405 Method Not Allowed error - twice
+        self.fake_github.merge_not_allowed_count = 2
+        D = self.fake_github.openFakePullRequest('org/project', 'master')
+        self.fake_github.emitEvent(D.getCommentAddedEvent('merge me'))
+        self.waitUntilSettled()
+        self.assertFalse(D.is_merged)
diff --git a/zuul/driver/github/githubconnection.py b/zuul/driver/github/githubconnection.py
index 6604d81..3c1faff 100644
--- a/zuul/driver/github/githubconnection.py
+++ b/zuul/driver/github/githubconnection.py
@@ -21,9 +21,11 @@
 import webob.dec
 import voluptuous as v
 import github3
+from github3.exceptions import MethodNotAllowed
 
 from zuul.connection import BaseConnection
 from zuul.model import PullRequest, Ref, TriggerEvent
+from zuul.exceptions import MergeFailure
 
 
 class GithubWebhookListener():
@@ -283,6 +285,17 @@
         pull_request = repository.issue(pr_number)
         pull_request.create_comment(message)
 
+    def mergePull(self, project, pr_number, sha=None):
+        owner, proj = project.split('/')
+        pull_request = self.github.pull_request(owner, proj, pr_number)
+        try:
+            result = pull_request.merge(sha=sha)
+        except MethodNotAllowed as e:
+            raise MergeFailure('Merge was not successful due to mergeability'
+                               ' conflict, original error is %s' % e)
+        if not result:
+            raise Exception('Pull request was not merged')
+
     def setCommitStatus(self, project, sha, state, url='', description='',
                         context=''):
         owner, proj = project.split('/')
diff --git a/zuul/driver/github/githubreporter.py b/zuul/driver/github/githubreporter.py
index ecbb486..80ab3c7 100644
--- a/zuul/driver/github/githubreporter.py
+++ b/zuul/driver/github/githubreporter.py
@@ -14,8 +14,10 @@
 
 import logging
 import voluptuous as v
+import time
 
 from zuul.reporter import BaseReporter
+from zuul.exceptions import MergeFailure
 
 
 class GithubReporter(BaseReporter):
@@ -28,6 +30,7 @@
         super(GithubReporter, self).__init__(driver, connection, config)
         self._commit_status = self.config.get('status', None)
         self._create_comment = self.config.get('comment', True)
+        self._merge = self.config.get('merge', False)
 
     def report(self, source, pipeline, item):
         """Comment on PR and set commit status."""
@@ -37,6 +40,9 @@
             hasattr(item.change, 'patchset') and
             item.change.patchset is not None):
             self.setPullStatus(pipeline, item)
+        if (self._merge and
+            hasattr(item.change, 'number')):
+            self.mergePull(item)
 
     def addPullComment(self, pipeline, item):
         message = self._formatItemReport(pipeline, item)
@@ -68,10 +74,25 @@
         self.connection.setCommitStatus(
             project, sha, state, url, description, context)
 
+    def mergePull(self, item):
+        project = item.change.project.name
+        pr_number = item.change.number
+        sha = item.change.patchset
+        self.log.debug('Reporting change %s, params %s, merging via API' %
+                       (item.change, self.config))
+        try:
+            self.connection.mergePull(project, pr_number, sha)
+        except MergeFailure:
+            time.sleep(2)
+            self.log.debug('Trying to merge change %s again...' % item.change)
+            self.connection.mergePull(project, pr_number, sha)
+        item.change.is_merged = True
+
 
 def getSchema():
     github_reporter = v.Schema({
         'status': v.Any('pending', 'success', 'failure'),
-        'comment': bool
+        'comment': bool,
+        'merge': bool
     })
     return github_reporter