Index: git_cl.py |
diff --git a/git_cl.py b/git_cl.py |
index 1f7854551cb112dfc9647896ad28089f874036c2..fd80fb701cfd2822aef83ecb1aa34db5b08280c5 100755 |
--- a/git_cl.py |
+++ b/git_cl.py |
@@ -11,6 +11,7 @@ from distutils.version import LooseVersion |
from multiprocessing.pool import ThreadPool |
import base64 |
import glob |
+import httplib |
import json |
import logging |
import optparse |
@@ -21,6 +22,8 @@ import stat |
import sys |
import tempfile |
import textwrap |
+import time |
+import traceback |
import urllib2 |
import urlparse |
import webbrowser |
@@ -31,8 +34,8 @@ try: |
except ImportError: |
pass |
- |
from third_party import colorama |
+from third_party import httplib2 |
from third_party import upload |
import auth |
import breakpad # pylint: disable=W0611 |
@@ -62,6 +65,9 @@ REFS_THAT_ALIAS_TO_OTHER_REFS = { |
'refs/remotes/origin/lkcr': 'refs/remotes/origin/master', |
} |
+# Buildbucket-related constants |
+BUILDBUCKET_HOST = 'cr-buildbucket.appspot.com' |
+ |
# Valid extensions for files we want to lint. |
DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)" |
DEFAULT_LINT_IGNORE_REGEX = r"$^" |
@@ -202,6 +208,115 @@ def add_git_similarity(parser): |
parser.parse_args = Parse |
+def _prefix_master(master): |
+ """Convert user-specified master name to full master name. |
+ |
+ Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket |
+ name, while the developers always use shortened master name |
+ (tryserver.chromium.linux) by stripping off the prefix 'master.'. This |
+ function does the conversion for buildbucket migration. |
+ """ |
+ prefix = 'master.' |
+ if master.startswith(prefix): |
+ return master |
+ return '%s%s' % (prefix, master) |
+ |
+ |
+def trigger_try_jobs(auth_config, changelist, options, masters, category): |
+ rietveld_url = settings.GetDefaultServerUrl() |
+ rietveld_host = urlparse.urlparse(rietveld_url).hostname |
+ authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config) |
+ http = authenticator.authorize(httplib2.Http()) |
+ http.force_exception_to_status_code = True |
+ issue_props = changelist.GetIssueProperties() |
+ issue = changelist.GetIssue() |
+ patchset = changelist.GetMostRecentPatchset() |
+ |
+ buildbucket_put_url = ( |
+ 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format( |
+ hostname=BUILDBUCKET_HOST)) |
+ buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format( |
+ hostname=rietveld_host, |
+ issue=issue, |
+ patch=patchset) |
+ |
+ batch_req_body = {'builds': []} |
+ print_text = [] |
+ print_text.append('Tried jobs on:') |
+ for master, builders_and_tests in sorted(masters.iteritems()): |
+ print_text.append('Master: %s' % master) |
+ bucket = _prefix_master(master) |
+ for builder, tests in sorted(builders_and_tests.iteritems()): |
+ print_text.append(' %s: %s' % (builder, tests)) |
+ parameters = { |
+ 'builder_name': builder, |
+ 'changes': [ |
+ {'author': {'email': issue_props['owner_email']}}, |
+ ], |
+ 'properties': { |
+ 'category': category, |
+ 'issue': issue, |
+ 'master': master, |
+ 'patch_project': issue_props['project'], |
+ 'patch_storage': 'rietveld', |
+ 'patchset': patchset, |
+ 'reason': options.name, |
+ 'revision': options.revision, |
+ 'rietveld': rietveld_url, |
+ 'testfilter': tests, |
+ }, |
+ } |
+ if options.clobber: |
+ parameters['properties']['clobber'] = True |
+ batch_req_body['builds'].append( |
+ { |
+ 'bucket': bucket, |
+ 'parameters_json': json.dumps(parameters), |
+ 'tags': ['builder:%s' % builder, |
+ 'buildset:%s' % buildset, |
+ 'master:%s' % master, |
+ 'user_agent:git_cl_try'] |
+ } |
+ ) |
+ |
+ 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 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) |
+ break |
+ if response.status < 500 or try_count >= 2: |
+ raise httplib2.HttpLib2Error(content) |
+ |
+ # 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 MatchSvnGlob(url, base_url, glob_spec, allow_wildcards): |
"""Return the corresponding git ref if |base_url| together with |glob_spec| |
matches the full |url|. |
@@ -269,6 +384,10 @@ def print_stats(similarity, find_copies, args): |
stdout=stdout, env=env) |
+class BuildbucketResponseException(Exception): |
+ pass |
+ |
+ |
class Settings(object): |
def __init__(self): |
self.default_server = None |
@@ -2764,6 +2883,9 @@ def CMDtry(parser, args): |
"server-side to define what default bot set to use") |
group.add_option( |
"-n", "--name", help="Try job name; default to current branch name") |
+ group.add_option( |
+ "--use-buildbucket", action="store_true", default=False, |
+ help="Use buildbucket to trigger try jobs.") |
parser.add_option_group(group) |
auth.add_auth_options(parser) |
options, args = parser.parse_args(args) |
@@ -2861,23 +2983,35 @@ def CMDtry(parser, args): |
'\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' % patchset) |
- try: |
- cl.RpcServer().trigger_distributed_try_jobs( |
- cl.GetIssue(), patchset, options.name, options.clobber, |
- options.revision, masters) |
- except urllib2.HTTPError, e: |
- if e.code == 404: |
- print('404 from rietveld; ' |
- 'did you mean to use "git try" instead of "git cl try"?') |
+ if options.use_buildbucket: |
+ try: |
+ trigger_try_jobs(auth_config, cl, options, masters, 'git_cl_try') |
+ except BuildbucketResponseException as ex: |
+ print 'ERROR: %s' % ex |
+ return 1 |
+ except Exception as e: |
+ stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc()) |
+ print 'ERROR: Exception when trying to trigger tryjobs: %s\n%s' % ( |
+ e, stacktrace) |
return 1 |
- print('Tried jobs on:') |
- |
- for (master, builders) in masters.iteritems(): |
- if master: |
- print 'Master: %s' % master |
- length = max(len(builder) for builder in builders) |
- for builder in sorted(builders): |
- print ' %*s: %s' % (length, builder, ','.join(builders[builder])) |
+ else: |
+ try: |
+ cl.RpcServer().trigger_distributed_try_jobs( |
+ cl.GetIssue(), patchset, options.name, options.clobber, |
+ options.revision, masters) |
+ except urllib2.HTTPError as e: |
+ if e.code == 404: |
+ print('404 from rietveld; ' |
+ 'did you mean to use "git try" instead of "git cl try"?') |
+ return 1 |
+ print('Tried jobs on:') |
+ |
+ for (master, builders) in sorted(masters.iteritems()): |
+ if master: |
+ print 'Master: %s' % master |
+ length = max(len(builder) for builder in builders) |
+ for builder in sorted(builders): |
+ print ' %*s: %s' % (length, builder, ','.join(builders[builder])) |
return 0 |