Allow merge failures to have unique reporters.

For example, we would like to be able to count (with the MySQL
reporter) what jobs have failed because they can't be merged
vs. real failures.

Change-Id: I98eb8b8817bbda57efc2ef5bfcc2a5076fe8f4fd
diff --git a/zuul/scheduler.py b/zuul/scheduler.py
index c941a98..1a4474c 100644
--- a/zuul/scheduler.py
+++ b/zuul/scheduler.py
@@ -226,6 +226,11 @@
             pipeline.precedence = precedence
             pipeline.failure_message = conf_pipeline.get('failure-message',
                                                          "Build failed.")
+            pipeline.merge_failure_message = conf_pipeline.get(
+                'merge-failure-message', "Merge Failed.\n\nThis change was "
+                "unable to be automatically merged with the current state of "
+                "the repository. Please rebase your change and upload a new "
+                "patchset.")
             pipeline.success_message = conf_pipeline.get('success-message',
                                                          "Build succeeded.")
             pipeline.footer_message = conf_pipeline.get('footer-message', "")
@@ -233,7 +238,7 @@
                 'dequeue-on-new-patchset', True)
 
             action_reporters = {}
-            for action in ['start', 'success', 'failure']:
+            for action in ['start', 'success', 'failure', 'merge-failure']:
                 action_reporters[action] = []
                 if conf_pipeline.get(action):
                     for reporter_name, params \
@@ -247,6 +252,11 @@
             pipeline.start_actions = action_reporters['start']
             pipeline.success_actions = action_reporters['success']
             pipeline.failure_actions = action_reporters['failure']
+            if len(action_reporters['merge-failure']) > 0:
+                pipeline.merge_failure_actions = \
+                    action_reporters['merge-failure']
+            else:
+                pipeline.merge_failure_actions = action_reporters['failure']
 
             pipeline.window = conf_pipeline.get('window', 20)
             pipeline.window_floor = conf_pipeline.get('window-floor', 3)
@@ -936,6 +946,8 @@
         self.log.info("    %s" % self.pipeline.success_actions)
         self.log.info("  On failure:")
         self.log.info("    %s" % self.pipeline.failure_actions)
+        self.log.info("  On merge-failure:")
+        self.log.info("    %s" % self.pipeline.merge_failure_actions)
 
     def getSubmitAllowNeeds(self):
         # Get a list of code review labels that are allowed to be
@@ -1334,10 +1346,7 @@
             build_set.commit = item.change.newrev
         if not build_set.commit:
             self.log.info("Unable to merge change %s" % item.change)
-            msg = ("This change was unable to be automatically merged "
-                   "with the current state of the repository. Please "
-                   "rebase your change and upload a new patchset.")
-            self.pipeline.setUnableToMerge(item, msg)
+            self.pipeline.setUnableToMerge(item)
 
     def reportItem(self, item):
         if item.reported:
@@ -1370,10 +1379,12 @@
         self.log.debug("Reporting change %s" % item.change)
         ret = True  # Means error as returned by trigger.report
         if self.pipeline.didAllJobsSucceed(item):
-            self.log.debug("success %s %s" % (self.pipeline.success_actions,
-                                              self.pipeline.failure_actions))
+            self.log.debug("success %s" % (self.pipeline.success_actions))
             actions = self.pipeline.success_actions
             item.setReportedResult('SUCCESS')
+        elif not self.pipeline.didMergerSucceed(item):
+            actions = self.pipeline.merge_failure_actions
+            item.setReportedResult('MERGER_FAILURE')
         else:
             actions = self.pipeline.failure_actions
             item.setReportedResult('FAILURE')
@@ -1395,66 +1406,72 @@
 
     def formatReport(self, item):
         ret = ''
+
+        if not self.pipeline.didMergerSucceed(item):
+            ret += self.pipeline.merge_failure_message
+            if item.dequeued_needing_change:
+                ret += ('\n\nThis change depends on a change that failed to '
+                        'merge.')
+            if self.pipeline.footer_message:
+                ret += '\n\n' + self.pipeline.footer_message
+            return ret
+
         if self.pipeline.didAllJobsSucceed(item):
             ret += self.pipeline.success_message + '\n\n'
         else:
             ret += self.pipeline.failure_message + '\n\n'
 
-        if item.dequeued_needing_change:
-            ret += "This change depends on a change that failed to merge."
-        elif item.current_build_set.unable_to_merge_message:
-            ret += item.current_build_set.unable_to_merge_message
+        if self.sched.config.has_option('zuul', 'url_pattern'):
+            url_pattern = self.sched.config.get('zuul', 'url_pattern')
         else:
-            if self.sched.config.has_option('zuul', 'url_pattern'):
-                url_pattern = self.sched.config.get('zuul', 'url_pattern')
+            url_pattern = None
+
+        for job in self.pipeline.getJobs(item.change):
+            build = item.current_build_set.getBuild(job.name)
+            result = build.result
+            pattern = url_pattern
+            if result == 'SUCCESS':
+                if job.success_message:
+                    result = job.success_message
+                if job.success_pattern:
+                    pattern = job.success_pattern
+            elif result == 'FAILURE':
+                if job.failure_message:
+                    result = job.failure_message
+                if job.failure_pattern:
+                    pattern = job.failure_pattern
+            if pattern:
+                url = pattern.format(change=item.change,
+                                     pipeline=self.pipeline,
+                                     job=job,
+                                     build=build)
             else:
-                url_pattern = None
-            for job in self.pipeline.getJobs(item.change):
-                build = item.current_build_set.getBuild(job.name)
-                result = build.result
-                pattern = url_pattern
-                if result == 'SUCCESS':
-                    if job.success_message:
-                        result = job.success_message
-                    if job.success_pattern:
-                        pattern = job.success_pattern
-                elif result == 'FAILURE':
-                    if job.failure_message:
-                        result = job.failure_message
-                    if job.failure_pattern:
-                        pattern = job.failure_pattern
-                if pattern:
-                    url = pattern.format(change=item.change,
-                                         pipeline=self.pipeline,
-                                         job=job,
-                                         build=build)
+                url = build.url or job.name
+            if not job.voting:
+                voting = ' (non-voting)'
+            else:
+                voting = ''
+            if self.report_times and build.end_time and build.start_time:
+                dt = int(build.end_time - build.start_time)
+                m, s = divmod(dt, 60)
+                h, m = divmod(m, 60)
+                if h:
+                    elapsed = ' in %dh %02dm %02ds' % (h, m, s)
+                elif m:
+                    elapsed = ' in %dm %02ds' % (m, s)
                 else:
-                    url = build.url or job.name
-                if not job.voting:
-                    voting = ' (non-voting)'
-                else:
-                    voting = ''
-                if self.report_times and build.end_time and build.start_time:
-                    dt = int(build.end_time - build.start_time)
-                    m, s = divmod(dt, 60)
-                    h, m = divmod(m, 60)
-                    if h:
-                        elapsed = ' in %dh %02dm %02ds' % (h, m, s)
-                    elif m:
-                        elapsed = ' in %dm %02ds' % (m, s)
-                    else:
-                        elapsed = ' in %ds' % (s)
-                else:
-                    elapsed = ''
-                name = ''
-                if self.sched.config.has_option('zuul', 'job_name_in_report'):
-                    if self.sched.config.getboolean('zuul',
-                                                    'job_name_in_report'):
-                        name = job.name + ' '
-                ret += '- %s%s : %s%s%s\n' % (name, url, result, elapsed,
-                                              voting)
-            ret += '\n'
-        ret += self.pipeline.footer_message
+                    elapsed = ' in %ds' % (s)
+            else:
+                elapsed = ''
+            name = ''
+            if self.sched.config.has_option('zuul', 'job_name_in_report'):
+                if self.sched.config.getboolean('zuul',
+                                                'job_name_in_report'):
+                    name = job.name + ' '
+            ret += '- %s%s : %s%s%s\n' % (name, url, result, elapsed,
+                                          voting)
+        if self.pipeline.footer_message:
+            ret += '\n' + self.pipeline.footer_message
         return ret
 
     def formatDescription(self, build):