1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/tools/mercurial/hgsetup/wizard.py Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,310 @@ 1.4 +# This Source Code Form is subject to the terms of the Mozilla Public 1.5 +# License, v. 2.0. If a copy of the MPL was not distributed with this, 1.6 +# file, You can obtain one at http://mozilla.org/MPL/2.0/. 1.7 + 1.8 +from __future__ import unicode_literals 1.9 + 1.10 +import difflib 1.11 +import errno 1.12 +import os 1.13 +import shutil 1.14 +import sys 1.15 +import which 1.16 + 1.17 +from configobj import ConfigObjError 1.18 +from StringIO import StringIO 1.19 + 1.20 +from mozversioncontrol.repoupdate import ( 1.21 + update_mercurial_repo, 1.22 + update_git_repo, 1.23 +) 1.24 + 1.25 +from .config import ( 1.26 + HOST_FINGERPRINTS, 1.27 + MercurialConfig, 1.28 +) 1.29 + 1.30 + 1.31 +INITIAL_MESSAGE = ''' 1.32 +I'm going to help you ensure your Mercurial is configured for optimal 1.33 +development on Mozilla projects. 1.34 + 1.35 +If your environment is missing some recommended settings, I'm going to prompt 1.36 +you whether you want me to make changes: I won't change anything you might not 1.37 +want me changing without your permission! 1.38 + 1.39 +If your config is up-to-date, I'm just going to ensure all 3rd party extensions 1.40 +are up to date and you won't have to do anything. 1.41 + 1.42 +To begin, press the enter/return key. 1.43 +'''.strip() 1.44 + 1.45 +MISSING_USERNAME = ''' 1.46 +You don't have a username defined in your Mercurial config file. In order to 1.47 +send patches to Mozilla, you'll need to attach a name and email address. If you 1.48 +aren't comfortable giving us your full name, pseudonames are acceptable. 1.49 +'''.strip() 1.50 + 1.51 +BAD_DIFF_SETTINGS = ''' 1.52 +Mozilla developers produce patches in a standard format, but your Mercurial is 1.53 +not configured to produce patches in that format. 1.54 +'''.strip() 1.55 + 1.56 +BZEXPORT_INFO = ''' 1.57 +If you plan on uploading patches to Mozilla, there is an extension called 1.58 +bzexport that makes it easy to upload patches from the command line via the 1.59 +|hg bzexport| command. More info is available at 1.60 +https://hg.mozilla.org/hgcustom/version-control-tools/file/default/hgext/bzexport/README 1.61 + 1.62 +Would you like to activate bzexport 1.63 +'''.strip() 1.64 + 1.65 +MQEXT_INFO = ''' 1.66 +The mqext extension (https://bitbucket.org/sfink/mqext) provides a number of 1.67 +useful abilities to Mercurial, including automatically committing changes to 1.68 +your mq patch queue. 1.69 + 1.70 +Would you like to activate mqext 1.71 +'''.strip() 1.72 + 1.73 +QIMPORTBZ_INFO = ''' 1.74 +The qimportbz extension 1.75 +(https://hg.mozilla.org/hgcustom/version-control-tools/file/default/hgext/qimportbz/README) makes it possible to 1.76 +import patches from Bugzilla using a friendly bz:// URL handler. e.g. 1.77 +|hg qimport bz://123456|. 1.78 + 1.79 +Would you like to activate qimportbz 1.80 +'''.strip() 1.81 + 1.82 +QNEWCURRENTUSER_INFO = ''' 1.83 +The mercurial queues command |hg qnew|, which creates new patches in your patch 1.84 +queue does not set patch author information by default. Author information 1.85 +should be included when uploading for review. 1.86 +'''.strip() 1.87 + 1.88 +FINISHED = ''' 1.89 +Your Mercurial should now be properly configured and recommended extensions 1.90 +should be up to date! 1.91 +'''.strip() 1.92 + 1.93 + 1.94 +class MercurialSetupWizard(object): 1.95 + """Command-line wizard to help users configure Mercurial.""" 1.96 + 1.97 + def __init__(self, state_dir): 1.98 + # We use normpath since Mercurial expects the hgrc to use native path 1.99 + # separators, but state_dir uses unix style paths even on Windows. 1.100 + self.state_dir = os.path.normpath(state_dir) 1.101 + self.ext_dir = os.path.join(self.state_dir, 'mercurial', 'extensions') 1.102 + self.vcs_tools_dir = os.path.join(self.state_dir, 'version-control-tools') 1.103 + self.update_vcs_tools = False 1.104 + 1.105 + def run(self, config_paths): 1.106 + try: 1.107 + os.makedirs(self.ext_dir) 1.108 + except OSError as e: 1.109 + if e.errno != errno.EEXIST: 1.110 + raise 1.111 + 1.112 + try: 1.113 + hg = which.which('hg') 1.114 + except which.WhichError as e: 1.115 + print(e) 1.116 + print('Try running |mach bootstrap| to ensure your environment is ' 1.117 + 'up to date.') 1.118 + return 1 1.119 + 1.120 + try: 1.121 + c = MercurialConfig(config_paths) 1.122 + except ConfigObjError as e: 1.123 + print('Error importing existing Mercurial config!\n' 1.124 + '%s\n' 1.125 + 'If using quotes, they must wrap the entire string.' % e) 1.126 + return 1 1.127 + 1.128 + print(INITIAL_MESSAGE) 1.129 + raw_input() 1.130 + 1.131 + if not c.have_valid_username(): 1.132 + print(MISSING_USERNAME) 1.133 + print('') 1.134 + 1.135 + name = self._prompt('What is your name?') 1.136 + email = self._prompt('What is your email address?') 1.137 + c.set_username(name, email) 1.138 + print('Updated your username.') 1.139 + print('') 1.140 + 1.141 + if not c.have_recommended_diff_settings(): 1.142 + print(BAD_DIFF_SETTINGS) 1.143 + print('') 1.144 + if self._prompt_yn('Would you like me to fix this for you'): 1.145 + c.ensure_recommended_diff_settings() 1.146 + print('Fixed patch settings.') 1.147 + print('') 1.148 + 1.149 + self.prompt_native_extension(c, 'progress', 1.150 + 'Would you like to see progress bars during Mercurial operations') 1.151 + 1.152 + self.prompt_native_extension(c, 'color', 1.153 + 'Would you like Mercurial to colorize output to your terminal') 1.154 + 1.155 + self.prompt_native_extension(c, 'rebase', 1.156 + 'Would you like to enable the rebase extension to allow you to move' 1.157 + ' changesets around (which can help maintain a linear history)') 1.158 + 1.159 + self.prompt_native_extension(c, 'mq', 1.160 + 'Would you like to activate the mq extension to manage patches') 1.161 + 1.162 + self.prompt_external_extension(c, 'bzexport', BZEXPORT_INFO) 1.163 + 1.164 + if 'mq' in c.extensions: 1.165 + self.prompt_external_extension(c, 'mqext', MQEXT_INFO, 1.166 + os.path.join(self.ext_dir, 'mqext')) 1.167 + 1.168 + if 'mqext' in c.extensions: 1.169 + self.update_mercurial_repo( 1.170 + hg, 1.171 + 'https://bitbucket.org/sfink/mqext', 1.172 + os.path.join(self.ext_dir, 'mqext'), 1.173 + 'default', 1.174 + 'Ensuring mqext extension is up to date...') 1.175 + 1.176 + if 'mqext' in c.extensions and not c.have_mqext_autocommit_mq(): 1.177 + if self._prompt_yn('Would you like to configure mqext to ' 1.178 + 'automatically commit changes as you modify patches'): 1.179 + c.ensure_mqext_autocommit_mq() 1.180 + print('Configured mqext to auto-commit.\n') 1.181 + 1.182 + self.prompt_external_extension(c, 'qimportbz', QIMPORTBZ_INFO) 1.183 + 1.184 + if not c.have_qnew_currentuser_default(): 1.185 + print(QNEWCURRENTUSER_INFO) 1.186 + if self._prompt_yn('Would you like qnew to set patch author by ' 1.187 + 'default'): 1.188 + c.ensure_qnew_currentuser_default() 1.189 + print('Configured qnew to set patch author by default.') 1.190 + print('') 1.191 + 1.192 + if self.update_vcs_tools: 1.193 + self.update_mercurial_repo( 1.194 + hg, 1.195 + 'https://hg.mozilla.org/hgcustom/version-control-tools', 1.196 + self.vcs_tools_dir, 1.197 + 'default', 1.198 + 'Ensuring version-control-tools is up to date...') 1.199 + 1.200 + # Look for and clean up old extensions. 1.201 + for ext in {'bzexport', 'qimportbz'}: 1.202 + path = os.path.join(self.ext_dir, ext) 1.203 + if os.path.exists(path): 1.204 + if self._prompt_yn('Would you like to remove the old and no ' 1.205 + 'longer referenced repository at %s' % path): 1.206 + print('Cleaning up old repository: %s' % path) 1.207 + shutil.rmtree(path) 1.208 + 1.209 + c.add_mozilla_host_fingerprints() 1.210 + 1.211 + b = StringIO() 1.212 + c.write(b) 1.213 + new_lines = [line.rstrip() for line in b.getvalue().splitlines()] 1.214 + old_lines = [] 1.215 + 1.216 + config_path = c.config_path 1.217 + if os.path.exists(config_path): 1.218 + with open(config_path, 'rt') as fh: 1.219 + old_lines = [line.rstrip() for line in fh.readlines()] 1.220 + 1.221 + diff = list(difflib.unified_diff(old_lines, new_lines, 1.222 + 'hgrc.old', 'hgrc.new')) 1.223 + 1.224 + if len(diff): 1.225 + print('Your Mercurial config file needs updating. I can do this ' 1.226 + 'for you if you like!') 1.227 + if self._prompt_yn('Would you like to see a diff of the changes ' 1.228 + 'first'): 1.229 + for line in diff: 1.230 + print(line) 1.231 + print('') 1.232 + 1.233 + if self._prompt_yn('Would you like me to update your hgrc file'): 1.234 + with open(config_path, 'wt') as fh: 1.235 + c.write(fh) 1.236 + print('Wrote changes to %s.' % config_path) 1.237 + else: 1.238 + print('hgrc changes not written to file. I would have ' 1.239 + 'written the following:\n') 1.240 + c.write(sys.stdout) 1.241 + return 1 1.242 + 1.243 + print(FINISHED) 1.244 + return 0 1.245 + 1.246 + def prompt_native_extension(self, c, name, prompt_text): 1.247 + # Ask the user if the specified extension bundled with Mercurial should be enabled. 1.248 + if name in c.extensions: 1.249 + return 1.250 + if self._prompt_yn(prompt_text): 1.251 + c.activate_extension(name) 1.252 + print('Activated %s extension.\n' % name) 1.253 + 1.254 + def prompt_external_extension(self, c, name, prompt_text, path=None): 1.255 + # Ask the user if the specified extension should be enabled. Defaults 1.256 + # to treating the extension as one in version-control-tools/hgext/ 1.257 + # in a directory with the same name as the extension and thus also 1.258 + # flagging the version-control-tools repo as needing an update. 1.259 + if name not in c.extensions: 1.260 + if not self._prompt_yn(prompt_text): 1.261 + return 1.262 + print('Activated %s extension.\n' % name) 1.263 + if not path: 1.264 + path = os.path.join(self.vcs_tools_dir, 'hgext', name) 1.265 + self.update_vcs_tools = True 1.266 + c.activate_extension(name, path) 1.267 + 1.268 + def update_mercurial_repo(self, hg, url, dest, branch, msg): 1.269 + # We always pass the host fingerprints that we "know" to be canonical 1.270 + # because the existing config may have outdated fingerprints and this 1.271 + # may cause Mercurial to abort. 1.272 + return self._update_repo(hg, url, dest, branch, msg, 1.273 + update_mercurial_repo, hostfingerprints=HOST_FINGERPRINTS) 1.274 + 1.275 + def update_git_repo(self, git, url, dest, ref, msg): 1.276 + return self._update_repo(git, url, dest, ref, msg, update_git_repo) 1.277 + 1.278 + def _update_repo(self, binary, url, dest, branch, msg, fn, *args, **kwargs): 1.279 + print('=' * 80) 1.280 + print(msg) 1.281 + try: 1.282 + fn(binary, url, dest, branch, *args, **kwargs) 1.283 + finally: 1.284 + print('=' * 80) 1.285 + print('') 1.286 + 1.287 + def _prompt(self, msg): 1.288 + print(msg) 1.289 + 1.290 + while True: 1.291 + response = raw_input() 1.292 + 1.293 + if response: 1.294 + return response 1.295 + 1.296 + print('You must type something!') 1.297 + 1.298 + def _prompt_yn(self, msg): 1.299 + print('%s? [Y/n]' % msg) 1.300 + 1.301 + while True: 1.302 + choice = raw_input().lower().strip() 1.303 + 1.304 + if not choice: 1.305 + return True 1.306 + 1.307 + if choice in ('y', 'yes'): 1.308 + return True 1.309 + 1.310 + if choice in ('n', 'no'): 1.311 + return False 1.312 + 1.313 + print('Must reply with one of {yes, no, y, n}.')