OLD | NEW |
1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
2 # Copyright (c) 2012 The Chromium Authors. All rights reserved. | 2 # Copyright (c) 2012 The Chromium Authors. All rights reserved. |
3 # Use of this source code is governed by a BSD-style license that can be | 3 # Use of this source code is governed by a BSD-style license that can be |
4 # found in the LICENSE file. | 4 # found in the LICENSE file. |
5 | 5 |
6 # Copyright (C) 2008 Evan Martin <martine@danga.com> | 6 # Copyright (C) 2008 Evan Martin <martine@danga.com> |
7 | 7 |
8 """A git-command for integrating reviews on Rietveld.""" | 8 """A git-command for integrating reviews on Rietveld.""" |
9 | 9 |
10 from distutils.version import LooseVersion | 10 from distutils.version import LooseVersion |
11 from multiprocessing.pool import ThreadPool | 11 from multiprocessing.pool import ThreadPool |
12 import base64 | 12 import base64 |
13 import collections | 13 import collections |
14 import glob | 14 import glob |
15 import httplib | 15 import httplib |
16 import json | 16 import json |
17 import logging | 17 import logging |
18 import optparse | 18 import optparse |
19 import os | 19 import os |
20 import Queue | 20 import Queue |
21 import re | 21 import re |
22 import stat | 22 import stat |
23 import sys | 23 import sys |
24 import tempfile | 24 import tempfile |
25 import textwrap | 25 import textwrap |
26 import time | 26 import time |
27 import traceback | 27 import traceback |
| 28 import urllib |
28 import urllib2 | 29 import urllib2 |
29 import urlparse | 30 import urlparse |
| 31 import uuid |
30 import webbrowser | 32 import webbrowser |
31 import zlib | 33 import zlib |
32 | 34 |
33 try: | 35 try: |
34 import readline # pylint: disable=F0401,W0611 | 36 import readline # pylint: disable=F0401,W0611 |
35 except ImportError: | 37 except ImportError: |
36 pass | 38 pass |
37 | 39 |
38 from third_party import colorama | 40 from third_party import colorama |
39 from third_party import httplib2 | 41 from third_party import httplib2 |
(...skipping 192 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
232 name, while the developers always use shortened master name | 234 name, while the developers always use shortened master name |
233 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This | 235 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This |
234 function does the conversion for buildbucket migration. | 236 function does the conversion for buildbucket migration. |
235 """ | 237 """ |
236 prefix = 'master.' | 238 prefix = 'master.' |
237 if master.startswith(prefix): | 239 if master.startswith(prefix): |
238 return master | 240 return master |
239 return '%s%s' % (prefix, master) | 241 return '%s%s' % (prefix, master) |
240 | 242 |
241 | 243 |
| 244 def _buildbucket_retry(operation_name, http, *args, **kwargs): |
| 245 """Retries requests to buildbucket service and returns parsed json content.""" |
| 246 try_count = 0 |
| 247 while True: |
| 248 response, content = http.request(*args, **kwargs) |
| 249 try: |
| 250 content_json = json.loads(content) |
| 251 except ValueError: |
| 252 content_json = None |
| 253 |
| 254 # Buildbucket could return an error even if status==200. |
| 255 if content_json and content_json.get('error'): |
| 256 msg = 'Error in response. Reason: %s. Message: %s.' % ( |
| 257 content_json['error'].get('reason', ''), |
| 258 content_json['error'].get('message', '')) |
| 259 raise BuildbucketResponseException(msg) |
| 260 |
| 261 if response.status == 200: |
| 262 if not content_json: |
| 263 raise BuildbucketResponseException( |
| 264 'Buildbucket returns invalid json content: %s.\n' |
| 265 'Please file bugs at http://crbug.com, label "Infra-BuildBucket".' % |
| 266 content) |
| 267 return content_json |
| 268 if response.status < 500 or try_count >= 2: |
| 269 raise httplib2.HttpLib2Error(content) |
| 270 |
| 271 # status >= 500 means transient failures. |
| 272 logging.debug('Transient errors when %s. Will retry.', operation_name) |
| 273 time.sleep(0.5 + 1.5*try_count) |
| 274 try_count += 1 |
| 275 assert False, 'unreachable' |
| 276 |
| 277 |
242 def trigger_luci_job(changelist, masters, options): | 278 def trigger_luci_job(changelist, masters, options): |
243 """Send a job to run on LUCI.""" | 279 """Send a job to run on LUCI.""" |
244 issue_props = changelist.GetIssueProperties() | 280 issue_props = changelist.GetIssueProperties() |
245 issue = changelist.GetIssue() | 281 issue = changelist.GetIssue() |
246 patchset = changelist.GetMostRecentPatchset() | 282 patchset = changelist.GetMostRecentPatchset() |
247 for builders_and_tests in sorted(masters.itervalues()): | 283 for builders_and_tests in sorted(masters.itervalues()): |
248 # TODO(hinoka et al): add support for other properties. | 284 # TODO(hinoka et al): add support for other properties. |
249 # Currently, this completely ignores testfilter and other properties. | 285 # Currently, this completely ignores testfilter and other properties. |
250 for builder in sorted(builders_and_tests): | 286 for builder in sorted(builders_and_tests): |
251 luci_trigger.trigger( | 287 luci_trigger.trigger( |
(...skipping 47 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
299 if tests: | 335 if tests: |
300 parameters['properties']['testfilter'] = tests | 336 parameters['properties']['testfilter'] = tests |
301 if properties: | 337 if properties: |
302 parameters['properties'].update(properties) | 338 parameters['properties'].update(properties) |
303 if options.clobber: | 339 if options.clobber: |
304 parameters['properties']['clobber'] = True | 340 parameters['properties']['clobber'] = True |
305 batch_req_body['builds'].append( | 341 batch_req_body['builds'].append( |
306 { | 342 { |
307 'bucket': bucket, | 343 'bucket': bucket, |
308 'parameters_json': json.dumps(parameters), | 344 'parameters_json': json.dumps(parameters), |
| 345 'client_operation_id': str(uuid.uuid4()), |
309 'tags': ['builder:%s' % builder, | 346 'tags': ['builder:%s' % builder, |
310 'buildset:%s' % buildset, | 347 'buildset:%s' % buildset, |
311 'master:%s' % master, | 348 'master:%s' % master, |
312 'user_agent:git_cl_try'] | 349 'user_agent:git_cl_try'] |
313 } | 350 } |
314 ) | 351 ) |
315 | 352 |
316 for try_count in xrange(3): | 353 _buildbucket_retry( |
317 response, content = http.request( | 354 'triggering tryjobs', |
318 buildbucket_put_url, | 355 http, |
319 'PUT', | 356 buildbucket_put_url, |
320 body=json.dumps(batch_req_body), | 357 'PUT', |
321 headers={'Content-Type': 'application/json'}, | 358 body=json.dumps(batch_req_body), |
322 ) | 359 headers={'Content-Type': 'application/json'} |
323 content_json = None | 360 ) |
324 try: | |
325 content_json = json.loads(content) | |
326 except ValueError: | |
327 pass | |
328 | |
329 # Buildbucket could return an error even if status==200. | |
330 if content_json and content_json.get('error'): | |
331 msg = 'Error in response. Code: %d. Reason: %s. Message: %s.' % ( | |
332 content_json['error'].get('code', ''), | |
333 content_json['error'].get('reason', ''), | |
334 content_json['error'].get('message', '')) | |
335 raise BuildbucketResponseException(msg) | |
336 | |
337 if response.status == 200: | |
338 if not content_json: | |
339 raise BuildbucketResponseException( | |
340 'Buildbucket returns invalid json content: %s.\n' | |
341 'Please file bugs at crbug.com, label "Infra-BuildBucket".' % | |
342 content) | |
343 break | |
344 if response.status < 500 or try_count >= 2: | |
345 raise httplib2.HttpLib2Error(content) | |
346 | |
347 # status >= 500 means transient failures. | |
348 logging.debug('Transient errors when triggering tryjobs. Will retry.') | |
349 time.sleep(0.5 + 1.5*try_count) | |
350 | |
351 print '\n'.join(print_text) | 361 print '\n'.join(print_text) |
352 | 362 |
353 | 363 |
| 364 def fetch_try_jobs(auth_config, changelist, options): |
| 365 """Fetches tryjobs from buildbucket. |
| 366 |
| 367 Returns a map from build id to build info as json dictionary. |
| 368 """ |
| 369 rietveld_url = settings.GetDefaultServerUrl() |
| 370 rietveld_host = urlparse.urlparse(rietveld_url).hostname |
| 371 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config) |
| 372 if authenticator.has_cached_credentials(): |
| 373 http = authenticator.authorize(httplib2.Http()) |
| 374 else: |
| 375 print ('Warning: Some results might be missing because %s' % |
| 376 # Get the message on how to login. |
| 377 auth.LoginRequiredError(rietveld_host).message) |
| 378 http = httplib2.Http() |
| 379 |
| 380 http.force_exception_to_status_code = True |
| 381 |
| 382 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format( |
| 383 hostname=rietveld_host, |
| 384 issue=changelist.GetIssue(), |
| 385 patch=options.patchset) |
| 386 params = {'tag': 'buildset:%s' % buildset} |
| 387 |
| 388 builds = {} |
| 389 while True: |
| 390 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format( |
| 391 hostname=options.buildbucket_host, |
| 392 params=urllib.urlencode(params)) |
| 393 content = _buildbucket_retry('fetching tryjobs', http, url, 'GET') |
| 394 for build in content.get('builds', []): |
| 395 builds[build['id']] = build |
| 396 if 'next_cursor' in content: |
| 397 params['start_cursor'] = content['next_cursor'] |
| 398 else: |
| 399 break |
| 400 return builds |
| 401 |
| 402 |
| 403 def print_tryjobs(options, builds): |
| 404 """Prints nicely result of fetch_try_jobs.""" |
| 405 if not builds: |
| 406 print 'No tryjobs scheduled' |
| 407 return |
| 408 |
| 409 # Make a copy, because we'll be modifying builds dictionary. |
| 410 builds = builds.copy() |
| 411 builder_names_cache = {} |
| 412 |
| 413 def get_builder(b): |
| 414 try: |
| 415 return builder_names_cache[b['id']] |
| 416 except KeyError: |
| 417 try: |
| 418 parameters = json.loads(b['parameters_json']) |
| 419 name = parameters['builder_name'] |
| 420 except (ValueError, KeyError) as error: |
| 421 print 'WARNING: failed to get builder name for build %s: %s' % ( |
| 422 b['id'], error) |
| 423 name = None |
| 424 builder_names_cache[b['id']] = name |
| 425 return name |
| 426 |
| 427 def get_bucket(b): |
| 428 bucket = b['bucket'] |
| 429 if bucket.startswith('master.'): |
| 430 return bucket[len('master.'):] |
| 431 return bucket |
| 432 |
| 433 if options.print_master: |
| 434 name_fmt = '%%-%ds %%-%ds' % ( |
| 435 max(len(str(get_bucket(b))) for b in builds.itervalues()), |
| 436 max(len(str(get_builder(b))) for b in builds.itervalues())) |
| 437 def get_name(b): |
| 438 return name_fmt % (get_bucket(b), get_builder(b)) |
| 439 else: |
| 440 name_fmt = '%%-%ds' % ( |
| 441 max(len(str(get_builder(b))) for b in builds.itervalues())) |
| 442 def get_name(b): |
| 443 return name_fmt % get_builder(b) |
| 444 |
| 445 def sort_key(b): |
| 446 return b['status'], b.get('result'), get_name(b), b.get('url') |
| 447 |
| 448 def pop(title, f, color=None, **kwargs): |
| 449 """Pop matching builds from `builds` dict and print them.""" |
| 450 |
| 451 if not sys.stdout.isatty() or color is None: |
| 452 colorize = str |
| 453 else: |
| 454 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET) |
| 455 |
| 456 result = [] |
| 457 for b in builds.values(): |
| 458 if all(b.get(k) == v for k, v in kwargs.iteritems()): |
| 459 builds.pop(b['id']) |
| 460 result.append(b) |
| 461 if result: |
| 462 print colorize(title) |
| 463 for b in sorted(result, key=sort_key): |
| 464 print ' ', colorize('\t'.join(map(str, f(b)))) |
| 465 |
| 466 total = len(builds) |
| 467 pop(status='COMPLETED', result='SUCCESS', |
| 468 title='Successes:', color=Fore.GREEN, |
| 469 f=lambda b: (get_name(b), b.get('url'))) |
| 470 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE', |
| 471 title='Infra Failures:', color=Fore.MAGENTA, |
| 472 f=lambda b: (get_name(b), b.get('url'))) |
| 473 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE', |
| 474 title='Failures:', color=Fore.RED, |
| 475 f=lambda b: (get_name(b), b.get('url'))) |
| 476 pop(status='COMPLETED', result='CANCELED', |
| 477 title='Canceled:', color=Fore.MAGENTA, |
| 478 f=lambda b: (get_name(b),)) |
| 479 pop(status='COMPLETED', result='FAILURE', |
| 480 failure_reason='INVALID_BUILD_DEFINITION', |
| 481 title='Wrong master/builder name:', color=Fore.MAGENTA, |
| 482 f=lambda b: (get_name(b),)) |
| 483 pop(status='COMPLETED', result='FAILURE', |
| 484 title='Other failures:', |
| 485 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url'))) |
| 486 pop(status='COMPLETED', |
| 487 title='Other finished:', |
| 488 f=lambda b: (get_name(b), b.get('result'), b.get('url'))) |
| 489 pop(status='STARTED', |
| 490 title='Started:', color=Fore.YELLOW, |
| 491 f=lambda b: (get_name(b), b.get('url'))) |
| 492 pop(status='SCHEDULED', |
| 493 title='Scheduled:', |
| 494 f=lambda b: (get_name(b), 'id=%s' % b['id'])) |
| 495 # The last section is just in case buildbucket API changes OR there is a bug. |
| 496 pop(title='Other:', |
| 497 f=lambda b: (get_name(b), 'id=%s' % b['id'])) |
| 498 assert len(builds) == 0 |
| 499 print 'Total: %d tryjobs' % total |
| 500 |
| 501 |
354 def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards): | 502 def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards): |
355 """Return the corresponding git ref if |base_url| together with |glob_spec| | 503 """Return the corresponding git ref if |base_url| together with |glob_spec| |
356 matches the full |url|. | 504 matches the full |url|. |
357 | 505 |
358 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below). | 506 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below). |
359 """ | 507 """ |
360 fetch_suburl, as_ref = glob_spec.split(':') | 508 fetch_suburl, as_ref = glob_spec.split(':') |
361 if allow_wildcards: | 509 if allow_wildcards: |
362 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl) | 510 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl) |
363 if glob_match: | 511 if glob_match: |
(...skipping 2994 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
3358 | 3506 |
3359 for (master, builders) in sorted(masters.iteritems()): | 3507 for (master, builders) in sorted(masters.iteritems()): |
3360 if master: | 3508 if master: |
3361 print 'Master: %s' % master | 3509 print 'Master: %s' % master |
3362 length = max(len(builder) for builder in builders) | 3510 length = max(len(builder) for builder in builders) |
3363 for builder in sorted(builders): | 3511 for builder in sorted(builders): |
3364 print ' %*s: %s' % (length, builder, ','.join(builders[builder])) | 3512 print ' %*s: %s' % (length, builder, ','.join(builders[builder])) |
3365 return 0 | 3513 return 0 |
3366 | 3514 |
3367 | 3515 |
| 3516 def CMDtry_results(parser, args): |
| 3517 group = optparse.OptionGroup(parser, "Try job results options") |
| 3518 group.add_option( |
| 3519 "-p", "--patchset", type=int, help="patchset number if not current.") |
| 3520 group.add_option( |
| 3521 "--print-master", action='store_true', help="print master name as well") |
| 3522 group.add_option( |
| 3523 "--buildbucket-host", default='cr-buildbucket.appspot.com', |
| 3524 help="Host of buildbucket. The default host is %default.") |
| 3525 parser.add_option_group(group) |
| 3526 auth.add_auth_options(parser) |
| 3527 options, args = parser.parse_args(args) |
| 3528 if args: |
| 3529 parser.error('Unrecognized args: %s' % ' '.join(args)) |
| 3530 |
| 3531 auth_config = auth.extract_auth_config_from_options(options) |
| 3532 cl = Changelist(auth_config=auth_config) |
| 3533 if not cl.GetIssue(): |
| 3534 parser.error('Need to upload first') |
| 3535 |
| 3536 if not options.patchset: |
| 3537 options.patchset = cl.GetMostRecentPatchset() |
| 3538 if options.patchset and options.patchset != cl.GetPatchset(): |
| 3539 print( |
| 3540 '\nWARNING Mismatch between local config and server. Did a previous ' |
| 3541 'upload fail?\ngit-cl try always uses latest patchset from rietveld. ' |
| 3542 'Continuing using\npatchset %s.\n' % options.patchset) |
| 3543 try: |
| 3544 jobs = fetch_try_jobs(auth_config, cl, options) |
| 3545 except BuildbucketResponseException as ex: |
| 3546 print 'Buildbucket error: %s' % ex |
| 3547 return 1 |
| 3548 except Exception as e: |
| 3549 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc()) |
| 3550 print 'ERROR: Exception when trying to fetch tryjobs: %s\n%s' % ( |
| 3551 e, stacktrace) |
| 3552 return 1 |
| 3553 print_tryjobs(options, jobs) |
| 3554 return 0 |
| 3555 |
| 3556 |
3368 @subcommand.usage('[new upstream branch]') | 3557 @subcommand.usage('[new upstream branch]') |
3369 def CMDupstream(parser, args): | 3558 def CMDupstream(parser, args): |
3370 """Prints or sets the name of the upstream branch, if any.""" | 3559 """Prints or sets the name of the upstream branch, if any.""" |
3371 _, args = parser.parse_args(args) | 3560 _, args = parser.parse_args(args) |
3372 if len(args) > 1: | 3561 if len(args) > 1: |
3373 parser.error('Unrecognized args: %s' % ' '.join(args)) | 3562 parser.error('Unrecognized args: %s' % ' '.join(args)) |
3374 | 3563 |
3375 cl = Changelist() | 3564 cl = Changelist() |
3376 if args: | 3565 if args: |
3377 # One arg means set upstream branch. | 3566 # One arg means set upstream branch. |
(...skipping 382 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
3760 if __name__ == '__main__': | 3949 if __name__ == '__main__': |
3761 # These affect sys.stdout so do it outside of main() to simplify mocks in | 3950 # These affect sys.stdout so do it outside of main() to simplify mocks in |
3762 # unit testing. | 3951 # unit testing. |
3763 fix_encoding.fix_encoding() | 3952 fix_encoding.fix_encoding() |
3764 colorama.init() | 3953 colorama.init() |
3765 try: | 3954 try: |
3766 sys.exit(main(sys.argv[1:])) | 3955 sys.exit(main(sys.argv[1:])) |
3767 except KeyboardInterrupt: | 3956 except KeyboardInterrupt: |
3768 sys.stderr.write('interrupted\n') | 3957 sys.stderr.write('interrupted\n') |
3769 sys.exit(1) | 3958 sys.exit(1) |
OLD | NEW |