Backed out 2 changesets (bug 1413922) for breaking Nightly L10n a=backout
authorCoroiu Cristina <ccoroiu@mozilla.com>
Tue, 10 Jul 2018 03:11:28 +0300
changeset 815812 9f03501341bf35bc502f69f870746ffac73992ae
parent 815811 19edc7c22303a37b7b5fea326171288eba17d788
child 815813 22f843d81df81978d4415a9fff7f30e6f1e0ab90
child 815829 773c06bb22cf2da24fd11be9be8d7b0376da3e36
child 815854 bcb418c4738d66afbf0557670d9d3cb0ba80ac51
child 815883 9d90c12b3c93e4dfd95095ce29a26e5fdd83f952
child 815892 2be3afeedd8b9ab8c16aaa7c19852d8dfaa1bdd6
child 815925 85d2a85c70a5d90cd567d044bd6da1fb69d4c28f
child 815939 66b864d9efecb996a5d438b32a3d468c70940a5b
child 815977 6a96a0501457de491e629151f9f923b4f941bf54
child 815994 4ae7c4abd12f750420dd1a7685548267d29a4167
child 816064 e88f133424455320c23c355b0e9868e933bc1dc5
child 816067 136d9ab6413d04126c98aadd8cea24edb4ecc40d
child 816069 4a6c9201aa7f0fa580dfcfcc2f0b42cc8ea81917
child 816073 9f72001efadd021d88049505d055fd6213574287
child 817250 e7c7576f0fd45fde658456caaa783fdc9bd0d147
child 817322 2d21bd0f8bb48f7f2cacd6c87c5cbac55bb61dfa
child 817373 7bba8b8fcb0db362bc3066bfbf25335a96f1f685
child 830982 2a7b4ffd71597e53d94e76ab7058b4e8229fe2a4
push id115654
push usergsquelart@mozilla.com
push dateTue, 10 Jul 2018 00:58:09 +0000
reviewersbackout
bugs1413922
milestone63.0a1
backs oute7a1d749ff9acbab36d397e56024eda0621e8e60
b3bead1f5729baa85b8333ace936f1130c9414da
Backed out 2 changesets (bug 1413922) for breaking Nightly L10n a=backout Backed out changeset e7a1d749ff9a (bug 1413922) Backed out changeset b3bead1f5729 (bug 1413922)
python/mozversioncontrol/mozversioncontrol/__init__.py
python/mozversioncontrol/test/conftest.py
python/mozversioncontrol/test/python.ini
python/mozversioncontrol/test/test_context_manager.py
python/mozversioncontrol/test/test_push_to_try.py
python/mozversioncontrol/test/test_workdir_outgoing.py
tools/tryselect/push.py
tools/tryselect/selectors/empty.py
tools/tryselect/selectors/fuzzy.py
tools/tryselect/selectors/syntax.py
tools/tryselect/vcs.py
--- a/python/mozversioncontrol/mozversioncontrol/__init__.py
+++ b/python/mozversioncontrol/mozversioncontrol/__init__.py
@@ -21,25 +21,16 @@ class MissingVCSTool(Exception):
 class MissingVCSInfo(Exception):
     """Represents a general failure to resolve a VCS interface."""
 
 
 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."""
 
 
 class MissingUpstreamRepo(Exception):
     """Represents a failure to automatically detect an upstream repo."""
 
 
@@ -112,21 +103,16 @@ class Repository(object):
         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
 
-    @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."""
 
     @abc.abstractproperty
     def head_ref(self):
         """Hash of HEAD revision."""
 
@@ -197,26 +183,16 @@ class Repository(object):
         Returns True if the working directory does not have any file
         modifications. False otherwise.
 
         By default, untracked and ignored files are not considered. If
         ``untracked`` or ``ignored`` are set, they influence the clean check
         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.'''
     def __init__(self, path, hg='hg'):
         import hglib.client
 
         super(HgRepository, self).__init__(path, tool=hg)
         self._env[b'HGPLAIN'] = b'1'
@@ -255,19 +231,20 @@ class HgRepository(Repository):
             finally:
                 os.chdir(old_cwd)
 
         return self
 
     def __exit__(self, exc_type, exc_val, exc_tb):
         self._client.close()
 
-    def _run(self, *args, **runargs):
+    def _run_in_client(self, args):
         if not self._client.server:
-            return super(HgRepository, self)._run(*args, **runargs)
+            raise Exception('active HgRepository context manager required')
+
         return self._client.rawcommand(args)
 
     def sparse_checkout_present(self):
         # We assume a sparse checkout is enabled if the .hg/sparse file
         # has data. Strictly speaking, we should look for a requirement in
         # .hg/requires. But since the requirement is still experimental
         # as of Mercurial 4.3, it's probably more trouble than its worth
         # to verify it.
@@ -322,41 +299,30 @@ class HgRepository(Repository):
         self._run(*args)
 
     def forget_add_remove_files(self, path):
         self._run('forget', path)
 
     def get_files_in_working_directory(self):
         # Can return backslashes on Windows. Normalize to forward slashes.
         return list(p.replace('\\', '/') for p in
-                    self._run(b'files', b'-0').split(b'\0') if p)
+                    self._run_in_client([b'files', b'-0']).split(b'\0')
+                    if p)
 
     def working_directory_clean(self, untracked=False, ignored=False):
-        args = [b'status', b'--modified', b'--added', b'--removed',
+        args = [b'status', b'\0', b'--modified', b'--added', b'--removed',
                 b'--deleted']
         if untracked:
             args.append(b'--unknown')
         if ignored:
             args.append(b'--ignored')
 
         # If output is empty, there are no entries of requested status, which
         # 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')
+        return not len(self._run_in_client(args).strip())
 
 
 class GitRepository(Repository):
     '''An implementation of `Repository` for Git repositories.'''
     def __init__(self, path, git='git'):
         super(GitRepository, self).__init__(path, tool=git)
 
     @property
@@ -371,24 +337,16 @@ class GitRepository(Repository):
     def base_ref(self):
         refs = self._run('for-each-ref', 'refs/heads', 'refs/remotes',
                          '--format=%(objectname)').splitlines()
         head = self.head_ref
         if head in refs:
             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
 
     def get_upstream(self):
         ref = self._run('symbolic-ref', '-q', 'HEAD').strip()
         upstream = self._run('for-each-ref', '--format=%(upstream:short)', ref).strip()
 
@@ -432,27 +390,16 @@ class GitRepository(Repository):
         args = ['status', '--porcelain']
         if untracked:
             args.append('--untracked-files')
         if ignored:
             args.append('--ignored')
 
         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`.
     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')):
deleted file mode 100644
--- a/python/mozversioncontrol/test/conftest.py
+++ /dev/null
@@ -1,74 +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
-
-import os
-import subprocess
-
-import pytest
-
-
-SETUP = {
-    'hg': [
-        """
-        echo "foo" > foo
-        echo "bar" > bar
-        hg init
-        hg add *
-        hg commit -m "Initial commit"
-        """,
-        """
-        echo "[paths]\ndefault = ../remoterepo" > .hg/hgrc
-        """,
-    ],
-    'git': [
-        """
-        echo "foo" > foo
-        echo "bar" > bar
-        git init
-        git add *
-        git commit -am "Initial commit"
-        """,
-        """
-        git remote add upstream ../remoterepo
-        git fetch upstream
-        git branch -u upstream/master
-        """,
-    ]
-}
-
-
-def shell(cmd):
-    subprocess.check_call(cmd, shell=True)
-
-
-@pytest.yield_fixture(params=['git', 'hg'])
-def repo(tmpdir, request):
-    vcs = request.param
-    steps = SETUP[vcs]
-
-    if hasattr(request.module, 'STEPS'):
-        steps.extend(request.module.STEPS[vcs])
-
-    # tmpdir and repo are py.path objects
-    # http://py.readthedocs.io/en/latest/path.html
-    repo = tmpdir.mkdir('repo')
-    repo.vcs = vcs
-
-    # This creates a step iterator. Each time next() is called
-    # on it, the next set of instructions will be executed.
-    repo.step = (shell(cmd) for cmd in steps)
-
-    oldcwd = os.getcwd()
-    os.chdir(repo.strpath)
-
-    next(repo.step)
-
-    repo.copy(tmpdir.join('remoterepo'))
-
-    next(repo.step)
-
-    yield repo
-    os.chdir(oldcwd)
--- a/python/mozversioncontrol/test/python.ini
+++ b/python/mozversioncontrol/test/python.ini
@@ -1,7 +1,5 @@
 [DEFAULT]
 subsuite=mozversioncontrol
 skip-if = python == 3
 
-[test_context_manager.py]
-[test_push_to_try.py]
 [test_workdir_outgoing.py]
deleted file mode 100644
--- a/python/mozversioncontrol/test/test_context_manager.py
+++ /dev/null
@@ -1,30 +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
-
-import mozunit
-
-from mozversioncontrol import get_repository_object
-
-
-def test_context_manager(repo):
-    is_git = repo.vcs == 'git'
-    cmd = ['show', '--no-patch'] if is_git else ['tip']
-
-    vcs = get_repository_object(repo.strpath)
-    output_subprocess = vcs._run(*cmd)
-    assert is_git or vcs._client.server is None
-    assert "Initial commit" in output_subprocess
-
-    with vcs:
-        assert is_git or vcs._client.server is not None
-        output_client = vcs._run(*cmd)
-
-    assert is_git or vcs._client.server is None
-    assert output_subprocess == output_client
-
-
-if __name__ == '__main__':
-    mozunit.main()
deleted file mode 100644
--- a/python/mozversioncontrol/test/test_push_to_try.py
+++ /dev/null
@@ -1,71 +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, 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()
--- a/python/mozversioncontrol/test/test_workdir_outgoing.py
+++ b/python/mozversioncontrol/test/test_workdir_outgoing.py
@@ -1,68 +1,122 @@
 # 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
 
 import os
+import subprocess
 
 import mozunit
+import pytest
 
 from mozversioncontrol import get_repository_object
 
 
-STEPS = {
+setup = {
     'hg': [
         """
+        echo "foo" > foo
+        echo "bar" > bar
+        hg init
+        hg add *
+        hg commit -m "Initial commit"
+        """,
+        """
+        echo "[paths]\ndefault = ../remoterepo" > .hg/hgrc
+        """,
+        """
         echo "bar" >> bar
         echo "baz" > baz
         hg add baz
         hg rm foo
         """,
         """
         hg commit -m "Remove foo; modify bar; add baz"
         """,
     ],
     'git': [
         """
+        echo "foo" > foo
+        echo "bar" > bar
+        git init
+        git add *
+        git commit -am "Initial commit"
+        """,
+        """
+        git remote add upstream ../remoterepo
+        git fetch upstream
+        git branch -u upstream/master
+        """,
+        """
         echo "bar" >> bar
         echo "baz" > baz
         git add baz
         git rm foo
         """,
         """
         git commit -am "Remove foo; modify bar; add baz"
         """
     ]
 }
 
 
+def shell(cmd):
+    subprocess.check_call(cmd, shell=True)
+
+
+@pytest.yield_fixture(params=['git', 'hg'])
+def repo(tmpdir, request):
+    vcs = request.param
+
+    # tmpdir and repo are py.path objects
+    # http://py.readthedocs.io/en/latest/path.html
+    repo = tmpdir.mkdir('repo')
+    repo.vcs = vcs
+
+    # This creates a setup iterator. Each time next() is called
+    # on it, the next set of instructions will be executed.
+    repo.setup = (shell(cmd) for cmd in setup[vcs])
+
+    oldcwd = os.getcwd()
+    os.chdir(repo.strpath)
+
+    next(repo.setup)
+
+    repo.copy(tmpdir.join('remoterepo'))
+
+    next(repo.setup)
+
+    yield repo
+    os.chdir(oldcwd)
+
+
 def assert_files(actual, expected):
     assert set(map(os.path.basename, actual)) == set(expected)
 
 
 def test_workdir_outgoing(repo):
     vcs = get_repository_object(repo.strpath)
     assert vcs.path == repo.strpath
 
     remotepath = '../remoterepo' if repo.vcs == 'hg' else 'upstream/master'
 
-    next(repo.step)
+    next(repo.setup)
 
     assert_files(vcs.get_changed_files('AM', 'all'), ['bar', 'baz'])
     if repo.vcs == 'git':
         assert_files(vcs.get_changed_files('AM', mode='staged'), ['baz'])
     elif repo.vcs == 'hg':
         assert_files(vcs.get_changed_files('AM', 'staged'), ['bar', 'baz'])
     assert_files(vcs.get_outgoing_files('AM'), [])
     assert_files(vcs.get_outgoing_files('AM', remotepath), [])
 
-    next(repo.step)
+    next(repo.setup)
 
     assert_files(vcs.get_changed_files('AM', 'all'), [])
     assert_files(vcs.get_changed_files('AM', 'staged'), [])
     assert_files(vcs.get_outgoing_files('AM'), ['bar', 'baz'])
     assert_files(vcs.get_outgoing_files('AM', remotepath), ['bar', 'baz'])
 
 
 if __name__ == '__main__':
--- a/tools/tryselect/selectors/empty.py
+++ b/tools/tryselect/selectors/empty.py
@@ -1,19 +1,20 @@
 # 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, unicode_literals
 
 from ..cli import BaseTryParser
-from ..push import push_to_try
+from ..vcs import VCSHelper
 
 
 class EmptyParser(BaseTryParser):
     name = 'empty'
     common_groups = ['push']
 
 
 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 push_to_try('empty', message.format(msg=msg), [], push=push,
-                       closed_tree=kwargs["closed_tree"])
+    return vcs.push_to_try('empty', message.format(msg=msg), [], push=push,
+                           closed_tree=kwargs["closed_tree"])
--- a/tools/tryselect/selectors/fuzzy.py
+++ b/tools/tryselect/selectors/fuzzy.py
@@ -14,17 +14,17 @@ from distutils.spawn import find_executa
 from mozboot.util import get_state_dir
 from mozterm import Terminal
 from moztest.resolve import TestResolver, get_suite_definition
 from six import string_types
 
 from .. import preset as pset
 from ..cli import BaseTryParser
 from ..tasks import generate_tasks
-from ..push import check_working_directory, push_to_try, vcs
+from ..vcs import VCSHelper
 
 terminal = Terminal()
 
 here = os.path.abspath(os.path.dirname(__file__))
 
 FZF_NOT_FOUND = """
 Could not find the `fzf` binary.
 
@@ -216,18 +216,20 @@ def run_fuzzy_try(update=False, query=No
         return getattr(pset, mod_presets)(section='fuzzy')
 
     fzf = fzf_bootstrap(update)
 
     if not fzf:
         print(FZF_NOT_FOUND)
         return 1
 
-    check_working_directory(push)
-    all_tasks = generate_tasks(parameters, full, root=vcs.path)
+    vcs = VCSHelper.create()
+    vcs.check_working_directory(push)
+
+    all_tasks = generate_tasks(parameters, full, root=vcs.root)
 
     if paths:
         all_tasks = filter_by_paths(all_tasks, paths)
         if not all_tasks:
             return 1
 
     key_shortcuts = [k + ':' + v for k, v in fzf_shortcuts.iteritems()]
     base_cmd = [
@@ -274,10 +276,10 @@ def run_fuzzy_try(update=False, query=No
     msg = "Fuzzy"
     args = []
     if paths:
         args.append("paths={}".format(':'.join(paths)))
     if query:
         args.extend(["query={}".format(q) for q in queries])
     if args:
         msg = "{} {}".format(msg, '&'.join(args))
-    return push_to_try('fuzzy', message.format(msg=msg), selected, templates, push=push,
-                       closed_tree=kwargs["closed_tree"])
+    return vcs.push_to_try('fuzzy', message.format(msg=msg), selected, templates, push=push,
+                           closed_tree=kwargs["closed_tree"])
--- a/tools/tryselect/selectors/syntax.py
+++ b/tools/tryselect/selectors/syntax.py
@@ -9,17 +9,17 @@ import re
 import sys
 from collections import defaultdict
 
 import mozpack.path as mozpath
 from moztest.resolve import TestResolver
 
 from .. import preset
 from ..cli import BaseTryParser
-from ..push import push_to_try
+from ..vcs import VCSHelper
 
 here = os.path.abspath(os.path.dirname(__file__))
 
 
 class SyntaxParser(BaseTryParser):
     name = 'syntax'
     arguments = [
         [['paths'],
@@ -309,16 +309,17 @@ class AutoTry(object):
         "web-platform-tests",
         "xpcshell",
     ]
 
     def __init__(self, topsrcdir, mach_context):
         self.topsrcdir = topsrcdir
         self._resolver = None
         self.mach_context = mach_context
+        self.vcs = VCSHelper.create()
 
     @property
     def resolver(self):
         if self._resolver is None:
             self._resolver = TestResolver.from_environment(cwd=here)
         return self._resolver
 
     def split_try_string(self, data):
@@ -614,15 +615,15 @@ class AutoTry(object):
         if kwargs["verbose"] and paths_by_flavor:
             print('The following tests will be selected: ')
             for flavor, paths in paths_by_flavor.iteritems():
                 print("%s: %s" % (flavor, ",".join(paths)))
 
         if kwargs["verbose"]:
             print('The following try syntax was calculated:\n%s' % msg)
 
-        push_to_try('syntax', kwargs["message"].format(msg=msg), push=kwargs['push'],
-                    closed_tree=kwargs["closed_tree"])
+        self.vcs.push_to_try('syntax', kwargs["message"].format(msg=msg), push=kwargs['push'],
+                             closed_tree=kwargs["closed_tree"])
 
         if kwargs["save"]:
             assert msg.startswith("try: ")
             msg = msg[len("try: "):]
             preset.save('try', kwargs["save"], msg)
rename from tools/tryselect/push.py
rename to tools/tryselect/vcs.py
--- a/tools/tryselect/push.py
+++ b/tools/tryselect/vcs.py
@@ -1,20 +1,19 @@
 # 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 mozbuild.base import MozbuildObject
-from mozversioncontrol import get_repository_object, MissingVCSExtension
+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:
 
@@ -34,71 +33,180 @@ 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)
+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
 
-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
+            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
 
-        json.dump(try_task_config, fh, indent=2, separators=(',', ':'))
-        fh.write('\n')
-    return config
+    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)
 
-def check_working_directory(push=True):
-    if not push:
-        return
+        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
 
-    if not vcs.working_directory_clean():
-        print(UNCOMMITTED_CHANGES)
-        sys.exit(1)
+            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
 
 
-def push_to_try(method, msg, labels=None, templates=None, push=True, closed_tree=False):
-    check_working_directory(push)
+class HgHelper(VCSHelper):
 
-    # 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))
+    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'])
 
-    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
+    @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:
-            vcs.add_remove_files(config)
-
+            self.run(['git', 'add', config])
+        subprocess.check_call(['git', 'commit', '--allow-empty', '-m', msg])
         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)
+            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,
+}