add NewGitCheckout to git_utils.py

rebaseline_server will use this to check out expectations at specific revisions

BUG=skia:1918
TBR=rmistry

Review URL: https://codereview.chromium.org/464413003
diff --git a/py/utils/git_utils.py b/py/utils/git_utils.py
index 5ac63ec..52ad83a 100644
--- a/py/utils/git_utils.py
+++ b/py/utils/git_utils.py
@@ -5,9 +5,12 @@
 
 """This module contains functions for using git."""
 
-
+import os
 import re
 import shell_utils
+import shutil
+import subprocess
+import tempfile
 
 
 def _FindGit():
@@ -144,3 +147,79 @@
         shell_utils.run([GIT, 'checkout', 'master'])
         if self._delete_when_finished:
           shell_utils.run([GIT, 'branch', '-D', self._branch_name])
+
+
+class NewGitCheckout(object):
+  """Creates a new local checkout of a Git repository."""
+
+  def __init__(self, repository, refspec=None, subdir=None,
+               containing_dir=None):
+    """Check out a new local copy of the repository.
+
+    Because this is a new checkout, rather than a reference to an existing
+    checkout on disk, it is safe to assume that the calling thread is the
+    only thread manipulating the checkout.
+
+    You can use the 'with' statement to create this object in such a way that
+    it cleans up after itself:
+
+    with NewGitCheckout(*args) as checkout:
+      # use checkout instance
+    # the checkout is automatically cleaned up here
+
+    Args:
+      repository: name of the remote repository
+      refspec: an arbitrary remote ref (e.g., the name of a branch);
+          if None, allow the git command to pick a default
+      subdir: if specified, the caller only wants access to files within this
+          subdir in the repository.
+          For now, we check out the entire repository regardless of this param,
+          and just hide the rest of the repository; but later on we may
+          optimize performance by only checking out this part of the repo.
+      containing_dir: if specified, the new checkout will be created somewhere
+          within this directory; otherwise, a system-dependent default location
+          will be used, as determined by tempfile.mkdtemp()
+    """
+    # _git_root points to the tree holding the git checkout in its entirety;
+    # _file_root points to the files the caller wants to look at
+    self._git_root = tempfile.mkdtemp(dir=containing_dir)
+    if subdir:
+      self._file_root = os.path.join(self._git_root, subdir)
+    else:
+      self._file_root = self._git_root
+
+    pull_cmd = [GIT, 'pull', repository]
+    if refspec:
+      pull_cmd.append(refspec)
+    self._run_in_git_root(args=[GIT, 'init'])
+    self._run_in_git_root(args=pull_cmd)
+
+  @property
+  def root(self):
+    """Returns the root directory containing the checked-out files.
+
+    If you specified the subdir parameter in the constructor, this directory
+    will point at just the subdir you requested.
+    """
+    return self._file_root
+
+  def commithash(self):
+    """Returns the commithash of the local checkout."""
+    return self._run_in_git_root(
+        args=[GIT, 'rev-parse', 'HEAD']).strip()
+
+  def __enter__(self):
+    return self
+
+  # pylint: disable=W0622
+  def __exit__(self, type, value, traceback):
+    shutil.rmtree(self._git_root)
+
+  def _run_in_git_root(self, args):
+    """Run an external command with cwd set to self._git_root.
+
+    Returns the command's output as a byte string.
+
+    Raises an Exception if the command fails.
+    """
+    return subprocess.check_output(args=args, cwd=self._git_root)
diff --git a/py/utils/git_utils_test.py b/py/utils/git_utils_test.py
new file mode 100755
index 0000000..88a2736
--- /dev/null
+++ b/py/utils/git_utils_test.py
@@ -0,0 +1,103 @@
+#!/usr/bin/python
+
+"""
+Copyright 2014 Google Inc.
+
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+
+Test git_utils.py.
+"""
+
+# System-level imports
+import os
+import tempfile
+import unittest
+
+# Imports from within Skia
+import git_utils
+
+
+# A git repo we can use for tests.
+REPO = 'https://skia.googlesource.com/common'
+
+# A file in some subdirectory within REPO.
+REPO_FILE = os.path.join('py', 'utils', 'git_utils.py')
+
+
+class NewGitCheckoutTest(unittest.TestCase):
+
+  def test_defaults(self):
+    """Test NewGitCheckout created using default parameters."""
+    with git_utils.NewGitCheckout(repository=REPO) as checkout:
+      filepath = os.path.join(checkout.root, REPO_FILE)
+      self.assertTrue(
+          os.path.exists(filepath),
+          'file %s should exist' % filepath)
+    # Confirm that NewGitCheckout cleaned up after itself.
+    self.assertFalse(
+        os.path.exists(filepath),
+        'file %s should not exist' % filepath)
+
+  def test_subdir(self):
+    """Create NewGitCheckout with a specific subdirectory."""
+    subdir = os.path.dirname(REPO_FILE)
+    file_within_subdir = os.path.basename(REPO_FILE)
+
+    containing_dir = tempfile.mkdtemp()
+    try:
+      with git_utils.NewGitCheckout(repository=REPO, subdir=subdir,
+                                    containing_dir=containing_dir) as checkout:
+        self.assertTrue(
+            checkout.root.startswith(containing_dir),
+            'checkout.root %s should be within %s' % (
+                checkout.root, containing_dir))
+        filepath = os.path.join(checkout.root, file_within_subdir)
+        self.assertTrue(
+            os.path.exists(filepath),
+            'file %s should exist' % filepath)
+    finally:
+      os.rmdir(containing_dir)
+
+  def test_refspec(self):
+    """Create NewGitCheckout with a specific refspec.
+
+    This test depends on the fact that the whitespace.txt file was added to the
+    repo in a particular commit.
+    See https://skia.googlesource.com/common/+/c2200447734f13070fb3b2808dea58847241ab0e
+    ('Initial commit of whitespace.txt')
+    """
+    filename = 'whitespace.txt'
+    hash_without_file = 'f63e1cfff23615157e28942af5f5e8298351cb10'
+    hash_with_file = 'c2200447734f13070fb3b2808dea58847241ab0e'
+
+    with git_utils.NewGitCheckout(
+        repository=REPO, refspec=hash_without_file) as checkout:
+      filepath = os.path.join(checkout.root, filename)
+      self.assertEquals(
+          hash_without_file, checkout.commithash(),
+          '"%s" != "%s"' % (hash_without_file, checkout.commithash()))
+      self.assertFalse(
+          os.path.exists(filepath),
+          'file %s should not exist' % filepath)
+
+    with git_utils.NewGitCheckout(
+        repository=REPO, refspec=hash_with_file) as checkout:
+      filepath = os.path.join(checkout.root, filename)
+      self.assertEquals(
+          hash_with_file, checkout.commithash(),
+          '"%s" != "%s"' % (hash_with_file, checkout.commithash()))
+      self.assertTrue(
+          os.path.exists(filepath),
+          'file %s should exist' % filepath)
+
+
+def main(test_case_class):
+  """Run the unit tests within this class."""
+  suite = unittest.TestLoader().loadTestsFromTestCase(NewGitCheckoutTest)
+  results = unittest.TextTestRunner(verbosity=2).run(suite)
+  if not results.wasSuccessful():
+    raise Exception('failed unittest %s' % test_case_class)
+
+if __name__ == '__main__':
+  main(NewGitCheckoutTest)