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