Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(303)

Side by Side Diff: git_cl.py

Issue 2274743003: Add a --json option to git cl try-results. (Closed) Base URL: https://chromium.googlesource.com/chromium/tools/depot_tools.git@master
Patch Set: WIP: Make --json option output the same info as human-readable option; refactor Created 4 years, 3 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « no previous file | tests/git_cl_test.py » ('j') | tests/git_cl_test.py » ('J')
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
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
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
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
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
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)
OLDNEW
« no previous file with comments | « no previous file | tests/git_cl_test.py » ('j') | tests/git_cl_test.py » ('J')

Powered by Google App Engine
This is Rietveld 408576698