michael@0: # This Source Code Form is subject to the terms of the Mozilla Public michael@0: # License, v. 2.0. If a copy of the MPL was not distributed with this, michael@0: # file, You can obtain one at http://mozilla.org/MPL/2.0/. michael@0: michael@0: from __future__ import unicode_literals michael@0: michael@0: import difflib michael@0: import errno michael@0: import os michael@0: import shutil michael@0: import sys michael@0: import which michael@0: michael@0: from configobj import ConfigObjError michael@0: from StringIO import StringIO michael@0: michael@0: from mozversioncontrol.repoupdate import ( michael@0: update_mercurial_repo, michael@0: update_git_repo, michael@0: ) michael@0: michael@0: from .config import ( michael@0: HOST_FINGERPRINTS, michael@0: MercurialConfig, michael@0: ) michael@0: michael@0: michael@0: INITIAL_MESSAGE = ''' michael@0: I'm going to help you ensure your Mercurial is configured for optimal michael@0: development on Mozilla projects. michael@0: michael@0: If your environment is missing some recommended settings, I'm going to prompt michael@0: you whether you want me to make changes: I won't change anything you might not michael@0: want me changing without your permission! michael@0: michael@0: If your config is up-to-date, I'm just going to ensure all 3rd party extensions michael@0: are up to date and you won't have to do anything. michael@0: michael@0: To begin, press the enter/return key. michael@0: '''.strip() michael@0: michael@0: MISSING_USERNAME = ''' michael@0: You don't have a username defined in your Mercurial config file. In order to michael@0: send patches to Mozilla, you'll need to attach a name and email address. If you michael@0: aren't comfortable giving us your full name, pseudonames are acceptable. michael@0: '''.strip() michael@0: michael@0: BAD_DIFF_SETTINGS = ''' michael@0: Mozilla developers produce patches in a standard format, but your Mercurial is michael@0: not configured to produce patches in that format. michael@0: '''.strip() michael@0: michael@0: BZEXPORT_INFO = ''' michael@0: If you plan on uploading patches to Mozilla, there is an extension called michael@0: bzexport that makes it easy to upload patches from the command line via the michael@0: |hg bzexport| command. More info is available at michael@0: https://hg.mozilla.org/hgcustom/version-control-tools/file/default/hgext/bzexport/README michael@0: michael@0: Would you like to activate bzexport michael@0: '''.strip() michael@0: michael@0: MQEXT_INFO = ''' michael@0: The mqext extension (https://bitbucket.org/sfink/mqext) provides a number of michael@0: useful abilities to Mercurial, including automatically committing changes to michael@0: your mq patch queue. michael@0: michael@0: Would you like to activate mqext michael@0: '''.strip() michael@0: michael@0: QIMPORTBZ_INFO = ''' michael@0: The qimportbz extension michael@0: (https://hg.mozilla.org/hgcustom/version-control-tools/file/default/hgext/qimportbz/README) makes it possible to michael@0: import patches from Bugzilla using a friendly bz:// URL handler. e.g. michael@0: |hg qimport bz://123456|. michael@0: michael@0: Would you like to activate qimportbz michael@0: '''.strip() michael@0: michael@0: QNEWCURRENTUSER_INFO = ''' michael@0: The mercurial queues command |hg qnew|, which creates new patches in your patch michael@0: queue does not set patch author information by default. Author information michael@0: should be included when uploading for review. michael@0: '''.strip() michael@0: michael@0: FINISHED = ''' michael@0: Your Mercurial should now be properly configured and recommended extensions michael@0: should be up to date! michael@0: '''.strip() michael@0: michael@0: michael@0: class MercurialSetupWizard(object): michael@0: """Command-line wizard to help users configure Mercurial.""" michael@0: michael@0: def __init__(self, state_dir): michael@0: # We use normpath since Mercurial expects the hgrc to use native path michael@0: # separators, but state_dir uses unix style paths even on Windows. michael@0: self.state_dir = os.path.normpath(state_dir) michael@0: self.ext_dir = os.path.join(self.state_dir, 'mercurial', 'extensions') michael@0: self.vcs_tools_dir = os.path.join(self.state_dir, 'version-control-tools') michael@0: self.update_vcs_tools = False michael@0: michael@0: def run(self, config_paths): michael@0: try: michael@0: os.makedirs(self.ext_dir) michael@0: except OSError as e: michael@0: if e.errno != errno.EEXIST: michael@0: raise michael@0: michael@0: try: michael@0: hg = which.which('hg') michael@0: except which.WhichError as e: michael@0: print(e) michael@0: print('Try running |mach bootstrap| to ensure your environment is ' michael@0: 'up to date.') michael@0: return 1 michael@0: michael@0: try: michael@0: c = MercurialConfig(config_paths) michael@0: except ConfigObjError as e: michael@0: print('Error importing existing Mercurial config!\n' michael@0: '%s\n' michael@0: 'If using quotes, they must wrap the entire string.' % e) michael@0: return 1 michael@0: michael@0: print(INITIAL_MESSAGE) michael@0: raw_input() michael@0: michael@0: if not c.have_valid_username(): michael@0: print(MISSING_USERNAME) michael@0: print('') michael@0: michael@0: name = self._prompt('What is your name?') michael@0: email = self._prompt('What is your email address?') michael@0: c.set_username(name, email) michael@0: print('Updated your username.') michael@0: print('') michael@0: michael@0: if not c.have_recommended_diff_settings(): michael@0: print(BAD_DIFF_SETTINGS) michael@0: print('') michael@0: if self._prompt_yn('Would you like me to fix this for you'): michael@0: c.ensure_recommended_diff_settings() michael@0: print('Fixed patch settings.') michael@0: print('') michael@0: michael@0: self.prompt_native_extension(c, 'progress', michael@0: 'Would you like to see progress bars during Mercurial operations') michael@0: michael@0: self.prompt_native_extension(c, 'color', michael@0: 'Would you like Mercurial to colorize output to your terminal') michael@0: michael@0: self.prompt_native_extension(c, 'rebase', michael@0: 'Would you like to enable the rebase extension to allow you to move' michael@0: ' changesets around (which can help maintain a linear history)') michael@0: michael@0: self.prompt_native_extension(c, 'mq', michael@0: 'Would you like to activate the mq extension to manage patches') michael@0: michael@0: self.prompt_external_extension(c, 'bzexport', BZEXPORT_INFO) michael@0: michael@0: if 'mq' in c.extensions: michael@0: self.prompt_external_extension(c, 'mqext', MQEXT_INFO, michael@0: os.path.join(self.ext_dir, 'mqext')) michael@0: michael@0: if 'mqext' in c.extensions: michael@0: self.update_mercurial_repo( michael@0: hg, michael@0: 'https://bitbucket.org/sfink/mqext', michael@0: os.path.join(self.ext_dir, 'mqext'), michael@0: 'default', michael@0: 'Ensuring mqext extension is up to date...') michael@0: michael@0: if 'mqext' in c.extensions and not c.have_mqext_autocommit_mq(): michael@0: if self._prompt_yn('Would you like to configure mqext to ' michael@0: 'automatically commit changes as you modify patches'): michael@0: c.ensure_mqext_autocommit_mq() michael@0: print('Configured mqext to auto-commit.\n') michael@0: michael@0: self.prompt_external_extension(c, 'qimportbz', QIMPORTBZ_INFO) michael@0: michael@0: if not c.have_qnew_currentuser_default(): michael@0: print(QNEWCURRENTUSER_INFO) michael@0: if self._prompt_yn('Would you like qnew to set patch author by ' michael@0: 'default'): michael@0: c.ensure_qnew_currentuser_default() michael@0: print('Configured qnew to set patch author by default.') michael@0: print('') michael@0: michael@0: if self.update_vcs_tools: michael@0: self.update_mercurial_repo( michael@0: hg, michael@0: 'https://hg.mozilla.org/hgcustom/version-control-tools', michael@0: self.vcs_tools_dir, michael@0: 'default', michael@0: 'Ensuring version-control-tools is up to date...') michael@0: michael@0: # Look for and clean up old extensions. michael@0: for ext in {'bzexport', 'qimportbz'}: michael@0: path = os.path.join(self.ext_dir, ext) michael@0: if os.path.exists(path): michael@0: if self._prompt_yn('Would you like to remove the old and no ' michael@0: 'longer referenced repository at %s' % path): michael@0: print('Cleaning up old repository: %s' % path) michael@0: shutil.rmtree(path) michael@0: michael@0: c.add_mozilla_host_fingerprints() michael@0: michael@0: b = StringIO() michael@0: c.write(b) michael@0: new_lines = [line.rstrip() for line in b.getvalue().splitlines()] michael@0: old_lines = [] michael@0: michael@0: config_path = c.config_path michael@0: if os.path.exists(config_path): michael@0: with open(config_path, 'rt') as fh: michael@0: old_lines = [line.rstrip() for line in fh.readlines()] michael@0: michael@0: diff = list(difflib.unified_diff(old_lines, new_lines, michael@0: 'hgrc.old', 'hgrc.new')) michael@0: michael@0: if len(diff): michael@0: print('Your Mercurial config file needs updating. I can do this ' michael@0: 'for you if you like!') michael@0: if self._prompt_yn('Would you like to see a diff of the changes ' michael@0: 'first'): michael@0: for line in diff: michael@0: print(line) michael@0: print('') michael@0: michael@0: if self._prompt_yn('Would you like me to update your hgrc file'): michael@0: with open(config_path, 'wt') as fh: michael@0: c.write(fh) michael@0: print('Wrote changes to %s.' % config_path) michael@0: else: michael@0: print('hgrc changes not written to file. I would have ' michael@0: 'written the following:\n') michael@0: c.write(sys.stdout) michael@0: return 1 michael@0: michael@0: print(FINISHED) michael@0: return 0 michael@0: michael@0: def prompt_native_extension(self, c, name, prompt_text): michael@0: # Ask the user if the specified extension bundled with Mercurial should be enabled. michael@0: if name in c.extensions: michael@0: return michael@0: if self._prompt_yn(prompt_text): michael@0: c.activate_extension(name) michael@0: print('Activated %s extension.\n' % name) michael@0: michael@0: def prompt_external_extension(self, c, name, prompt_text, path=None): michael@0: # Ask the user if the specified extension should be enabled. Defaults michael@0: # to treating the extension as one in version-control-tools/hgext/ michael@0: # in a directory with the same name as the extension and thus also michael@0: # flagging the version-control-tools repo as needing an update. michael@0: if name not in c.extensions: michael@0: if not self._prompt_yn(prompt_text): michael@0: return michael@0: print('Activated %s extension.\n' % name) michael@0: if not path: michael@0: path = os.path.join(self.vcs_tools_dir, 'hgext', name) michael@0: self.update_vcs_tools = True michael@0: c.activate_extension(name, path) michael@0: michael@0: def update_mercurial_repo(self, hg, url, dest, branch, msg): michael@0: # We always pass the host fingerprints that we "know" to be canonical michael@0: # because the existing config may have outdated fingerprints and this michael@0: # may cause Mercurial to abort. michael@0: return self._update_repo(hg, url, dest, branch, msg, michael@0: update_mercurial_repo, hostfingerprints=HOST_FINGERPRINTS) michael@0: michael@0: def update_git_repo(self, git, url, dest, ref, msg): michael@0: return self._update_repo(git, url, dest, ref, msg, update_git_repo) michael@0: michael@0: def _update_repo(self, binary, url, dest, branch, msg, fn, *args, **kwargs): michael@0: print('=' * 80) michael@0: print(msg) michael@0: try: michael@0: fn(binary, url, dest, branch, *args, **kwargs) michael@0: finally: michael@0: print('=' * 80) michael@0: print('') michael@0: michael@0: def _prompt(self, msg): michael@0: print(msg) michael@0: michael@0: while True: michael@0: response = raw_input() michael@0: michael@0: if response: michael@0: return response michael@0: michael@0: print('You must type something!') michael@0: michael@0: def _prompt_yn(self, msg): michael@0: print('%s? [Y/n]' % msg) michael@0: michael@0: while True: michael@0: choice = raw_input().lower().strip() michael@0: michael@0: if not choice: michael@0: return True michael@0: michael@0: if choice in ('y', 'yes'): michael@0: return True michael@0: michael@0: if choice in ('n', 'no'): michael@0: return False michael@0: michael@0: print('Must reply with one of {yes, no, y, n}.')