SHOW:
|
|
- or go back to the newest paste.
1 | #!/usr/bin/env python | |
2 | # -*- coding: utf-8 | |
3 | ||
4 | from __future__ import division | |
5 | from __future__ import absolute_import | |
6 | from __future__ import print_function | |
7 | ||
8 | import os.path | |
9 | import os | |
10 | import ConfigParser | |
11 | import sys | |
12 | import argparse | |
13 | import logging | |
14 | import json | |
15 | import urllib | |
16 | import urllib2 | |
17 | import urlparse | |
18 | import base64 | |
19 | import subprocess | |
20 | import re | |
21 | import getpass | |
22 | import traceback | |
23 | import locale | |
24 | ||
25 | MIN_CRUCIBLE_VERSION = '3.0.0' | |
26 | SCRIPT_NAME = os.path.basename(__file__) | |
27 | SCRIPT_VERSION='732ec691cc8ddde4b140f2b2022347c2' | |
28 | ||
29 | ||
30 | ### subprocess wrappers | |
31 | def check_output(*popenargs, **kwargs): | |
32 | """Run command with arguments and return its output as a byte string.""" | |
33 | if 'stdout' in kwargs or 'stderr' in kwargs: | |
34 | raise ValueError('stdout argument not allowed, it will be overridden.') | |
35 | logging.debug('Trying to execute %s', popenargs) | |
36 | process = subprocess.Popen(stdout=subprocess.PIPE, stderr=subprocess.PIPE, *popenargs, **kwargs) | |
37 | output, err = process.communicate() | |
38 | retcode = process.poll() | |
39 | if retcode: | |
40 | cmd = kwargs.get("args") | |
41 | if cmd is None: | |
42 | cmd = popenargs[0] | |
43 | logging.debug('Error executing, exit code %s\nstdout=%s\nstderr=%s', retcode, output, err) | |
44 | raise CalledProcessError(retcode, cmd, output=output, error=err) | |
45 | logging.debug('Finished executing, exit code 0\nstdout=%s\nstderr=%s', output, err) | |
46 | return output | |
47 | ||
48 | ||
49 | class CalledProcessError(subprocess.CalledProcessError): | |
50 | def __init__(self, returncode, cmd, output=None, error=None): | |
51 | super(CalledProcessError, self).__init__(returncode, cmd, output) | |
52 | self.error = error | |
53 | ||
54 | ||
55 | class Console: | |
56 | NO_ANSI = sys.platform == 'win32' or not sys.stdout.isatty() | |
57 | GREEN = '\033[92m' | |
58 | RED = '\033[91m' | |
59 | ESCAPE = '\033[0m' | |
60 | ||
61 | @staticmethod | |
62 | def print(s, color=None): | |
63 | if Console.NO_ANSI or not color: | |
64 | print(s) | |
65 | else: | |
66 | print('%s%s%s' % (color, s, Console.ESCAPE)) | |
67 | ||
68 | @staticmethod | |
69 | def error(s): | |
70 | Console.print(s, color=Console.RED) | |
71 | ||
72 | @staticmethod | |
73 | def success(s): | |
74 | Console.print(s, color=Console.GREEN) | |
75 | ||
76 | ||
77 | class HTTPRedirectHandler(urllib2.HTTPRedirectHandler): | |
78 | """ Override HTTPRedirectHandler to make sure the request data is preserved on redirect """ | |
79 | def redirect_request(self, req, fp, code, msg, headers, newurl): | |
80 | m = req.get_method() | |
81 | if code in (301, 302, 303, 307) and m in ("GET", "HEAD") or code in (301, 302, 303) and m == "POST": | |
82 | newurl = newurl.replace(' ', '%20') | |
83 | newheaders = dict((k, v) for k, v in req.headers.items() if k.lower() not in 'content-length') | |
84 | return urllib2.Request(newurl, | |
85 | headers=newheaders, | |
86 | data=req.data, | |
87 | origin_req_host=req.get_origin_req_host(), | |
88 | unverifiable=True) | |
89 | else: | |
90 | raise urllib2.HTTPError(req.get_full_url(), code, msg, headers, fp) | |
91 | ||
92 | ||
93 | class Configuration(object): | |
94 | """Represents the configuration of the current execution of the script""" | |
95 | def __init__(self): | |
96 | self._url = None | |
97 | self.username = None | |
98 | self.password = None | |
99 | self.authtoken = None | |
100 | self._id = None | |
101 | self.title = None | |
102 | self.reviewers = [] | |
103 | self.moderator = None | |
104 | self.executables = { | |
105 | 'hg': 'hg', | |
106 | 'svn': 'svn', | |
107 | 'git': 'git', | |
108 | 'p4': 'p4', | |
109 | 'cvs': 'cvs', | |
110 | } | |
111 | self.repository = None | |
112 | self.last_project = None | |
113 | self.initial_fill = True | |
114 | self.no_anchor = False | |
115 | self.encoding = None | |
116 | self.diff_file = None | |
117 | self.patch_source = None | |
118 | self.new_patch_source = False | |
119 | ||
120 | @property | |
121 | def url(self): | |
122 | return self._url | |
123 | ||
124 | @url.setter | |
125 | def url(self, value): | |
126 | self._url = value.rstrip('/').strip() if value else None | |
127 | ||
128 | @property | |
129 | def id(self): | |
130 | return self._id | |
131 | ||
132 | @id.setter | |
133 | def id(self, value): | |
134 | self._id = value.strip() if value else None | |
135 | ||
136 | def fill_from_defaults(self): | |
137 | self.url, self.username = self.url or 'http://atlassian-demo:8060', self.username or 'pmonteagudo' | |
138 | return self | |
139 | ||
140 | def fill_from_config_file(self, config_file): | |
141 | if not self.url: | |
142 | self.url = config_file.get_default_url() | |
143 | ||
144 | if not self.password: | |
145 | stored_token, stored_user = config_file.get_token(self.url) | |
146 | if not self.username or self.username == stored_user: | |
147 | self.authtoken, self.username = stored_token, stored_user | |
148 | ||
149 | return self | |
150 | ||
151 | def fill_from_args(self, args): | |
152 | self.url = args.server or self.url | |
153 | self.username = args.user or self.username | |
154 | self.password = args.password or self.password | |
155 | self.title = args.title or self.title | |
156 | ||
157 | args_dict = vars(args) | |
158 | self.id = args_dict[str('project/review')] or self.id | |
159 | if '@reviewer' in args_dict: | |
160 | for reviewer in args_dict[str('@reviewer')]: | |
161 | self.reviewers.append(reviewer.lstrip('@').strip()) | |
162 | logging.debug('Parsed reviewers: %s', self.reviewers) | |
163 | self.moderator = args.moderator.lstrip('@').strip() if args.moderator else self.moderator | |
164 | self.repository = args.repository or self.repository | |
165 | self.no_anchor = args.noanchor or self.no_anchor | |
166 | self.encoding = args.encoding or self.encoding | |
167 | self.diff_file = args.file or self.diff_file | |
168 | self.new_patch_source = args.newpatch or self.new_patch_source | |
169 | ||
170 | return self | |
171 | ||
172 | def validate(self, check_title=False, check_id=False): | |
173 | """Checks if all the required server options are set""" | |
174 | if self.url and (self.authtoken or (self.username and self.password)) and (not check_id or self.id) and (not check_title or self.title): | |
175 | return True | |
176 | ||
177 | if not self.url: | |
178 | Console.error('ERROR: Please specify a Crucible server') | |
179 | if not self.authtoken and not self.username: | |
180 | Console.error('ERROR: Please specify a Crucible username') | |
181 | if not self.authtoken and not self.password: | |
182 | Console.error('ERROR: Please specify a Crucible password') | |
183 | if check_id and not self.id: | |
184 | Console.error('ERROR: Please specify a project or review id') | |
185 | if check_title and not self.title: | |
186 | Console.error('ERROR: Please specify a review title') | |
187 | print(CommandLine().help_blurb()) | |
188 | sys.exit(1) | |
189 | ||
190 | def fill_interactively(self, get_title=False, get_id=False, get_reviewers=False): | |
191 | """Fills the required parameters by prompting the user if they're not specified""" | |
192 | if not sys.stdin.isatty(): | |
193 | logging.debug('Not prompting for parameters interactively because stdin is not a tty') | |
194 | else: | |
195 | if not self.url: | |
196 | self.url = raw_input('Please enter the Crucible server URL: ') | |
197 | elif self.initial_fill: | |
198 | print('Crucible server: %s' % self.url) | |
199 | ||
200 | if not self.username: | |
201 | self.username = raw_input('Please enter your Crucible username: ') | |
202 | elif self.initial_fill: | |
203 | print("Crucible username: %s" % self.username) | |
204 | ||
205 | if not self.password and not self.authtoken: | |
206 | self.password = getpass.getpass(str('Please enter your Crucible password: ')) | |
207 | ||
208 | if get_id: | |
209 | while not self.id: | |
210 | last_project_prompt = ' [%s]' % self.last_project if self.last_project else '' | |
211 | self.id = raw_input('Please specify a project to create the review in or an existing review id to add to%s: ' % last_project_prompt).strip() | |
212 | self.id = self.id or self.last_project | |
213 | ||
214 | if get_title: | |
215 | while not self.title: | |
216 | self.title = raw_input('Please specify the review title: ') | |
217 | ||
218 | if get_reviewers and not self.reviewers: | |
219 | while True: | |
220 | reviewer = raw_input('Please specify a reviewer to be added to the review, or press Enter to continue: ').strip() | |
221 | if not reviewer: | |
222 | break | |
223 | if not reviewer in self.reviewers: | |
224 | self.reviewers.append(reviewer) | |
225 | ||
226 | self.validate(check_title=get_title, check_id=get_id) | |
227 | self.initial_fill = False | |
228 | ||
229 | return self | |
230 | ||
231 | def choose_anchor(self, repositories): | |
232 | """Allows choosing an anchor repository if none detected""" | |
233 | if not sys.stdin.isatty(): | |
234 | logging.debug('Not prompting for anchor interactively because stdin is not a tty') | |
235 | return | |
236 | ||
237 | repository_names = [str(repository.get('name')) | |
238 | for repository in filter(lambda repository: repository.get('enabled'), repositories)] | |
239 | while not self.repository and not self.no_anchor: | |
240 | repository = raw_input("Please choose a repository to anchor to, or press Enter to skip anchoring: ") | |
241 | if not repository: | |
242 | self.no_anchor = True | |
243 | elif repository in repository_names: | |
244 | self.repository = repository | |
245 | else: | |
246 | print('The repository doesn\'t exist or is disabled') | |
247 | ||
248 | def choose_source(self, matching_patch_groups): | |
249 | """Allows choosing a source from a list of matching ones""" | |
250 | if len(matching_patch_groups) == 1: | |
251 | print('Adding patch to existing one: %s. Use --newpatch to add as a new patch instead.' % matching_patch_groups[0]['displayName']) | |
252 | self.patch_source = matching_patch_groups[0]['sourceName'] | |
253 | elif len(matching_patch_groups) > 1: | |
254 | if not sys.stdin.isatty(): | |
255 | logging.debug('Not prompting for source interactively because stdin is not a tty') | |
256 | return | |
257 | ||
258 | print('Found %s patches to add to:' % len(matching_patch_groups)) | |
259 | i = 1 | |
260 | print('0. Create a new patch') | |
261 | for patch_group in matching_patch_groups: | |
262 | print('%s. Add to %s' % (i, patch_group['displayName'])) | |
263 | i += 1 | |
264 | ||
265 | while not self.patch_source and not self.new_patch_source: | |
266 | try: | |
267 | choice = int(raw_input('Pick patch to add to [0-%s]: ' % len(matching_patch_groups))) | |
268 | if 0 < choice <= len(matching_patch_groups): | |
269 | self.new_patch_source = False | |
270 | self.patch_source = matching_patch_groups[choice - 1]['sourceName'] | |
271 | elif choice == 0: | |
272 | self.new_patch_source = True | |
273 | self.patch_source = None | |
274 | except ValueError: | |
275 | pass | |
276 | ||
277 | ||
278 | ||
279 | class ConfigFile: | |
280 | """Handles reading and storing settings from the config file""" | |
281 | userConfigPath = os.path.expanduser('~/.atlassian/crucible.conf') | |
282 | DEFAULT_SECTION = 'DEFAULT' | |
283 | EXECUTABLES_SECTION = 'executables' | |
284 | URL = 'url' | |
285 | TOKEN = 'authtoken' | |
286 | USER = 'user' | |
287 | ||
288 | def __init__(self): | |
289 | self.config_parser = ConfigParser.RawConfigParser() | |
290 | self.config_parser.read([self.userConfigPath]) | |
291 | ||
292 | def _get(self, section, key): | |
293 | if not self.config_parser.has_option(section, key): | |
294 | return None | |
295 | return self.config_parser.get(section, key) | |
296 | ||
297 | def get_default_url(self): | |
298 | return self._get(self.DEFAULT_SECTION, self.URL) | |
299 | ||
300 | def store_configuration(self, configuration): | |
301 | if self._get(self.DEFAULT_SECTION, self.URL) != configuration.url: | |
302 | self.config_parser.set(self.DEFAULT_SECTION, self.URL, configuration.url) | |
303 | print('Saved the default server URL %s to %s' % (configuration.url, self.userConfigPath)) | |
304 | ||
305 | self.store_token(configuration.url, configuration.username, configuration.authtoken) | |
306 | self.save() | |
307 | ||
308 | def get_token(self, url): | |
309 | if not self.config_parser.has_section(url): | |
310 | return None, None | |
311 | return self._get(url, self.TOKEN), self._get(url, self.USER) | |
312 | ||
313 | def store_token(self, url, user, token): | |
314 | if not self.get_token(url) == (token, user): | |
315 | if not self.config_parser.has_section(url): | |
316 | self.config_parser.add_section(url) | |
317 | self.config_parser.set(url, self.TOKEN, token) | |
318 | self.config_parser.set(url, self.USER, user) | |
319 | print ('Saved an authentication token for %s to %s' % (url, self.userConfigPath)) | |
320 | ||
321 | def save(self): | |
322 | try: | |
323 | if not os.path.exists(os.path.dirname(self.userConfigPath)): | |
324 | os.makedirs(os.path.dirname(self.userConfigPath)) | |
325 | ||
326 | with os.fdopen(os.open(self.userConfigPath, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600), 'wb') as configfile: | |
327 | self.config_parser.write(configfile) | |
328 | except IOError as e: | |
329 | print('Error saving the configuration file %s - %s' % (self.userConfigPath, e)) | |
330 | logging.debug(traceback.format_exc()) | |
331 | ||
332 | ||
333 | class CommandLine: | |
334 | """Handles parsing the commandline parameters""" | |
335 | def __init__(self): | |
336 | self.parser = argparse.ArgumentParser( | |
337 | formatter_class=argparse.RawTextHelpFormatter, | |
338 | description='Creates reviews in Atlassian Crucible from the command line', | |
339 | add_help=False, | |
340 | epilog=''' | |
341 | EXAMPLE USAGE: | |
342 | %(executable)s | |
343 | run interactively, try to get a patch from SCM, prompt for review details and create a new review\n | |
344 | cat diff | %(executable)s CR -m "Review title" | |
345 | take the patch from the output of first command, and create a review in project CR with title "Review title"\n | |
346 | %(executable)s CR @ted @matt --moderator @john | |
347 | try to get a patch from SCM, create a review in project CR add ted and matt as reviewers, and john as moderator\n | |
348 | %(executable)s CR-120 | |
349 | update the review CR-120, adding the current patch from SCM\n | |
350 | %(executable)s -r repository1 | |
351 | create a review and anchor it to repository1, instead of trying to detect the repository automatically\n | |
352 | ''' % {'executable': SCRIPT_NAME}) | |
353 | ||
354 | class HelpAction(argparse._HelpAction): | |
355 | def __call__(self, parser, namespace, values, option_string=None): | |
356 | print(parser.format_help() | |
357 | .replace('usage:', 'USAGE:') | |
358 | .replace('positional arguments:', 'POSITIONAL ARGUMENTS:') | |
359 | .replace('optional arguments:', 'OPTIONAL ARGUMENTS:')) | |
360 | parser.exit() | |
361 | ||
362 | self.parser.add_argument('project/review', type=str, nargs='?', default=None, help='the name of the project to create the review in or the id of a review to update\n\n') | |
363 | self.parser.add_argument('@reviewer', type=str, nargs='*', default=[], help='the usernames of the reviewers to be added') | |
364 | self.parser.add_argument('-h', '--help', action=HelpAction, help='show this help message and exit\n\n') | |
365 | self.parser.add_argument('-m', '--title', type=str, help="the title for the review\n\n") | |
366 | self.parser.add_argument('-M', '--moderator', type=str, help="the moderator for the review\n\n") | |
367 | self.parser.add_argument('-r', '--repository', type=str, help='the repository to anchor to\n\n') | |
368 | self.parser.add_argument('-f', '--file', type=str, help='get the diff from the specified file\n\n') | |
369 | self.parser.add_argument('-s', '--server', type=str, help="the url of the Crucible server to connect to\n\n") | |
370 | self.parser.add_argument('-u', '--user', type=str, help="the Crucible username to create the review as\n\n") | |
371 | self.parser.add_argument('-p', '--password', type=str, help="the Crucible user password\n\n") | |
372 | self.parser.add_argument('-n', '--noanchor', action='store_const', const=True, help='don\'t try to detect the repository to anchor the patch\n\n') | |
373 | self.parser.add_argument('-N', '--newpatch', action='store_const', const=True, help='add as a new patch instead of trying to an existing one\n\n') | |
374 | self.parser.add_argument('-e', '--encoding', type=str, help='the name of the encoding to use, see http://docs.python.org/2/library/codecs.html#standard-encodings\n\n') | |
375 | self.parser.add_argument('-d', '--debug', action='store_const', const=True, help="print debugging information\n\n") | |
376 | self.parser.add_argument('-v', '--version', action='version', version=('%s %s' % (SCRIPT_NAME, SCRIPT_VERSION))) | |
377 | ||
378 | def parse_args(self): | |
379 | args = self.parser.parse_args() | |
380 | if args.debug: | |
381 | logging.basicConfig(stream=sys.stderr, level=logging.DEBUG) | |
382 | if args.noanchor and args.repository: | |
383 | Console.error('Please choose either --noanchor or --repository') | |
384 | print(self.help_blurb()) | |
385 | sys.exit(1) | |
386 | ||
387 | return args | |
388 | ||
389 | def help_blurb(self): | |
390 | return 'Try \'%s --help\' for more information.' % os.path.basename(sys.argv[0]) | |
391 | ||
392 | class CrucibleRest: | |
393 | """Encapsulates Crucible REST endopoints""" | |
394 | timeout = 30000 | |
395 | code_anchor_failed = 'PatchAnchorFailed' | |
396 | code_content_too_large = 'ChangeSetContentTooLarge' | |
397 | code_review_content_too_large = 'ReviewContentTooLarge' | |
398 | ||
399 | http_handlers_none_on_errors = { | |
400 | 400: lambda http_error, error_body: None, | |
401 | 404: lambda http_error, error_body: None, | |
402 | } | |
403 | ||
404 | def __init__(self, configuration): | |
405 | self.configuration = configuration | |
406 | self.headers = { | |
407 | 'Content-Type': 'application/json', | |
408 | 'Accept': 'application/json', | |
409 | } | |
410 | self.repositories = None | |
411 | ||
412 | def __build_headers(self, custom_headers, use_token): | |
413 | headers = {} | |
414 | for key, value in self.headers.items(): | |
415 | headers[key] = value | |
416 | if not use_token and self.configuration.username and self.configuration.password: | |
417 | headers['Authorization'] = 'Basic ' + base64.b64encode( | |
418 | - | str('%s:%s') % (self.configuration.username, self.configuration.password)).strip(), |
418 | + | str('%s:%s') % (self.configuration.username, self.configuration.password)).strip() |
419 | for key, value in custom_headers.items(): | |
420 | headers[key] = value | |
421 | return headers | |
422 | ||
423 | def __build_payload(self, data): | |
424 | if not data: | |
425 | return None | |
426 | ||
427 | encodings = [] | |
428 | if self.configuration.encoding: | |
429 | encodings.append(self.configuration.encoding) | |
430 | else: | |
431 | encodings.append('UTF-8') | |
432 | default_encoding = locale.getdefaultlocale()[1] | |
433 | if default_encoding and default_encoding not in encodings: | |
434 | encodings.append(default_encoding) | |
435 | if sys.stdin.encoding not in encodings: | |
436 | encodings.append(sys.stdin.encoding) | |
437 | ||
438 | payload = None | |
439 | for encoding in encodings: | |
440 | try: | |
441 | if isinstance(data, unicode): | |
442 | payload = data | |
443 | if isinstance(data, str): | |
444 | logging.debug('Trying to encode str as %s', encoding) | |
445 | payload = unicode(data, encoding=encoding).encode('utf-8') | |
446 | else: | |
447 | logging.debug('Trying to encode json as %s', encoding) | |
448 | payload = json.dumps(data, encoding=encoding) | |
449 | break | |
450 | except ValueError: | |
451 | logging.debug('Encoding failed: %s', traceback.format_exc()) | |
452 | if not payload: | |
453 | Console.error( | |
454 | 'Error encoding the request (tried %s), please specify an --encoding parameter' % ', '.join( | |
455 | encodings)) | |
456 | sys.exit(1) | |
457 | ||
458 | return payload | |
459 | ||
460 | def __build_url(self, url, use_token): | |
461 | joined_url = self.configuration.url + url | |
462 | if not use_token: | |
463 | return joined_url | |
464 | else: | |
465 | scheme, netloc, path, query, fragment = urlparse.urlsplit(joined_url) | |
466 | query_dict = urlparse.parse_qs(query) | |
467 | query_dict['FEAUTH'] = [self.configuration.authtoken] | |
468 | return urlparse.urlunsplit((scheme, netloc, path, urllib.urlencode(query_dict, doseq=True), fragment)) | |
469 | ||
470 | def _log_request(self, headers, payload, resourceUrl): | |
471 | resourceUrl = re.sub(r'FEAUTH=(.*)%3A([0-9]+)%3A([0-9a-f]+)', r'FEAUTH=\1%3A\2%3A++SANITIZED++', resourceUrl) | |
472 | if payload: | |
473 | payload = re.sub(r'password=(.*)([\s&]?)', r'password=++SANITIZED++\2', payload) | |
474 | if headers and headers.get('Authorization'): | |
475 | headers['Authorization'] = '++SANITIZED++' | |
476 | logging.debug('RestRequest: %s - %s - %s', resourceUrl, headers, payload) | |
477 | ||
478 | def _log_response(self, response): | |
479 | if response: | |
480 | response = re.sub(r'"token":"(.*):([0-9]+):([0-9a-f]+)"', r'"token":"\1:\2:++SANITIZED++"', response) | |
481 | logging.debug('RestResponse: %s', response) | |
482 | ||
483 | def _request(self, url, data=None, http_handlers = {}, use_token=True, custom_headers={}): | |
484 | """Executes a REST request the given rest resource, It's a POST if data is set, a GET otherwise""" | |
485 | resourceUrl = self.__build_url(url, use_token) | |
486 | headers = self.__build_headers(custom_headers, use_token) | |
487 | request = urllib2.Request(url=resourceUrl, headers=headers) | |
488 | payload = self.__build_payload(data) | |
489 | ||
490 | self._log_request(headers, payload, resourceUrl) | |
491 | ||
492 | try: | |
493 | response = urllib2.build_opener(HTTPRedirectHandler()).open(request, data=payload, timeout=self.timeout).read() | |
494 | except urllib2.HTTPError as error: | |
495 | logging.debug('RestError: %s', error) | |
496 | error_body = error.read() | |
497 | try: | |
498 | error_body = json.loads(error_body) | |
499 | except ValueError: | |
500 | pass | |
501 | logging.debug('RestErrorBody: %s', error_body) | |
502 | ||
503 | if error.code in http_handlers: | |
504 | return http_handlers[error.code](error, error_body) | |
505 | ||
506 | if error.code == 404: | |
507 | error_msg = error_body['message'] if isinstance(error_body, dict) and 'message' in error_body \ | |
508 | else 'Please check that %s is a Crucible server and the version is at least %s' % (self.configuration.url, MIN_CRUCIBLE_VERSION) | |
509 | Console.error('Error : %s' % error_msg) | |
510 | elif error.code == 401: | |
511 | Console.error('Authorization error: please check that your username and password are valid, and that you have the correct permissions.') | |
512 | elif error.code == 403: | |
513 | error_msg = error_body['message'] if isinstance(error_body, dict) and 'message' in error_body else 'You do not have permission to perform this operation' | |
514 | permissions_msg = 'Please contact your administrator if you need access' | |
515 | Console.error('Error: %s\n%s' % (error_msg, permissions_msg)) | |
516 | elif error.code == 500: | |
517 | Console.error('Server Error: %s' % error_body['message'] if isinstance(error_body, dict) and 'message' in error_body else error_body) | |
518 | else: | |
519 | Console.error('Received an unexpected response %s. Please check that %s is a Crucible server' % (error, self.configuration.url)) | |
520 | sys.exit(1) | |
521 | except Exception as error: | |
522 | logging.debug(traceback.format_exc()) | |
523 | Console.error('Eror executing request %s - %s' % (resourceUrl, error)) | |
524 | sys.exit(1) | |
525 | ||
526 | self._log_response(response) | |
527 | if 200 in http_handlers: | |
528 | return http_handlers[200](response) | |
529 | ||
530 | try: | |
531 | return json.loads(response) | |
532 | except ValueError: | |
533 | return response | |
534 | ||
535 | def check_connection(self): | |
536 | """ Makes sure we can connect and authenticate with Crucible""" | |
537 | # if there's a token, check that it's still valid | |
538 | server_info = None | |
539 | if self.configuration.authtoken: | |
540 | server_info = self._request(url="/rest-service-fecru/server-v1", http_handlers={ | |
541 | 401: lambda error, error_body: None | |
542 | }) | |
543 | if not server_info: | |
544 | print("Your login token has expired, please re-authenticate") | |
545 | self.configuration.authtoken = None | |
546 | self.configuration.fill_interactively() | |
547 | ||
548 | # get a token if none defined at this point | |
549 | if not self.configuration.authtoken: | |
550 | logging.debug('No authtoken, trying to get one') | |
551 | login_success, login_response = self._request( | |
552 | url='/rest-service/auth-v1/login', | |
553 | data=urllib.urlencode({'userName':self.configuration.username, 'password':self.configuration.password}), | |
554 | http_handlers={ | |
555 | 403: lambda error, error_body: (False, error_body), | |
556 | 200: lambda response: (True, json.loads(response)) | |
557 | }, | |
558 | use_token=False, | |
559 | custom_headers={'Content-Type':'application/x-www-form-urlencoded'} | |
560 | ) | |
561 | ||
562 | if not login_success: | |
563 | message = login_response['error'] if isinstance(login_response, dict) and 'error' in login_response else 'Please check that the username and password provided are correct.' | |
564 | Console.error('Error authenticating with Crucible. ' + message) | |
565 | sys.exit(1) | |
566 | self.configuration.authtoken = login_response['token'] | |
567 | ||
568 | server_info = server_info or self._request(url="/rest-service-fecru/server-v1") | |
569 | if not server_info['isCrucible']: | |
570 | Console.error('Connected successfully to %s but no Crucible license is present.' % self.configuration.url) | |
571 | sys.exit(1) | |
572 | logging.debug('Connected successfully to %s - Crucible version %s', self.configuration.url, server_info['version']['releaseNumber']) | |
573 | ||
574 | def add_reviewers(self, review_id, reviewers): | |
575 | """Adds the configuration.reviewers to the review with the given id. Returns True if any reviewers were added""" | |
576 | if not reviewers: return False | |
577 | ||
578 | logging.debug('Adding reviewers: %s', ', '.join(reviewers)) | |
579 | reviewers_added = False | |
580 | for reviewer in reviewers: | |
581 | if reviewer: | |
582 | reviewer_added, reviewer_msg = self._request("/rest-service/reviews-v1/%s/reviewers" % str(review_id), data=reviewer, | |
583 | http_handlers={ | |
584 | 404: lambda http_error, error_body: (False, 'No user \'%s\' found - not adding as a reviewer' % reviewer), | |
585 | 400: lambda http_error, error_body: (False, error_body['message'] if 'message' in error_body else 'Error adding reviewer %s' % reviewer ), | |
586 | 200: lambda response: (True, '') | |
587 | }) | |
588 | if not reviewer_added: | |
589 | print(reviewer_msg) | |
590 | else: | |
591 | reviewers_added = True | |
592 | return reviewers_added | |
593 | ||
594 | def handle_anchor_error(self, http_error, error_body): | |
595 | if error_body.get('code') == CrucibleRest.code_anchor_failed: | |
596 | Console.error('Error: Failed to anchor the patch to repository %s, please check that the repository is the correct one' % self.configuration.repository) | |
597 | sys.exit(1) | |
598 | elif error_body.get('code') == CrucibleRest.code_content_too_large or error_body.get('code') == CrucibleRest.code_review_content_too_large: | |
599 | Console.error('Error: The patch you\'re trying to upload is too large. ' + error_body.get('message')) | |
600 | sys.exit(1) | |
601 | else: | |
602 | Console.error('Received a 409 Conflict response: %s' % error_body['message']) | |
603 | ||
604 | def create_review(self, patch, project): | |
605 | """Creates a new review from the patch, in the given project""" | |
606 | logging.debug('Creating new review in project %s', project['key']) | |
607 | review_data = {"reviewData": { | |
608 | "projectKey": project['key'], | |
609 | "name": self.configuration.title, | |
610 | "description": project['defaultObjectives'] if 'defaultObjectives' in project else None, | |
611 | }, | |
612 | } | |
613 | ||
614 | review_data = self.add_patch_data(patch, request_dict=review_data) | |
615 | ||
616 | if self.configuration.moderator: | |
617 | if not project['moderatorEnabled']: | |
618 | print('Project %s doesn\'t have a moderator role enabled, not setting a moderator' % project['key']) | |
619 | else: | |
620 | review_data['reviewData']['moderator'] = {'userName':self.configuration.moderator} | |
621 | ||
622 | ||
623 | create_response = self._request("/rest-service/reviews-v1", review_data, http_handlers={ | |
624 | 409: self.handle_anchor_error | |
625 | }) | |
626 | review_id = create_response['permaId']['id'] | |
627 | review_state = create_response['state'] | |
628 | ||
629 | reviewers_added = self.add_reviewers(review_id, self.configuration.reviewers) | |
630 | ||
631 | if reviewers_added or project['defaultReviewers']: | |
632 | logging.debug('Starting review') | |
633 | approve_response = self._request('/rest-service/reviews-v1/%s/transition?action=action:approveReview' % review_id, data=' ', http_handlers={ | |
634 | 401: lambda http_error, error_body: print('You don\'t have permission to approve the review') | |
635 | }) | |
636 | if approve_response: | |
637 | review_state = approve_response['state'] | |
638 | else: | |
639 | print('No reviewers added, review will be left in Draft state') | |
640 | ||
641 | Console.success('Created review %(id)s (state: %(state)s) - %(url)s/cru/%(id)s' | |
642 | % ({'id': review_id, 'state': review_state,'url':self.configuration.url})) | |
643 | ||
644 | def add_patch_data(self, patch, request_dict={}): | |
645 | request_dict['patch'] = patch | |
646 | if self.configuration.repository: | |
647 | request_dict['anchor'] = {'anchorRepository' : self.configuration.repository} | |
648 | return request_dict | |
649 | ||
650 | def get_iterable_patchgroups(self, review_id, repository): | |
651 | patch_groups = self._request('/rest-service/reviews-v1/%s/patch' % review_id)['patchGroup'] | |
652 | matching_repo = lambda patch_group: 'anchor' in patch_group['patches'][0] and \ | |
653 | patch_group['patches'][0]['anchor'].get('anchorRepository') == repository | |
654 | return filter(matching_repo, patch_groups) | |
655 | ||
656 | def add_to_review(self, patch, review): | |
657 | """Adds the patch and reviewers to the given review""" | |
658 | review_id = review['permaId']['id'] | |
659 | data = self.add_patch_data(patch) | |
660 | ||
661 | if 'anchor' in data and not self.configuration.new_patch_source: | |
662 | matching_patch_groups = self.get_iterable_patchgroups(review_id, data['anchor']['anchorRepository']) | |
663 | self.configuration.choose_source(matching_patch_groups) | |
664 | if self.configuration.patch_source: | |
665 | data['source'] = self.configuration.patch_source | |
666 | ||
667 | logging.debug('Adding patch to review %s' % review_id) | |
668 | ||
669 | patch_response = self._request('/rest-service/reviews-v1/%s/patch' % review_id, data=data, http_handlers={ | |
670 | 409: self.handle_anchor_error | |
671 | }) | |
672 | self.add_reviewers(review_id, self.configuration.reviewers) | |
673 | ||
674 | Console.success('Updated review %(id)s (state: %(state)s) - %(url)s/cru/%(id)s' | |
675 | % ({'id': review_id, 'state': patch_response['state'],'url':self.configuration.url})) | |
676 | ||
677 | def get_review(self, id): | |
678 | """A ReviewData json if the id represents an existing review, None otherwise""" | |
679 | try: | |
680 | return self._request('/rest-service/reviews-v1/%s' % id, http_handlers=CrucibleRest.http_handlers_none_on_errors) | |
681 | except StandardError: | |
682 | return None | |
683 | ||
684 | def get_project(self, id): | |
685 | """A ProjectDat json if the id represents an existing project, None otherwise""" | |
686 | try: | |
687 | return self._request('/rest-service/projects-v1/%s?excludeAllowedReviewers=true' % id, http_handlers=CrucibleRest.http_handlers_none_on_errors) | |
688 | except StandardError: | |
689 | return None | |
690 | ||
691 | def get_last_project(self): | |
692 | """Retrieves the most recent project, or None if it fails""" | |
693 | try: | |
694 | recent_projects = self._request('/rest-service-fecru/recently-visited-v1/projects')['project'] | |
695 | if(len(recent_projects)) == 0: return None | |
696 | return recent_projects[0]['entityId'] | |
697 | except StandardError: | |
698 | logging.debug('Error getting last project: %s', traceback.format_exc()) | |
699 | return None | |
700 | ||
701 | def find_repository(self, source): | |
702 | """Tries to find the anchor repository for the source given""" | |
703 | try: | |
704 | for repository in self.get_repositories(): | |
705 | logging.debug('Matching remote repository %s', repository['name']) | |
706 | if repository['enabled'] and source.matches_repository(repository): | |
707 | return repository['name'] | |
708 | except StandardError: | |
709 | logging.debug('Error suggesting anchor repository: %s', traceback.format_exc()) | |
710 | ||
711 | return None | |
712 | ||
713 | def get_repositories(self): | |
714 | if not self.repositories: | |
715 | self.repositories = self._request('/rest-service/repositories-v1')['repoData'] | |
716 | return self.repositories | |
717 | ||
718 | def is_script_update_available(self): | |
719 | return self._request('/rest/review-cli/1.0/version/updateCheck?version=%s' % SCRIPT_VERSION)['isUpdateAvailable'] | |
720 | ||
721 | class PatchSource(object): | |
722 | """Base class for different ways to get a patch""" | |
723 | def __init__(self, configuration): | |
724 | self.configuration = configuration | |
725 | self.paths = [] | |
726 | ||
727 | def is_active(self): | |
728 | """Should return True if this is the source can be used""" | |
729 | ||
730 | def get_patch(self): | |
731 | """Should return the patch content""" | |
732 | ||
733 | def get_review(self): | |
734 | """A user visible name for the source""" | |
735 | ||
736 | def executable(self): | |
737 | """The binary to execute commands on the repository""" | |
738 | ||
739 | def matches_repository(self, repository_data): | |
740 | """Whether the source matches the given RepositoryData json""" | |
741 | ||
742 | def load_patch(self): | |
743 | """Queries different patch sources for a patch""" | |
744 | for source_class in PatchSource.__subclasses__(): | |
745 | logging.debug('Checking %s.is_active', source_class) | |
746 | source = source_class(self.configuration) | |
747 | if source.is_active(): | |
748 | source._validate_executable() | |
749 | logging.debug('Getting patch from %s', source) | |
750 | patch = source.get_patch() | |
751 | if patch: | |
752 | logging.debug('Got %s bytes', len(patch)) | |
753 | ||
754 | if not self.configuration.no_anchor and not self.configuration.repository: | |
755 | source.load_paths() | |
756 | logging.debug('Loaded paths: %s', source.paths) | |
757 | ||
758 | return patch, source | |
759 | return None, None | |
760 | ||
761 | def _validate_executable(self): | |
762 | try: | |
763 | self.validate_executable() | |
764 | except (OSError, CalledProcessError) as e: | |
765 | Console.error('A %s repository was detected, but there was an error executing \'%s\': %s' % (self, self.configuration.executables[self.executable()], e)) | |
766 | sys.exit(1) | |
767 | ||
768 | def validate_executable(self): | |
769 | pass | |
770 | ||
771 | def load_paths(self): | |
772 | """Loads the potential remote paths for the given repo type to be used when trying to detect a repository to anchor to""" | |
773 | pass | |
774 | ||
775 | def matches_url(self, url): | |
776 | requested_path = urlparse.urlsplit(url) | |
777 | logging.debug('Matching remote url: %s', requested_path.__str__()) | |
778 | for path in self.paths: | |
779 | local_path = urlparse.urlsplit(path) | |
780 | logging.debug('Matching local url: %s', local_path.__str__()) | |
781 | if requested_path.hostname == local_path.hostname and requested_path.port == local_path.port and requested_path.path == local_path.path: | |
782 | return True | |
783 | return False | |
784 | ||
785 | def find_metadata_dir(self, dirname): | |
786 | """Searches for the specified directory name starting with the current directory and going upwards the directory tree""" | |
787 | cwd = os.getcwd() | |
788 | if os.path.exists(os.path.join(cwd, dirname)): | |
789 | return True | |
790 | tmp = os.path.abspath(os.path.join(cwd, os.pardir)) | |
791 | while tmp is not None and tmp != cwd: | |
792 | cwd = tmp | |
793 | if os.path.exists(os.path.join(cwd, dirname)): | |
794 | return True | |
795 | tmp = os.path.abspath(os.path.join(cwd, os.pardir)) | |
796 | return False | |
797 | ||
798 | class FileSource(PatchSource): | |
799 | """Gets the patch from the file specified by the commandline""" | |
800 | def is_active(self): | |
801 | return self.configuration.diff_file | |
802 | ||
803 | def get_patch(self): | |
804 | path = self.configuration.diff_file | |
805 | if not os.path.isfile(path): | |
806 | print("No such file %s" % path) | |
807 | return None | |
808 | ||
809 | with file(path) as f: | |
810 | return f.read() | |
811 | ||
812 | def __str__(self): return 'file %s' % self.configuration.diff_file | |
813 | ||
814 | class StdInSource(PatchSource): | |
815 | """Gets a patch from stdin""" | |
816 | def is_active(self): | |
817 | return not sys.stdin.isatty() | |
818 | ||
819 | def get_patch(self): | |
820 | return sys.stdin.read() | |
821 | ||
822 | def __str__(self): | |
823 | return 'standard input' | |
824 | ||
825 | class HgSource(PatchSource): | |
826 | """Gets a patch from a mercurial repository""" | |
827 | ||
828 | def is_active(self): | |
829 | return self.find_metadata_dir('.hg') | |
830 | ||
831 | def get_patch(self): | |
832 | output = check_output([self.configuration.executables[self.executable()], 'diff']) | |
833 | return output | |
834 | ||
835 | def __str__(self): | |
836 | return 'Mercurial' | |
837 | ||
838 | def executable(self): | |
839 | return 'hg' | |
840 | ||
841 | def validate_executable(self): | |
842 | check_output([self.configuration.executables[self.executable()], 'help']) | |
843 | ||
844 | def load_paths(self): | |
845 | paths_output = check_output([self.configuration.executables[self.executable()], 'paths']).splitlines() | |
846 | if paths_output: | |
847 | for path in paths_output: | |
848 | path_split = path.split('=') | |
849 | if len(path_split) > 1: | |
850 | self.paths.append(path_split[1].strip()) | |
851 | ||
852 | def matches_repository(self, repository_data): | |
853 | return repository_data['type'] == 'hg' and self.matches_url(repository_data['location']) | |
854 | ||
855 | ||
856 | class GitSource(PatchSource): | |
857 | """Gets a patch from a git repository""" | |
858 | def is_active(self): | |
859 | return self.find_metadata_dir('.git') | |
860 | ||
861 | def get_patch(self): | |
862 | output = check_output([self.configuration.executables[self.executable()], 'diff', '--cached', '--no-prefix']) | |
863 | if not output: | |
864 | output = check_output([self.configuration.executables[self.executable()], 'diff', '--no-prefix']) | |
865 | if output: | |
866 | print('No staged changes were found in the repository, creating a review for all unstaged changes.') | |
867 | ||
868 | return output | |
869 | ||
870 | def __str__(self): | |
871 | return 'Git' | |
872 | ||
873 | def executable(self): | |
874 | return 'git' | |
875 | ||
876 | def validate_executable(self): | |
877 | check_output([self.configuration.executables[self.executable()], 'help']) | |
878 | ||
879 | def load_paths(self): | |
880 | paths_output = check_output([self.configuration.executables[self.executable()], 'remote', '-v']).splitlines() | |
881 | if paths_output: | |
882 | for path in paths_output: | |
883 | path_split = path.split() | |
884 | if len(path_split) > 1: | |
885 | self.paths.append(path_split[1].strip()) | |
886 | ||
887 | def matches_repository(self, repository_data): | |
888 | return repository_data['type'] == 'git' and self.matches_url(repository_data['location']) | |
889 | ||
890 | ||
891 | class SvnSource(PatchSource): | |
892 | """Gets a patch from a subversion repository""" | |
893 | def is_active(self): | |
894 | return self.find_metadata_dir('.svn') | |
895 | ||
896 | def get_patch(self): | |
897 | try: | |
898 | return check_output([self.configuration.executables[self.executable()], 'diff']) | |
899 | except CalledProcessError as e: | |
900 | print('svn diff returned error: %s' % e.error.strip()) | |
901 | return None | |
902 | ||
903 | def __str__(self): | |
904 | return 'Subversion' | |
905 | ||
906 | def executable(self): | |
907 | return 'svn' | |
908 | ||
909 | def validate_executable(self): | |
910 | check_output([self.configuration.executables[self.executable()], 'help']) | |
911 | ||
912 | def load_paths(self): | |
913 | info_output = check_output([self.configuration.executables[self.executable()], 'info']).splitlines() | |
914 | if info_output: | |
915 | for line in info_output: | |
916 | if line.startswith('URL:'): | |
917 | line_split = line.split() | |
918 | if (len(line_split)) > 1: | |
919 | self.paths.append(line_split[1].strip()) | |
920 | break | |
921 | ||
922 | def matches_repository(self, repository_data): | |
923 | if not repository_data['type'] == 'svn': | |
924 | return False | |
925 | remote_url = (repository_data['url'].rstrip('/') + '/' + repository_data['path']).rstrip('/') | |
926 | logging.debug('Matching remote url %s', remote_url) | |
927 | return self.paths[0].startswith(remote_url) | |
928 | ||
929 | ||
930 | class CvsSource(PatchSource): | |
931 | """Gets a patch from a CVS repository""" | |
932 | def is_active(self): | |
933 | try: | |
934 | cmd = [self.configuration.executables[self.executable()], 'status'] | |
935 | check_output(cmd) | |
936 | return True | |
937 | except (OSError, CalledProcessError): | |
938 | return False | |
939 | ||
940 | def get_patch(self): | |
941 | try: | |
942 | return check_output([self.configuration.executables[self.executable()], 'diff', '-N', '-U', '10000']) | |
943 | except CalledProcessError as e: | |
944 | return e.output | |
945 | ||
946 | def __str__(self): | |
947 | return 'CVS' | |
948 | ||
949 | def executable(self): | |
950 | return 'cvs' | |
951 | ||
952 | def load_paths(self): | |
953 | try: | |
954 | with open('CVS/Root') as root_file: | |
955 | self.paths.append(root_file.read().rstrip('/\n')) | |
956 | except IOError as e: | |
957 | logging.debug('Failed getting file root file: %s', e) | |
958 | ||
959 | if 'CVSROOT' in os.environ: | |
960 | self.paths.append(os.environ['CVSROOT'].rstrip('/')) | |
961 | ||
962 | def matches_repository(self, repository_data): | |
963 | return repository_data['type'] == 'cvs' and self.matches_url(repository_data['dir'].rstrip('/')) | |
964 | ||
965 | ||
966 | class P4Source(PatchSource): | |
967 | def __init__(self, configuration): | |
968 | super(P4Source, self).__init__(configuration) | |
969 | self.p4info = {} | |
970 | ||
971 | def is_active(self): | |
972 | """Gets a patch from a perforce repository.""" | |
973 | try: | |
974 | check_output([self.configuration.executables[self.executable()]]) | |
975 | except (OSError, CalledProcessError): | |
976 | return False | |
977 | ||
978 | self.get_client_info() | |
979 | ||
980 | clientRoot = self.p4info['Client root'] | |
981 | if not clientRoot: | |
982 | logging.debug('Cannot find Perforce Client root while executing \'p4 info\'') | |
983 | return False | |
984 | ||
985 | return (os.path.normcase(os.getcwd())).startswith(os.path.normcase(clientRoot)) | |
986 | ||
987 | def get_client_info(self): | |
988 | info = check_output([self.configuration.executables[self.executable()], 'info']).splitlines() | |
989 | for line in info: | |
990 | line_split = line.split(':', 1) | |
991 | if len(line_split) > 1: | |
992 | self.p4info[line_split[0].strip()] = line_split[1].strip() | |
993 | logging.debug('p4info: %s ', self.p4info) | |
994 | ||
995 | def get_path(self, file): | |
996 | output = check_output([self.configuration.executables[self.executable()], 'fstat', '-T', 'clientFile', file]) | |
997 | return output[len('... clientFile '):].rstrip() | |
998 | ||
999 | def get_add(self, file, rev): | |
1000 | f = open(self.get_path(file)) | |
1001 | lines = f.read().replace('\xc2\x85', '\n').splitlines() | |
1002 | f.close() | |
1003 | ||
1004 | output = '--- /dev/null\n' + \ | |
1005 | '+++ ' + file + '\t(revision ' + rev + ')\n' + \ | |
1006 | '@@ -0,0 +1,' + str(len(lines)) + ' @@\n' | |
1007 | output += '+' + '\n+'.join(lines) | |
1008 | return output | |
1009 | ||
1010 | def get_delete(self, file, rev): | |
1011 | lines = check_output([self.configuration.executables[self.executable()], 'print', '-q', file + '#' + rev]).splitlines() | |
1012 | ||
1013 | output = '--- ' + file + '\t(revision ' + rev + ')\n' + \ | |
1014 | '+++ /dev/null\n' + \ | |
1015 | '@@ -1,' + str(len(lines)) + ' +0,0 @@\n' | |
1016 | output += '-' + '\n-'.join(lines) | |
1017 | return output | |
1018 | ||
1019 | def get_patch(self): | |
1020 | p4 = self.configuration.executables[self.executable()] | |
1021 | output = check_output([p4, 'diff', '-dcu']) | |
1022 | ||
1023 | opened = check_output([p4, 'opened']).splitlines() | |
1024 | ||
1025 | pattern = re.compile('([^#]+)#([\\d]+) - ([^\\s]+)[^\\(](.*)') | |
1026 | for line in opened: | |
1027 | match = pattern.match(line) | |
1028 | if match: | |
1029 | file = match.group(1) | |
1030 | rev = match.group(2) | |
1031 | change = match.group(3) | |
1032 | fileType = match.group(4) | |
1033 | if change == 'add': | |
1034 | output += '\n' + self.get_add(file, rev) | |
1035 | elif change == 'delete': | |
1036 | output += '\n' + self.get_delete(file, rev) | |
1037 | ||
1038 | return output | |
1039 | ||
1040 | def __str__(self): | |
1041 | return 'Perforce' | |
1042 | ||
1043 | def executable(self): | |
1044 | return 'p4' | |
1045 | ||
1046 | def load_paths(self): | |
1047 | where_output = check_output([self.configuration.executables[self.executable()], 'where']).splitlines() | |
1048 | for where in where_output: | |
1049 | where_split = where.split() | |
1050 | if len(where_split) == 3 and not where_split[0].startswith('-') and where_split[1].startswith('//%s' % self.p4info['Client name']): | |
1051 | self.paths.append(where_split[0]) | |
1052 | ||
1053 | def matches_repository(self, repository_data): | |
1054 | if not repository_data['type'] == 'p4': return False | |
1055 | remote_address = '%s:%s' % (repository_data['server'], repository_data['port']) | |
1056 | logging.debug('Matching remote url: %s', remote_address) | |
1057 | if self.p4info.get('Server address') == remote_address or self.p4info.get('P4Sandbox broker port') == remote_address: | |
1058 | for path in self.paths: | |
1059 | logging.debug('Matching path: %s', path) | |
1060 | if path.startswith(repository_data['path']): | |
1061 | return True | |
1062 | ||
1063 | return False | |
1064 | ||
1065 | def main(): | |
1066 | """ | |
1067 | Runs the script to create a review | |
1068 | """ | |
1069 | ||
1070 | # read commandline | |
1071 | command_line = CommandLine() | |
1072 | args = command_line.parse_args() | |
1073 | ||
1074 | # parse/fill config | |
1075 | config_file = ConfigFile() | |
1076 | configuration = Configuration().fill_from_args(args).fill_from_config_file(config_file).fill_from_defaults().fill_interactively() | |
1077 | ||
1078 | # set up authentication, check for updates | |
1079 | rest = CrucibleRest(configuration) | |
1080 | rest.check_connection() | |
1081 | config_file.store_configuration(configuration) | |
1082 | ||
1083 | if rest.is_script_update_available(): | |
1084 | print('An updated version of this script is available. Visit %s to download it' % (configuration.url + '/plugins/servlet/viewReviewCLI')) | |
1085 | ||
1086 | # get a patch | |
1087 | patch, source = PatchSource(configuration).load_patch() | |
1088 | if not patch: | |
1089 | Console.error('Failed to get a patch%s. Please make sure to call the script from an SCM directory with local changes, or pipe in some input.' | |
1090 | % (' from %s' % source if source else '')) | |
1091 | sys.exit(1) | |
1092 | ||
1093 | # create or update the review | |
1094 | if not configuration.repository and not configuration.no_anchor: | |
1095 | detected_repository = rest.find_repository(source) | |
1096 | if detected_repository: | |
1097 | configuration.repository = detected_repository | |
1098 | print('Detected Crucible repository %s. Use --repository or --noanchor options to override' % configuration.repository) | |
1099 | else: | |
1100 | print('No matching FishEye repository detected') | |
1101 | configuration.choose_anchor(rest.get_repositories()) | |
1102 | ||
1103 | configuration.last_project = rest.get_last_project() | |
1104 | configuration.fill_interactively(get_id=True) | |
1105 | ||
1106 | review = rest.get_review(configuration.id) | |
1107 | if review: | |
1108 | rest.add_to_review(patch, review) | |
1109 | sys.exit(0) | |
1110 | ||
1111 | project = rest.get_project(configuration.id) | |
1112 | if project: | |
1113 | configuration.fill_interactively(get_title=True, get_reviewers=True) | |
1114 | rest.create_review(patch, project) | |
1115 | sys.exit(0) | |
1116 | ||
1117 | Console.error('Failed to find a review or project with an id %s, make sure the id is correct.' % configuration.id) | |
1118 | sys.exit(1) | |
1119 | ||
1120 | if __name__ == '__main__': | |
1121 | main() |