Merge instead of rebasing upstream changes in `gclient sync` when --merge is given.

Merge ALL the things!

BUG=none

Review URL: https://codereview.chromium.org/61823002

git-svn-id: svn://svn.chromium.org/chrome/trunk/tools/depot_tools@246575 0039d316-1c4b-4281-b951-d872f2087c98
diff --git a/gclient_scm.py b/gclient_scm.py
index f7f0b88..e7c7aa5 100644
--- a/gclient_scm.py
+++ b/gclient_scm.py
@@ -78,18 +78,6 @@
     return re.sub("[a|b]/" + self._current_file, self._replacement_file, line)
 
 
-def ask_for_data(prompt, options):
-  if options.jobs > 1:
-    raise gclient_utils.Error("Background task requires input. Rerun "
-                              "gclient with --jobs=1 so that\n"
-                              "interaction is possible.")
-  try:
-    return raw_input(prompt)
-  except KeyboardInterrupt:
-    # Hide the exception.
-    sys.exit(1)
-
-
 ### SCM abstraction layer
 
 # Factory Method for SCM wrapper creation
@@ -473,7 +461,8 @@
       if scm.GIT.IsGitSvn(self.checkout_path) and upstream_branch is not None:
         # Our git-svn branch (upstream_branch) is our upstream
         self._AttemptRebase(upstream_branch, files, options,
-                            newbase=revision, printed_path=printed_path)
+                            newbase=revision, printed_path=printed_path,
+                            merge=options.merge)
         printed_path = True
       else:
         # Can't find a merge-base since we don't know our upstream. That makes
@@ -483,12 +472,13 @@
         if options.revision or deps_revision:
           upstream_branch = revision
         self._AttemptRebase(upstream_branch, files, options,
-                            printed_path=printed_path)
+                            printed_path=printed_path, merge=options.merge)
         printed_path = True
     elif rev_type == 'hash':
       # case 2
       self._AttemptRebase(upstream_branch, files, options,
-                          newbase=revision, printed_path=printed_path)
+                          newbase=revision, printed_path=printed_path,
+                          merge=options.merge)
       printed_path = True
     elif revision.replace('heads', 'remotes/' + self.remote) != upstream_branch:
       # case 4
@@ -509,25 +499,30 @@
         print('Trying fast-forward merge to branch : %s' % upstream_branch)
       try:
         merge_args = ['merge']
-        if not options.merge:
+        if options.merge:
+          merge_args.append('--ff')
+        else:
           merge_args.append('--ff-only')
         merge_args.append(upstream_branch)
         merge_output = scm.GIT.Capture(merge_args, cwd=self.checkout_path)
       except subprocess2.CalledProcessError as e:
         if re.match('fatal: Not possible to fast-forward, aborting.', e.stderr):
+          files = []
           if not printed_path:
             print('\n_____ %s%s' % (self.relpath, rev_str))
             printed_path = True
           while True:
             try:
-              action = ask_for_data(
-                  'Cannot fast-forward merge, attempt to rebase? '
-                  '(y)es / (q)uit / (s)kip : ', options)
+              action = self._AskForData(
+                  'Cannot %s, attempt to rebase? '
+                  '(y)es / (q)uit / (s)kip : ' %
+                      ('merge' if options.merge else 'fast-forward merge'),
+                  options)
             except ValueError:
               raise gclient_utils.Error('Invalid Character')
             if re.match(r'yes|y', action, re.I):
               self._AttemptRebase(upstream_branch, files, options,
-                                  printed_path=printed_path)
+                                  printed_path=printed_path, merge=False)
               printed_path = True
               break
             elif re.match(r'quit|q', action, re.I):
@@ -883,20 +878,40 @@
          'an existing branch or use \'git checkout %s -b <branch>\' to\n'
          'create a new branch for your work.') % (revision, self.remote))
 
+  @staticmethod
+  def _AskForData(prompt, options):
+    if options.jobs > 1:
+      raise gclient_utils.Error("Background task requires input. Rerun "
+                                "gclient with --jobs=1 so that\n"
+                                "interaction is possible.")
+    try:
+      return raw_input(prompt)
+    except KeyboardInterrupt:
+      # Hide the exception.
+      sys.exit(1)
+
+
   def _AttemptRebase(self, upstream, files, options, newbase=None,
-                     branch=None, printed_path=False):
+                     branch=None, printed_path=False, merge=False):
     """Attempt to rebase onto either upstream or, if specified, newbase."""
     if files is not None:
       files.extend(self._Capture(['diff', upstream, '--name-only']).split())
     revision = upstream
     if newbase:
       revision = newbase
+    action = 'merge' if merge else 'rebase'
     if not printed_path:
-      print('\n_____ %s : Attempting rebase onto %s...' % (
-          self.relpath, revision))
+      print('\n_____ %s : Attempting %s onto %s...' % (
+          self.relpath, action, revision))
       printed_path = True
     else:
-      print('Attempting rebase onto %s...' % revision)
+      print('Attempting %s onto %s...' % (action, revision))
+
+    if merge:
+      merge_output = self._Capture(['merge', revision])
+      if options.verbose:
+        print(merge_output)
+      return
 
     # Build the rebase command here using the args
     # git rebase [options] [--onto <newbase>] <upstream> [<branch>]
@@ -916,7 +931,7 @@
           re.match(r'cannot rebase: your index contains uncommitted changes',
                    e.stderr)):
         while True:
-          rebase_action = ask_for_data(
+          rebase_action = self._AskForData(
               'Cannot rebase because of unstaged changes.\n'
               '\'git reset --hard HEAD\' ?\n'
               'WARNING: destroys any uncommitted work in your current branch!'
diff --git a/tests/gclient_scm_test.py b/tests/gclient_scm_test.py
index 5607f85..741727e 100755
--- a/tests/gclient_scm_test.py
+++ b/tests/gclient_scm_test.py
@@ -16,7 +16,6 @@
 import sys
 import tempfile
 import unittest
-import __builtin__
 
 sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
 
@@ -752,6 +751,20 @@
 M 100644 :4 a
 M 100644 :5 b
 
+blob
+mark :7
+data 5
+Mooh
+
+commit refs/heads/feature
+mark :8
+author Bob <bob@example.com> 1390311986 -0000
+committer Bob <bob@example.com> 1390311986 -0000
+data 6
+Add C
+from :3
+M 100644 :7 c
+
 reset refs/heads/master
 from :3
 """
@@ -785,6 +798,12 @@
         stderr=STDOUT, cwd=path).communicate()
     return True
 
+  def _GetAskForDataCallback(self, expected_prompt, return_value):
+    def AskForData(prompt, options):
+      self.assertEquals(prompt, expected_prompt)
+      return return_value
+    return AskForData
+
   def setUp(self):
     TestCaseUtils.setUp(self)
     unittest.TestCase.setUp(self)
@@ -967,6 +986,51 @@
                       'a7142dc9f0009350b96a11f372b6ea658592aa95')
     sys.stdout.close()
 
+  def testUpdateMerge(self):
+    if not self.enabled:
+      return
+    options = self.Options()
+    options.merge = True
+    scm = gclient_scm.CreateSCM(url=self.url, root_dir=self.root_dir,
+                                relpath=self.relpath)
+    scm._Run(['checkout', '-q', 'feature'], options)
+    rev = scm.revinfo(options, (), None)
+    file_list = []
+    scm.update(options, (), file_list)
+    self.assertEquals(file_list, [join(self.base_path, x)
+                                  for x in ['a', 'b', 'c']])
+    # The actual commit that is created is unstable, so we verify its tree and
+    # parents instead.
+    self.assertEquals(scm._Capture(['rev-parse', 'HEAD:']),
+                      'd2e35c10ac24d6c621e14a1fcadceb533155627d')
+    self.assertEquals(scm._Capture(['rev-parse', 'HEAD^1']), rev)
+    self.assertEquals(scm._Capture(['rev-parse', 'HEAD^2']),
+                      scm._Capture(['rev-parse', 'origin/master']))
+    sys.stdout.close()
+
+  def testUpdateRebase(self):
+    if not self.enabled:
+      return
+    options = self.Options()
+    scm = gclient_scm.CreateSCM(url=self.url, root_dir=self.root_dir,
+                                relpath=self.relpath)
+    scm._Run(['checkout', '-q', 'feature'], options)
+    file_list = []
+    # Fake a 'y' key press.
+    scm._AskForData = self._GetAskForDataCallback(
+        'Cannot fast-forward merge, attempt to rebase? '
+        '(y)es / (q)uit / (s)kip : ', 'y')
+    scm.update(options, (), file_list)
+    self.assertEquals(file_list, [join(self.base_path, x)
+                                  for x in ['a', 'b', 'c']])
+    # The actual commit that is created is unstable, so we verify its tree and
+    # parent instead.
+    self.assertEquals(scm._Capture(['rev-parse', 'HEAD:']),
+                      'd2e35c10ac24d6c621e14a1fcadceb533155627d')
+    self.assertEquals(scm._Capture(['rev-parse', 'HEAD^']),
+                      scm._Capture(['rev-parse', 'origin/master']))
+    sys.stdout.close()
+
   def testUpdateReset(self):
     if not self.enabled:
       return
@@ -1039,7 +1103,9 @@
     file_path = join(self.base_path, 'b')
     open(file_path, 'w').writelines('conflict\n')
     scm._Run(['commit', '-am', 'test'], options)
-    __builtin__.raw_input = lambda x: 'y'
+    scm._AskForData = self._GetAskForDataCallback(
+        'Cannot fast-forward merge, attempt to rebase? '
+        '(y)es / (q)uit / (s)kip : ', 'y')
     exception = ('Conflict while rebasing this branch.\n'
                  'Fix the conflict and run gclient again.\n'
                  'See \'man git-rebase\' for details.\n')