зеркало из https://github.com/mozilla/gecko-dev.git
560 строки
20 KiB
Python
560 строки
20 KiB
Python
# 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 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()
|
|
|
|
OLDEST_NON_LEGACY_VERSION = LooseVersion('3.2.4')
|
|
LEGACY_MERCURIAL = '''
|
|
You are running an out of date Mercurial client (%s).
|
|
|
|
For a faster and better Mercurial experience, we HIGHLY recommend you
|
|
upgrade.
|
|
'''.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()
|
|
|
|
MQ_INFO = '''
|
|
The mq extension manages patches as separate files. It provides an
|
|
alternative to the recommended bookmark-based development workflow.
|
|
|
|
If you are a newcomer to Mercurial or are coming from Git, it is
|
|
recommended to avoid mq.
|
|
|
|
(Relevant config option: extensions.mq)
|
|
|
|
Would you like to activate the mq extension
|
|
'''.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()
|
|
|
|
MQEXT_INFO = '''
|
|
The mqext extension adds a number of features, including automatically committing
|
|
changes to your mq patch queue. More info is available at
|
|
https://hg.mozilla.org/hgcustom/version-control-tools/file/default/hgext/mqext/README.txt
|
|
|
|
(Relevant config option: extensions.mqext)
|
|
|
|
Would you like to activate mqext
|
|
'''.strip()
|
|
|
|
QIMPORTBZ_INFO = '''
|
|
The qimportbz extension
|
|
(https://hg.mozilla.org/hgcustom/version-control-tools/file/default/hgext/qimportbz/README) makes it possible to
|
|
import patches from Bugzilla using a friendly bz:// URL handler. e.g.
|
|
|hg qimport bz://123456|.
|
|
|
|
(Relevant config option: extensions.qimportbz)
|
|
|
|
Would you like to activate qimportbz
|
|
'''.strip()
|
|
|
|
QNEWCURRENTUSER_INFO = '''
|
|
The mercurial queues command |hg qnew|, which creates new patches in your patch
|
|
queue does not set patch author information by default. Author information
|
|
should be included when uploading for review.
|
|
'''.strip()
|
|
|
|
FINISHED = '''
|
|
Your Mercurial should now be properly configured and recommended extensions
|
|
should be up to date!
|
|
'''.strip()
|
|
|
|
REVIEWBOARD_MINIMUM_VERSION = LooseVersion('3.1')
|
|
|
|
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.1')
|
|
|
|
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.1')
|
|
|
|
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.3')
|
|
|
|
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()
|
|
|
|
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()
|
|
|
|
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`)')
|
|
|
|
self.prompt_native_extension(c, 'mq', MQ_INFO)
|
|
|
|
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)
|
|
|
|
if 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 'mq' in c.extensions:
|
|
self.prompt_external_extension(c, 'mqext', MQEXT_INFO)
|
|
|
|
if 'mqext' in c.extensions and not c.have_mqext_autocommit_mq():
|
|
if self._prompt_yn('Would you like to configure mqext to '
|
|
'automatically commit changes as you modify patches'):
|
|
c.ensure_mqext_autocommit_mq()
|
|
print('Configured mqext to auto-commit.\n')
|
|
|
|
self.prompt_external_extension(c, 'qimportbz', QIMPORTBZ_INFO)
|
|
|
|
if not c.have_qnew_currentuser_default():
|
|
print(QNEWCURRENTUSER_INFO)
|
|
if self._prompt_yn('Would you like qnew to set patch author by '
|
|
'default'):
|
|
c.ensure_qnew_currentuser_default()
|
|
print('Configured qnew to set patch author by default.')
|
|
print('')
|
|
|
|
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)
|
|
|
|
c.add_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:
|
|
path = os.path.join(self.vcs_tools_dir, '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}.')
|