| Index: git_cl.py
|
| diff --git a/git_cl.py b/git_cl.py
|
| index 3023213ddd903a9d696f48eea7c79f04964f079e..30956f54a0ea3ee261df71a66303fef94607d917 100755
|
| --- a/git_cl.py
|
| +++ b/git_cl.py
|
| @@ -25,8 +25,10 @@ import tempfile
|
| import textwrap
|
| import time
|
| import traceback
|
| +import urllib
|
| import urllib2
|
| import urlparse
|
| +import uuid
|
| import webbrowser
|
| import zlib
|
|
|
| @@ -239,6 +241,40 @@ def _prefix_master(master):
|
| return '%s%s' % (prefix, master)
|
|
|
|
|
| +def _buildbucket_retry(operation_name, http, *args, **kwargs):
|
| + """Retries requests to buildbucket service and returns parsed json content."""
|
| + try_count = 0
|
| + while True:
|
| + response, content = http.request(*args, **kwargs)
|
| + try:
|
| + content_json = json.loads(content)
|
| + except ValueError:
|
| + content_json = None
|
| +
|
| + # Buildbucket could return an error even if status==200.
|
| + if content_json and content_json.get('error'):
|
| + msg = 'Error in response. Reason: %s. Message: %s.' % (
|
| + content_json['error'].get('reason', ''),
|
| + content_json['error'].get('message', ''))
|
| + raise BuildbucketResponseException(msg)
|
| +
|
| + if response.status == 200:
|
| + if not content_json:
|
| + raise BuildbucketResponseException(
|
| + 'Buildbucket returns invalid json content: %s.\n'
|
| + 'Please file bugs at http://crbug.com, label "Infra-BuildBucket".' %
|
| + content)
|
| + return content_json
|
| + if response.status < 500 or try_count >= 2:
|
| + raise httplib2.HttpLib2Error(content)
|
| +
|
| + # status >= 500 means transient failures.
|
| + logging.debug('Transient errors when %s. Will retry.', operation_name)
|
| + time.sleep(0.5 + 1.5*try_count)
|
| + try_count += 1
|
| + assert False, 'unreachable'
|
| +
|
| +
|
| def trigger_luci_job(changelist, masters, options):
|
| """Send a job to run on LUCI."""
|
| issue_props = changelist.GetIssueProperties()
|
| @@ -306,6 +342,7 @@ def trigger_try_jobs(auth_config, changelist, options, masters, category):
|
| {
|
| 'bucket': bucket,
|
| 'parameters_json': json.dumps(parameters),
|
| + 'client_operation_id': str(uuid.uuid4()),
|
| 'tags': ['builder:%s' % builder,
|
| 'buildset:%s' % buildset,
|
| 'master:%s' % master,
|
| @@ -313,42 +350,153 @@ def trigger_try_jobs(auth_config, changelist, options, masters, category):
|
| }
|
| )
|
|
|
| - for try_count in xrange(3):
|
| - response, content = http.request(
|
| - buildbucket_put_url,
|
| - 'PUT',
|
| - body=json.dumps(batch_req_body),
|
| - headers={'Content-Type': 'application/json'},
|
| - )
|
| - content_json = None
|
| - try:
|
| - content_json = json.loads(content)
|
| - except ValueError:
|
| - pass
|
| + _buildbucket_retry(
|
| + 'triggering tryjobs',
|
| + http,
|
| + buildbucket_put_url,
|
| + 'PUT',
|
| + body=json.dumps(batch_req_body),
|
| + headers={'Content-Type': 'application/json'}
|
| + )
|
| + print '\n'.join(print_text)
|
|
|
| - # Buildbucket could return an error even if status==200.
|
| - if content_json and content_json.get('error'):
|
| - msg = 'Error in response. Code: %d. Reason: %s. Message: %s.' % (
|
| - content_json['error'].get('code', ''),
|
| - content_json['error'].get('reason', ''),
|
| - content_json['error'].get('message', ''))
|
| - raise BuildbucketResponseException(msg)
|
|
|
| - if response.status == 200:
|
| - if not content_json:
|
| - raise BuildbucketResponseException(
|
| - 'Buildbucket returns invalid json content: %s.\n'
|
| - 'Please file bugs at crbug.com, label "Infra-BuildBucket".' %
|
| - content)
|
| +def fetch_try_jobs(auth_config, changelist, options):
|
| + """Fetches tryjobs from buildbucket.
|
| +
|
| + Returns a map from build id to build info as json dictionary.
|
| + """
|
| + rietveld_url = settings.GetDefaultServerUrl()
|
| + rietveld_host = urlparse.urlparse(rietveld_url).hostname
|
| + authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
|
| + if authenticator.has_cached_credentials():
|
| + http = authenticator.authorize(httplib2.Http())
|
| + else:
|
| + print ('Warning: Some results might be missing because %s' %
|
| + # Get the message on how to login.
|
| + auth.LoginRequiredError(rietveld_host).message)
|
| + http = httplib2.Http()
|
| +
|
| + http.force_exception_to_status_code = True
|
| +
|
| + buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
|
| + hostname=rietveld_host,
|
| + issue=changelist.GetIssue(),
|
| + patch=options.patchset)
|
| + params = {'tag': 'buildset:%s' % buildset}
|
| +
|
| + builds = {}
|
| + while True:
|
| + url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
|
| + hostname=options.buildbucket_host,
|
| + params=urllib.urlencode(params))
|
| + content = _buildbucket_retry('fetching tryjobs', http, url, 'GET')
|
| + for build in content.get('builds', []):
|
| + builds[build['id']] = build
|
| + if 'next_cursor' in content:
|
| + params['start_cursor'] = content['next_cursor']
|
| + else:
|
| break
|
| - if response.status < 500 or try_count >= 2:
|
| - raise httplib2.HttpLib2Error(content)
|
| + return builds
|
|
|
| - # status >= 500 means transient failures.
|
| - logging.debug('Transient errors when triggering tryjobs. Will retry.')
|
| - time.sleep(0.5 + 1.5*try_count)
|
|
|
| - print '\n'.join(print_text)
|
| +def print_tryjobs(options, builds):
|
| + """Prints nicely result of fetch_try_jobs."""
|
| + if not builds:
|
| + print 'No tryjobs scheduled'
|
| + return
|
| +
|
| + # Make a copy, because we'll be modifying builds dictionary.
|
| + builds = builds.copy()
|
| + builder_names_cache = {}
|
| +
|
| + def get_builder(b):
|
| + try:
|
| + return builder_names_cache[b['id']]
|
| + except KeyError:
|
| + try:
|
| + parameters = json.loads(b['parameters_json'])
|
| + name = parameters['builder_name']
|
| + except (ValueError, KeyError) as error:
|
| + print 'WARNING: failed to get builder name for build %s: %s' % (
|
| + b['id'], error)
|
| + name = None
|
| + builder_names_cache[b['id']] = name
|
| + return name
|
| +
|
| + def get_bucket(b):
|
| + bucket = b['bucket']
|
| + if bucket.startswith('master.'):
|
| + return bucket[len('master.'):]
|
| + return bucket
|
| +
|
| + if options.print_master:
|
| + name_fmt = '%%-%ds %%-%ds' % (
|
| + max(len(str(get_bucket(b))) for b in builds.itervalues()),
|
| + max(len(str(get_builder(b))) for b in builds.itervalues()))
|
| + def get_name(b):
|
| + return name_fmt % (get_bucket(b), get_builder(b))
|
| + else:
|
| + name_fmt = '%%-%ds' % (
|
| + max(len(str(get_builder(b))) for b in builds.itervalues()))
|
| + def get_name(b):
|
| + return name_fmt % get_builder(b)
|
| +
|
| + def sort_key(b):
|
| + return b['status'], b.get('result'), get_name(b), b.get('url')
|
| +
|
| + def pop(title, f, color=None, **kwargs):
|
| + """Pop matching builds from `builds` dict and print them."""
|
| +
|
| + if not sys.stdout.isatty() or color is None:
|
| + colorize = str
|
| + else:
|
| + colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
|
| +
|
| + result = []
|
| + for b in builds.values():
|
| + if all(b.get(k) == v for k, v in kwargs.iteritems()):
|
| + builds.pop(b['id'])
|
| + result.append(b)
|
| + if result:
|
| + print colorize(title)
|
| + for b in sorted(result, key=sort_key):
|
| + print ' ', colorize('\t'.join(map(str, f(b))))
|
| +
|
| + total = len(builds)
|
| + pop(status='COMPLETED', result='SUCCESS',
|
| + title='Successes:', color=Fore.GREEN,
|
| + f=lambda b: (get_name(b), b.get('url')))
|
| + pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
|
| + title='Infra Failures:', color=Fore.MAGENTA,
|
| + f=lambda b: (get_name(b), b.get('url')))
|
| + pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
|
| + title='Failures:', color=Fore.RED,
|
| + f=lambda b: (get_name(b), b.get('url')))
|
| + pop(status='COMPLETED', result='CANCELED',
|
| + title='Canceled:', color=Fore.MAGENTA,
|
| + f=lambda b: (get_name(b),))
|
| + pop(status='COMPLETED', result='FAILURE',
|
| + failure_reason='INVALID_BUILD_DEFINITION',
|
| + title='Wrong master/builder name:', color=Fore.MAGENTA,
|
| + f=lambda b: (get_name(b),))
|
| + pop(status='COMPLETED', result='FAILURE',
|
| + title='Other failures:',
|
| + f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
|
| + pop(status='COMPLETED',
|
| + title='Other finished:',
|
| + f=lambda b: (get_name(b), b.get('result'), b.get('url')))
|
| + pop(status='STARTED',
|
| + title='Started:', color=Fore.YELLOW,
|
| + f=lambda b: (get_name(b), b.get('url')))
|
| + pop(status='SCHEDULED',
|
| + title='Scheduled:',
|
| + f=lambda b: (get_name(b), 'id=%s' % b['id']))
|
| + # The last section is just in case buildbucket API changes OR there is a bug.
|
| + pop(title='Other:',
|
| + f=lambda b: (get_name(b), 'id=%s' % b['id']))
|
| + assert len(builds) == 0
|
| + print 'Total: %d tryjobs' % total
|
|
|
|
|
| def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
|
| @@ -3365,6 +3513,47 @@ def CMDtry(parser, args):
|
| return 0
|
|
|
|
|
| +def CMDtry_results(parser, args):
|
| + group = optparse.OptionGroup(parser, "Try job results options")
|
| + group.add_option(
|
| + "-p", "--patchset", type=int, help="patchset number if not current.")
|
| + group.add_option(
|
| + "--print-master", action='store_true', help="print master name as well")
|
| + group.add_option(
|
| + "--buildbucket-host", default='cr-buildbucket.appspot.com',
|
| + help="Host of buildbucket. The default host is %default.")
|
| + parser.add_option_group(group)
|
| + auth.add_auth_options(parser)
|
| + options, args = parser.parse_args(args)
|
| + if args:
|
| + parser.error('Unrecognized args: %s' % ' '.join(args))
|
| +
|
| + auth_config = auth.extract_auth_config_from_options(options)
|
| + cl = Changelist(auth_config=auth_config)
|
| + if not cl.GetIssue():
|
| + parser.error('Need to upload first')
|
| +
|
| + if not options.patchset:
|
| + options.patchset = cl.GetMostRecentPatchset()
|
| + if options.patchset and options.patchset != cl.GetPatchset():
|
| + print(
|
| + '\nWARNING Mismatch between local config and server. Did a previous '
|
| + 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
|
| + 'Continuing using\npatchset %s.\n' % options.patchset)
|
| + try:
|
| + jobs = fetch_try_jobs(auth_config, cl, options)
|
| + except BuildbucketResponseException as ex:
|
| + print 'Buildbucket error: %s' % ex
|
| + return 1
|
| + except Exception as e:
|
| + stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
|
| + print 'ERROR: Exception when trying to fetch tryjobs: %s\n%s' % (
|
| + e, stacktrace)
|
| + return 1
|
| + print_tryjobs(options, jobs)
|
| + return 0
|
| +
|
| +
|
| @subcommand.usage('[new upstream branch]')
|
| def CMDupstream(parser, args):
|
| """Prints or sets the name of the upstream branch, if any."""
|
|
|