Chromium Code Reviews| 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 glob | 13 import glob |
| 14 import httplib | |
| 14 import json | 15 import json |
| 15 import logging | 16 import logging |
| 16 import optparse | 17 import optparse |
| 17 import os | 18 import os |
| 18 import Queue | 19 import Queue |
| 19 import re | 20 import re |
| 20 import stat | 21 import stat |
| 21 import sys | 22 import sys |
| 22 import tempfile | 23 import tempfile |
| 23 import textwrap | 24 import textwrap |
| 25 import time | |
| 26 import traceback | |
| 24 import urllib2 | 27 import urllib2 |
| 25 import urlparse | 28 import urlparse |
| 26 import webbrowser | 29 import webbrowser |
| 27 import zlib | 30 import zlib |
| 28 | 31 |
| 29 try: | 32 try: |
| 30 import readline # pylint: disable=F0401,W0611 | 33 import readline # pylint: disable=F0401,W0611 |
| 31 except ImportError: | 34 except ImportError: |
| 32 pass | 35 pass |
| 33 | 36 |
| 34 | |
| 35 from third_party import colorama | 37 from third_party import colorama |
| 38 from third_party import httplib2 | |
| 36 from third_party import upload | 39 from third_party import upload |
| 37 import auth | 40 import auth |
| 38 import breakpad # pylint: disable=W0611 | 41 import breakpad # pylint: disable=W0611 |
| 39 import clang_format | 42 import clang_format |
| 40 import dart_format | 43 import dart_format |
| 41 import fix_encoding | 44 import fix_encoding |
| 42 import gclient_utils | 45 import gclient_utils |
| 43 import git_common | 46 import git_common |
| 44 import owners | 47 import owners |
| 45 import owners_finder | 48 import owners_finder |
| 46 import presubmit_support | 49 import presubmit_support |
| 47 import rietveld | 50 import rietveld |
| 48 import scm | 51 import scm |
| 49 import subcommand | 52 import subcommand |
| 50 import subprocess2 | 53 import subprocess2 |
| 51 import watchlists | 54 import watchlists |
| 52 | 55 |
| 53 __version__ = '1.0' | 56 __version__ = '1.0' |
| 54 | 57 |
| 55 DEFAULT_SERVER = 'https://codereview.appspot.com' | 58 DEFAULT_SERVER = 'https://codereview.appspot.com' |
| 56 POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s' | 59 POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s' |
| 57 DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup' | 60 DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup' |
| 58 GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingGit' | 61 GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingGit' |
| 59 CHANGE_ID = 'Change-Id:' | 62 CHANGE_ID = 'Change-Id:' |
| 60 REFS_THAT_ALIAS_TO_OTHER_REFS = { | 63 REFS_THAT_ALIAS_TO_OTHER_REFS = { |
| 61 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master', | 64 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master', |
| 62 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master', | 65 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master', |
| 63 } | 66 } |
| 64 | 67 |
| 68 # Buildbucket-related constants | |
| 69 BUILDBUCKET_PUT_URL = ( | |
|
M-A Ruel
2015/05/12 00:46:21
Please only code the host here. Host != API, so th
sheyang
2015/05/12 01:37:56
Done.
| |
| 70 'https://cr-buildbucket.appspot.com/_ah/api/buildbucket/v1/builds/batch') | |
|
M-A Ruel
2015/05/11 22:21:28
That URL shouldn't be hardcoded there.
nodir
2015/05/11 22:48:29
I think hardcoding the default hostname is fine, b
M-A Ruel
2015/05/11 23:01:27
Not lgtm.
It's not optional nor nice to have.
sheyang
2015/05/12 01:37:56
Done.
| |
| 71 BUILDSET_STR = 'patch/rietveld/{hostname}/{issue}/{patch}' | |
| 72 | |
| 65 # Valid extensions for files we want to lint. | 73 # Valid extensions for files we want to lint. |
| 66 DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)" | 74 DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)" |
| 67 DEFAULT_LINT_IGNORE_REGEX = r"$^" | 75 DEFAULT_LINT_IGNORE_REGEX = r"$^" |
| 68 | 76 |
| 69 # Shortcut since it quickly becomes redundant. | 77 # Shortcut since it quickly becomes redundant. |
| 70 Fore = colorama.Fore | 78 Fore = colorama.Fore |
| 71 | 79 |
| 72 # Initialized in main() | 80 # Initialized in main() |
| 73 settings = None | 81 settings = None |
| 74 | 82 |
| (...skipping 120 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 195 else: | 203 else: |
| 196 git_set_branch_value('git-find-copies', int(options.find_copies)) | 204 git_set_branch_value('git-find-copies', int(options.find_copies)) |
| 197 | 205 |
| 198 print('Using %d%% similarity for rename/copy detection. ' | 206 print('Using %d%% similarity for rename/copy detection. ' |
| 199 'Override with --similarity.' % options.similarity) | 207 'Override with --similarity.' % options.similarity) |
| 200 | 208 |
| 201 return options, args | 209 return options, args |
| 202 parser.parse_args = Parse | 210 parser.parse_args = Parse |
| 203 | 211 |
| 204 | 212 |
| 213 def _prefix_master(master): | |
|
M-A Ruel
2015/05/12 00:46:21
Why?
sheyang
2015/05/12 01:37:56
See the comments about 'black magic' below.
| |
| 214 prefix = 'master.' | |
| 215 if master.startswith(prefix): | |
| 216 return master | |
| 217 else: | |
| 218 return '%s%s' % (prefix, master) | |
| 219 | |
| 220 | |
| 221 def trigger_try_jobs( | |
| 222 auth_config, changelist, options, masters, category): | |
| 223 rietveld_url = settings.GetDefaultServerUrl() | |
| 224 rietveld_host = urlparse.urlparse(rietveld_url).hostname | |
| 225 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config) | |
| 226 http = authenticator.authorize(httplib2.Http()) | |
| 227 http.force_exception_to_status_code = True | |
| 228 issue_props = changelist.GetIssueProperties() | |
| 229 issue = changelist.GetIssue() | |
| 230 patchset = changelist.GetMostRecentPatchset() | |
| 231 buildset = BUILDSET_STR.format( | |
| 232 hostname=rietveld_host, | |
| 233 issue=issue, | |
| 234 patch=patchset) | |
| 235 | |
| 236 batch_req_body = {'builds': []} | |
| 237 print_text = [] | |
| 238 print_text.append('Tried jobs on:') | |
|
M-A Ruel
2015/05/11 23:13:59
Why not printing as it's happening? Why hang outpu
sheyang
2015/05/12 01:37:56
Here we're only preparing parameters for the PUT c
| |
| 239 for master, builders_and_tests in masters.iteritems(): | |
|
M-A Ruel
2015/05/11 23:13:59
Please sort.
sheyang
2015/05/12 01:37:56
Done.
| |
| 240 print_text.append('Master: %s' % master) | |
| 241 bucket = _prefix_master(master) | |
|
M-A Ruel
2015/05/12 00:46:21
This looks like black magic, explain the context.
sheyang
2015/05/12 01:37:56
Buildbucket uses full master name as bucket name '
M-A Ruel
2015/05/12 13:05:27
Please add this to _prefix_master docstring then.
sheyang
2015/05/12 17:40:38
Done.
| |
| 242 for builder, tests in builders_and_tests.iteritems(): | |
|
M-A Ruel
2015/05/12 00:46:21
sort this too.
sheyang
2015/05/12 01:37:56
Done.
| |
| 243 print_text.append(' %s: %s' % (builder, tests)) | |
| 244 parameters = { | |
| 245 'builder_name': builder, | |
| 246 'changes':[ | |
| 247 {'author': {'email': issue_props['owner_email']}}, | |
| 248 ], | |
| 249 'properties': { | |
| 250 'category': category, | |
| 251 'issue': issue, | |
| 252 'master': master, | |
| 253 'patch_project': issue_props['project'], | |
| 254 'patch_storage': 'rietveld', | |
| 255 'patchset': patchset, | |
| 256 'reason': options.name, | |
| 257 'revision': options.revision, | |
| 258 'rietveld': rietveld_url, | |
| 259 'testfilter': tests, | |
| 260 }, | |
| 261 } | |
| 262 if options.clobber: | |
| 263 parameters['properties']['clobber'] = True | |
| 264 batch_req_body['builds'].append( | |
| 265 { | |
| 266 'bucket': bucket, | |
| 267 'parameters_json': json.dumps(parameters), | |
|
M-A Ruel
2015/05/12 00:46:21
What. You are encoding json inside json? Why? Beca
sheyang
2015/05/12 01:37:56
Yes this parameter property is for buildbot about
| |
| 268 'tags': ['builder:%s' % builder, | |
| 269 'buildset:%s' % buildset, | |
| 270 'master:%s' % master, | |
| 271 'user_agent:git_cl_try'] | |
| 272 } | |
| 273 ) | |
| 274 | |
| 275 wait = 1 | |
| 276 try_count = 3 | |
| 277 while try_count > 0: | |
| 278 try_count -= 1 | |
|
M-A Ruel
2015/05/11 23:13:59
try_count--
nodir
2015/05/11 23:19:36
this is invalid Python syntax
M-A Ruel
2015/05/11 23:20:22
Eh, damn python.
sheyang
2015/05/12 01:37:56
Acknowledged.
| |
| 279 response, content = http.request( | |
| 280 BUILDBUCKET_PUT_URL, | |
| 281 'PUT', | |
|
M-A Ruel
2015/05/11 23:01:27
If I understand correctly, the sole reason http is
nodir
2015/05/11 23:19:36
to authenticate the request.
sheyang
2015/05/12 01:37:56
Discussed with maruel@ offline and the recommended
| |
| 282 body=json.dumps(batch_req_body), | |
| 283 headers={'Content-Type': 'application/json'}, | |
| 284 ) | |
| 285 content_json = None | |
| 286 try: | |
| 287 content_json = json.loads(content) | |
| 288 except ValueError: | |
| 289 pass | |
| 290 | |
| 291 # Buildbucket could return an error even if status==200. | |
| 292 if content_json and content_json.get('error'): | |
| 293 msg = 'Error in response. Code: %d. Reason: %s. Message: %s.' % ( | |
| 294 content_json['error'].get('code', ''), | |
| 295 content_json['error'].get('reason', ''), | |
| 296 content_json['error'].get('message', '')) | |
| 297 raise BuildbucketResponseException(msg) | |
| 298 | |
| 299 if response.status == 200: | |
| 300 break | |
| 301 | |
| 302 if response.status < 500 or try_count <= 0: | |
| 303 raise httplib2.HttpLib2Error(content) | |
| 304 | |
| 305 # status >= 500 means transient failures. | |
| 306 logging.debug('Transient errors when triggering tryjobs. ' | |
| 307 'Will retry in %d seconds.', wait) | |
| 308 time.sleep(wait) | |
|
Vadim Sh.
2015/05/11 22:14:01
nit: no need to wait if it was the last attempt (i
M-A Ruel
2015/05/11 23:13:59
Please address this.
sheyang
2015/05/12 01:37:56
if try_count <=0, we'll raise an exception above s
| |
| 309 wait *= 2 | |
| 310 | |
| 311 print '\n'.join(print_text) | |
| 312 | |
| 313 | |
| 205 def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards): | 314 def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards): |
| 206 """Return the corresponding git ref if |base_url| together with |glob_spec| | 315 """Return the corresponding git ref if |base_url| together with |glob_spec| |
| 207 matches the full |url|. | 316 matches the full |url|. |
| 208 | 317 |
| 209 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below). | 318 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below). |
| 210 """ | 319 """ |
| 211 fetch_suburl, as_ref = glob_spec.split(':') | 320 fetch_suburl, as_ref = glob_spec.split(':') |
| 212 if allow_wildcards: | 321 if allow_wildcards: |
| 213 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl) | 322 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl) |
| 214 if glob_match: | 323 if glob_match: |
| (...skipping 47 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 262 try: | 371 try: |
| 263 stdout = sys.stdout.fileno() | 372 stdout = sys.stdout.fileno() |
| 264 except AttributeError: | 373 except AttributeError: |
| 265 stdout = None | 374 stdout = None |
| 266 return subprocess2.call( | 375 return subprocess2.call( |
| 267 ['git', | 376 ['git', |
| 268 'diff', '--no-ext-diff', '--stat'] + similarity_options + args, | 377 'diff', '--no-ext-diff', '--stat'] + similarity_options + args, |
| 269 stdout=stdout, env=env) | 378 stdout=stdout, env=env) |
| 270 | 379 |
| 271 | 380 |
| 381 class BuildbucketResponseException(Exception): | |
| 382 pass | |
| 383 | |
| 384 | |
| 272 class Settings(object): | 385 class Settings(object): |
| 273 def __init__(self): | 386 def __init__(self): |
| 274 self.default_server = None | 387 self.default_server = None |
| 275 self.cc = None | 388 self.cc = None |
| 276 self.root = None | 389 self.root = None |
| 277 self.is_git_svn = None | 390 self.is_git_svn = None |
| 278 self.svn_branch = None | 391 self.svn_branch = None |
| 279 self.tree_status_url = None | 392 self.tree_status_url = None |
| 280 self.viewvc_url = None | 393 self.viewvc_url = None |
| 281 self.updated = False | 394 self.updated = False |
| (...skipping 2475 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 2757 group.add_option( | 2870 group.add_option( |
| 2758 "-c", "--clobber", action="store_true", default=False, | 2871 "-c", "--clobber", action="store_true", default=False, |
| 2759 help="Force a clobber before building; e.g. don't do an " | 2872 help="Force a clobber before building; e.g. don't do an " |
| 2760 "incremental build") | 2873 "incremental build") |
| 2761 group.add_option( | 2874 group.add_option( |
| 2762 "--project", | 2875 "--project", |
| 2763 help="Override which project to use. Projects are defined " | 2876 help="Override which project to use. Projects are defined " |
| 2764 "server-side to define what default bot set to use") | 2877 "server-side to define what default bot set to use") |
| 2765 group.add_option( | 2878 group.add_option( |
| 2766 "-n", "--name", help="Try job name; default to current branch name") | 2879 "-n", "--name", help="Try job name; default to current branch name") |
| 2880 group.add_option( | |
| 2881 "--use-buildbucket", action="store_true", default=False, | |
| 2882 help="Use buildbucket to trigger try jobs.") | |
| 2767 parser.add_option_group(group) | 2883 parser.add_option_group(group) |
| 2768 auth.add_auth_options(parser) | 2884 auth.add_auth_options(parser) |
| 2769 options, args = parser.parse_args(args) | 2885 options, args = parser.parse_args(args) |
| 2770 auth_config = auth.extract_auth_config_from_options(options) | 2886 auth_config = auth.extract_auth_config_from_options(options) |
| 2771 | 2887 |
| 2772 if args: | 2888 if args: |
| 2773 parser.error('Unknown arguments: %s' % args) | 2889 parser.error('Unknown arguments: %s' % args) |
| 2774 | 2890 |
| 2775 cl = Changelist(auth_config=auth_config) | 2891 cl = Changelist(auth_config=auth_config) |
| 2776 if not cl.GetIssue(): | 2892 if not cl.GetIssue(): |
| (...skipping 77 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 2854 'Instead send your job to the parent.\n' | 2970 'Instead send your job to the parent.\n' |
| 2855 'Bot list: %s' % builders) | 2971 'Bot list: %s' % builders) |
| 2856 return 1 | 2972 return 1 |
| 2857 | 2973 |
| 2858 patchset = cl.GetMostRecentPatchset() | 2974 patchset = cl.GetMostRecentPatchset() |
| 2859 if patchset and patchset != cl.GetPatchset(): | 2975 if patchset and patchset != cl.GetPatchset(): |
| 2860 print( | 2976 print( |
| 2861 '\nWARNING Mismatch between local config and server. Did a previous ' | 2977 '\nWARNING Mismatch between local config and server. Did a previous ' |
| 2862 'upload fail?\ngit-cl try always uses latest patchset from rietveld. ' | 2978 'upload fail?\ngit-cl try always uses latest patchset from rietveld. ' |
| 2863 'Continuing using\npatchset %s.\n' % patchset) | 2979 'Continuing using\npatchset %s.\n' % patchset) |
| 2864 try: | 2980 if options.use_buildbucket: |
| 2865 cl.RpcServer().trigger_distributed_try_jobs( | 2981 try: |
| 2866 cl.GetIssue(), patchset, options.name, options.clobber, | 2982 trigger_try_jobs( |
| 2867 options.revision, masters) | 2983 auth_config, cl, options, masters, 'git_cl_try') |
| 2868 except urllib2.HTTPError, e: | 2984 except BuildbucketResponseException as ex: |
| 2869 if e.code == 404: | 2985 print 'ERROR: %s' % ex |
| 2870 print('404 from rietveld; ' | |
| 2871 'did you mean to use "git try" instead of "git cl try"?') | |
| 2872 return 1 | 2986 return 1 |
| 2873 print('Tried jobs on:') | 2987 except Exception as e: |
| 2988 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc()) | |
| 2989 print 'ERROR: Exception when trying to trigger tryjobs: %s\n%s' % ( | |
| 2990 e, stacktrace) | |
| 2991 return 1 | |
| 2992 else: | |
| 2993 try: | |
| 2994 cl.RpcServer().trigger_distributed_try_jobs( | |
| 2995 cl.GetIssue(), patchset, options.name, options.clobber, | |
| 2996 options.revision, masters) | |
| 2997 except urllib2.HTTPError, e: | |
| 2998 if e.code == 404: | |
| 2999 print('404 from rietveld; ' | |
| 3000 'did you mean to use "git try" instead of "git cl try"?') | |
| 3001 return 1 | |
| 3002 print('Tried jobs on:') | |
| 2874 | 3003 |
| 2875 for (master, builders) in masters.iteritems(): | 3004 for (master, builders) in masters.iteritems(): |
| 2876 if master: | 3005 if master: |
| 2877 print 'Master: %s' % master | 3006 print 'Master: %s' % master |
| 2878 length = max(len(builder) for builder in builders) | 3007 length = max(len(builder) for builder in builders) |
| 2879 for builder in sorted(builders): | 3008 for builder in sorted(builders): |
| 2880 print ' %*s: %s' % (length, builder, ','.join(builders[builder])) | 3009 print ' %*s: %s' % (length, builder, ','.join(builders[builder])) |
| 2881 return 0 | 3010 return 0 |
| 2882 | 3011 |
| 2883 | 3012 |
| 2884 @subcommand.usage('[new upstream branch]') | 3013 @subcommand.usage('[new upstream branch]') |
| 2885 def CMDupstream(parser, args): | 3014 def CMDupstream(parser, args): |
| 2886 """Prints or sets the name of the upstream branch, if any.""" | 3015 """Prints or sets the name of the upstream branch, if any.""" |
| 2887 _, args = parser.parse_args(args) | 3016 _, args = parser.parse_args(args) |
| 2888 if len(args) > 1: | 3017 if len(args) > 1: |
| 2889 parser.error('Unrecognized args: %s' % ' '.join(args)) | 3018 parser.error('Unrecognized args: %s' % ' '.join(args)) |
| 2890 | 3019 |
| (...skipping 314 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 3205 if __name__ == '__main__': | 3334 if __name__ == '__main__': |
| 3206 # These affect sys.stdout so do it outside of main() to simplify mocks in | 3335 # These affect sys.stdout so do it outside of main() to simplify mocks in |
| 3207 # unit testing. | 3336 # unit testing. |
| 3208 fix_encoding.fix_encoding() | 3337 fix_encoding.fix_encoding() |
| 3209 colorama.init() | 3338 colorama.init() |
| 3210 try: | 3339 try: |
| 3211 sys.exit(main(sys.argv[1:])) | 3340 sys.exit(main(sys.argv[1:])) |
| 3212 except KeyboardInterrupt: | 3341 except KeyboardInterrupt: |
| 3213 sys.stderr.write('interrupted\n') | 3342 sys.stderr.write('interrupted\n') |
| 3214 sys.exit(1) | 3343 sys.exit(1) |
| OLD | NEW |