зеркало из https://github.com/mozilla/gecko-dev.git
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:
Родитель
cd200f6d34
Коммит
23c2416671
|
@ -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()
|
|
@ -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,
|
||||
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,
|
||||
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,7 +619,7 @@ 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'],
|
||||
push_to_try('syntax', kwargs["message"].format(msg=msg), push=kwargs['push'],
|
||||
closed_tree=kwargs["closed_tree"])
|
||||
|
||||
if kwargs["save"]:
|
||||
|
|
|
@ -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,
|
||||
}
|
Загрузка…
Ссылка в новой задаче