Bug 1384593 - Abstract version control functionality out of syntax.py to vcs.py, r?armenzg
This copies the vcs abstraction from python/mozlint/mozlint/vcs.py. Consumers can call:
VCSHelper.create()
and that will automatically detect whether we're in hg or git and return the appropriate
abstraction class.
MozReview-Commit-ID: 4xHwZ9fATLv
--- a/tools/tryselect/selectors/syntax.py
+++ b/tools/tryselect/selectors/syntax.py
@@ -3,22 +3,21 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
from __future__ import absolute_import, print_function, unicode_literals
import ConfigParser
import argparse
import os
import re
-import subprocess
import sys
-import which
from collections import defaultdict
import mozpack.path as mozpath
+from ..vcs import VCSHelper
def arg_parser():
parser = argparse.ArgumentParser()
parser.add_argument('paths', nargs='*', help='Paths to search for tests to run on try.')
parser.add_argument('-b', '--build', dest='builds', default='do',
help='Build types to run (d for debug, o for optimized).')
parser.add_argument('-p', '--platform', dest='platforms', action='append',
@@ -271,21 +270,17 @@ class AutoTry(object):
},
}
def __init__(self, topsrcdir, resolver_func, mach_context):
self.topsrcdir = topsrcdir
self._resolver_func = resolver_func
self._resolver = None
self.mach_context = mach_context
-
- if os.path.exists(os.path.join(self.topsrcdir, '.hg')):
- self._use_git = False
- else:
- self._use_git = True
+ self.vcs = VCSHelper.create()
@property
def resolver(self):
if self._resolver is None:
self._resolver = self._resolver_func()
return self._resolver
@property
@@ -479,121 +474,19 @@ class AutoTry(object):
parts.append(arg)
parts.append(e)
if action in ('store_true', 'store_false'):
parts.append(arg)
try_syntax = " ".join(parts)
return try_syntax
- def _run_git(self, *args):
- args = ['git'] + list(args)
- ret = subprocess.call(args)
- if ret:
- print('ERROR git command %s returned %s' %
- (args, ret))
- sys.exit(1)
-
- def _git_push_to_try(self, msg):
- self._run_git('commit', '--allow-empty', '-m', msg)
- try:
- self._run_git('push', 'hg::ssh://hg.mozilla.org/try',
- '+HEAD:refs/heads/branches/default/tip')
- finally:
- self._run_git('reset', 'HEAD~')
-
- def _git_find_changed_files(self):
- # This finds the files changed on the current branch based on the
- # diff of the current branch its merge-base base with other branches.
- try:
- args = ['git', 'rev-parse', 'HEAD']
- current_branch = subprocess.check_output(args).strip()
- args = ['git', 'for-each-ref', 'refs/heads', 'refs/remotes',
- '--format=%(objectname)']
- all_branches = subprocess.check_output(args).splitlines()
- other_branches = set(all_branches) - set([current_branch])
- args = ['git', 'merge-base', 'HEAD'] + list(other_branches)
- base_commit = subprocess.check_output(args).strip()
- args = ['git', 'diff', '--name-only', '-z', 'HEAD', base_commit]
- return subprocess.check_output(args).strip('\0').split('\0')
- except subprocess.CalledProcessError as e:
- print('Failed while determining files changed on this branch')
- print('Failed whle running: %s' % args)
- print(e.output)
- sys.exit(1)
-
- def _hg_find_changed_files(self):
- hg_args = [
- 'hg', 'log', '-r',
- '::. and not public()',
- '--template',
- '{join(files, "\n")}\n',
- ]
- try:
- return subprocess.check_output(hg_args).splitlines()
- except subprocess.CalledProcessError as e:
- print('Failed while finding files changed since the last '
- 'public ancestor')
- print('Failed whle running: %s' % hg_args)
- print(e.output)
- sys.exit(1)
-
- def find_changed_files(self):
- """Finds files changed in a local source tree.
-
- For hg, changes since the last public ancestor of '.' are
- considered. For git, changes in the current branch are considered.
- """
- if self._use_git:
- return self._git_find_changed_files()
- return self._hg_find_changed_files()
-
- def push_to_try(self, msg, verbose):
- if not self._use_git:
- try:
- hg_args = ['hg', 'push-to-try', '-m', msg]
- subprocess.check_call(hg_args, stderr=subprocess.STDOUT)
- except subprocess.CalledProcessError as e:
- print('ERROR hg command %s returned %s' % (hg_args, e.returncode))
- print('\nmach failed to push to try. There may be a problem '
- 'with your ssh key, or another issue with your mercurial '
- 'installation.')
- # Check for the presence of the "push-to-try" extension, and
- # provide instructions if it can't be found.
- try:
- subprocess.check_output(['hg', 'showconfig',
- 'extensions.push-to-try'])
- except subprocess.CalledProcessError:
- print('\nThe "push-to-try" hg extension is required. It '
- 'can be installed to Mercurial 3.3 or above by '
- 'running ./mach mercurial-setup')
- sys.exit(1)
- else:
- try:
- which.which('git-cinnabar')
- self._git_push_to_try(msg)
- except which.WhichError:
- print('ERROR git-cinnabar is required to push from git to try with'
- 'the autotry command.\n\nMore information can by found at '
- 'https://github.com/glandium/git-cinnabar')
- sys.exit(1)
-
- def find_uncommited_changes(self):
- if self._use_git:
- stat = subprocess.check_output(['git', 'status', '-z'])
- return any(len(entry.strip()) and entry.strip()[0] in ('A', 'M', 'D')
- for entry in stat.split('\0'))
- else:
- stat = subprocess.check_output(['hg', 'status'])
- return any(len(entry.strip()) and entry.strip()[0] in ('A', 'M', 'R')
- for entry in stat.splitlines())
-
def find_paths_and_tags(self, verbose):
paths, tags = set(), set()
- changed_files = self.find_changed_files()
+ changed_files = self.vcs.files_changed
if changed_files:
if verbose:
print("Pushing tests based on modifications to the "
"following files:\n\t%s" % "\n\t".join(changed_files))
from mozbuild.frontend.reader import (
BuildReader,
EmptyConfig,
@@ -705,20 +598,16 @@ class AutoTry(object):
if defaults is None:
print("No saved configuration called %s found in autotry.ini" % kwargs["load"],
file=sys.stderr)
for key, value in kwargs.iteritems():
if value in (None, []) and key in defaults:
kwargs[key] = defaults[key]
- if kwargs["push"] and self.find_uncommited_changes():
- print('ERROR please commit changes before continuing')
- sys.exit(1)
-
if not any(kwargs[item] for item in ("paths", "tests", "tags")):
kwargs["paths"], kwargs["tags"] = self.find_paths_and_tags(kwargs["verbose"])
builds, platforms, tests, talos, jobs, paths, tags, extra = self.validate_args(**kwargs)
if paths or tags:
paths = [os.path.relpath(os.path.normpath(os.path.abspath(item)), self.topsrcdir)
for item in paths]
@@ -768,12 +657,12 @@ class AutoTry(object):
print('The following tests will be selected: ')
for flavor, paths in paths_by_flavor.iteritems():
print("%s: %s" % (flavor, ",".join(paths)))
if kwargs["verbose"] or not kwargs["push"]:
print('The following try syntax was calculated:\n%s' % msg)
if kwargs["push"]:
- self.push_to_try(msg, kwargs["verbose"])
+ self.vcs.push_to_try(msg)
if kwargs["save"] is not None:
self.save_config(kwargs["save"], msg)
new file mode 100644
--- /dev/null
+++ b/tools/tryselect/vcs.py
@@ -0,0 +1,160 @@
+# 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/.
+
+import subprocess
+import sys
+from abc import ABCMeta, abstractmethod, abstractproperty
+from distutils.spawn import find_executable
+
+GIT_CINNABAR_NOT_FOUND = """
+Could not detect `git-cinnabar`.
+
+The `mach try` command requires git-cinnabar to be installed when
+pushing from git. For more information and installation instruction,
+please see:
+
+ https://github.com/glandium/git-cinnabar
+""".lstrip()
+
+HG_PUSH_TO_TRY_NOT_FOUND = """
+Could not detect `push-to-try`.
+
+The `mach try` command requires the push-to-try extension enabled
+when pushing from hg. Please install it by running:
+
+ $ ./mach mercurial-setup
+""".lstrip()
+
+VCS_NOT_FOUND = """
+Could not detect version control. Only `hg` or `git` are supported.
+""".strip()
+
+UNCOMMITTED_CHANGES = """
+ERROR please commit changes before continuing
+""".strip()
+
+
+class VCSHelper(object):
+ """A abstract base VCS helper that detects hg or git"""
+ __metaclass__ = ABCMeta
+
+ def __init__(self, root):
+ self.root = root
+
+ @classmethod
+ def find_vcs(cls):
+ # First check if we're in an hg repo, if not try git
+ commands = (
+ ['hg', 'root'],
+ ['git', 'rev-parse', '--show-toplevel'],
+ )
+
+ for cmd in commands:
+ proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ output = proc.communicate()[0].strip()
+
+ if proc.returncode == 0:
+ return cmd[0], output
+ return None, ''
+
+ @classmethod
+ def create(cls):
+ vcs, root = cls.find_vcs()
+ if not vcs:
+ print(VCS_NOT_FOUND)
+ sys.exit(1)
+ return vcs_class[vcs](root)
+
+ def run(self, cmd):
+ try:
+ return subprocess.check_output(cmd, stderr=subprocess.STDOUT)
+ except subprocess.CalledProcessError as e:
+ print("Error running `{}`:".format(' '.join(cmd)))
+ print(e.output)
+ raise
+
+ def check_working_directory(self):
+ if self.has_uncommitted_changes:
+ print(UNCOMMITTED_CHANGES)
+ sys.exit(1)
+
+ @abstractmethod
+ def push_to_try(self, msg):
+ pass
+
+ @abstractproperty
+ def files_changed(self):
+ pass
+
+ @abstractproperty
+ def has_uncommitted_changes(self):
+ pass
+
+
+class HgHelper(VCSHelper):
+
+ def push_to_try(self, msg):
+ self.check_working_directory()
+
+ try:
+ return subprocess.check_call(['hg', 'push-to-try', '-m', msg])
+ except subprocess.CalledProcessError:
+ try:
+ self.run(['hg', 'showconfig', 'extensions.push-to-try'])
+ except subprocess.CalledProcessError:
+ print(HG_PUSH_TO_TRY_NOT_FOUND)
+ return 1
+ finally:
+ self.run(['hg', 'revert', '-a'])
+
+ @property
+ def files_changed(self):
+ return self.run(['hg', 'log', '-r', '::. and not public()',
+ '--template', '{join(files, "\n")}\n'])
+
+ @property
+ def has_uncommitted_changes(self):
+ stat = [s for s in self.run(['hg', 'status', '-amrn']).split() if s]
+ return len(stat) > 0
+
+
+class GitHelper(VCSHelper):
+
+ def push_to_try(self, msg):
+ self.check_working_directory()
+
+ if not find_executable('git-cinnabar'):
+ print(GIT_CINNABAR_NOT_FOUND)
+ sys.exit(1)
+
+ subprocess.check_call(['git', 'commit', '--allow-empty', '-m', msg])
+ try:
+ return subprocess.call(['git', 'push', 'hg::ssh://hg.mozilla.org/try',
+ '+HEAD:refs/heads/branches/default/tip'])
+ finally:
+ self.run(['git', 'reset', 'HEAD~'])
+
+ @property
+ def files_changed(self):
+ # This finds the files changed on the current branch based on the
+ # diff of the current branch its merge-base base with other branches.
+ current_branch = self.run(['git', 'rev-parse', 'HEAD']).strip()
+ all_branches = self.run(['git', 'for-each-ref', 'refs/heads', 'refs/remotes',
+ '--format=%(objectname)']).splitlines()
+ other_branches = set(all_branches) - set([current_branch])
+ base_commit = self.run(['git', 'merge-base', 'HEAD'] + list(other_branches)).strip()
+ return self.run(['git', 'diff', '--name-only', '-z', 'HEAD',
+ base_commit]).strip('\0').split('\0')
+
+ @property
+ def has_uncommitted_changes(self):
+ stat = [s for s in self.run(['git', 'diff', '--cached', '--name-only',
+ '--diff-filter=AMD']).split() if s]
+ return len(stat) > 0
+
+
+vcs_class = {
+ 'git': GitHelper,
+ 'hg': HgHelper,
+}