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 and Gerrit.""" | 8 """A git-command for integrating reviews on Rietveld and Gerrit.""" |
9 | 9 |
10 from __future__ import print_function | 10 from __future__ import print_function |
(...skipping 380 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
391 headers={'Content-Type': 'application/json'} | 391 headers={'Content-Type': 'application/json'} |
392 ) | 392 ) |
393 print_text.append('To see results here, run: git cl try-results') | 393 print_text.append('To see results here, run: git cl try-results') |
394 print_text.append('To see results in browser, run: git cl web') | 394 print_text.append('To see results in browser, run: git cl web') |
395 print('\n'.join(print_text)) | 395 print('\n'.join(print_text)) |
396 | 396 |
397 | 397 |
398 def fetch_try_jobs(auth_config, changelist, options): | 398 def fetch_try_jobs(auth_config, changelist, options): |
399 """Fetches try jobs from buildbucket. | 399 """Fetches try jobs from buildbucket. |
400 | 400 |
401 Returns a map from build id to build info as json dictionary. | 401 Returns a map from build id to build info as a dictionary. |
402 """ | 402 """ |
403 rietveld_url = settings.GetDefaultServerUrl() | 403 rietveld_url = settings.GetDefaultServerUrl() |
404 rietveld_host = urlparse.urlparse(rietveld_url).hostname | 404 rietveld_host = urlparse.urlparse(rietveld_url).hostname |
405 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config) | 405 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config) |
406 if authenticator.has_cached_credentials(): | 406 if authenticator.has_cached_credentials(): |
407 http = authenticator.authorize(httplib2.Http()) | 407 http = authenticator.authorize(httplib2.Http()) |
408 else: | 408 else: |
409 print('Warning: Some results might be missing because %s' % | 409 print('Warning: Some results might be missing because %s' % |
410 # Get the message on how to login. | 410 # Get the message on how to login. |
411 (auth.LoginRequiredError(rietveld_host).message,)) | 411 (auth.LoginRequiredError(rietveld_host).message,)) |
(...skipping 15 matching lines...) Expand all Loading... |
427 content = _buildbucket_retry('fetching try jobs', http, url, 'GET') | 427 content = _buildbucket_retry('fetching try jobs', http, url, 'GET') |
428 for build in content.get('builds', []): | 428 for build in content.get('builds', []): |
429 builds[build['id']] = build | 429 builds[build['id']] = build |
430 if 'next_cursor' in content: | 430 if 'next_cursor' in content: |
431 params['start_cursor'] = content['next_cursor'] | 431 params['start_cursor'] = content['next_cursor'] |
432 else: | 432 else: |
433 break | 433 break |
434 return builds | 434 return builds |
435 | 435 |
436 | 436 |
437 def print_try_jobs(options, builds): | 437 def try_job_buckets(builds): |
438 """Prints nicely result of fetch_try_jobs.""" | 438 """Groups a set of try jobs into buckets for output. |
439 if not builds: | |
440 print('No try jobs scheduled') | |
441 return | |
442 | 439 |
| 440 Args: |
| 441 builds: A list of dicts, one for each try job. This is the output |
| 442 of fetch_try_jobs. |
| 443 |
| 444 Returns: |
| 445 A dict mapping categories of try jobs (e.g. successes, failures, |
| 446 infra failures, etc.) to lists of dicts, one for each try job. |
| 447 The information here is a subset of the information in the original |
| 448 input build dicts. |
| 449 """ |
443 # Make a copy, because we'll be modifying builds dictionary. | 450 # Make a copy, because we'll be modifying builds dictionary. |
444 builds = builds.copy() | 451 builds = builds.copy() |
445 builder_names_cache = {} | 452 builder_names_cache = {} |
446 | 453 |
447 def get_builder(b): | 454 def get_builder_name(b): |
448 try: | 455 try: |
449 return builder_names_cache[b['id']] | 456 return builder_names_cache[b['id']] |
450 except KeyError: | 457 except KeyError: |
451 try: | 458 try: |
452 parameters = json.loads(b['parameters_json']) | 459 parameters = json.loads(b['parameters_json']) |
453 name = parameters['builder_name'] | 460 name = parameters['builder_name'] |
454 except (ValueError, KeyError) as error: | 461 except (ValueError, KeyError) as error: |
455 print('WARNING: failed to get builder name for build %s: %s' % ( | 462 print('WARNING: failed to get builder name for build %s: %s' % |
456 b['id'], error)) | 463 (b['id'], error)) |
457 name = None | 464 name = None |
458 builder_names_cache[b['id']] = name | 465 builder_names_cache[b['id']] = name |
459 return name | 466 return name |
460 | 467 |
461 def get_bucket(b): | 468 def get_master_name(b): |
462 bucket = b['bucket'] | 469 bucket = b['bucket'] |
463 if bucket.startswith('master.'): | 470 if bucket.startswith('master.'): |
464 return bucket[len('master.'):] | 471 return bucket[len('master.'):] |
465 return bucket | 472 return bucket |
466 | 473 |
467 if options.print_master: | 474 def sort_key(b): |
468 name_fmt = '%%-%ds %%-%ds' % ( | 475 return b['status'], b.get('result'), get_builder_name(b), b.get('url') |
469 max(len(str(get_bucket(b))) for b in builds.itervalues()), | |
470 max(len(str(get_builder(b))) for b in builds.itervalues())) | |
471 def get_name(b): | |
472 return name_fmt % (get_bucket(b), get_builder(b)) | |
473 else: | |
474 name_fmt = '%%-%ds' % ( | |
475 max(len(str(get_builder(b))) for b in builds.itervalues())) | |
476 def get_name(b): | |
477 return name_fmt % get_builder(b) | |
478 | 476 |
479 def sort_key(b): | 477 buckets = collections.OrderedDict() |
480 return b['status'], b.get('result'), get_name(b), b.get('url') | |
481 | 478 |
482 def pop(title, f, color=None, **kwargs): | 479 def pop(title, **kwargs): |
483 """Pop matching builds from `builds` dict and print them.""" | 480 """Pops matching builds from `builds` and adds it to `buckets`.""" |
484 | |
485 if not options.color or color is None: | |
486 colorize = str | |
487 else: | |
488 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET) | |
489 | |
490 result = [] | 481 result = [] |
491 for b in builds.values(): | 482 for b in builds.values(): |
492 if all(b.get(k) == v for k, v in kwargs.iteritems()): | 483 if all(b.get(k) == v for k, v in kwargs.iteritems()): |
493 builds.pop(b['id']) | 484 builds.pop(b['id']) |
494 result.append(b) | 485 result.append(b) |
495 if result: | 486 if result: |
496 print(colorize(title)) | 487 buckets[title] = [] |
497 for b in sorted(result, key=sort_key): | 488 for b in sorted(result, key=sort_key): |
498 print(' ', colorize('\t'.join(map(str, f(b))))) | 489 buckets[title].append({ |
| 490 'builder_name': get_builder_name(b), |
| 491 'master_name': get_master_name(b), |
| 492 'url': b.get('url'), |
| 493 'buildbucket_id': b.get('id'), |
| 494 'status': b.get('status'), |
| 495 'result': b.get('result'), |
| 496 'failure_reason': b.get('failure_reason'), |
| 497 }) |
499 | 498 |
500 total = len(builds) | 499 pop('Successes', status='COMPLETED', result='SUCCESS') |
501 pop(status='COMPLETED', result='SUCCESS', | 500 pop('Infra Failures', status='COMPLETED', result='FAILURE', |
502 title='Successes:', color=Fore.GREEN, | 501 failure_reason='INFRA_FAILURE') |
503 f=lambda b: (get_name(b), b.get('url'))) | 502 pop('Failures', status='COMPLETED', result='FAILURE', |
504 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE', | 503 failure_reason='BUILD_FAILURE') |
505 title='Infra Failures:', color=Fore.MAGENTA, | 504 pop('Canceled', status='COMPLETED', result='CANCELED') |
506 f=lambda b: (get_name(b), b.get('url'))) | 505 pop('Wrong master/builder name', status='COMPLETED', result='FAILURE', |
507 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE', | 506 failure_reason='INVALID_BUILD_DEFINITION') |
508 title='Failures:', color=Fore.RED, | 507 pop('Other failures', status='COMPLETED', result='FAILURE') |
509 f=lambda b: (get_name(b), b.get('url'))) | 508 pop('Other finished', status='COMPLETED') |
510 pop(status='COMPLETED', result='CANCELED', | 509 pop('Started', status='STARTED') |
511 title='Canceled:', color=Fore.MAGENTA, | 510 pop('Scheduled', status='SCHEDULED') |
512 f=lambda b: (get_name(b),)) | |
513 pop(status='COMPLETED', result='FAILURE', | |
514 failure_reason='INVALID_BUILD_DEFINITION', | |
515 title='Wrong master/builder name:', color=Fore.MAGENTA, | |
516 f=lambda b: (get_name(b),)) | |
517 pop(status='COMPLETED', result='FAILURE', | |
518 title='Other failures:', | |
519 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url'))) | |
520 pop(status='COMPLETED', | |
521 title='Other finished:', | |
522 f=lambda b: (get_name(b), b.get('result'), b.get('url'))) | |
523 pop(status='STARTED', | |
524 title='Started:', color=Fore.YELLOW, | |
525 f=lambda b: (get_name(b), b.get('url'))) | |
526 pop(status='SCHEDULED', | |
527 title='Scheduled:', | |
528 f=lambda b: (get_name(b), 'id=%s' % b['id'])) | |
529 # The last section is just in case buildbucket API changes OR there is a bug. | 511 # The last section is just in case buildbucket API changes OR there is a bug. |
530 pop(title='Other:', | 512 pop('Other') |
531 f=lambda b: (get_name(b), 'id=%s' % b['id'])) | |
532 assert len(builds) == 0 | 513 assert len(builds) == 0 |
533 print('Total: %d try jobs' % total) | 514 return buckets |
| 515 |
| 516 |
| 517 def print_try_jobs(options, builds): |
| 518 """Nicely prints the result of fetch_try_jobs.""" |
| 519 if not builds: |
| 520 print('No try jobs scheduled') |
| 521 return |
| 522 |
| 523 buckets = try_job_buckets(builds) |
| 524 |
| 525 def color_for_bucket(title): |
| 526 return { |
| 527 'Successes': Fore.GREEN, |
| 528 'Infra Failures': Fore.MAGENTA, |
| 529 'Failures': Fore.RED, |
| 530 'Canceled': Fore.MAGENTA, |
| 531 'Wrong master/builder name': Fore.MAGENTA, |
| 532 'Started:': Fore.YELLOW, |
| 533 }.get(title) |
| 534 |
| 535 def extra_columns_for_bucket(title): |
| 536 return { |
| 537 'Successes': ['url'], |
| 538 'Infra Failures': ['url'], |
| 539 'Failures': ['url'], |
| 540 'Canceled': [], |
| 541 'Wrong master/builder name': ['builder_name'], |
| 542 'Other failures': ['failure_reason', 'url'], |
| 543 'Other finished': ['result', 'url'], |
| 544 'Started': ['url'], |
| 545 'Scheduled': ['buildbucket_id'], |
| 546 'Other': ['buildbucket_id'], |
| 547 }.get(title) |
| 548 |
| 549 for title, builds in buckets.iteritems(): |
| 550 if not builds: |
| 551 continue |
| 552 color = color_for_bucket(title) |
| 553 if not options.color or color is None: |
| 554 colorize = str |
| 555 else: |
| 556 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET) |
| 557 print(colorize(title + ':')) |
| 558 for build in builds: |
| 559 row = [build['builder_name']] |
| 560 # TODO: Make sure the spacing is right when master name is printed. |
| 561 if options.print_master: |
| 562 row.append([build['master_name']]) |
| 563 for column in extra_columns_for_bucket(title): |
| 564 if column == 'id': |
| 565 row.append('id=%s' % build['id']) |
| 566 else: |
| 567 row.append(build[column]) |
| 568 print(' ', colorize('\t'.join(row))) |
| 569 print('Total: %d try jobs' % sum(len(b) for b in buckets)) |
| 570 |
| 571 |
| 572 def write_try_results_json(output_file, builds): |
| 573 """This outputs results from fetch_try_jobs as JSON. |
| 574 |
| 575 Args: |
| 576 builds: Results fetched from fetch_try_jobs. This is a list of dicts. |
| 577 |
| 578 Returns: |
| 579 A dict mapping "status buckets" to lists of builds. |
| 580 """ |
| 581 write_json(output_file + '.full', builds) |
| 582 buckets = try_job_buckets(builds) |
| 583 write_json(output_file, buckets) |
534 | 584 |
535 | 585 |
536 def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards): | 586 def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards): |
537 """Return the corresponding git ref if |base_url| together with |glob_spec| | 587 """Return the corresponding git ref if |base_url| together with |glob_spec| |
538 matches the full |url|. | 588 matches the full |url|. |
539 | 589 |
540 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below). | 590 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below). |
541 """ | 591 """ |
542 fetch_suburl, as_ref = glob_spec.split(':') | 592 fetch_suburl, as_ref = glob_spec.split(':') |
543 if allow_wildcards: | 593 if allow_wildcards: |
(...skipping 4206 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
4750 group.add_option( | 4800 group.add_option( |
4751 "-p", "--patchset", type=int, help="patchset number if not current.") | 4801 "-p", "--patchset", type=int, help="patchset number if not current.") |
4752 group.add_option( | 4802 group.add_option( |
4753 "--print-master", action='store_true', help="print master name as well.") | 4803 "--print-master", action='store_true', help="print master name as well.") |
4754 group.add_option( | 4804 group.add_option( |
4755 "--color", action='store_true', default=setup_color.IS_TTY, | 4805 "--color", action='store_true', default=setup_color.IS_TTY, |
4756 help="force color output, useful when piping output.") | 4806 help="force color output, useful when piping output.") |
4757 group.add_option( | 4807 group.add_option( |
4758 "--buildbucket-host", default='cr-buildbucket.appspot.com', | 4808 "--buildbucket-host", default='cr-buildbucket.appspot.com', |
4759 help="Host of buildbucket. The default host is %default.") | 4809 help="Host of buildbucket. The default host is %default.") |
| 4810 group.add_option( |
| 4811 '--json', help='Path of JSON output file to write try job results to.') |
4760 parser.add_option_group(group) | 4812 parser.add_option_group(group) |
4761 auth.add_auth_options(parser) | 4813 auth.add_auth_options(parser) |
4762 options, args = parser.parse_args(args) | 4814 options, args = parser.parse_args(args) |
4763 if args: | 4815 if args: |
4764 parser.error('Unrecognized args: %s' % ' '.join(args)) | 4816 parser.error('Unrecognized args: %s' % ' '.join(args)) |
4765 | 4817 |
4766 auth_config = auth.extract_auth_config_from_options(options) | 4818 auth_config = auth.extract_auth_config_from_options(options) |
4767 cl = Changelist(auth_config=auth_config) | 4819 cl = Changelist(auth_config=auth_config) |
4768 if not cl.GetIssue(): | 4820 if not cl.GetIssue(): |
4769 parser.error('Need to upload first') | 4821 parser.error('Need to upload first') |
4770 | 4822 |
4771 if not options.patchset: | 4823 if not options.patchset: |
4772 options.patchset = cl.GetMostRecentPatchset() | 4824 options.patchset = cl.GetMostRecentPatchset() |
4773 if options.patchset and options.patchset != cl.GetPatchset(): | 4825 if options.patchset and options.patchset != cl.GetPatchset(): |
4774 print( | 4826 print( |
4775 '\nWARNING Mismatch between local config and server. Did a previous ' | 4827 '\nWARNING Mismatch between local config and server. Did a previous ' |
4776 'upload fail?\ngit-cl try always uses latest patchset from rietveld. ' | 4828 'upload fail?\ngit-cl try always uses latest patchset from rietveld. ' |
4777 'Continuing using\npatchset %s.\n' % options.patchset) | 4829 'Continuing using\npatchset %s.\n' % options.patchset) |
4778 try: | 4830 try: |
4779 jobs = fetch_try_jobs(auth_config, cl, options) | 4831 jobs = fetch_try_jobs(auth_config, cl, options) |
4780 except BuildbucketResponseException as ex: | 4832 except BuildbucketResponseException as ex: |
4781 print('Buildbucket error: %s' % ex) | 4833 print('Buildbucket error: %s' % ex) |
4782 return 1 | 4834 return 1 |
4783 except Exception as e: | 4835 except Exception as e: |
4784 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc()) | 4836 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc()) |
4785 print('ERROR: Exception when trying to fetch try jobs: %s\n%s' % | 4837 print('ERROR: Exception when trying to fetch try jobs: %s\n%s' % |
4786 (e, stacktrace)) | 4838 (e, stacktrace)) |
4787 return 1 | 4839 return 1 |
4788 print_try_jobs(options, jobs) | 4840 if options.json: |
| 4841 write_try_results_json(options.json, jobs) |
| 4842 else: |
| 4843 print_try_jobs(options, jobs) |
4789 return 0 | 4844 return 0 |
4790 | 4845 |
4791 | 4846 |
4792 @subcommand.usage('[new upstream branch]') | 4847 @subcommand.usage('[new upstream branch]') |
4793 def CMDupstream(parser, args): | 4848 def CMDupstream(parser, args): |
4794 """Prints or sets the name of the upstream branch, if any.""" | 4849 """Prints or sets the name of the upstream branch, if any.""" |
4795 _, args = parser.parse_args(args) | 4850 _, args = parser.parse_args(args) |
4796 if len(args) > 1: | 4851 if len(args) > 1: |
4797 parser.error('Unrecognized args: %s' % ' '.join(args)) | 4852 parser.error('Unrecognized args: %s' % ' '.join(args)) |
4798 | 4853 |
(...skipping 419 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
5218 if __name__ == '__main__': | 5273 if __name__ == '__main__': |
5219 # These affect sys.stdout so do it outside of main() to simplify mocks in | 5274 # These affect sys.stdout so do it outside of main() to simplify mocks in |
5220 # unit testing. | 5275 # unit testing. |
5221 fix_encoding.fix_encoding() | 5276 fix_encoding.fix_encoding() |
5222 setup_color.init() | 5277 setup_color.init() |
5223 try: | 5278 try: |
5224 sys.exit(main(sys.argv[1:])) | 5279 sys.exit(main(sys.argv[1:])) |
5225 except KeyboardInterrupt: | 5280 except KeyboardInterrupt: |
5226 sys.stderr.write('interrupted\n') | 5281 sys.stderr.write('interrupted\n') |
5227 sys.exit(1) | 5282 sys.exit(1) |
OLD | NEW |