Wed, 31 Dec 2014 07:16:47 +0100
Revert simplistic fix pending revisit of Mozilla integration attempt.
michael@0 | 1 | # This Source Code Form is subject to the terms of the Mozilla Public |
michael@0 | 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this, |
michael@0 | 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. |
michael@0 | 4 | |
michael@0 | 5 | from __future__ import unicode_literals |
michael@0 | 6 | |
michael@0 | 7 | import difflib |
michael@0 | 8 | import errno |
michael@0 | 9 | import os |
michael@0 | 10 | import shutil |
michael@0 | 11 | import sys |
michael@0 | 12 | import which |
michael@0 | 13 | |
michael@0 | 14 | from configobj import ConfigObjError |
michael@0 | 15 | from StringIO import StringIO |
michael@0 | 16 | |
michael@0 | 17 | from mozversioncontrol.repoupdate import ( |
michael@0 | 18 | update_mercurial_repo, |
michael@0 | 19 | update_git_repo, |
michael@0 | 20 | ) |
michael@0 | 21 | |
michael@0 | 22 | from .config import ( |
michael@0 | 23 | HOST_FINGERPRINTS, |
michael@0 | 24 | MercurialConfig, |
michael@0 | 25 | ) |
michael@0 | 26 | |
michael@0 | 27 | |
michael@0 | 28 | INITIAL_MESSAGE = ''' |
michael@0 | 29 | I'm going to help you ensure your Mercurial is configured for optimal |
michael@0 | 30 | development on Mozilla projects. |
michael@0 | 31 | |
michael@0 | 32 | If your environment is missing some recommended settings, I'm going to prompt |
michael@0 | 33 | you whether you want me to make changes: I won't change anything you might not |
michael@0 | 34 | want me changing without your permission! |
michael@0 | 35 | |
michael@0 | 36 | If your config is up-to-date, I'm just going to ensure all 3rd party extensions |
michael@0 | 37 | are up to date and you won't have to do anything. |
michael@0 | 38 | |
michael@0 | 39 | To begin, press the enter/return key. |
michael@0 | 40 | '''.strip() |
michael@0 | 41 | |
michael@0 | 42 | MISSING_USERNAME = ''' |
michael@0 | 43 | You don't have a username defined in your Mercurial config file. In order to |
michael@0 | 44 | send patches to Mozilla, you'll need to attach a name and email address. If you |
michael@0 | 45 | aren't comfortable giving us your full name, pseudonames are acceptable. |
michael@0 | 46 | '''.strip() |
michael@0 | 47 | |
michael@0 | 48 | BAD_DIFF_SETTINGS = ''' |
michael@0 | 49 | Mozilla developers produce patches in a standard format, but your Mercurial is |
michael@0 | 50 | not configured to produce patches in that format. |
michael@0 | 51 | '''.strip() |
michael@0 | 52 | |
michael@0 | 53 | BZEXPORT_INFO = ''' |
michael@0 | 54 | If you plan on uploading patches to Mozilla, there is an extension called |
michael@0 | 55 | bzexport that makes it easy to upload patches from the command line via the |
michael@0 | 56 | |hg bzexport| command. More info is available at |
michael@0 | 57 | https://hg.mozilla.org/hgcustom/version-control-tools/file/default/hgext/bzexport/README |
michael@0 | 58 | |
michael@0 | 59 | Would you like to activate bzexport |
michael@0 | 60 | '''.strip() |
michael@0 | 61 | |
michael@0 | 62 | MQEXT_INFO = ''' |
michael@0 | 63 | The mqext extension (https://bitbucket.org/sfink/mqext) provides a number of |
michael@0 | 64 | useful abilities to Mercurial, including automatically committing changes to |
michael@0 | 65 | your mq patch queue. |
michael@0 | 66 | |
michael@0 | 67 | Would you like to activate mqext |
michael@0 | 68 | '''.strip() |
michael@0 | 69 | |
michael@0 | 70 | QIMPORTBZ_INFO = ''' |
michael@0 | 71 | The qimportbz extension |
michael@0 | 72 | (https://hg.mozilla.org/hgcustom/version-control-tools/file/default/hgext/qimportbz/README) makes it possible to |
michael@0 | 73 | import patches from Bugzilla using a friendly bz:// URL handler. e.g. |
michael@0 | 74 | |hg qimport bz://123456|. |
michael@0 | 75 | |
michael@0 | 76 | Would you like to activate qimportbz |
michael@0 | 77 | '''.strip() |
michael@0 | 78 | |
michael@0 | 79 | QNEWCURRENTUSER_INFO = ''' |
michael@0 | 80 | The mercurial queues command |hg qnew|, which creates new patches in your patch |
michael@0 | 81 | queue does not set patch author information by default. Author information |
michael@0 | 82 | should be included when uploading for review. |
michael@0 | 83 | '''.strip() |
michael@0 | 84 | |
michael@0 | 85 | FINISHED = ''' |
michael@0 | 86 | Your Mercurial should now be properly configured and recommended extensions |
michael@0 | 87 | should be up to date! |
michael@0 | 88 | '''.strip() |
michael@0 | 89 | |
michael@0 | 90 | |
michael@0 | 91 | class MercurialSetupWizard(object): |
michael@0 | 92 | """Command-line wizard to help users configure Mercurial.""" |
michael@0 | 93 | |
michael@0 | 94 | def __init__(self, state_dir): |
michael@0 | 95 | # We use normpath since Mercurial expects the hgrc to use native path |
michael@0 | 96 | # separators, but state_dir uses unix style paths even on Windows. |
michael@0 | 97 | self.state_dir = os.path.normpath(state_dir) |
michael@0 | 98 | self.ext_dir = os.path.join(self.state_dir, 'mercurial', 'extensions') |
michael@0 | 99 | self.vcs_tools_dir = os.path.join(self.state_dir, 'version-control-tools') |
michael@0 | 100 | self.update_vcs_tools = False |
michael@0 | 101 | |
michael@0 | 102 | def run(self, config_paths): |
michael@0 | 103 | try: |
michael@0 | 104 | os.makedirs(self.ext_dir) |
michael@0 | 105 | except OSError as e: |
michael@0 | 106 | if e.errno != errno.EEXIST: |
michael@0 | 107 | raise |
michael@0 | 108 | |
michael@0 | 109 | try: |
michael@0 | 110 | hg = which.which('hg') |
michael@0 | 111 | except which.WhichError as e: |
michael@0 | 112 | print(e) |
michael@0 | 113 | print('Try running |mach bootstrap| to ensure your environment is ' |
michael@0 | 114 | 'up to date.') |
michael@0 | 115 | return 1 |
michael@0 | 116 | |
michael@0 | 117 | try: |
michael@0 | 118 | c = MercurialConfig(config_paths) |
michael@0 | 119 | except ConfigObjError as e: |
michael@0 | 120 | print('Error importing existing Mercurial config!\n' |
michael@0 | 121 | '%s\n' |
michael@0 | 122 | 'If using quotes, they must wrap the entire string.' % e) |
michael@0 | 123 | return 1 |
michael@0 | 124 | |
michael@0 | 125 | print(INITIAL_MESSAGE) |
michael@0 | 126 | raw_input() |
michael@0 | 127 | |
michael@0 | 128 | if not c.have_valid_username(): |
michael@0 | 129 | print(MISSING_USERNAME) |
michael@0 | 130 | print('') |
michael@0 | 131 | |
michael@0 | 132 | name = self._prompt('What is your name?') |
michael@0 | 133 | email = self._prompt('What is your email address?') |
michael@0 | 134 | c.set_username(name, email) |
michael@0 | 135 | print('Updated your username.') |
michael@0 | 136 | print('') |
michael@0 | 137 | |
michael@0 | 138 | if not c.have_recommended_diff_settings(): |
michael@0 | 139 | print(BAD_DIFF_SETTINGS) |
michael@0 | 140 | print('') |
michael@0 | 141 | if self._prompt_yn('Would you like me to fix this for you'): |
michael@0 | 142 | c.ensure_recommended_diff_settings() |
michael@0 | 143 | print('Fixed patch settings.') |
michael@0 | 144 | print('') |
michael@0 | 145 | |
michael@0 | 146 | self.prompt_native_extension(c, 'progress', |
michael@0 | 147 | 'Would you like to see progress bars during Mercurial operations') |
michael@0 | 148 | |
michael@0 | 149 | self.prompt_native_extension(c, 'color', |
michael@0 | 150 | 'Would you like Mercurial to colorize output to your terminal') |
michael@0 | 151 | |
michael@0 | 152 | self.prompt_native_extension(c, 'rebase', |
michael@0 | 153 | 'Would you like to enable the rebase extension to allow you to move' |
michael@0 | 154 | ' changesets around (which can help maintain a linear history)') |
michael@0 | 155 | |
michael@0 | 156 | self.prompt_native_extension(c, 'mq', |
michael@0 | 157 | 'Would you like to activate the mq extension to manage patches') |
michael@0 | 158 | |
michael@0 | 159 | self.prompt_external_extension(c, 'bzexport', BZEXPORT_INFO) |
michael@0 | 160 | |
michael@0 | 161 | if 'mq' in c.extensions: |
michael@0 | 162 | self.prompt_external_extension(c, 'mqext', MQEXT_INFO, |
michael@0 | 163 | os.path.join(self.ext_dir, 'mqext')) |
michael@0 | 164 | |
michael@0 | 165 | if 'mqext' in c.extensions: |
michael@0 | 166 | self.update_mercurial_repo( |
michael@0 | 167 | hg, |
michael@0 | 168 | 'https://bitbucket.org/sfink/mqext', |
michael@0 | 169 | os.path.join(self.ext_dir, 'mqext'), |
michael@0 | 170 | 'default', |
michael@0 | 171 | 'Ensuring mqext extension is up to date...') |
michael@0 | 172 | |
michael@0 | 173 | if 'mqext' in c.extensions and not c.have_mqext_autocommit_mq(): |
michael@0 | 174 | if self._prompt_yn('Would you like to configure mqext to ' |
michael@0 | 175 | 'automatically commit changes as you modify patches'): |
michael@0 | 176 | c.ensure_mqext_autocommit_mq() |
michael@0 | 177 | print('Configured mqext to auto-commit.\n') |
michael@0 | 178 | |
michael@0 | 179 | self.prompt_external_extension(c, 'qimportbz', QIMPORTBZ_INFO) |
michael@0 | 180 | |
michael@0 | 181 | if not c.have_qnew_currentuser_default(): |
michael@0 | 182 | print(QNEWCURRENTUSER_INFO) |
michael@0 | 183 | if self._prompt_yn('Would you like qnew to set patch author by ' |
michael@0 | 184 | 'default'): |
michael@0 | 185 | c.ensure_qnew_currentuser_default() |
michael@0 | 186 | print('Configured qnew to set patch author by default.') |
michael@0 | 187 | print('') |
michael@0 | 188 | |
michael@0 | 189 | if self.update_vcs_tools: |
michael@0 | 190 | self.update_mercurial_repo( |
michael@0 | 191 | hg, |
michael@0 | 192 | 'https://hg.mozilla.org/hgcustom/version-control-tools', |
michael@0 | 193 | self.vcs_tools_dir, |
michael@0 | 194 | 'default', |
michael@0 | 195 | 'Ensuring version-control-tools is up to date...') |
michael@0 | 196 | |
michael@0 | 197 | # Look for and clean up old extensions. |
michael@0 | 198 | for ext in {'bzexport', 'qimportbz'}: |
michael@0 | 199 | path = os.path.join(self.ext_dir, ext) |
michael@0 | 200 | if os.path.exists(path): |
michael@0 | 201 | if self._prompt_yn('Would you like to remove the old and no ' |
michael@0 | 202 | 'longer referenced repository at %s' % path): |
michael@0 | 203 | print('Cleaning up old repository: %s' % path) |
michael@0 | 204 | shutil.rmtree(path) |
michael@0 | 205 | |
michael@0 | 206 | c.add_mozilla_host_fingerprints() |
michael@0 | 207 | |
michael@0 | 208 | b = StringIO() |
michael@0 | 209 | c.write(b) |
michael@0 | 210 | new_lines = [line.rstrip() for line in b.getvalue().splitlines()] |
michael@0 | 211 | old_lines = [] |
michael@0 | 212 | |
michael@0 | 213 | config_path = c.config_path |
michael@0 | 214 | if os.path.exists(config_path): |
michael@0 | 215 | with open(config_path, 'rt') as fh: |
michael@0 | 216 | old_lines = [line.rstrip() for line in fh.readlines()] |
michael@0 | 217 | |
michael@0 | 218 | diff = list(difflib.unified_diff(old_lines, new_lines, |
michael@0 | 219 | 'hgrc.old', 'hgrc.new')) |
michael@0 | 220 | |
michael@0 | 221 | if len(diff): |
michael@0 | 222 | print('Your Mercurial config file needs updating. I can do this ' |
michael@0 | 223 | 'for you if you like!') |
michael@0 | 224 | if self._prompt_yn('Would you like to see a diff of the changes ' |
michael@0 | 225 | 'first'): |
michael@0 | 226 | for line in diff: |
michael@0 | 227 | print(line) |
michael@0 | 228 | print('') |
michael@0 | 229 | |
michael@0 | 230 | if self._prompt_yn('Would you like me to update your hgrc file'): |
michael@0 | 231 | with open(config_path, 'wt') as fh: |
michael@0 | 232 | c.write(fh) |
michael@0 | 233 | print('Wrote changes to %s.' % config_path) |
michael@0 | 234 | else: |
michael@0 | 235 | print('hgrc changes not written to file. I would have ' |
michael@0 | 236 | 'written the following:\n') |
michael@0 | 237 | c.write(sys.stdout) |
michael@0 | 238 | return 1 |
michael@0 | 239 | |
michael@0 | 240 | print(FINISHED) |
michael@0 | 241 | return 0 |
michael@0 | 242 | |
michael@0 | 243 | def prompt_native_extension(self, c, name, prompt_text): |
michael@0 | 244 | # Ask the user if the specified extension bundled with Mercurial should be enabled. |
michael@0 | 245 | if name in c.extensions: |
michael@0 | 246 | return |
michael@0 | 247 | if self._prompt_yn(prompt_text): |
michael@0 | 248 | c.activate_extension(name) |
michael@0 | 249 | print('Activated %s extension.\n' % name) |
michael@0 | 250 | |
michael@0 | 251 | def prompt_external_extension(self, c, name, prompt_text, path=None): |
michael@0 | 252 | # Ask the user if the specified extension should be enabled. Defaults |
michael@0 | 253 | # to treating the extension as one in version-control-tools/hgext/ |
michael@0 | 254 | # in a directory with the same name as the extension and thus also |
michael@0 | 255 | # flagging the version-control-tools repo as needing an update. |
michael@0 | 256 | if name not in c.extensions: |
michael@0 | 257 | if not self._prompt_yn(prompt_text): |
michael@0 | 258 | return |
michael@0 | 259 | print('Activated %s extension.\n' % name) |
michael@0 | 260 | if not path: |
michael@0 | 261 | path = os.path.join(self.vcs_tools_dir, 'hgext', name) |
michael@0 | 262 | self.update_vcs_tools = True |
michael@0 | 263 | c.activate_extension(name, path) |
michael@0 | 264 | |
michael@0 | 265 | def update_mercurial_repo(self, hg, url, dest, branch, msg): |
michael@0 | 266 | # We always pass the host fingerprints that we "know" to be canonical |
michael@0 | 267 | # because the existing config may have outdated fingerprints and this |
michael@0 | 268 | # may cause Mercurial to abort. |
michael@0 | 269 | return self._update_repo(hg, url, dest, branch, msg, |
michael@0 | 270 | update_mercurial_repo, hostfingerprints=HOST_FINGERPRINTS) |
michael@0 | 271 | |
michael@0 | 272 | def update_git_repo(self, git, url, dest, ref, msg): |
michael@0 | 273 | return self._update_repo(git, url, dest, ref, msg, update_git_repo) |
michael@0 | 274 | |
michael@0 | 275 | def _update_repo(self, binary, url, dest, branch, msg, fn, *args, **kwargs): |
michael@0 | 276 | print('=' * 80) |
michael@0 | 277 | print(msg) |
michael@0 | 278 | try: |
michael@0 | 279 | fn(binary, url, dest, branch, *args, **kwargs) |
michael@0 | 280 | finally: |
michael@0 | 281 | print('=' * 80) |
michael@0 | 282 | print('') |
michael@0 | 283 | |
michael@0 | 284 | def _prompt(self, msg): |
michael@0 | 285 | print(msg) |
michael@0 | 286 | |
michael@0 | 287 | while True: |
michael@0 | 288 | response = raw_input() |
michael@0 | 289 | |
michael@0 | 290 | if response: |
michael@0 | 291 | return response |
michael@0 | 292 | |
michael@0 | 293 | print('You must type something!') |
michael@0 | 294 | |
michael@0 | 295 | def _prompt_yn(self, msg): |
michael@0 | 296 | print('%s? [Y/n]' % msg) |
michael@0 | 297 | |
michael@0 | 298 | while True: |
michael@0 | 299 | choice = raw_input().lower().strip() |
michael@0 | 300 | |
michael@0 | 301 | if not choice: |
michael@0 | 302 | return True |
michael@0 | 303 | |
michael@0 | 304 | if choice in ('y', 'yes'): |
michael@0 | 305 | return True |
michael@0 | 306 | |
michael@0 | 307 | if choice in ('n', 'no'): |
michael@0 | 308 | return False |
michael@0 | 309 | |
michael@0 | 310 | print('Must reply with one of {yes, no, y, n}.') |