Bug 1413922 - [tryselect] Merge vcs.py into mozversioncontrol r=gps

Differential Revision: https://phabricator.services.mozilla.com/D1808

--HG--
rename : tools/tryselect/vcs.py => tools/tryselect/push.py
extra : moz-landing-system : lando
This commit is contained in:
Andrew Halberstadt 2018-07-07 00:32:11 +00:00
Родитель cd200f6d34
Коммит 23c2416671
8 изменённых файлов: 243 добавлений и 228 удалений

Просмотреть файл

@ -26,6 +26,15 @@ class MissingConfigureInfo(MissingVCSInfo):
"""Represents error finding VCS info from configure data."""
class MissingVCSExtension(MissingVCSInfo):
"""Represents error finding a required VCS extension."""
def __init__(self, ext):
self.ext = ext
msg = "Could not detect required extension '{}'".format(self.ext)
super(MissingVCSExtension, self).__init__(msg)
class InvalidRepoPath(Exception):
"""Represents a failure to find a VCS repo at a specified path."""
@ -108,6 +117,11 @@ class Repository(object):
self.version = LooseVersion(match.group(1))
return self.version
@property
def has_git_cinnabar(self):
"""True if the repository is using git cinnabar."""
return False
@abc.abstractproperty
def name(self):
"""Name of the tool."""
@ -188,6 +202,16 @@ class Repository(object):
to factor these file classes into consideration.
"""
@abc.abstractmethod
def push_to_try(self, message):
"""Create a temporary commit, push it to try and clean it up
afterwards.
With mercurial, MissingVCSExtension will be raised if the `push-to-try`
extension is not installed. On git, MissingVCSExtension will be raised
if git cinnabar is not present.
"""
class HgRepository(Repository):
'''An implementation of `Repository` for Mercurial repositories.'''
@ -306,7 +330,7 @@ class HgRepository(Repository):
self._run(b'files', b'-0').split(b'\0') if p)
def working_directory_clean(self, untracked=False, ignored=False):
args = [b'status', b'\0', b'--modified', b'--added', b'--removed',
args = [b'status', b'--modified', b'--added', b'--removed',
b'--deleted']
if untracked:
args.append(b'--unknown')
@ -317,6 +341,18 @@ class HgRepository(Repository):
# means we are clean.
return not len(self._run(*args).strip())
def push_to_try(self, message):
try:
subprocess.check_call((self._tool, 'push-to-try', '-m', message), cwd=self.path)
except subprocess.CalledProcessError:
try:
self._run('showconfig', 'extensions.push-to-try')
except subprocess.CalledProcessError:
raise MissingVCSExtension('push-to-try')
raise
finally:
self._run('revert', '-a')
class GitRepository(Repository):
'''An implementation of `Repository` for Git repositories.'''
@ -340,6 +376,14 @@ class GitRepository(Repository):
refs.remove(head)
return self._run('merge-base', 'HEAD', *refs).strip()
@property
def has_git_cinnabar(self):
try:
self._run('cinnabar', '--version')
except subprocess.CalledProcessError:
return False
return True
def sparse_checkout_present(self):
# Not yet implemented.
return False
@ -393,6 +437,17 @@ class GitRepository(Repository):
return not len(self._run(*args).strip())
def push_to_try(self, message):
if not self.has_git_cinnabar:
raise MissingVCSExtension('cinnabar')
self._run('commit', '--allow-empty', '-m', message)
try:
subprocess.check_call((self._tool, 'push', 'hg::ssh://hg.mozilla.org/try',
'+HEAD:refs/heads/branches/default/tip'), cwd=self.path)
finally:
self._run('reset', 'HEAD~')
def get_repository_object(path, hg='hg', git='git'):
'''Get a repository object for the repository at `path`.

Просмотреть файл

@ -3,4 +3,5 @@ subsuite=mozversioncontrol
skip-if = python == 3
[test_context_manager.py]
[test_push_to_try.py]
[test_workdir_outgoing.py]

Просмотреть файл

@ -0,0 +1,71 @@
# 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 absolute_import, unicode_literals
import subprocess
import mozunit
import pytest
from mozversioncontrol import (
get_repository_object,
MissingVCSExtension,
)
def test_push_to_try(repo, monkeypatch):
commit_message = "commit message"
vcs = get_repository_object(repo.strpath)
captured_commands = []
def fake_run(*args, **kwargs):
captured_commands.append(args[0])
monkeypatch.setattr(subprocess, 'check_output', fake_run)
monkeypatch.setattr(subprocess, 'check_call', fake_run)
vcs.push_to_try(commit_message)
tool = vcs._tool
if repo.vcs == 'hg':
expected = [
(tool, 'push-to-try', '-m', commit_message),
(tool, 'revert', '-a'),
]
else:
expected = [
(tool, 'cinnabar', '--version'),
(tool, 'commit', '--allow-empty', '-m', commit_message),
(tool, 'push', 'hg::ssh://hg.mozilla.org/try',
'+HEAD:refs/heads/branches/default/tip'),
(tool, 'reset', 'HEAD~'),
]
for i, value in enumerate(captured_commands):
assert value == expected[i]
assert len(captured_commands) == len(expected)
def test_push_to_try_missing_extensions(repo, monkeypatch):
vcs = get_repository_object(repo.strpath)
orig = vcs._run
def cinnabar_raises(*args, **kwargs):
# Simulate not having git cinnabar
if args[0] == 'cinnabar':
raise subprocess.CalledProcessError(1, args)
return orig(*args, **kwargs)
monkeypatch.setattr(vcs, '_run', cinnabar_raises)
with pytest.raises(MissingVCSExtension):
vcs.push_to_try("commit message")
if __name__ == '__main__':
mozunit.main()

104
tools/tryselect/push.py Normal file
Просмотреть файл

@ -0,0 +1,104 @@
# 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 absolute_import, print_function
import json
import os
import sys
from mozbuild.base import MozbuildObject
from mozversioncontrol import get_repository_object, MissingVCSExtension
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()
here = os.path.abspath(os.path.dirname(__file__))
build = MozbuildObject.from_environment(cwd=here)
vcs = get_repository_object(build.topsrcdir)
def write_task_config(labels, templates=None):
config = os.path.join(vcs.path, 'try_task_config.json')
with open(config, 'w') as fh:
try_task_config = {'tasks': sorted(labels)}
if templates:
try_task_config['templates'] = templates
json.dump(try_task_config, fh, indent=2, separators=(',', ':'))
fh.write('\n')
return config
def check_working_directory(push=True):
if not push:
return
if not vcs.working_directory_clean():
print(UNCOMMITTED_CHANGES)
sys.exit(1)
def push_to_try(method, msg, labels=None, templates=None, push=True, closed_tree=False):
check_working_directory(push)
# Format the commit message
closed_tree_string = " ON A CLOSED TREE" if closed_tree else ""
commit_message = ('%s%s\n\nPushed via `mach try %s`' %
(msg, closed_tree_string, method))
config = None
if labels or labels == []:
config = write_task_config(labels, templates)
try:
if not push:
print("Commit message:")
print(commit_message)
if config:
print("Calculated try_task_config.json:")
with open(config) as fh:
print(fh.read())
return
if config:
vcs.add_remove_files(config)
try:
vcs.push_to_try(commit_message)
except MissingVCSExtension as e:
if e.ext == 'push-to-try':
print(HG_PUSH_TO_TRY_NOT_FOUND)
elif e.ext == 'cinnabar':
print(GIT_CINNABAR_NOT_FOUND)
else:
raise
sys.exit(1)
finally:
if config and os.path.isfile(config):
os.remove(config)

Просмотреть файл

@ -5,7 +5,7 @@
from __future__ import absolute_import, print_function, unicode_literals
from ..cli import BaseTryParser
from ..vcs import VCSHelper
from ..push import push_to_try
class EmptyParser(BaseTryParser):
@ -14,7 +14,6 @@ class EmptyParser(BaseTryParser):
def run_empty_try(message='{msg}', push=True, **kwargs):
vcs = VCSHelper.create()
msg = 'No try selector specified, use "Add New Jobs" to select tasks.'
return vcs.push_to_try('empty', message.format(msg=msg), [], push=push,
closed_tree=kwargs["closed_tree"])
return push_to_try('empty', message.format(msg=msg), [], push=push,
closed_tree=kwargs["closed_tree"])

Просмотреть файл

@ -19,7 +19,7 @@ from six import string_types
from .. import preset as pset
from ..cli import BaseTryParser
from ..tasks import generate_tasks
from ..vcs import VCSHelper
from ..push import check_working_directory, push_to_try, vcs
terminal = Terminal()
@ -221,10 +221,8 @@ def run_fuzzy_try(update=False, query=None, templates=None, full=False, paramete
print(FZF_NOT_FOUND)
return 1
vcs = VCSHelper.create()
vcs.check_working_directory(push)
all_tasks = generate_tasks(parameters, full, root=vcs.root)
check_working_directory(push)
all_tasks = generate_tasks(parameters, full, root=vcs.path)
if paths:
all_tasks = filter_by_paths(all_tasks, paths)
@ -281,5 +279,5 @@ def run_fuzzy_try(update=False, query=None, templates=None, full=False, paramete
args.extend(["query={}".format(q) for q in queries])
if args:
msg = "{} {}".format(msg, '&'.join(args))
return vcs.push_to_try('fuzzy', message.format(msg=msg), selected, templates, push=push,
closed_tree=kwargs["closed_tree"])
return push_to_try('fuzzy', message.format(msg=msg), selected, templates, push=push,
closed_tree=kwargs["closed_tree"])

Просмотреть файл

@ -14,7 +14,7 @@ from moztest.resolve import TestResolver
from .. import preset
from ..cli import BaseTryParser
from ..vcs import VCSHelper
from ..push import push_to_try
here = os.path.abspath(os.path.dirname(__file__))
@ -314,7 +314,6 @@ class AutoTry(object):
self.topsrcdir = topsrcdir
self._resolver = None
self.mach_context = mach_context
self.vcs = VCSHelper.create()
@property
def resolver(self):
@ -620,8 +619,8 @@ class AutoTry(object):
if kwargs["verbose"]:
print('The following try syntax was calculated:\n%s' % msg)
self.vcs.push_to_try('syntax', kwargs["message"].format(msg=msg), push=kwargs['push'],
closed_tree=kwargs["closed_tree"])
push_to_try('syntax', kwargs["message"].format(msg=msg), push=kwargs['push'],
closed_tree=kwargs["closed_tree"])
if kwargs["save"]:
assert msg.startswith("try: ")

Просмотреть файл

@ -1,212 +0,0 @@
# 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 absolute_import, print_function
import json
import os
import subprocess
import sys
from abc import ABCMeta, abstractmethod, abstractproperty
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:
try:
output = subprocess.check_output(cmd, stderr=open(os.devnull, 'w')).strip()
except (subprocess.CalledProcessError, OSError):
continue
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):
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
out, err = proc.communicate()
if proc.returncode:
print("Error running `{}`:".format(' '.join(cmd)))
if out:
print("stdout:\n{}".format(out))
if err:
print("stderr:\n{}".format(err))
raise subprocess.CalledProcessError(proc.returncode, cmd, out)
return out
def write_task_config(self, labels, templates=None):
config = os.path.join(self.root, 'try_task_config.json')
with open(config, 'w') as fh:
try_task_config = {'tasks': sorted(labels)}
if templates:
try_task_config['templates'] = templates
json.dump(try_task_config, fh, indent=2, separators=(',', ':'))
fh.write('\n')
return config
def check_working_directory(self, push=True):
if not push:
return
if self.has_uncommitted_changes:
print(UNCOMMITTED_CHANGES)
sys.exit(1)
def push_to_try(self, method, msg, labels=None, templates=None, push=True,
closed_tree=False):
closed_tree_string = " ON A CLOSED TREE" if closed_tree else ""
commit_message = ('%s%s\n\nPushed via `mach try %s`' %
(msg, closed_tree_string, method))
self.check_working_directory(push)
config = None
if labels or labels == []:
config = self.write_task_config(labels, templates)
try:
if not push:
print("Commit message:")
print(commit_message)
if config:
print("Calculated try_task_config.json:")
with open(config) as fh:
print(fh.read())
return
self._push_to_try(commit_message, config)
finally:
if config and os.path.isfile(config):
os.remove(config)
@abstractmethod
def _push_to_try(self, msg, config):
pass
@abstractproperty
def files_changed(self):
pass
@abstractproperty
def has_uncommitted_changes(self):
pass
class HgHelper(VCSHelper):
def _push_to_try(self, msg, config):
try:
if config:
self.run(['hg', 'add', config])
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']).splitlines()
@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, config):
try:
subprocess.check_output(['git', 'cinnabar', '--version'], stderr=subprocess.STDOUT)
except subprocess.CalledProcessError:
print(GIT_CINNABAR_NOT_FOUND)
return 1
if config:
self.run(['git', 'add', config])
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,
}