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 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 321 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
332 if response.status < 500 or try_count >= 2: | 332 if response.status < 500 or try_count >= 2: |
333 raise httplib2.HttpLib2Error(content) | 333 raise httplib2.HttpLib2Error(content) |
334 | 334 |
335 # status >= 500 means transient failures. | 335 # status >= 500 means transient failures. |
336 logging.debug('Transient errors when %s. Will retry.', operation_name) | 336 logging.debug('Transient errors when %s. Will retry.', operation_name) |
337 time_sleep(0.5 + 1.5*try_count) | 337 time_sleep(0.5 + 1.5*try_count) |
338 try_count += 1 | 338 try_count += 1 |
339 assert False, 'unreachable' | 339 assert False, 'unreachable' |
340 | 340 |
341 | 341 |
342 def _get_bucket_map(changelist, options, option_parser): | |
343 """Returns a dict mapping bucket names (or master names) to | |
344 builders and tests, for triggering try jobs. | |
345 """ | |
346 if not options.bot: | |
347 change = changelist.GetChange( | |
348 changelist.GetCommonAncestorWithUpstream(), None) | |
349 | |
350 # Get try masters from PRESUBMIT.py files. | |
351 masters = presubmit_support.DoGetTryMasters( | |
352 change=change, | |
353 changed_files=change.LocalPaths(), | |
354 repository_root=settings.GetRoot(), | |
355 default_presubmit=None, | |
356 project=None, | |
357 verbose=options.verbose, | |
358 output_stream=sys.stdout) | |
359 | |
360 if masters: | |
361 return masters | |
362 | |
363 # Fall back to deprecated method: get try slaves from PRESUBMIT.py | |
tandrii(chromium)
2016/10/23 16:55:55
we should kill this, it has lived for too long, bu
qyearsley
2016/10/24 19:09:52
Sounds good!
| |
364 # files. | |
365 options.bot = presubmit_support.DoGetTrySlaves( | |
366 change=change, | |
367 changed_files=change.LocalPaths(), | |
368 repository_root=settings.GetRoot(), | |
369 default_presubmit=None, | |
370 project=None, | |
371 verbose=options.verbose, | |
372 output_stream=sys.stdout) | |
373 | |
374 if not options.bot: | |
375 return {} | |
376 | |
377 if options.bucket: | |
378 return {options.bucket: {b: [] for b in options.bot}} | |
379 | |
380 builders_and_tests = {} | |
381 | |
382 # TODO(machenbach): The old style command-line options don't support | |
383 # multiple try masters yet. | |
384 old_style = filter(lambda x: isinstance(x, basestring), options.bot) | |
385 new_style = filter(lambda x: isinstance(x, tuple), options.bot) | |
386 | |
387 for bot in old_style: | |
388 if ':' in bot: | |
389 option_parser.error('Specifying testfilter is no longer supported') | |
390 elif ',' in bot: | |
391 option_parser.error('Specify one bot per --bot flag') | |
392 else: | |
393 builders_and_tests.setdefault(bot, []) | |
394 | |
395 for bot, tests in new_style: | |
396 builders_and_tests.setdefault(bot, []).extend(tests) | |
397 | |
398 if not options.master: | |
399 # TODO(crbug.com/640740): git cl try should be able to trigger | |
tandrii(chromium)
2016/10/23 16:55:55
lol :) I think styleguide mandates TODO(qyearsley)
qyearsley
2016/10/24 19:09:51
Ah, makes sense; done.
| |
400 # builders on multiple masters if no master is given. | |
401 options.master, error_message = _get_builder_master(options.bot) | |
402 if error_message: | |
403 option_parser.error( | |
404 'Tryserver master cannot be found because: %s\n' | |
405 'Please manually specify the tryserver master, e.g. ' | |
406 '"-m tryserver.chromium.linux".' % error_message) | |
qyearsley
2016/10/22 19:14:31
Note: This block is moved from lines 4810-4815 and
| |
407 | |
408 # Return a master map with one master to be backwards compatible. The | |
409 # master name defaults to an empty string, which will cause the master | |
410 # not to be set on rietveld (deprecated). | |
411 bucket = '' | |
412 if options.master: | |
413 # Add the "master." prefix to the master name to obtain the bucket name. | |
414 bucket = _prefix_master(options.master) | |
415 return {bucket: builders_and_tests} | |
416 | |
417 | |
418 def _get_builder_master(bot_list): | |
419 """Fetches a master for the given list of builders. | |
420 | |
421 Returns a pair (master, error_message), where either master or | |
422 error_message is None. | |
423 """ | |
424 map_url = 'https://builders-map.appspot.com/' | |
425 try: | |
426 master_map = json.load(urllib2.urlopen(map_url)) | |
427 except urllib2.URLError as e: | |
428 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' % | |
429 (map_url, e)) | |
430 except ValueError as e: | |
431 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e)) | |
432 if not master_map: | |
433 return None, 'Failed to build master map.' | |
434 | |
435 result_master = '' | |
436 for bot in bot_list: | |
437 builder = bot.split(':', 1)[0] | |
438 master_list = master_map.get(builder, []) | |
439 if not master_list: | |
440 return None, ('No matching master for builder %s.' % builder) | |
441 elif len(master_list) > 1: | |
442 return None, ('The builder name %s exists in multiple masters %s.' % | |
443 (builder, master_list)) | |
444 else: | |
445 cur_master = master_list[0] | |
446 if not result_master: | |
447 result_master = cur_master | |
448 elif result_master != cur_master: | |
449 return None, 'The builders do not belong to the same master.' | |
450 return result_master, None | |
qyearsley
2016/10/22 19:14:31
This function is moved from below and renamed; in
tandrii(chromium)
2016/10/23 16:55:55
SGTM. +1 for "_" name.
| |
451 | |
452 | |
342 def _trigger_try_jobs(auth_config, changelist, buckets, options, | 453 def _trigger_try_jobs(auth_config, changelist, buckets, options, |
343 category='git_cl_try', patchset=None): | 454 category='git_cl_try', patchset=None): |
455 """Sends a request to Buildbucket to trigger try jobs for a changelist. | |
456 | |
457 Args: | |
458 auth_config: AuthConfig for Rietveld. | |
459 changelist: Changelist that the try jobs are associated with. | |
460 buckets: A nested dict mapping bucket names to builders to tests. | |
461 options: Command-line options. | |
462 """ | |
344 assert changelist.GetIssue(), 'CL must be uploaded first' | 463 assert changelist.GetIssue(), 'CL must be uploaded first' |
345 codereview_url = changelist.GetCodereviewServer() | 464 codereview_url = changelist.GetCodereviewServer() |
346 assert codereview_url, 'CL must be uploaded first' | 465 assert codereview_url, 'CL must be uploaded first' |
347 patchset = patchset or changelist.GetMostRecentPatchset() | 466 patchset = patchset or changelist.GetMostRecentPatchset() |
348 assert patchset, 'CL must be uploaded first' | 467 assert patchset, 'CL must be uploaded first' |
349 | 468 |
350 codereview_host = urlparse.urlparse(codereview_url).hostname | 469 codereview_host = urlparse.urlparse(codereview_url).hostname |
351 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config) | 470 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config) |
352 http = authenticator.authorize(httplib2.Http()) | 471 http = authenticator.authorize(httplib2.Http()) |
353 http.force_exception_to_status_code = True | 472 http.force_exception_to_status_code = True |
(...skipping 73 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
427 buildbucket_put_url, | 546 buildbucket_put_url, |
428 'PUT', | 547 'PUT', |
429 body=json.dumps(batch_req_body), | 548 body=json.dumps(batch_req_body), |
430 headers={'Content-Type': 'application/json'} | 549 headers={'Content-Type': 'application/json'} |
431 ) | 550 ) |
432 print_text.append('To see results here, run: git cl try-results') | 551 print_text.append('To see results here, run: git cl try-results') |
433 print_text.append('To see results in browser, run: git cl web') | 552 print_text.append('To see results in browser, run: git cl web') |
434 print('\n'.join(print_text)) | 553 print('\n'.join(print_text)) |
435 | 554 |
436 | 555 |
556 def _trigger_dry_run(changelist): | |
557 """Triggers a dry run and prints a warning on failure.""" | |
558 try: | |
559 changelist.SetCQState(_CQState.DRY_RUN) | |
560 print('scheduled CQ Dry Run on %s' % changelist.GetIssueURL()) | |
561 return 0 | |
562 except KeyboardInterrupt: | |
563 raise | |
564 except: | |
565 print('WARNING: failed to trigger CQ Dry Run.\n' | |
566 'Either:\n' | |
567 ' * your project has no CQ\n' | |
568 ' * you don\'t have permission to trigger Dry Run\n' | |
569 ' * bug in this code (see stack trace below).\n' | |
570 'Consider specifying which bots to trigger manually ' | |
571 'or asking your project owners for permissions ' | |
572 'or contacting Chrome Infrastructure team at ' | |
573 'https://www.chromium.org/infra\n\n') | |
574 # Still raise exception so that stack trace is printed. | |
575 raise | |
qyearsley
2016/10/22 19:14:31
This block is extracted from below; potentially, i
tandrii(chromium)
2016/10/23 16:55:55
SGTM, how about making it method of ChangeList its
qyearsley
2016/10/24 19:09:51
Done, and added TODO note to make use of this func
| |
576 | |
577 | |
437 def fetch_try_jobs(auth_config, changelist, buildbucket_host, | 578 def fetch_try_jobs(auth_config, changelist, buildbucket_host, |
438 patchset=None): | 579 patchset=None): |
439 """Fetches try jobs from buildbucket. | 580 """Fetches try jobs from buildbucket. |
440 | 581 |
441 Returns a map from build id to build info as a dictionary. | 582 Returns a map from build id to build info as a dictionary. |
442 """ | 583 """ |
443 assert buildbucket_host | 584 assert buildbucket_host |
444 assert changelist.GetIssue(), 'CL must be uploaded first' | 585 assert changelist.GetIssue(), 'CL must be uploaded first' |
445 assert changelist.GetCodereviewServer(), 'CL must be uploaded first' | 586 assert changelist.GetCodereviewServer(), 'CL must be uploaded first' |
446 patchset = patchset or changelist.GetMostRecentPatchset() | 587 patchset = patchset or changelist.GetMostRecentPatchset() |
(...skipping 4229 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
4676 """Fetches the tree status from a json url and returns the message | 4817 """Fetches the tree status from a json url and returns the message |
4677 with the reason for the tree to be opened or closed.""" | 4818 with the reason for the tree to be opened or closed.""" |
4678 url = settings.GetTreeStatusUrl() | 4819 url = settings.GetTreeStatusUrl() |
4679 json_url = urlparse.urljoin(url, '/current?format=json') | 4820 json_url = urlparse.urljoin(url, '/current?format=json') |
4680 connection = urllib2.urlopen(json_url) | 4821 connection = urllib2.urlopen(json_url) |
4681 status = json.loads(connection.read()) | 4822 status = json.loads(connection.read()) |
4682 connection.close() | 4823 connection.close() |
4683 return status['message'] | 4824 return status['message'] |
4684 | 4825 |
4685 | 4826 |
4686 def GetBuilderMaster(bot_list): | |
4687 """For a given builder, fetch the master from AE if available.""" | |
4688 map_url = 'https://builders-map.appspot.com/' | |
4689 try: | |
4690 master_map = json.load(urllib2.urlopen(map_url)) | |
4691 except urllib2.URLError as e: | |
4692 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' % | |
4693 (map_url, e)) | |
4694 except ValueError as e: | |
4695 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e)) | |
4696 if not master_map: | |
4697 return None, 'Failed to build master map.' | |
4698 | |
4699 result_master = '' | |
4700 for bot in bot_list: | |
4701 builder = bot.split(':', 1)[0] | |
4702 master_list = master_map.get(builder, []) | |
4703 if not master_list: | |
4704 return None, ('No matching master for builder %s.' % builder) | |
4705 elif len(master_list) > 1: | |
4706 return None, ('The builder name %s exists in multiple masters %s.' % | |
4707 (builder, master_list)) | |
4708 else: | |
4709 cur_master = master_list[0] | |
4710 if not result_master: | |
4711 result_master = cur_master | |
4712 elif result_master != cur_master: | |
4713 return None, 'The builders do not belong to the same master.' | |
4714 return result_master, None | |
4715 | |
4716 | |
4717 def CMDtree(parser, args): | 4827 def CMDtree(parser, args): |
4718 """Shows the status of the tree.""" | 4828 """Shows the status of the tree.""" |
4719 _, args = parser.parse_args(args) | 4829 _, args = parser.parse_args(args) |
4720 status = GetTreeStatus() | 4830 status = GetTreeStatus() |
4721 if 'unset' == status: | 4831 if 'unset' == status: |
4722 print('You must configure your tree status URL by running "git cl config".') | 4832 print('You must configure your tree status URL by running "git cl config".') |
4723 return 2 | 4833 return 2 |
4724 | 4834 |
4725 print('The tree is %s' % status) | 4835 print('The tree is %s' % status) |
4726 print() | 4836 print() |
4727 print(GetTreeStatusReason()) | 4837 print(GetTreeStatusReason()) |
4728 if status != 'open': | 4838 if status != 'open': |
4729 return 1 | 4839 return 1 |
4730 return 0 | 4840 return 0 |
4731 | 4841 |
4732 | 4842 |
4733 def CMDtry(parser, args): | 4843 def CMDtry(parser, args): |
4734 """Triggers try jobs using CQ dry run or BuildBucket for individual builders. | 4844 """Triggers try jobs using either BuildBucket or CQ dry run.""" |
4735 """ | |
4736 group = optparse.OptionGroup(parser, 'Try job options') | 4845 group = optparse.OptionGroup(parser, 'Try job options') |
4737 group.add_option( | 4846 group.add_option( |
4738 '-b', '--bot', action='append', | 4847 '-b', '--bot', action='append', |
4739 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple ' | 4848 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple ' |
4740 'times to specify multiple builders. ex: ' | 4849 'times to specify multiple builders. ex: ' |
4741 '"-b win_rel -b win_layout". See ' | 4850 '"-b win_rel -b win_layout". See ' |
4742 'the try server waterfall for the builders name and the tests ' | 4851 'the try server waterfall for the builders name and the tests ' |
4743 'available.')) | 4852 'available.')) |
4744 group.add_option( | 4853 group.add_option( |
4745 '-B', '--bucket', default='', | 4854 '-B', '--bucket', default='', |
(...skipping 54 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
4800 error_message = cl.CannotTriggerTryJobReason() | 4909 error_message = cl.CannotTriggerTryJobReason() |
4801 if error_message: | 4910 if error_message: |
4802 parser.error('Can\'t trigger try jobs: %s') | 4911 parser.error('Can\'t trigger try jobs: %s') |
4803 | 4912 |
4804 if not options.name: | 4913 if not options.name: |
4805 options.name = cl.GetBranch() | 4914 options.name = cl.GetBranch() |
4806 | 4915 |
4807 if options.bucket and options.master: | 4916 if options.bucket and options.master: |
4808 parser.error('Only one of --bucket and --master may be used.') | 4917 parser.error('Only one of --bucket and --master may be used.') |
4809 | 4918 |
4810 if options.bot and not options.master and not options.bucket: | 4919 buckets = _get_bucket_map(cl, options, parser) |
4811 options.master, err_msg = GetBuilderMaster(options.bot) | |
4812 if err_msg: | |
4813 parser.error('Tryserver master cannot be found because: %s\n' | |
4814 'Please manually specify the tryserver master' | |
4815 ', e.g. "-m tryserver.chromium.linux".' % err_msg) | |
4816 | 4920 |
4817 def GetMasterMap(): | 4921 if not buckets: |
4818 # Process --bot. | 4922 # Default to triggering Dry Run (see http://crbug.com/625697). |
4819 if not options.bot: | 4923 if options.verbose: |
4820 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None) | 4924 print('git cl try with no bots now defaults to CQ Dry Run.') |
4821 | 4925 return _trigger_dry_run(cl) |
4822 # Get try masters from PRESUBMIT.py files. | |
4823 masters = presubmit_support.DoGetTryMasters( | |
4824 change, | |
4825 change.LocalPaths(), | |
4826 settings.GetRoot(), | |
4827 None, | |
4828 None, | |
4829 options.verbose, | |
4830 sys.stdout) | |
4831 if masters: | |
4832 return masters | |
4833 | |
4834 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files. | |
4835 options.bot = presubmit_support.DoGetTrySlaves( | |
4836 change, | |
4837 change.LocalPaths(), | |
4838 settings.GetRoot(), | |
4839 None, | |
4840 None, | |
4841 options.verbose, | |
4842 sys.stdout) | |
4843 | |
4844 if not options.bot: | |
4845 return {} | |
4846 | |
4847 builders_and_tests = {} | |
4848 # TODO(machenbach): The old style command-line options don't support | |
4849 # multiple try masters yet. | |
4850 old_style = filter(lambda x: isinstance(x, basestring), options.bot) | |
4851 new_style = filter(lambda x: isinstance(x, tuple), options.bot) | |
4852 | |
4853 for bot in old_style: | |
4854 if ':' in bot: | |
4855 parser.error('Specifying testfilter is no longer supported') | |
4856 elif ',' in bot: | |
4857 parser.error('Specify one bot per --bot flag') | |
4858 else: | |
4859 builders_and_tests.setdefault(bot, []) | |
4860 | |
4861 for bot, tests in new_style: | |
4862 builders_and_tests.setdefault(bot, []).extend(tests) | |
4863 | |
4864 # Return a master map with one master to be backwards compatible. The | |
4865 # master name defaults to an empty string, which will cause the master | |
4866 # not to be set on rietveld (deprecated). | |
4867 bucket = '' | |
4868 if options.master: | |
4869 # Add the "master." prefix to the master name to obtain the bucket name. | |
4870 bucket = _prefix_master(options.master) | |
4871 return {bucket: builders_and_tests} | |
4872 | |
4873 if options.bucket: | |
4874 buckets = {options.bucket: {b: [] for b in options.bot}} | |
4875 else: | |
4876 buckets = GetMasterMap() | |
4877 if not buckets: | |
4878 # Default to triggering Dry Run (see http://crbug.com/625697). | |
4879 if options.verbose: | |
4880 print('git cl try with no bots now defaults to CQ Dry Run.') | |
4881 try: | |
4882 cl.SetCQState(_CQState.DRY_RUN) | |
4883 print('scheduled CQ Dry Run on %s' % cl.GetIssueURL()) | |
4884 return 0 | |
4885 except KeyboardInterrupt: | |
4886 raise | |
4887 except: | |
4888 print('WARNING: failed to trigger CQ Dry Run.\n' | |
4889 'Either:\n' | |
4890 ' * your project has no CQ\n' | |
4891 ' * you don\'t have permission to trigger Dry Run\n' | |
4892 ' * bug in this code (see stack trace below).\n' | |
4893 'Consider specifying which bots to trigger manually ' | |
4894 'or asking your project owners for permissions ' | |
4895 'or contacting Chrome Infrastructure team at ' | |
4896 'https://www.chromium.org/infra\n\n') | |
4897 # Still raise exception so that stack trace is printed. | |
4898 raise | |
4899 | 4926 |
4900 for builders in buckets.itervalues(): | 4927 for builders in buckets.itervalues(): |
4901 if any('triggered' in b for b in builders): | 4928 if any('triggered' in b for b in builders): |
4902 print('ERROR You are trying to send a job to a triggered bot. This type ' | 4929 print('ERROR You are trying to send a job to a triggered bot. This type ' |
4903 'of bot requires an initial job from a parent (usually a builder). ' | 4930 'of bot requires an initial job from a parent (usually a builder). ' |
4904 'Instead send your job to the parent.\n' | 4931 'Instead send your job to the parent.\n' |
4905 'Bot list: %s' % builders, file=sys.stderr) | 4932 'Bot list: %s' % builders, file=sys.stderr) |
4906 return 1 | 4933 return 1 |
4907 | 4934 |
4908 patchset = cl.GetMostRecentPatchset() | 4935 patchset = cl.GetMostRecentPatchset() |
4909 if patchset != cl.GetPatchset(): | 4936 if patchset != cl.GetPatchset(): |
4910 print('Warning: Codereview server has newer patchsets (%s) than most ' | 4937 print('Warning: Codereview server has newer patchsets (%s) than most ' |
4911 'recent upload from local checkout (%s). Did a previous upload ' | 4938 'recent upload from local checkout (%s). Did a previous upload ' |
4912 'fail?\n' | 4939 'fail?\n' |
4913 'By default, git cl try uses the latest patchset from ' | 4940 'By default, git cl try uses the latest patchset from ' |
4914 'codereview, continuing to use patchset %s.\n' % | 4941 'codereview, continuing to use patchset %s.\n' % |
4915 (patchset, cl.GetPatchset(), patchset)) | 4942 (patchset, cl.GetPatchset(), patchset)) |
4943 | |
4916 try: | 4944 try: |
4917 _trigger_try_jobs(auth_config, cl, buckets, options, 'git_cl_try', | 4945 _trigger_try_jobs(auth_config, cl, buckets, options, 'git_cl_try', |
4918 patchset) | 4946 patchset) |
4919 except BuildbucketResponseException as ex: | 4947 except BuildbucketResponseException as ex: |
4920 print('ERROR: %s' % ex) | 4948 print('ERROR: %s' % ex) |
4921 return 1 | 4949 return 1 |
4922 return 0 | 4950 return 0 |
4923 | 4951 |
4924 | 4952 |
4925 def CMDtry_results(parser, args): | 4953 def CMDtry_results(parser, args): |
(...skipping 488 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
5414 if __name__ == '__main__': | 5442 if __name__ == '__main__': |
5415 # These affect sys.stdout so do it outside of main() to simplify mocks in | 5443 # These affect sys.stdout so do it outside of main() to simplify mocks in |
5416 # unit testing. | 5444 # unit testing. |
5417 fix_encoding.fix_encoding() | 5445 fix_encoding.fix_encoding() |
5418 setup_color.init() | 5446 setup_color.init() |
5419 try: | 5447 try: |
5420 sys.exit(main(sys.argv[1:])) | 5448 sys.exit(main(sys.argv[1:])) |
5421 except KeyboardInterrupt: | 5449 except KeyboardInterrupt: |
5422 sys.stderr.write('interrupted\n') | 5450 sys.stderr.write('interrupted\n') |
5423 sys.exit(1) | 5451 sys.exit(1) |
OLD | NEW |