Handle branch or commit refs in the url for unmanaged git solutions

This allows having a git repo cloned on a specific branch or commit, similar to
what's possible for svn solutions by specifying a url like svnrepo/branches/foo

Having the possibility to check the code out on a specific branch makes the
initial code checkout workflow simpler, and allows specifying all the relevant
info directly in the fetch recipe, instead of  having to deal with branches
manually afterwards (which is the whole purpose of recipes in the first place)

So with this change, I'm able to do the following in my custom fetch recipe:

    solution = {
        'name' : 'src',
        'url' : 'git@github.snei.sony.com:SNEI/chromium.git@refs/heads/custombranch',
        <following lines skipped>

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

git-svn-id: svn://svn.chromium.org/chrome/trunk/tools/depot_tools@273675 0039d316-1c4b-4281-b951-d872f2087c98
diff --git a/tests/gclient_scm_test.py b/tests/gclient_scm_test.py
index a79bc25..0341229 100755
--- a/tests/gclient_scm_test.py
+++ b/tests/gclient_scm_test.py
@@ -891,7 +891,9 @@
     TestCaseUtils.setUp(self)
     unittest.TestCase.setUp(self)
     self.url = 'git://foo'
-    self.root_dir = tempfile.mkdtemp()
+    # The .git suffix allows gclient_scm to recognize the dir as a git repo
+    # when cloning it locally
+    self.root_dir = tempfile.mkdtemp('.git')
     self.relpath = '.'
     self.base_path = join(self.root_dir, self.relpath)
     self.enabled = self.CreateGitRepo(self.sample_git_import, self.base_path)
@@ -1377,6 +1379,181 @@
 
 
 class UnmanagedGitWrapperTestCase(BaseGitWrapperTestCase):
+  def checkInStdout(self, expected):
+    value = sys.stdout.getvalue()
+    sys.stdout.close()
+    # pylint: disable=E1101
+    self.assertIn(expected, value)
+
+  def checkNotInStdout(self, expected):
+    value = sys.stdout.getvalue()
+    sys.stdout.close()
+    # pylint: disable=E1101
+    self.assertNotIn(expected, value)
+
+  def getCurrentBranch(self):
+    # Returns name of current branch or HEAD for detached HEAD
+    branch = gclient_scm.scm.GIT.Capture(['rev-parse', '--abbrev-ref', 'HEAD'],
+                                          cwd=self.base_path)
+    if branch == 'HEAD':
+      return None
+    return branch
+
+  def testUpdateClone(self):
+    if not self.enabled:
+      return
+    options = self.Options()
+
+    origin_root_dir = self.root_dir
+    self.root_dir = tempfile.mkdtemp()
+    self.relpath = '.'
+    self.base_path = join(self.root_dir, self.relpath)
+
+    scm = gclient_scm.CreateSCM(url=origin_root_dir,
+                                root_dir=self.root_dir,
+                                relpath=self.relpath)
+
+    expected_file_list = [join(self.base_path, "a"),
+                          join(self.base_path, "b")]
+    file_list = []
+    options.revision = 'unmanaged'
+    scm.update(options, (), file_list)
+
+    self.assertEquals(file_list, expected_file_list)
+    self.assertEquals(scm.revinfo(options, (), None),
+                      '069c602044c5388d2d15c3f875b057c852003458')
+    # indicates detached HEAD
+    self.assertEquals(self.getCurrentBranch(), None)
+    self.checkInStdout(
+      'Checked out refs/remotes/origin/master to a detached HEAD')
+
+    rmtree(origin_root_dir)
+
+  def testUpdateCloneOnCommit(self):
+    if not self.enabled:
+      return
+    options = self.Options()
+
+    origin_root_dir = self.root_dir
+    self.root_dir = tempfile.mkdtemp()
+    self.relpath = '.'
+    self.base_path = join(self.root_dir, self.relpath)
+    url_with_commit_ref = origin_root_dir +\
+                          '@a7142dc9f0009350b96a11f372b6ea658592aa95'
+
+    scm = gclient_scm.CreateSCM(url=url_with_commit_ref,
+                                root_dir=self.root_dir,
+                                relpath=self.relpath)
+
+    expected_file_list = [join(self.base_path, "a"),
+                          join(self.base_path, "b")]
+    file_list = []
+    options.revision = 'unmanaged'
+    scm.update(options, (), file_list)
+
+    self.assertEquals(file_list, expected_file_list)
+    self.assertEquals(scm.revinfo(options, (), None),
+                      'a7142dc9f0009350b96a11f372b6ea658592aa95')
+    # indicates detached HEAD
+    self.assertEquals(self.getCurrentBranch(), None)
+    self.checkInStdout(
+      'Checked out a7142dc9f0009350b96a11f372b6ea658592aa95 to a detached HEAD')
+
+    rmtree(origin_root_dir)
+
+  def testUpdateCloneOnBranch(self):
+    if not self.enabled:
+      return
+    options = self.Options()
+
+    origin_root_dir = self.root_dir
+    self.root_dir = tempfile.mkdtemp()
+    self.relpath = '.'
+    self.base_path = join(self.root_dir, self.relpath)
+    url_with_branch_ref = origin_root_dir + '@feature'
+
+    scm = gclient_scm.CreateSCM(url=url_with_branch_ref,
+                                root_dir=self.root_dir,
+                                relpath=self.relpath)
+
+    expected_file_list = [join(self.base_path, "a"),
+                          join(self.base_path, "b"),
+                          join(self.base_path, "c")]
+    file_list = []
+    options.revision = 'unmanaged'
+    scm.update(options, (), file_list)
+
+    self.assertEquals(file_list, expected_file_list)
+    self.assertEquals(scm.revinfo(options, (), None),
+                      '9a51244740b25fa2ded5252ca00a3178d3f665a9')
+    self.assertEquals(self.getCurrentBranch(), 'feature')
+    self.checkNotInStdout('Checked out feature to a detached HEAD')
+
+    rmtree(origin_root_dir)
+
+  def testUpdateCloneOnDetachedBranch(self):
+    if not self.enabled:
+      return
+    options = self.Options()
+
+    origin_root_dir = self.root_dir
+    self.root_dir = tempfile.mkdtemp()
+    self.relpath = '.'
+    self.base_path = join(self.root_dir, self.relpath)
+    url_with_branch_ref = origin_root_dir + '@refs/remotes/origin/feature'
+
+    scm = gclient_scm.CreateSCM(url=url_with_branch_ref,
+                                root_dir=self.root_dir,
+                                relpath=self.relpath)
+
+    expected_file_list = [join(self.base_path, "a"),
+                          join(self.base_path, "b"),
+                          join(self.base_path, "c")]
+    file_list = []
+    options.revision = 'unmanaged'
+    scm.update(options, (), file_list)
+
+    self.assertEquals(file_list, expected_file_list)
+    self.assertEquals(scm.revinfo(options, (), None),
+                      '9a51244740b25fa2ded5252ca00a3178d3f665a9')
+    # indicates detached HEAD
+    self.assertEquals(self.getCurrentBranch(), None)
+    self.checkInStdout(
+      'Checked out refs/remotes/origin/feature to a detached HEAD')
+
+    rmtree(origin_root_dir)
+
+  def testUpdateCloneOnBranchHead(self):
+    if not self.enabled:
+      return
+    options = self.Options()
+
+    origin_root_dir = self.root_dir
+    self.root_dir = tempfile.mkdtemp()
+    self.relpath = '.'
+    self.base_path = join(self.root_dir, self.relpath)
+    url_with_branch_ref = origin_root_dir + '@refs/heads/feature'
+
+    scm = gclient_scm.CreateSCM(url=url_with_branch_ref,
+                                root_dir=self.root_dir,
+                                relpath=self.relpath)
+
+    expected_file_list = [join(self.base_path, "a"),
+                          join(self.base_path, "b"),
+                          join(self.base_path, "c")]
+    file_list = []
+    options.revision = 'unmanaged'
+    scm.update(options, (), file_list)
+
+    self.assertEquals(file_list, expected_file_list)
+    self.assertEquals(scm.revinfo(options, (), None),
+                      '9a51244740b25fa2ded5252ca00a3178d3f665a9')
+    self.assertEquals(self.getCurrentBranch(), 'feature')
+    self.checkNotInStdout(
+      'Checked out refs/heads/feature to a detached HEAD')
+
+    rmtree(origin_root_dir)
+
   def testUpdateUpdate(self):
     if not self.enabled:
       return