configwizard: import files from mozilla-central (bug 1277406); r=glob draft
authorGregory Szorc <gps@mozilla.com>
Wed, 01 Jun 2016 16:01:38 -0700
changeset 8490 29d3dc704982ac84a1b40df10316d080cdab5939
parent 8489 89c4597518a5447deed726e64f180c6ce26474c7
child 8491 0d4bbbd1a8425e7f2c7a2d7b1b43c1c73364a9cb
push id918
push userbmo:gps@mozilla.com
push dateThu, 09 Jun 2016 19:23:31 +0000
reviewersglob
bugs1277406
configwizard: import files from mozilla-central (bug 1277406); r=glob The files constituting the heart of `mach mercurial-setup` in mozilla-central have been imported. b0096c5c727749ad3e79cbdf20d2e96bd179c213 was used. Files are unmodified. Upcoming patches will teach version-control-tools how to call into them. MozReview-Commit-ID: 72We6dgDLXR
hgext/configwizard/hgsetup/__init__.py
hgext/configwizard/hgsetup/config.py
hgext/configwizard/hgsetup/wizard.py
new file mode 100644
new file mode 100644
--- /dev/null
+++ b/hgext/configwizard/hgsetup/config.py
@@ -0,0 +1,235 @@
+# 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 unicode_literals
+
+from configobj import ConfigObj
+import codecs
+import re
+import os
+
+
+HOST_FINGERPRINTS = {
+    'bitbucket.org': '3f:d3:c5:17:23:3c:cd:f5:2d:17:76:06:93:7e:ee:97:42:21:14:aa',
+    'bugzilla.mozilla.org': '7c:7a:c4:6c:91:3b:6b:89:cf:f2:8c:13:b8:02:c4:25:bd:1e:25:17',
+    'hg.mozilla.org': 'af:27:b9:34:47:4e:e5:98:01:f6:83:2b:51:c9:aa:d8:df:fb:1a:27',
+}
+
+
+def config_file(files):
+    """Select the most appropriate config file from a list."""
+    if not files:
+        return None
+
+    if len(files) > 1:
+        picky = [(os.path.getsize(f), f) for f in files if os.path.isfile(f)]
+        if picky:
+            return max(picky)[1]
+
+    return files[0]
+
+
+class ParseException(Exception):
+    def __init__(self, line, msg):
+        self.line = line
+        super(Exception, self).__init__(msg)
+
+
+class MercurialConfig(object):
+    """Interface for manipulating a Mercurial config file."""
+
+    def __init__(self, path=None):
+        """Create a new instance, optionally from an existing hgrc file."""
+
+        self.config_path = path
+
+        # Mercurial configuration files allow an %include directive to include
+        # other files, this is not supported by ConfigObj, so throw a useful
+        # error saying this.
+        if os.path.exists(path):
+            with codecs.open(path, 'r', encoding='utf-8') as f:
+                for i, line in enumerate(f):
+                    if line.startswith('%include'):
+                        raise ParseException(i + 1,
+                            '%include directive is not supported by MercurialConfig')
+                    if line.startswith(';'):
+                        raise ParseException(i + 1,
+                            'semicolon (;) comments are not supported; '
+                            'use # instead')
+
+        # write_empty_values is necessary to prevent built-in extensions (which
+        # have no value) from being dropped on write.
+        # list_values aren't needed by Mercurial and disabling them prevents
+        # quotes from being added.
+        self._c = ConfigObj(infile=path, encoding='utf-8',
+            write_empty_values=True, list_values=False)
+
+    @property
+    def config(self):
+        return self._c
+
+    @property
+    def extensions(self):
+        """Returns the set of currently enabled extensions (by name)."""
+        return set(self._c.get('extensions', {}).keys())
+
+    def write(self, fh):
+        return self._c.write(fh)
+
+    def have_valid_username(self):
+        if 'ui' not in self._c:
+            return False
+
+        if 'username' not in self._c['ui']:
+            return False
+
+        # TODO perform actual validation here.
+
+        return True
+
+    def add_mozilla_host_fingerprints(self):
+        """Add host fingerprints so SSL connections don't warn."""
+        if 'hostfingerprints' not in self._c:
+            self._c['hostfingerprints'] = {}
+
+        for k, v in HOST_FINGERPRINTS.items():
+            self._c['hostfingerprints'][k] = v
+
+    def update_mozilla_host_fingerprints(self):
+        """Update host fingerprints if they are present."""
+        if 'hostfingerprints' not in self._c:
+            return
+
+        for k, v in HOST_FINGERPRINTS.items():
+            if k in self._c['hostfingerprints']:
+                self._c['hostfingerprints'][k] = v
+
+    def set_username(self, name, email):
+        """Set the username to use for commits.
+
+        The username consists of a name (typically <firstname> <lastname>) and
+        a well-formed e-mail address.
+        """
+        if 'ui' not in self._c:
+            self._c['ui'] = {}
+
+        username = '%s <%s>' % (name, email)
+
+        self._c['ui']['username'] = username.strip()
+
+    def activate_extension(self, name, path=None):
+        """Activate an extension.
+
+        An extension is defined by its name (in the config) and a filesystem
+        path). For built-in extensions, an empty path is specified.
+        """
+        if not path:
+            path = ''
+
+        if 'extensions' not in self._c:
+            self._c['extensions'] = {}
+
+        self._c['extensions'][name] = path
+
+    def have_recommended_diff_settings(self):
+        if 'diff' not in self._c:
+            return False
+
+        old = dict(self._c['diff'])
+        try:
+            self.ensure_recommended_diff_settings()
+        finally:
+            self._c['diff'].update(old)
+
+        return self._c['diff'] == old
+
+    def ensure_recommended_diff_settings(self):
+        if 'diff' not in self._c:
+            self._c['diff'] = {}
+
+        d = self._c['diff']
+        d['git'] = 1
+        d['showfunc'] = 1
+        d['unified'] = 8
+
+    def get_bugzilla_credentials(self):
+        if 'bugzilla' not in self._c:
+            return None, None, None, None, None
+
+        b = self._c['bugzilla']
+        return (
+            b.get('username', None),
+            b.get('password', None),
+            b.get('userid', None),
+            b.get('cookie', None),
+            b.get('apikey', None),
+        )
+
+    def set_bugzilla_credentials(self, username, api_key):
+        b = self._c.setdefault('bugzilla', {})
+        if username:
+            b['username'] = username
+        if api_key:
+            b['apikey'] = api_key
+
+    def clear_legacy_bugzilla_credentials(self):
+        if 'bugzilla' not in self._c:
+            return
+
+        b = self._c['bugzilla']
+        for k in ('password', 'userid', 'cookie'):
+            if k in b:
+                del b[k]
+
+    def have_clonebundles(self):
+        return 'clonebundles' in self._c.get('experimental', {})
+
+    def activate_clonebundles(self):
+        exp = self._c.setdefault('experimental', {})
+        exp['clonebundles'] = 'true'
+
+        # bundleclone is redundant with clonebundles. Remove it if it
+        # is installed.
+        ext = self._c.get('extensions', {})
+        try:
+            del ext['bundleclone']
+        except KeyError:
+            pass
+
+    def have_wip(self):
+        return 'wip' in self._c.get('alias', {})
+
+    def install_wip_alias(self):
+        """hg wip shows a concise view of work in progress."""
+        alias = self._c.setdefault('alias', {})
+        alias['wip'] = 'log --graph --rev=wip --template=wip'
+
+        revsetalias = self._c.setdefault('revsetalias', {})
+        revsetalias['wip'] = ('('
+                'parents(not public()) '
+                'or not public() '
+                'or . '
+                'or (head() and branch(default))'
+            ') and (not obsolete() or unstable()^) '
+            'and not closed()')
+
+        templates = self._c.setdefault('templates', {})
+        templates['wip'] = ("'"
+            # prefix with branch name
+            '{label("log.branch", branches)} '
+            # rev:node
+            '{label("changeset.{phase}", rev)}'
+            '{label("changeset.{phase}", ":")}'
+            '{label("changeset.{phase}", short(node))} '
+            # just the username part of the author, for brevity
+            '{label("grep.user", author|user)}'
+            # tags and bookmarks
+            '{label("log.tag", if(tags," {tags}"))}'
+            '{label("log.tag", if(fxheads," {fxheads}"))} '
+            '{label("log.bookmark", if(bookmarks," {bookmarks}"))}'
+            '\\n'
+            # first line of commit message
+            '{label(ifcontains(rev, revset("."), "desc.here"),desc|firstline)}'
+            "'"
+        )
new file mode 100644
--- /dev/null
+++ b/hgext/configwizard/hgsetup/wizard.py
@@ -0,0 +1,604 @@
+# 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 unicode_literals
+
+import difflib
+import errno
+import os
+import shutil
+import ssl
+import stat
+import sys
+import subprocess
+
+from distutils.version import LooseVersion
+
+from configobj import ConfigObjError
+from StringIO import StringIO
+
+from mozversioncontrol import get_hg_path, get_hg_version
+
+from .update import MercurialUpdater
+from .config import (
+    config_file,
+    MercurialConfig,
+    ParseException,
+)
+
+
+INITIAL_MESSAGE = '''
+I'm going to help you ensure your Mercurial is configured for optimal
+development on Mozilla projects.
+
+If your environment is missing some recommended settings, I'm going to prompt
+you whether you want me to make changes: I won't change anything you might not
+want me changing without your permission!
+
+If your config is up-to-date, I'm just going to ensure all 3rd party extensions
+are up to date and you won't have to do anything.
+
+To begin, press the enter/return key.
+'''.strip()
+
+# This should match MODERN_MERCURIAL_VERSION in
+# python/mozboot/mozboot/base.py.
+OLDEST_NON_LEGACY_VERSION = LooseVersion('3.7.3')
+LEGACY_MERCURIAL = '''
+You are running an out of date Mercurial client (%s).
+
+For a faster and better Mercurial experience, we HIGHLY recommend you
+upgrade.
+
+Legacy versions of Mercurial have known security vulnerabilities. Failure
+to upgrade may leave you exposed. You are highly encouraged to upgrade
+in case you aren't running a patched version.
+'''.strip()
+
+MISSING_USERNAME = '''
+You don't have a username defined in your Mercurial config file. In order to
+send patches to Mozilla, you'll need to attach a name and email address. If you
+aren't comfortable giving us your full name, pseudonames are acceptable.
+
+(Relevant config option: ui.username)
+'''.strip()
+
+BAD_DIFF_SETTINGS = '''
+Mozilla developers produce patches in a standard format, but your Mercurial is
+not configured to produce patches in that format.
+
+(Relevant config options: diff.git, diff.showfunc, diff.unified)
+'''.strip()
+
+BZEXPORT_INFO = '''
+If you plan on uploading patches to Mozilla, there is an extension called
+bzexport that makes it easy to upload patches from the command line via the
+|hg bzexport| command. More info is available at
+https://hg.mozilla.org/hgcustom/version-control-tools/file/default/hgext/bzexport/README
+
+(Relevant config option: extensions.bzexport)
+
+Would you like to activate bzexport
+'''.strip()
+
+FINISHED = '''
+Your Mercurial should now be properly configured and recommended extensions
+should be up to date!
+'''.strip()
+
+REVIEWBOARD_MINIMUM_VERSION = LooseVersion('3.5')
+
+REVIEWBOARD_INCOMPATIBLE = '''
+Your Mercurial is too old to use the reviewboard extension, which is necessary
+to conduct code review.
+
+Please upgrade to Mercurial %s or newer to use this extension.
+'''.strip()
+
+MISSING_BUGZILLA_CREDENTIALS = '''
+You do not have your Bugzilla API Key defined in your Mercurial config.
+
+Various extensions make use of a Bugzilla API Key to interface with
+Bugzilla to enrich your development experience.
+
+The Bugzilla API Key is optional. If you do not provide one, associated
+functionality will not be enabled, we will attempt to find a Bugzilla cookie
+from a Firefox profile, or you will be prompted for your Bugzilla credentials
+when they are needed.
+
+You should only need to configure a Bugzilla API Key once.
+'''.lstrip()
+
+BUGZILLA_API_KEY_INSTRUCTIONS = '''
+Bugzilla API Keys can only be obtained through the Bugzilla web interface.
+
+Please perform the following steps:
+
+  1) Open https://bugzilla.mozilla.org/userprefs.cgi?tab=apikey
+  2) Generate a new API Key
+  3) Copy the generated key and paste it here
+'''.lstrip()
+
+LEGACY_BUGZILLA_CREDENTIALS_DETECTED = '''
+Your existing Mercurial config uses a legacy method for defining Bugzilla
+credentials. Bugzilla API Keys are the most secure and preferred method
+for defining Bugzilla credentials. Bugzilla API Keys are also required
+if you have enabled 2 Factor Authentication in Bugzilla.
+
+All consumers formerly looking at these options should support API Keys.
+'''.lstrip()
+
+BZPOST_MINIMUM_VERSION = LooseVersion('3.5')
+
+BZPOST_INFO = '''
+The bzpost extension automatically records the URLs of pushed commits to
+referenced Bugzilla bugs after push.
+
+(Relevant config option: extensions.bzpost)
+
+Would you like to activate bzpost
+'''.strip()
+
+FIREFOXTREE_MINIMUM_VERSION = LooseVersion('3.5')
+
+FIREFOXTREE_INFO = '''
+The firefoxtree extension makes interacting with the multiple Firefox
+repositories easier:
+
+* Aliases for common trees are pre-defined. e.g. `hg pull central`
+* Pulling from known Firefox trees will create "remote refs" appearing as
+  tags. e.g. pulling from fx-team will produce a "fx-team" tag.
+* The `hg fxheads` command will list the heads of all pulled Firefox repos
+  for easy reference.
+* `hg push` will limit itself to pushing a single head when pushing to
+  Firefox repos.
+* A pre-push hook will prevent you from pushing multiple heads to known
+  Firefox repos. This acts quicker than a server-side hook.
+
+The firefoxtree extension is *strongly* recommended if you:
+
+a) aggregate multiple Firefox repositories into a single local repo
+b) perform head/bookmark-based development (as opposed to mq)
+
+(Relevant config option: extensions.firefoxtree)
+
+Would you like to activate firefoxtree
+'''.strip()
+
+PUSHTOTRY_MINIMUM_VERSION = LooseVersion('3.5')
+
+PUSHTOTRY_INFO = '''
+The push-to-try extension generates a temporary commit with a given
+try syntax and pushes it to the try server. The extension is intended
+to be used in concert with other tools generating try syntax so that
+they can push to try without depending on mq or other workarounds.
+
+(Relevant config option: extensions.push-to-try)
+
+Would you like to activate push-to-try
+'''.strip()
+
+CLONEBUNDLES_INFO = '''
+Mercurial 3.6 and hg.mozilla.org support transparently cloning from a CDN,
+making clones faster and more reliable.
+
+(Relevant config option: experimental.clonebundles)
+
+Would you like to activate this feature and have faster clones
+'''.strip()
+
+BUNDLECLONE_MINIMUM_VERSION = LooseVersion('3.1')
+
+BUNDLECLONE_INFO = '''
+The bundleclone extension makes cloning faster and saves server resources.
+
+We highly recommend you activate this extension.
+
+(Relevant config option: extensions.bundleclone)
+
+Would you like to activate bundleclone
+'''.strip()
+
+WIP_INFO = '''
+It is common to want a quick view of changesets that are in progress.
+
+The ``hg wip`` command provides should a view.
+
+Example Usage:
+
+  $ hg wip
+  o   4084:fcfa34d0387b dminor  @
+  |  mozreview: use repository name when displaying treeherder results (bug 1230548) r=mcote
+  | @   4083:786baf6d476a gps
+  | |  mozreview: create child review requests from batch API
+  | o   4082:3f100fa4a94f gps
+  | |  mozreview: copy more read-only processing code; r?smacleod
+  | o   4081:939417680cbe gps
+  |/   mozreview: add web API to submit an entire series of commits (bug 1229468); r?smacleod
+
+(Not shown are the colors that help denote the state each changeset
+is in.)
+
+(Relevant config options: alias.wip, revsetalias.wip, templates.wip)
+
+Would you like to install the `hg wip` alias?
+'''.strip()
+
+HGWATCHMAN_MINIMUM_VERSION = LooseVersion('3.5.2')
+
+HGWATCHMAN_INFO = '''
+The hgwatchman extension integrates the watchman filesystem watching
+tool with Mercurial. Commands like `hg status`, `hg diff`, and
+`hg commit` that need to examine filesystem state can query watchman
+and obtain filesystem state nearly instantaneously. The result is much
+faster command execution.
+
+When installed, the hgwatchman extension will launch a background
+watchman file watching daemon for accessed Mercurial repositories. It
+should "just work."
+
+Would you like to install hgwatchman
+'''.strip()
+
+FILE_PERMISSIONS_WARNING = '''
+Your hgrc file is currently readable by others.
+
+Sensitive information such as your Bugzilla credentials could be
+stolen if others have access to this file/machine.
+'''.strip()
+
+MULTIPLE_VCT = '''
+*** WARNING ***
+
+Multiple version-control-tools repositories are referenced in your
+Mercurial config. Extensions and other code within the
+version-control-tools repository could run with inconsistent results.
+
+Please manually edit the following file to reference a single
+version-control-tools repository:
+
+    %s
+'''.lstrip()
+
+
+class MercurialSetupWizard(object):
+    """Command-line wizard to help users configure Mercurial."""
+
+    def __init__(self, state_dir):
+        # We use normpath since Mercurial expects the hgrc to use native path
+        # separators, but state_dir uses unix style paths even on Windows.
+        self.state_dir = os.path.normpath(state_dir)
+        self.ext_dir = os.path.join(self.state_dir, 'mercurial', 'extensions')
+        self.vcs_tools_dir = os.path.join(self.state_dir, 'version-control-tools')
+        self.updater = MercurialUpdater(state_dir)
+
+    def run(self, config_paths):
+        try:
+            os.makedirs(self.ext_dir)
+        except OSError as e:
+            if e.errno != errno.EEXIST:
+                raise
+
+        hg = get_hg_path()
+        config_path = config_file(config_paths)
+
+        try:
+            c = MercurialConfig(config_path)
+        except ConfigObjError as e:
+            print('Error importing existing Mercurial config: %s\n' % config_path)
+            for error in e.errors:
+                print(error.message)
+
+            return 1
+        except ParseException as e:
+            print('Error importing existing Mercurial config: %s\n' % config_path)
+            print('Line %d: %s' % (e.line, e.message))
+
+            return 1
+
+        self.updater.update_all()
+
+        print(INITIAL_MESSAGE)
+        raw_input()
+
+        hg_version = get_hg_version(hg)
+        if hg_version < OLDEST_NON_LEGACY_VERSION:
+            print(LEGACY_MERCURIAL % hg_version)
+            print('')
+
+            if os.name == 'nt':
+                print('Please upgrade to the latest MozillaBuild to upgrade '
+                    'your Mercurial install.')
+                print('')
+            else:
+                print('Please run |mach bootstrap| to upgrade your Mercurial '
+                    'install.')
+                print('')
+
+            if not self._prompt_yn('Would you like to continue using an old '
+                'Mercurial version'):
+                return 1
+
+        if not c.have_valid_username():
+            print(MISSING_USERNAME)
+            print('')
+
+            name = self._prompt('What is your name?')
+            email = self._prompt('What is your email address?')
+            c.set_username(name, email)
+            print('Updated your username.')
+            print('')
+
+        if not c.have_recommended_diff_settings():
+            print(BAD_DIFF_SETTINGS)
+            print('')
+            if self._prompt_yn('Would you like me to fix this for you'):
+                c.ensure_recommended_diff_settings()
+                print('Fixed patch settings.')
+                print('')
+
+        # Progress is built into core and enabled by default in Mercurial 3.5.
+        if hg_version < LooseVersion('3.5'):
+            self.prompt_native_extension(c, 'progress',
+                'Would you like to see progress bars during Mercurial operations')
+
+        self.prompt_native_extension(c, 'color',
+            'Would you like Mercurial to colorize output to your terminal')
+
+        self.prompt_native_extension(c, 'rebase',
+            'Would you like to enable the rebase extension to allow you to move'
+            ' changesets around (which can help maintain a linear history)')
+
+        self.prompt_native_extension(c, 'histedit',
+            'Would you like to enable the histedit extension to allow history '
+            'rewriting via the "histedit" command (similar to '
+            '`git rebase -i`)')
+
+        # hgwatchman is provided by MozillaBuild and we don't yet support
+        # Linux/BSD.
+        if ('hgwatchman' not in c.extensions
+            and sys.platform.startswith('darwin')
+            and hg_version >= HGWATCHMAN_MINIMUM_VERSION
+            and self._prompt_yn(HGWATCHMAN_INFO)):
+            # Unlike other extensions, we need to run an installer
+            # to compile a Python C extension.
+            try:
+                subprocess.check_output(
+                    ['make', 'local'],
+                    cwd=self.updater.hgwatchman_dir,
+                    stderr=subprocess.STDOUT)
+
+                ext_path = os.path.join(self.updater.hgwatchman_dir,
+                                        'hgwatchman')
+                if self.can_use_extension(c, 'hgwatchman', ext_path):
+                    c.activate_extension('hgwatchman', ext_path)
+            except subprocess.CalledProcessError as e:
+                print('Error compiling hgwatchman; will not install hgwatchman')
+                print(e.output)
+
+        if 'reviewboard' not in c.extensions:
+            if hg_version < REVIEWBOARD_MINIMUM_VERSION:
+                print(REVIEWBOARD_INCOMPATIBLE % REVIEWBOARD_MINIMUM_VERSION)
+            else:
+                p = os.path.join(self.vcs_tools_dir, 'hgext', 'reviewboard',
+                    'client.py')
+                self.prompt_external_extension(c, 'reviewboard',
+                    'Would you like to enable the reviewboard extension so '
+                    'you can easily initiate code reviews against Mozilla '
+                    'projects',
+                    path=p)
+
+        self.prompt_external_extension(c, 'bzexport', BZEXPORT_INFO)
+
+        if hg_version >= BZPOST_MINIMUM_VERSION:
+            self.prompt_external_extension(c, 'bzpost', BZPOST_INFO)
+
+        if hg_version >= FIREFOXTREE_MINIMUM_VERSION:
+            self.prompt_external_extension(c, 'firefoxtree', FIREFOXTREE_INFO)
+
+        # Functionality from bundleclone is experimental in Mercurial 3.6.
+        # There was a bug in 3.6, so look for 3.6.1.
+        if hg_version >= LooseVersion('3.6.1'):
+            if not c.have_clonebundles() and self._prompt_yn(CLONEBUNDLES_INFO):
+                c.activate_clonebundles()
+                print('Enabled the clonebundles feature.\n')
+        elif hg_version >= BUNDLECLONE_MINIMUM_VERSION:
+            self.prompt_external_extension(c, 'bundleclone', BUNDLECLONE_INFO)
+
+        if hg_version >= PUSHTOTRY_MINIMUM_VERSION:
+            self.prompt_external_extension(c, 'push-to-try', PUSHTOTRY_INFO)
+
+        if not c.have_wip():
+            if self._prompt_yn(WIP_INFO):
+                c.install_wip_alias()
+
+        if 'reviewboard' in c.extensions or 'bzpost' in c.extensions:
+            bzuser, bzpass, bzuserid, bzcookie, bzapikey = c.get_bugzilla_credentials()
+
+            if not bzuser or not bzapikey:
+                print(MISSING_BUGZILLA_CREDENTIALS)
+
+            if not bzuser:
+                bzuser = self._prompt('What is your Bugzilla email address? (optional)',
+                    allow_empty=True)
+
+            if bzuser and not bzapikey:
+                print(BUGZILLA_API_KEY_INSTRUCTIONS)
+                bzapikey = self._prompt('Please enter a Bugzilla API Key: (optional)',
+                    allow_empty=True)
+
+            if bzuser or bzapikey:
+                c.set_bugzilla_credentials(bzuser, bzapikey)
+
+            if bzpass or bzuserid or bzcookie:
+                print(LEGACY_BUGZILLA_CREDENTIALS_DETECTED)
+
+                # Clear legacy credentials automatically if an API Key is
+                # found as it supercedes all other credentials.
+                if bzapikey:
+                    print('The legacy credentials have been removed.\n')
+                    c.clear_legacy_bugzilla_credentials()
+                elif self._prompt_yn('Remove legacy credentials'):
+                    c.clear_legacy_bugzilla_credentials()
+
+        # Look for and clean up old extensions.
+        for ext in {'bzexport', 'qimportbz', 'mqext'}:
+            path = os.path.join(self.ext_dir, ext)
+            if os.path.exists(path):
+                if self._prompt_yn('Would you like to remove the old and no '
+                    'longer referenced repository at %s' % path):
+                    print('Cleaning up old repository: %s' % path)
+                    shutil.rmtree(path)
+
+        # Python + Mercurial didn't have terrific TLS handling until Python
+        # 2.7.9 and Mercurial 3.4. For this reason, it was recommended to pin
+        # certificates in Mercurial config files. In modern versions of
+        # Mercurial, the system CA store is used and old, legacy TLS protocols
+        # are disabled. The default connection/security setting should
+        # be sufficient and pinning certificates is no longer needed.
+        have_modern_ssl = hasattr(ssl, 'SSLContext')
+        if hg_version < LooseVersion('3.4') or not have_modern_ssl:
+            c.add_mozilla_host_fingerprints()
+
+        # We always update fingerprints if they are present. We /could/ offer to
+        # remove fingerprints if running modern Python and Mercurial. But that
+        # just adds more UI complexity and isn't worth it.
+        c.update_mozilla_host_fingerprints()
+
+        # References to multiple version-control-tools checkouts can confuse
+        # version-control-tools, since various Mercurial extensions resolve
+        # dependencies via __file__ and repos could reference another copy.
+        seen_vct = set()
+        for k, v in c.config.get('extensions', {}).items():
+            if 'version-control-tools' not in v:
+                continue
+
+            i = v.index('version-control-tools')
+            vct = v[0:i + len('version-control-tools')]
+            seen_vct.add(os.path.realpath(os.path.expanduser(vct)))
+
+        if len(seen_vct) > 1:
+            print(MULTIPLE_VCT % c.config_path)
+
+        # At this point the config should be finalized.
+
+        b = StringIO()
+        c.write(b)
+        new_lines = [line.rstrip() for line in b.getvalue().splitlines()]
+        old_lines = []
+
+        config_path = c.config_path
+        if os.path.exists(config_path):
+            with open(config_path, 'rt') as fh:
+                old_lines = [line.rstrip() for line in fh.readlines()]
+
+        diff = list(difflib.unified_diff(old_lines, new_lines,
+            'hgrc.old', 'hgrc.new'))
+
+        if len(diff):
+            print('Your Mercurial config file needs updating. I can do this '
+                'for you if you like!')
+            if self._prompt_yn('Would you like to see a diff of the changes '
+                'first'):
+                for line in diff:
+                    print(line)
+                print('')
+
+            if self._prompt_yn('Would you like me to update your hgrc file'):
+                with open(config_path, 'wt') as fh:
+                    c.write(fh)
+                print('Wrote changes to %s.' % config_path)
+            else:
+                print('hgrc changes not written to file. I would have '
+                    'written the following:\n')
+                c.write(sys.stdout)
+                return 1
+
+        if sys.platform != 'win32':
+            # Config file may contain sensitive content, such as passwords.
+            # Prompt to remove global permissions.
+            mode = os.stat(config_path).st_mode
+            if mode & (stat.S_IRWXG | stat.S_IRWXO):
+                print(FILE_PERMISSIONS_WARNING)
+                if self._prompt_yn('Remove permissions for others to '
+                                   'read your hgrc file'):
+                    # We don't care about sticky and set UID bits because
+                    # this is a regular file.
+                    mode = mode & stat.S_IRWXU
+                    print('Changing permissions of %s' % config_path)
+                    os.chmod(config_path, mode)
+
+        print(FINISHED)
+        return 0
+
+    def prompt_native_extension(self, c, name, prompt_text):
+        # Ask the user if the specified extension bundled with Mercurial should be enabled.
+        if name in c.extensions:
+            return
+        if self._prompt_yn(prompt_text):
+            c.activate_extension(name)
+            print('Activated %s extension.\n' % name)
+
+    def can_use_extension(self, c, name, path=None):
+        # Load extension to hg and search stdout for printed exceptions
+        if not path:
+            path = os.path.join(self.vcs_tools_dir, 'hgext', name)
+        result = subprocess.check_output(['hg',
+             '--config', 'extensions.testmodule=%s' % path,
+             '--config', 'ui.traceback=true'],
+            stderr=subprocess.STDOUT)
+        return b"Traceback" not in result
+
+    def prompt_external_extension(self, c, name, prompt_text, path=None):
+        # Ask the user if the specified extension should be enabled. Defaults
+        # to treating the extension as one in version-control-tools/hgext/
+        # in a directory with the same name as the extension and thus also
+        # flagging the version-control-tools repo as needing an update.
+        if name not in c.extensions:
+            if not self.can_use_extension(c, name, path):
+                return
+            print(name)
+            print('=' * len(name))
+            print('')
+            if not self._prompt_yn(prompt_text):
+                print('')
+                return
+        if not path:
+            # We replace the user's home directory with ~ so the
+            # config file doesn't depend on the path to the home
+            # directory
+            path = os.path.join(self.vcs_tools_dir.replace(os.path.expanduser('~'), '~'), 'hgext', name)
+        c.activate_extension(name, path)
+        print('Activated %s extension.\n' % name)
+
+    def _prompt(self, msg, allow_empty=False):
+        print(msg)
+
+        while True:
+            response = raw_input().decode('utf-8')
+
+            if response:
+                return response
+
+            if allow_empty:
+                return None
+
+            print('You must type something!')
+
+    def _prompt_yn(self, msg):
+        print('%s? [Y/n]' % msg)
+
+        while True:
+            choice = raw_input().lower().strip()
+
+            if not choice:
+                return True
+
+            if choice in ('y', 'yes'):
+                return True
+
+            if choice in ('n', 'no'):
+                return False
+
+            print('Must reply with one of {yes, no, y, n}.')