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 threading | |
26 import time | |
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 | 37 |
35 from third_party import colorama | 38 from third_party import colorama |
39 from third_party import httplib2 | |
36 from third_party import upload | 40 from third_party import upload |
41 from third_party.google_api_python_client import apiclient | |
Vadim Sh.
2015/04/10 02:24:29
it is not vendored properly:
Traceback (most rece
nodir
2015/04/13 16:04:53
"import apiclient" should work, we use it in other
sheyang
2015/04/14 01:30:22
Per discussion with Nodir@ I added the directory t
| |
37 import breakpad # pylint: disable=W0611 | 42 import breakpad # pylint: disable=W0611 |
38 import clang_format | 43 import clang_format |
39 import dart_format | 44 import dart_format |
40 import fix_encoding | 45 import fix_encoding |
41 import gclient_utils | 46 import gclient_utils |
42 import git_common | 47 import git_common |
48 import oauth2 | |
Vadim Sh.
2015/04/10 02:24:29
this file has been replaced by auth.py in https://
sheyang
2015/04/14 01:30:22
Done.
| |
43 import owners | 49 import owners |
44 import owners_finder | 50 import owners_finder |
45 import presubmit_support | 51 import presubmit_support |
46 import rietveld | 52 import rietveld |
47 import scm | 53 import scm |
48 import subcommand | 54 import subcommand |
49 import subprocess2 | 55 import subprocess2 |
50 import watchlists | 56 import watchlists |
51 | 57 |
52 __version__ = '1.0' | 58 __version__ = '1.0' |
53 | 59 |
54 DEFAULT_SERVER = 'https://codereview.appspot.com' | 60 DEFAULT_SERVER = 'https://codereview.appspot.com' |
55 POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s' | 61 POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s' |
56 DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup' | 62 DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup' |
57 GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingGit' | 63 GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingGit' |
58 CHANGE_ID = 'Change-Id:' | 64 CHANGE_ID = 'Change-Id:' |
59 REFS_THAT_ALIAS_TO_OTHER_REFS = { | 65 REFS_THAT_ALIAS_TO_OTHER_REFS = { |
60 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master', | 66 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master', |
61 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master', | 67 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master', |
62 } | 68 } |
63 | 69 |
70 # Buildbucket-related constants | |
71 DISCOVERY_URL = ( | |
72 'https://cr-buildbucket.appspot.com/_ah/api/discovery/v1/apis/' | |
73 '{api}/{apiVersion}/rest') | |
74 DEFAULT_SCOPES = 'email' | |
Vadim Sh.
2015/04/10 02:24:29
not used
sheyang
2015/04/14 01:30:22
Removed.
| |
75 BUILDSET_STR = 'patch/rietveld/{hostname}/{issue}/{patch}' | |
76 | |
64 # Valid extensions for files we want to lint. | 77 # Valid extensions for files we want to lint. |
65 DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)" | 78 DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)" |
66 DEFAULT_LINT_IGNORE_REGEX = r"$^" | 79 DEFAULT_LINT_IGNORE_REGEX = r"$^" |
67 | 80 |
68 # Shortcut since it quickly becomes redundant. | 81 # Shortcut since it quickly becomes redundant. |
69 Fore = colorama.Fore | 82 Fore = colorama.Fore |
70 | 83 |
71 # Initialized in main() | 84 # Initialized in main() |
72 settings = None | 85 settings = None |
73 | 86 |
(...skipping 134 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
208 if dirty: | 221 if dirty: |
209 print 'Cannot %s with a dirty tree. You must commit locally first.' % cmd | 222 print 'Cannot %s with a dirty tree. You must commit locally first.' % cmd |
210 print 'Uncommitted files: (git diff-index --name-status HEAD)' | 223 print 'Uncommitted files: (git diff-index --name-status HEAD)' |
211 print dirty[:4096] | 224 print dirty[:4096] |
212 if len(dirty) > 4096: | 225 if len(dirty) > 4096: |
213 print '... (run "git diff-index --name-status HEAD" to see full output).' | 226 print '... (run "git diff-index --name-status HEAD" to see full output).' |
214 return True | 227 return True |
215 return False | 228 return False |
216 | 229 |
217 | 230 |
231 def _prefix_master(master): | |
232 prefix = 'master.' | |
233 if master.startswith(prefix): | |
234 return master | |
235 else: | |
236 return '%s%s' % (prefix, master) | |
237 | |
238 | |
239 def _get_buildbucket(credentials): | |
Vadim Sh.
2015/04/10 02:24:29
with auth.py it would be:
def _get_buildbucket(ri
sheyang
2015/04/14 01:30:22
Done.
| |
240 http = httplib2.Http() | |
241 http = credentials.authorize(http) | |
242 return apiclient.discovery.build( | |
243 'buildbucket', 'v1', | |
244 http=http, | |
245 discoveryServiceUrl=DISCOVERY_URL, | |
246 ) | |
247 | |
248 | |
249 def trigger_distributed_try_jobs( | |
250 credentials, changelist, options, masters, category): | |
251 buildbucket = _get_buildbucket(credentials) | |
252 creds_props = json.loads(credentials.to_json()) | |
253 requester = creds_props['id_token']['email'] | |
Vadim Sh.
2015/04/10 02:24:29
this is currently not exposed via auth.py, though
sheyang
2015/04/14 01:30:22
This field will be populated from oauth2 credentia
| |
254 issue_props = changelist.GetIssueProperties() | |
255 rietveld_host = urlparse.urlparse(changelist.GetRietveldServer()).hostname | |
256 issue = changelist.GetIssue() | |
257 patchset = changelist.GetMostRecentPatchset() | |
258 buildset = BUILDSET_STR.format( | |
259 hostname=rietveld_host, | |
260 issue=str(issue), | |
261 patch=str(patchset)) | |
262 print 'Tried jobs on:' | |
263 for (master, builders_and_tests) in masters.iteritems(): | |
264 print 'Master: %s' % master | |
265 bucket = _prefix_master(master) | |
266 for builder, tests in builders_and_tests.iteritems(): | |
267 req = buildbucket.put(body={ | |
268 'bucket': bucket, | |
269 'parameters_json': json.dumps({ | |
270 'builder_name': builder, | |
271 'changes':[ | |
272 {'author': {'email': issue_props['owner_email']}}, | |
273 ], | |
274 'properties': { | |
275 'category': category, | |
276 'clobber': options.clobber, | |
277 'issue': issue, | |
278 'master': master, | |
279 'patch_project': issue_props['project'], | |
280 'patch_storage': 'rietveld', | |
281 'patchset': patchset, | |
282 'reason': options.name, | |
283 'requester': requester, | |
Vadim Sh.
2015/04/10 02:24:29
the server should derive it from oauth credentials
nodir
2015/04/10 02:31:29
buidbucket does not read this property. It is inte
nodir
2015/04/13 16:04:53
This part was wrong because CQ sets requester to "
sheyang
2015/04/14 01:30:22
Acknowledged.
| |
284 'revision': options.revision, | |
285 'rietveld': changelist.GetRietveldServer(), | |
286 'testfilter': tests, | |
287 }, | |
288 }), | |
289 'tags': ['buildset:%s' % buildset, | |
290 'master:%s' % master, | |
291 'builder:%s' % builder, | |
292 'requester:%s' % requester] | |
nodir
2015/04/13 16:04:53
requester tag is not the user, but a tool. Maybe w
sheyang
2015/04/14 01:30:22
Use 'user_agent' instead.
| |
293 }) | |
294 wait = 1 | |
295 try_count = 3 | |
296 while try_count > 0: | |
297 try: | |
298 try_count -= 1 | |
299 response = req.execute() | |
300 if response.get('error'): | |
301 msg = 'Error in response. Reason: %s. Message: %s.' % ( | |
302 response['error'].get('reason', ''), | |
303 response['error'].get('message', '')) | |
304 raise BuildbucketResponseException(msg) | |
305 break | |
306 except apiclient.errors.HttpError as ex: | |
307 status = ex.resp.status if ex.resp else None | |
308 if status >= 500: | |
309 logging.debug('Transient errors when triggering tryjobs. ' | |
310 'Will retry in %d seconds.', wait) | |
311 time.sleep(wait) | |
312 wait *= 2 | |
313 if try_count <= 0: | |
314 raise | |
315 else: | |
316 raise | |
317 print ' %s: %s' % (builder, tests) | |
318 | |
319 | |
218 def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards): | 320 def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards): |
219 """Return the corresponding git ref if |base_url| together with |glob_spec| | 321 """Return the corresponding git ref if |base_url| together with |glob_spec| |
220 matches the full |url|. | 322 matches the full |url|. |
221 | 323 |
222 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below). | 324 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below). |
223 """ | 325 """ |
224 fetch_suburl, as_ref = glob_spec.split(':') | 326 fetch_suburl, as_ref = glob_spec.split(':') |
225 if allow_wildcards: | 327 if allow_wildcards: |
226 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl) | 328 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl) |
227 if glob_match: | 329 if glob_match: |
(...skipping 47 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
275 try: | 377 try: |
276 stdout = sys.stdout.fileno() | 378 stdout = sys.stdout.fileno() |
277 except AttributeError: | 379 except AttributeError: |
278 stdout = None | 380 stdout = None |
279 return subprocess2.call( | 381 return subprocess2.call( |
280 ['git', | 382 ['git', |
281 'diff', '--no-ext-diff', '--stat'] + similarity_options + args, | 383 'diff', '--no-ext-diff', '--stat'] + similarity_options + args, |
282 stdout=stdout, env=env) | 384 stdout=stdout, env=env) |
283 | 385 |
284 | 386 |
387 class BuildbucketResponseException(Exception): | |
388 pass | |
389 | |
390 | |
285 class Settings(object): | 391 class Settings(object): |
286 def __init__(self): | 392 def __init__(self): |
287 self.default_server = None | 393 self.default_server = None |
288 self.cc = None | 394 self.cc = None |
289 self.root = None | 395 self.root = None |
290 self.is_git_svn = None | 396 self.is_git_svn = None |
291 self.svn_branch = None | 397 self.svn_branch = None |
292 self.tree_status_url = None | 398 self.tree_status_url = None |
293 self.viewvc_url = None | 399 self.viewvc_url = None |
294 self.updated = False | 400 self.updated = False |
(...skipping 2434 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
2729 "-c", "--clobber", action="store_true", default=False, | 2835 "-c", "--clobber", action="store_true", default=False, |
2730 help="Force a clobber before building; e.g. don't do an " | 2836 help="Force a clobber before building; e.g. don't do an " |
2731 "incremental build") | 2837 "incremental build") |
2732 group.add_option( | 2838 group.add_option( |
2733 "--project", | 2839 "--project", |
2734 help="Override which project to use. Projects are defined " | 2840 help="Override which project to use. Projects are defined " |
2735 "server-side to define what default bot set to use") | 2841 "server-side to define what default bot set to use") |
2736 group.add_option( | 2842 group.add_option( |
2737 "-n", "--name", help="Try job name; default to current branch name") | 2843 "-n", "--name", help="Try job name; default to current branch name") |
2738 parser.add_option_group(group) | 2844 parser.add_option_group(group) |
2845 oauth2.add_oauth2_options(parser) | |
2739 options, args = parser.parse_args(args) | 2846 options, args = parser.parse_args(args) |
2740 | 2847 |
2741 if args: | 2848 if args: |
2742 parser.error('Unknown arguments: %s' % args) | 2849 parser.error('Unknown arguments: %s' % args) |
2743 | 2850 |
2744 cl = Changelist() | 2851 cl = Changelist() |
2745 if not cl.GetIssue(): | 2852 if not cl.GetIssue(): |
2746 parser.error('Need to upload first') | 2853 parser.error('Need to upload first') |
2747 | 2854 |
2748 props = cl.GetIssueProperties() | 2855 props = cl.GetIssueProperties() |
(...skipping 75 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
2824 'Bot list: %s' % builders) | 2931 'Bot list: %s' % builders) |
2825 return 1 | 2932 return 1 |
2826 | 2933 |
2827 patchset = cl.GetMostRecentPatchset() | 2934 patchset = cl.GetMostRecentPatchset() |
2828 if patchset and patchset != cl.GetPatchset(): | 2935 if patchset and patchset != cl.GetPatchset(): |
2829 print( | 2936 print( |
2830 '\nWARNING Mismatch between local config and server. Did a previous ' | 2937 '\nWARNING Mismatch between local config and server. Did a previous ' |
2831 'upload fail?\ngit-cl try always uses latest patchset from rietveld. ' | 2938 'upload fail?\ngit-cl try always uses latest patchset from rietveld. ' |
2832 'Continuing using\npatchset %s.\n' % patchset) | 2939 'Continuing using\npatchset %s.\n' % patchset) |
2833 try: | 2940 try: |
2834 cl.RpcServer().trigger_distributed_try_jobs( | 2941 creds = oauth2.get_oauth2_creds( |
2835 cl.GetIssue(), patchset, options.name, options.clobber, | 2942 options, |
2836 options.revision, masters) | 2943 urlparse.urlparse(cl.GetRietveldServer()).hostname) |
2837 except urllib2.HTTPError, e: | 2944 if not creds: |
2838 if e.code == 404: | 2945 print 'Failed to fetch credentials. Aborting...' |
2839 print('404 from rietveld; ' | |
2840 'did you mean to use "git try" instead of "git cl try"?') | |
2841 return 1 | 2946 return 1 |
2842 print('Tried jobs on:') | 2947 trigger_distributed_try_jobs(creds, cl, options, masters, 'git cl try') |
2843 | 2948 except apiclient.errors.HttpError as ex: |
2844 for (master, builders) in masters.iteritems(): | 2949 status = ex.resp.status if ex.resp else None |
2845 if master: | 2950 if status == httplib.FORBIDDEN: |
2846 print 'Master: %s' % master | 2951 print 'ERROR: Access denied. Please verify you have tryjob access.' |
2847 length = max(len(builder) for builder in builders) | 2952 else: |
2848 for builder in sorted(builders): | 2953 print 'ERROR: Tryjob request failed: %s.' % ex |
2849 print ' %*s: %s' % (length, builder, ','.join(builders[builder])) | 2954 return 1 |
2955 except BuildbucketResponseException as ex: | |
2956 print ex | |
2957 return 1 | |
2958 except Exception as e: | |
2959 print 'Unexpected error when trying to trigger tryjobs: %s.' % e | |
2960 return 1 | |
2850 return 0 | 2961 return 0 |
2851 | 2962 |
2852 | 2963 |
2853 @subcommand.usage('[new upstream branch]') | 2964 @subcommand.usage('[new upstream branch]') |
2854 def CMDupstream(parser, args): | 2965 def CMDupstream(parser, args): |
2855 """Prints or sets the name of the upstream branch, if any.""" | 2966 """Prints or sets the name of the upstream branch, if any.""" |
2856 _, args = parser.parse_args(args) | 2967 _, args = parser.parse_args(args) |
2857 if len(args) > 1: | 2968 if len(args) > 1: |
2858 parser.error('Unrecognized args: %s' % ' '.join(args)) | 2969 parser.error('Unrecognized args: %s' % ' '.join(args)) |
2859 | 2970 |
(...skipping 301 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
3161 if __name__ == '__main__': | 3272 if __name__ == '__main__': |
3162 # These affect sys.stdout so do it outside of main() to simplify mocks in | 3273 # These affect sys.stdout so do it outside of main() to simplify mocks in |
3163 # unit testing. | 3274 # unit testing. |
3164 fix_encoding.fix_encoding() | 3275 fix_encoding.fix_encoding() |
3165 colorama.init() | 3276 colorama.init() |
3166 try: | 3277 try: |
3167 sys.exit(main(sys.argv[1:])) | 3278 sys.exit(main(sys.argv[1:])) |
3168 except KeyboardInterrupt: | 3279 except KeyboardInterrupt: |
3169 sys.stderr.write('interrupted\n') | 3280 sys.stderr.write('interrupted\n') |
3170 sys.exit(1) | 3281 sys.exit(1) |
OLD | NEW |