--- a/python/mozversioncontrol/mozversioncontrol/__init__.py
+++ b/python/mozversioncontrol/mozversioncontrol/__init__.py
@@ -13,16 +13,32 @@ import which
from distutils.version import LooseVersion
class MissingVCSTool(Exception):
"""Represents a failure to find a version control tool binary."""
+class MissingVCSInfo(Exception):
+ """Represents a general failure to resolve a VCS interface."""
+
+
+class MissingConfigureInfo(MissingVCSInfo):
+ """Represents error finding VCS info from configure data."""
+
+
+class InvalidRepoPath(Exception):
+ """Represents a failure to find a VCS repo at a specified path."""
+
+
+class MissingUpstreamRepo(Exception):
+ """Represents a failure to automatically detect an upstream repo."""
+
+
def get_tool_path(tool):
"""Obtain the path of `tool`."""
if os.path.isabs(tool) and os.path.exists(tool):
return 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
@@ -94,16 +110,20 @@ class Repository(object):
A sparse checkout is defined as a working directory that only
materializes a subset of files in a given revision.
Returns a bool.
"""
@abc.abstractmethod
+ def get_upstream(self):
+ """Reference to the upstream remote."""
+
+ @abc.abstractmethod
def get_changed_files(self, diff_filter, mode='unstaged'):
"""Return a list of files that are changed in this repository's
working copy.
``diff_filter`` controls which kinds of modifications are returned.
It is a string which may only contain the following characters:
A - Include files that were added
@@ -112,16 +132,26 @@ class Repository(object):
By default, all three will be included.
``mode`` can be one of 'unstaged', 'staged' or 'all'. Only has an
affect on git. Defaults to 'unstaged'.
"""
@abc.abstractmethod
+ def get_outgoing_files(self, diff_filter, upstream='default'):
+ """Return a list of changed files compared to upstream.
+
+ ``diff_filter`` works the same as `get_changed_files`.
+ ``upstream`` is a remote ref to compare against. If unspecified,
+ this will be determined automatically. If there is no remote ref,
+ a MissingUpstreamRepo exception will be raised.
+ """
+
+ @abc.abstractmethod
def add_remove_files(self, path):
'''Add and remove files under `path` in this repository's working copy.
'''
@abc.abstractmethod
def forget_add_remove_files(self, path):
'''Undo the effects of a previous add_remove_files call for `path`.
'''
@@ -200,32 +230,49 @@ class HgRepository(Repository):
st = os.stat(sparse)
return st.st_size > 0
except OSError as e:
if e.errno != errno.ENOENT:
raise
return False
+ def get_upstream(self):
+ return 'default'
+
def _format_diff_filter(self, diff_filter):
df = diff_filter.lower()
assert all(f in self._valid_diff_filter for f in df)
# Mercurial uses 'r' to denote removed files whereas git uses 'd'.
if 'd' in df:
df.replace('d', 'r')
return df.lower()
def get_changed_files(self, diff_filter='ADM', mode='unstaged'):
df = self._format_diff_filter(diff_filter)
# Use --no-status to print just the filename.
return self._run('status', '--no-status', '-{}'.format(df)).splitlines()
+ def get_outgoing_files(self, diff_filter='ADM', upstream='default'):
+ df = self._format_diff_filter(diff_filter)
+
+ template = ''
+ if 'a' in df:
+ template += "{file_adds % '\\n{file}'}"
+ if 'd' in df:
+ template += "{file_dels % '\\n{file}'}"
+ if 'm' in df:
+ template += "{file_mods % '\\n{file}'}"
+
+ return self._run('outgoing', '-r', '.', '--quiet',
+ '--template', template, upstream).split()
+
def add_remove_files(self, path):
args = ['addremove', path]
if self.tool_version >= b'3.9':
args = ['--config', 'extensions.automv='] + args
self._run(*args)
def forget_add_remove_files(self, path):
self._run('forget', path)
@@ -257,27 +304,46 @@ class GitRepository(Repository):
@property
def name(self):
return 'git'
def sparse_checkout_present(self):
# Not yet implemented.
return False
+ def get_upstream(self):
+ ref = self._run('symbolic-ref', '-q', 'HEAD').strip()
+ upstream = self._run('for-each-ref', '--format=%(upstream:short)', ref).strip()
+
+ if not upstream:
+ raise MissingUpstreamRepo("Could not detect an upstream repository.")
+
+ return upstream
+
def get_changed_files(self, diff_filter='ADM', mode='unstaged'):
assert all(f.lower() in self._valid_diff_filter for f in diff_filter)
cmd = ['diff', '--diff-filter={}'.format(diff_filter.upper()), '--name-only']
if mode == 'staged':
cmd.append('--cached')
elif mode == 'all':
cmd.append('HEAD')
return self._run(*cmd).splitlines()
+ def get_outgoing_files(self, diff_filter='ADM', upstream='default'):
+ assert all(f.lower() in self._valid_diff_filter for f in diff_filter)
+
+ if upstream == 'default':
+ upstream = self.get_upstream()
+
+ compare = '{}..HEAD'.format(upstream)
+ return self._run('log', '--name-only', '--diff-filter={}'.format(diff_filter.upper()),
+ '--oneline', '--pretty=format:', compare).splitlines()
+
def add_remove_files(self, path):
self._run('add', path)
def forget_add_remove_files(self, path):
self._run('reset', path)
def get_files_in_working_directory(self):
return self._run('ls-files', '-z').split(b'\0')
@@ -287,41 +353,29 @@ class GitRepository(Repository):
if untracked:
args.append('--untracked-files')
if ignored:
args.append('--ignored')
return not len(self._run(*args).strip())
-class InvalidRepoPath(Exception):
- """Represents a failure to find a VCS repo at a specified path."""
-
-
def get_repository_object(path, hg='hg', git='git'):
'''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, hg=hg)
elif os.path.exists(os.path.join(path, '.git')):
return GitRepository(path, git=git)
else:
raise InvalidRepoPath('Unknown VCS, or not a source checkout: %s' %
path)
-class MissingVCSInfo(Exception):
- """Represents a general failure to resolve a VCS interface."""
-
-
-class MissingConfigureInfo(MissingVCSInfo):
- """Represents error finding VCS info from configure data."""
-
-
def get_repository_from_build_config(config):
"""Obtain a repository from the build configuration.
Accepts an object that has a ``topsrcdir`` and ``subst`` attribute.
"""
flavor = config.substs.get('VCS_CHECKOUT_TYPE')
# If in build mode, only use what configure found. That way we ensure