Use the 'resolve' merge strategy by default

We can more closely approximate Gerrit's behavior by using the
'resolve' git merge strategy.  Make that the default, and leave
the previous behavior ('git merge') as an option.  Also, finish
and correct the partially implemented plumbing for other merge
strategies (including cherry-pick).

(Note the previous unfinished implementation attempted to mimic
Gerrit's option names; the new implementation does not, but rather
documents the alignment.  It's not a perfect translation anyway,
and this gives us more room to support other strategies not
currently supported by Gerrit).

Change-Id: Ie1ce4fde5980adf99bba69a5aa1d4e81026db676
diff --git a/doc/source/zuul.rst b/doc/source/zuul.rst
index 73ebf71..f5e2226 100644
--- a/doc/source/zuul.rst
+++ b/doc/source/zuul.rst
@@ -558,6 +558,22 @@
 **name**
   The name of the project (as known by Gerrit).
 
+**merge-mode (optional)**
+  An optional value that indicates what strategy should be used to
+  merge changes to this project.  Supported values are:
+
+  ** merge-resolve **
+  Equivalent to 'git merge -s resolve'.  This corresponds closely to
+  what Gerrit performs (using JGit) for a project if the "Merge if
+  necessary" merge mode is selected and "Automatically resolve
+  conflicts" is checked.  This is the default.
+
+  ** merge **
+  Equivalent to 'git merge'.
+
+  ** cherry-pick **
+  Equivalent to 'git cherry-pick'.
+
 This is followed by a section for each of the pipelines defined above.
 Pipelines may be omitted if no jobs should run for this project in a
 given pipeline.  Within the pipeline section, the jobs that should be
diff --git a/zuul/layoutvalidator.py b/zuul/layoutvalidator.py
index 7f80d64..9ddef11 100644
--- a/zuul/layoutvalidator.py
+++ b/zuul/layoutvalidator.py
@@ -161,7 +161,8 @@
             self.templates_schemas[t_name] = v.Schema(schema)
 
         project = {'name': str,
-                   'merge-mode': v.Any('cherry-pick'),
+                   'merge-mode': v.Any('merge', 'merge-resolve,',
+                                       'cherry-pick'),
                    'template': self.validateTemplateCalls,
                    }
 
diff --git a/zuul/merger.py b/zuul/merger.py
index 94db499..218f7f2 100644
--- a/zuul/merger.py
+++ b/zuul/merger.py
@@ -88,11 +88,15 @@
         self.fetch(ref)
         self.repo.git.cherry_pick("FETCH_HEAD")
 
-    def merge(self, ref):
+    def merge(self, ref, strategy=None):
         self._ensure_cloned()
-        self.log.debug("Merging %s" % ref)
+        args = []
+        if strategy:
+            args += ['-s', strategy]
+        args.append('FETCH_HEAD')
         self.fetch(ref)
-        self.repo.git.merge("FETCH_HEAD")
+        self.log.debug("Merging %s with args %s" % (ref, args))
+        self.repo.git.merge(*args)
 
     def fetch(self, ref):
         self._ensure_cloned()
@@ -186,7 +190,7 @@
         except:
             self.log.exception("Unable to update %s", project)
 
-    def _mergeChange(self, change, ref, target_ref, mode):
+    def _mergeChange(self, change, ref, target_ref):
         repo = self.getRepo(change.project)
         try:
             repo.checkout(ref)
@@ -195,13 +199,16 @@
             return False
 
         try:
-            if not mode:
-                mode = change.project.merge_mode
-            if mode == model.MERGE_IF_NECESSARY:
+            mode = change.project.merge_mode
+            if mode == model.MERGER_MERGE:
                 repo.merge(change.refspec)
-            elif mode == model.CHERRY_PICK:
+            elif mode == model.MERGER_MERGE_RESOLVE:
+                repo.merge(change.refspec, 'resolve')
+            elif mode == model.MERGER_CHERRY_PICK:
                 repo.cherryPick(change.refspec)
-        except:
+            else:
+                raise Exception("Unsupported merge mode: %s" % mode)
+        except Exception:
             # Log exceptions at debug level because they are
             # usually benign merge conflicts
             self.log.debug("Unable to merge %s" % change, exc_info=True)
@@ -219,7 +226,7 @@
             return False
         return commit
 
-    def mergeChanges(self, items, target_ref=None, mode=None):
+    def mergeChanges(self, items, target_ref=None):
         # Merge shortcuts:
         # if this is the only change just merge it against its branch.
         # elif there are changes ahead of us that are from the same project and
@@ -244,13 +251,13 @@
                 return False
             commit = self._mergeChange(item.change,
                                        repo.getBranchHead(item.change.branch),
-                                       target_ref=target_ref, mode=mode)
+                                       target_ref=target_ref)
         # Sibling changes exist. Merge current change against newest sibling.
         elif (len(sibling_items) >= 2 and
               sibling_items[-2].current_build_set.commit):
             last_commit = sibling_items[-2].current_build_set.commit
             commit = self._mergeChange(item.change, last_commit,
-                                       target_ref=target_ref, mode=mode)
+                                       target_ref=target_ref)
         # Either change did not merge or we did not need to merge as there were
         # previous merge conflicts.
         if not commit:
diff --git a/zuul/model.py b/zuul/model.py
index 4823541..59fc1a4 100644
--- a/zuul/model.py
+++ b/zuul/model.py
@@ -17,10 +17,15 @@
 from uuid import uuid4
 
 
-FAST_FORWARD_ONLY = 1
-MERGE_ALWAYS = 2
-MERGE_IF_NECESSARY = 3
-CHERRY_PICK = 4
+MERGER_MERGE = 1          # "git merge"
+MERGER_MERGE_RESOLVE = 2  # "git merge -s resolve"
+MERGER_CHERRY_PICK = 3    # "git cherry-pick"
+
+MERGER_MAP = {
+    'merge': MERGER_MERGE,
+    'merge-resolve': MERGER_MERGE_RESOLVE,
+    'cherry-pick': MERGER_CHERRY_PICK,
+}
 
 PRECEDENCE_NORMAL = 0
 PRECEDENCE_LOW = 1
@@ -420,7 +425,7 @@
 class Project(object):
     def __init__(self, name):
         self.name = name
-        self.merge_mode = MERGE_IF_NECESSARY
+        self.merge_mode = MERGER_MERGE_RESOLVE
 
     def __str__(self):
         return self.name
diff --git a/zuul/scheduler.py b/zuul/scheduler.py
index b2d3be9..23a504e 100644
--- a/zuul/scheduler.py
+++ b/zuul/scheduler.py
@@ -238,9 +238,8 @@
                 config_project.update(expanded)
 
             layout.projects[config_project['name']] = project
-            mode = config_project.get('merge-mode')
-            if mode and mode == 'cherry-pick':
-                project.merge_mode = model.CHERRY_PICK
+            mode = config_project.get('merge-mode', 'merge-resolve')
+            project.merge_mode = model.MERGER_MAP[mode]
             for pipeline in layout.pipelines.values():
                 if pipeline.name in config_project:
                     job_tree = pipeline.addProject(project)