bug 1294565 - add some more helpers to mozversioncontrol, add MozbuildObject.repository. r=gps
authorTed Mielczarek <ted@mielczarek.org>
Thu, 29 Sep 2016 06:48:37 -0400
changeset 316236 ab11e060be3b22980b0a96532370ca9c20f26f05
parent 316235 caa8bf0eea94fb260be13176363c7b7083609cac
child 316237 6a14bfe49c90a45a832f2697b5daadd4355d2616
push id30766
push userphilringnalda@gmail.com
push dateTue, 04 Oct 2016 03:09:34 +0000
treeherdermozilla-central@c8a660c5f105 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
bug 1294565 - add some more helpers to mozversioncontrol, add MozbuildObject.repository. r=gps I wanted to be able to do some VCS interaction from a mach command, and we didn't have anything suitable, so I tore up mozversioncontrol and replaced it with a framework to hang new features off of. I've only implemented the bits I need currently (get_modified_files and add_remove_files), but it should be straightforward to add more functionality there. This patch also adds a `repository` property to `MozbuildObject`, which will return a `Repository` object for the topsrcdir to make using these helpers even easier for `MozbuildObject`-derived classes. MozReview-Commit-ID: Gw6Ixp1ltiN
--- a/build/virtualenv_packages.txt
+++ b/build/virtualenv_packages.txt
@@ -3,16 +3,17 @@ marionette_driver.pth:testing/marionette
--- a/python/mozbuild/mozbuild/base.py
+++ b/python/mozbuild/mozbuild/base.py
@@ -10,16 +10,17 @@ import mozpack.path as mozpath
 import multiprocessing
 import os
 import subprocess
 import sys
 import which
 from mach.mixin.logging import LoggingMixin
 from mach.mixin.process import ProcessExecutionMixin
+from mozversioncontrol import get_repository_object
 from .backend.configenvironment import ConfigEnvironment
 from .controller.clobber import Clobberer
 from .mozconfig import (
@@ -276,16 +277,22 @@ class MozbuildObject(ProcessExecutionMix
                 if line.startswith('export '):
                     exports = shellutil.split(line)[1:]
                     for e in exports:
                         if '=' in e:
                             key, value = e.split('=')
                             env[key] = value
         return env
+    @memoized_property
+    def repository(self):
+        '''Get a `mozversioncontrol.Repository` object for the
+        top source directory.'''
+        return get_repository_object(self.topsrcdir)
     def is_clobber_needed(self):
         if not os.path.exists(self.topobjdir):
             return False
         return Clobberer(self.topsrcdir, self.topobjdir).clobber_needed()
     def get_binary_path(self, what='app', validate_exists=True, where='default'):
         """Obtain the path to a compiled binary for this build configuration.
--- a/python/mozversioncontrol/mozversioncontrol/__init__.py
+++ b/python/mozversioncontrol/mozversioncontrol/__init__.py
@@ -1,45 +1,105 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this,
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
-from __future__ import unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
 import os
 import re
 import subprocess
 import which
 from distutils.version import LooseVersion
-def get_hg_path():
-    """Obtain the path of the Mercurial client."""
+def get_tool_path(tool):
+    """Obtain the path of `tool`."""
     # We use subprocess in places, which expects a Win32 executable or
     # batch script. On some versions of MozillaBuild, we have "hg.exe",
     # "hg.bat," and "hg" (a Python script). "which" will happily return the
     # Python script, which will cause subprocess to choke. Explicitly favor
     # the Windows version over the plain script.
-        return which.which('hg.exe')
+        return which.which(tool + '.exe')
     except which.WhichError:
-            return which.which('hg')
+            return which.which(tool)
         except which.WhichError as e:
-    raise Exception('Unable to obtain Mercurial path. Try running ' +
+    raise Exception('Unable to obtain %s path. Try running ' +
                     '|mach bootstrap| to ensure your environment is up to ' +
-                    'date.')
+                    'date.' % tool)
+class Repository(object):
+    '''A class wrapping utility methods around version control repositories.'''
+    def __init__(self, path, tool):
+        self.path = os.path.abspath(path)
+        self._tool = get_tool_path(tool)
+        self._env = os.environ.copy()
+        self._version = None
+    def _run(self, *args):
+        return subprocess.check_output((self._tool, ) + args,
+                                       cwd=self.path,
+                                       env=self._env)
-def get_hg_version(hg):
-    """Obtain the version of the Mercurial client."""
+    @property
+    def tool_version(self):
+        '''Return the version of the VCS tool in use as a `LooseVersion`.'''
+        if self._version:
+            return self._version
+        info = self._run('--version').strip()
+        match = re.search('version ([^\+\)]+)', info)
+        if not match:
+            raise Exception('Unable to identify tool version.')
+        self.version = LooseVersion(match.group(1))
+        return self.version
+    def get_modified_files(self):
+        '''Return a list of files that are modified in this repository's
+        working copy.'''
+        raise NotImplementedError
-    env = os.environ.copy()
-    env[b'HGPLAIN'] = b'1'
+    def add_remove_files(self, path):
+        '''Add and remove files under `path` in this repository's working copy.
+        '''
+        raise NotImplementedError
+class HgRepository(Repository):
+    '''An implementation of `Repository` for Mercurial repositories.'''
+    def __init__(self, path):
+        super(HgRepository, self).__init__(path, 'hg')
+        self._env[b'HGPLAIN'] = b'1'
+    def get_modified_files(self):
+        return [line.strip().split()[1] for line in self._run('status', '--modified').splitlines()]
+    def add_remove_files(self, path):
+        args = ['addremove', path]
+        if self.tool_version >= b'3.9':
+            args = ['--config', 'extensions.automv='] + args
+        self._run(*args)
-    info = subprocess.check_output([hg, '--version'], env=env)
-    match = re.search('version ([^\+\)]+)', info)
-    if not match:
-        raise Exception('Unable to identify Mercurial version.')
+class GitRepository(Repository):
+    '''An implementation of `Repository` for Git repositories.'''
+    def __init__(self, path):
+        super(GitRepository, self).__init__(path, 'git')
+    def get_modified_files(self):
+        # This is a little wonky, but it's good enough for this purpose.
+        return [bits[1] for bits in map(lambda line: line.strip().split(), self._run('status', '--porcelain').splitlines()) if 'M' in bits[0]]
-    return LooseVersion(match.group(1))
+    def add_remove_files(self, path):
+        self._run('add', path)
+def get_repository_object(path):
+    '''Get a repository object for the repository at `path`.
+    If `path` is not a known VCS repository, raise an exception.
+    '''
+    if os.path.isdir(os.path.join(path, '.hg')):
+        return HgRepository(path)
+    elif os.path.isdir(os.path.join(path, '.git')):
+        return GitRepository(path)
+    else:
+        raise Exception('Unknown VCS, or not a source checkout: %s' % path)