Have zuul handle merge failures.

If Zuul is unable to merge a change, don't run any jobs, and report
the merge failure to gerrit directly (but still observing the
dependent change queue, in case a change ahead caused the merge
failure).

Adds a test for this situation.

Change-Id: I1ee2a8846b159db385019352cc04af2140db81af
Reviewed-on: https://review.openstack.org/11421
Reviewed-by: Clark Boylan <clark.boylan@gmail.com>
Approved: James E. Blair <corvus@inaugust.com>
Tested-by: Jenkins
diff --git a/tests/test_scheduler.py b/tests/test_scheduler.py
index fc6d4a4..862c382 100644
--- a/tests/test_scheduler.py
+++ b/tests/test_scheduler.py
@@ -74,7 +74,7 @@
     repo.create_tag('init')
 
 
-def add_fake_change_to_repo(project, branch, change_num, patchset, msg):
+def add_fake_change_to_repo(project, branch, change_num, patchset, msg, fn):
     path = os.path.join("/tmp/zuul-test/upstream", project)
     repo = git.Repo(path)
     ref = ChangeReference.create(repo, '1/%s/%s' % (change_num,
@@ -85,9 +85,9 @@
     repo.git.clean('-x', '-f', '-d')
 
     path = os.path.join("/tmp/zuul-test/upstream", project)
-    fn = os.path.join(path, '%s-%s' % (branch, change_num))
+    fn = os.path.join(path, fn)
     f = open(fn, 'w')
-    f.write("test\n")
+    f.write("test %s %s %s\n" % (branch, change_num, patchset))
     f.close()
     repo.index.add([fn])
     repo.index.commit(msg)
@@ -177,9 +177,14 @@
         self.data['currentPatchSet'] = d
         self.patchsets.append(d)
         self.data['submitRecords'] = self.getSubmitRecords()
+        if files:
+            fn = files[0]
+        else:
+            fn = '%s-%s' % (self.branch, self.number)
         add_fake_change_to_repo(self.project, self.branch,
                                 self.number, self.latest_patchset,
-                                self.subject + '-' + str(self.latest_patchset))
+                                self.subject + '-' + str(self.latest_patchset),
+                                fn)
 
     def addApproval(self, category, value):
         approval = {'description': self.categories[category][0],
@@ -1009,6 +1014,7 @@
         ref = jobs[-1].parameters['ZUUL_REF']
         self.fake_jenkins.hold_jobs_in_queue = False
         self.fake_jenkins.fakeRelease()
+        self.waitUntilSettled()
 
         path = os.path.join("/tmp/zuul-test/git/org/project")
         repo = git.Repo(path)
@@ -1017,3 +1023,39 @@
         print '  repo messages  :', repo_messages
         correct_messages = ['initial commit', 'A-1', 'B-1', 'C-1']
         assert repo_messages == correct_messages
+
+    def test_build_configuration_conflict(self):
+        "Test that merge conflicts are handled"
+        self.fake_jenkins.hold_jobs_in_queue = True
+        A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
+        A.addPatchset(['conflict'])
+        B = self.fake_gerrit.addFakeChange('org/project', 'master', 'B')
+        B.addPatchset(['conflict'])
+        C = self.fake_gerrit.addFakeChange('org/project', 'master', 'C')
+        A.addApproval('CRVW', 2)
+        B.addApproval('CRVW', 2)
+        C.addApproval('CRVW', 2)
+        self.fake_gerrit.addEvent(A.addApproval('APRV', 1))
+        self.fake_gerrit.addEvent(B.addApproval('APRV', 1))
+        self.fake_gerrit.addEvent(C.addApproval('APRV', 1))
+        self.waitUntilSettled()
+
+        jobs = self.fake_jenkins.all_jobs
+
+        self.fake_jenkins.fakeRelease('.*-merge')
+        self.waitUntilSettled()
+        self.fake_jenkins.fakeRelease('.*-merge')
+        self.waitUntilSettled()
+        self.fake_jenkins.fakeRelease('.*-merge')
+        self.waitUntilSettled()
+        ref = jobs[-1].parameters['ZUUL_REF']
+        self.fake_jenkins.hold_jobs_in_queue = False
+        self.fake_jenkins.fakeRelease()
+        self.waitUntilSettled()
+
+        assert A.data['status'] == 'MERGED'
+        assert B.data['status'] == 'NEW'
+        assert C.data['status'] == 'MERGED'
+        assert A.reported == 2
+        assert B.reported == 2
+        assert C.reported == 2
diff --git a/zuul/merger.py b/zuul/merger.py
index a1345b8..3efc98e 100644
--- a/zuul/merger.py
+++ b/zuul/merger.py
@@ -120,7 +120,7 @@
                     repo.cherryPick(change.refspec)
                 repo.setZuulRef(change.branch + '/' + target_ref, 'HEAD')
             except:
-                self.log.exception("Unable to merge %s" % change)
+                self.log.info("Unable to merge %s" % change)
                 return False
 
         return True
diff --git a/zuul/model.py b/zuul/model.py
index 1674082..195aa88 100644
--- a/zuul/model.py
+++ b/zuul/model.py
@@ -164,17 +164,22 @@
         else:
             ret += 'Build failed\n\n'
 
-        for job in self.getJobs(changeish):
-            build = changeish.current_build_set.getBuild(job.name)
-            result = build.result
-            if result == 'SUCCESS' and job.success_message:
-                result = job.success_message
-            elif result == 'FAILURE' and job.failure_message:
-                result = job.failure_message
-            url = build.url
-            if not url:
-                url = job.name
-            ret += '- %s : %s\n' % (url, result)
+        if changeish.current_build_set.unable_to_merge:
+            ret += "This change was unable to be automatically merged "\
+                   "with the current state of the repository. Please "\
+                   "rebase your change and upload a new patchset."
+        else:
+            for job in self.getJobs(changeish):
+                build = changeish.current_build_set.getBuild(job.name)
+                result = build.result
+                if result == 'SUCCESS' and job.success_message:
+                    result = job.success_message
+                elif result == 'FAILURE' and job.failure_message:
+                    result = job.failure_message
+                url = build.url
+                if not url:
+                    url = job.name
+                ret += '- %s : %s\n' % (url, result)
         return ret
 
     def formatDescription(self, build):
@@ -287,6 +292,14 @@
                 fakebuild.result = 'SKIPPED'
                 changeish.addBuild(fakebuild)
 
+    def setUnableToMerge(self, changeish):
+        changeish.current_build_set.unable_to_merge = True
+        root = self.getJobTree(changeish.project)
+        for job in root.getJobs():
+            fakebuild = Build(job, None)
+            fakebuild.result = 'SKIPPED'
+            changeish.addBuild(fakebuild)
+
 
 class ChangeQueue(object):
     """DependentPipelines have multiple parallel queues shared by
@@ -433,6 +446,7 @@
         self.next_build_set = None
         self.previous_build_set = None
         self.ref = None
+        self.unable_to_merge = False
 
     def setConfiguration(self):
         # The change isn't enqueued until after it's created
diff --git a/zuul/scheduler.py b/zuul/scheduler.py
index 5fd2126..551af22 100644
--- a/zuul/scheduler.py
+++ b/zuul/scheduler.py
@@ -482,8 +482,13 @@
         if not ref:
             change.current_build_set.setConfiguration()
             ref = change.current_build_set.getRef()
-            self.sched.merger.mergeChanges([change], ref,
-                                           mode=model.MERGE_IF_NECESSARY)
+            merged = self.sched.merger.mergeChanges([change], ref,
+                         mode=model.MERGE_IF_NECESSARY)
+            if not merged:
+                self.log.info("Unable to merge change %s" % change)
+                self.pipeline.setUnableToMerge(change)
+                self.possiblyReportChange(change)
+                return
 
         for job in self.pipeline.findJobsToRun(change):
             self.log.debug("Found job %s for change %s" % (job, change))
@@ -747,7 +752,13 @@
             ref = change.current_build_set.getRef()
             dependent_changes = self._getDependentChanges(change)
             dependent_changes.reverse()
-            self.sched.merger.mergeChanges(dependent_changes + [change], ref)
+            all_changes = dependent_changes + [change]
+            merged = self.sched.merger.mergeChanges(all_changes, ref)
+            if not merged:
+                self.log.info("Unable to merge changes %s" % all_changes)
+                self.pipeline.setUnableToMerge(change)
+                self.possiblyReportChange(change)
+                return
 
         #TODO: remove this line after GERRIT_CHANGES is gone
         dependent_changes = self._getDependentChanges(change)