vcssync: context manager to create a temporary Git checkout (bug 1357597); r?glob draft
authorGregory Szorc <gps@mozilla.com>
Wed, 13 Sep 2017 12:06:56 -0700
changeset 11664 a7db7a031df5bc7f18d66fd31f673eda1d829105
parent 11663 2b2fd757c29130e8685df63f56794379f930b3f1
child 11665 b0d4ede2218c3f5e9a8c4e8f7d1484f9fc46e536
push id1785
push usergszorc@mozilla.com
push dateFri, 15 Sep 2017 01:22:24 +0000
reviewersglob
bugs1357597
vcssync: context manager to create a temporary Git checkout (bug 1357597); r?glob This seems like another useful primitive to establish. MozReview-Commit-ID: JtLaZB4jpvd
vcssync/mozvcssync/gitutil.py
--- a/vcssync/mozvcssync/gitutil.py
+++ b/vcssync/mozvcssync/gitutil.py
@@ -5,17 +5,19 @@
 """Utility functions for performing various Git functionality."""
 
 from __future__ import absolute_import, unicode_literals
 
 import contextlib
 import logging
 import os
 import pipes
+import shutil
 import subprocess
+import tempfile
 
 import dulwich.repo
 
 
 logger = logging.getLogger(__name__)
 
 
 class GitCommand(object):
@@ -114,16 +116,71 @@ def update_git_refs(repo, reason, *actio
     p.stdin.close()
     res = p.wait()
     # TODO could use a more rich exception type that captures output.
     if res:
         raise Exception('failed to update git refs')
 
 
 @contextlib.contextmanager
+def temporary_git_checkout(repo, revision_or_ref, branch=None,
+                           sparse_content=None):
+    """Create a temporary Git checkout from a local source repository.
+
+    A temporary directory will be created. It will be initialized into a Git
+    repository shared from the specified source ``repo`` (which must be local).
+    A checkout of the specified ``revision_or_ref`` will be made in the repo.
+
+    If ``branch`` is defined, a new branch will be created of that name.
+
+    If ``sparse_content`` is defined, its content will be written to
+    ``.git/info/sparse-checkout`` and a sparse checkout will be performed.
+
+    The context manager yields a string with the path to the temporary checkout.
+
+    When the context manager exits, the temporary checkout will be deleted.
+    """
+    source = repo.path if isinstance(repo, dulwich.repo.Repo) else repo
+
+    temp_dir = tempfile.mkdtemp(prefix='git-temp-checkout-')
+    try:
+        logger.warn('creating temporary Git checkout in %s' % temp_dir)
+
+        # Initialize the new Git repo with shared storage.
+        subprocess.check_call(
+            [b'git', b'clone', b'--shared', b'--no-checkout',
+             source, temp_dir],
+            cwd='/')
+
+        if sparse_content:
+            subprocess.check_call(
+                [b'git', b'config', b'--local', b'core.sparseCheckout',
+                 b'true'],
+                cwd=temp_dir)
+
+            sparse_path = os.path.join(temp_dir, '.git', 'info',
+                                       'sparse-checkout')
+            with open(sparse_path, 'wb') as fh:
+                fh.write(sparse_content)
+
+        # Do the checkout. This should be the most expensive part.
+        args = [b'git', b'checkout']
+        if branch:
+            args.extend([b'-b', branch])
+        args.append(revision_or_ref)
+        subprocess.check_call(args, cwd=temp_dir)
+
+        yield temp_dir
+
+    finally:
+        logger.warn('removing temporary Git repo %s' % temp_dir)
+        shutil.rmtree(temp_dir)
+
+
+@contextlib.contextmanager
 def temporary_git_refs_mutations(repo, reflog_message=b'restore repo state'):
     """Temporarily mutate Git refs during an active context manager.
 
     When entered, the context manager takes a "snapshot" of the state of the
     Git refs. When it exits, the Git refs are returned to their previous state.
 
     The reflog records mutations that occurred. So any changes made when the
     context manager is active can still be accessed until the reflog expires.