Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(248)

Side by Side Diff: git_cl.py

Issue 2442153002: Refactoring: Extract helper functions from CMDtry in git_cl.py. (Closed)
Patch Set: Added a TODO to maybe reuse TriggerDryRun. Created 4 years, 1 month ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « no previous file | no next file » | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
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 322 matching lines...) Expand 10 before | Expand all | Expand 10 after
333 if response.status < 500 or try_count >= 2: 333 if response.status < 500 or try_count >= 2:
334 raise httplib2.HttpLib2Error(content) 334 raise httplib2.HttpLib2Error(content)
335 335
336 # status >= 500 means transient failures. 336 # status >= 500 means transient failures.
337 logging.debug('Transient errors when %s. Will retry.', operation_name) 337 logging.debug('Transient errors when %s. Will retry.', operation_name)
338 time_sleep(0.5 + 1.5*try_count) 338 time_sleep(0.5 + 1.5*try_count)
339 try_count += 1 339 try_count += 1
340 assert False, 'unreachable' 340 assert False, 'unreachable'
341 341
342 342
343 def _get_bucket_map(changelist, options, option_parser):
344 """Returns a dict mapping bucket names (or master names) to
345 builders and tests, for triggering try jobs.
346 """
347 if not options.bot:
348 change = changelist.GetChange(
349 changelist.GetCommonAncestorWithUpstream(), None)
350
351 # Get try masters from PRESUBMIT.py files.
352 masters = presubmit_support.DoGetTryMasters(
353 change=change,
354 changed_files=change.LocalPaths(),
355 repository_root=settings.GetRoot(),
356 default_presubmit=None,
357 project=None,
358 verbose=options.verbose,
359 output_stream=sys.stdout)
360
361 if masters:
362 return masters
nodir 2016/11/04 21:49:40 this returns masters, not buckets. Caused https://
nodir 2016/11/04 21:55:42 https://codereview.chromium.org/2482523002
363
364 # Fall back to deprecated method: get try slaves from PRESUBMIT.py
365 # files.
366 options.bot = presubmit_support.DoGetTrySlaves(
367 change=change,
368 changed_files=change.LocalPaths(),
369 repository_root=settings.GetRoot(),
370 default_presubmit=None,
371 project=None,
372 verbose=options.verbose,
373 output_stream=sys.stdout)
374
375 if not options.bot:
376 return {}
377
378 if options.bucket:
379 return {options.bucket: {b: [] for b in options.bot}}
380
381 builders_and_tests = {}
382
383 # TODO(machenbach): The old style command-line options don't support
384 # multiple try masters yet.
385 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
386 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
387
388 for bot in old_style:
389 if ':' in bot:
390 option_parser.error('Specifying testfilter is no longer supported')
391 elif ',' in bot:
392 option_parser.error('Specify one bot per --bot flag')
393 else:
394 builders_and_tests.setdefault(bot, [])
395
396 for bot, tests in new_style:
397 builders_and_tests.setdefault(bot, []).extend(tests)
398
399 if not options.master:
400 # TODO(qyearsley): crbug.com/640740
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)
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
451
452
343 def _trigger_try_jobs(auth_config, changelist, buckets, options, 453 def _trigger_try_jobs(auth_config, changelist, buckets, options,
344 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 """
345 assert changelist.GetIssue(), 'CL must be uploaded first' 463 assert changelist.GetIssue(), 'CL must be uploaded first'
346 codereview_url = changelist.GetCodereviewServer() 464 codereview_url = changelist.GetCodereviewServer()
347 assert codereview_url, 'CL must be uploaded first' 465 assert codereview_url, 'CL must be uploaded first'
348 patchset = patchset or changelist.GetMostRecentPatchset() 466 patchset = patchset or changelist.GetMostRecentPatchset()
349 assert patchset, 'CL must be uploaded first' 467 assert patchset, 'CL must be uploaded first'
350 468
351 codereview_host = urlparse.urlparse(codereview_url).hostname 469 codereview_host = urlparse.urlparse(codereview_url).hostname
352 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config) 470 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
353 http = authenticator.authorize(httplib2.Http()) 471 http = authenticator.authorize(httplib2.Http())
354 http.force_exception_to_status_code = True 472 http.force_exception_to_status_code = True
(...skipping 1220 matching lines...) Expand 10 before | Expand all | Expand 10 after
1575 1693
1576 def SetCQState(self, new_state): 1694 def SetCQState(self, new_state):
1577 """Update the CQ state for latest patchset. 1695 """Update the CQ state for latest patchset.
1578 1696
1579 Issue must have been already uploaded and known. 1697 Issue must have been already uploaded and known.
1580 """ 1698 """
1581 assert new_state in _CQState.ALL_STATES 1699 assert new_state in _CQState.ALL_STATES
1582 assert self.GetIssue() 1700 assert self.GetIssue()
1583 return self._codereview_impl.SetCQState(new_state) 1701 return self._codereview_impl.SetCQState(new_state)
1584 1702
1703 def TriggerDryRun(self):
1704 """Triggers a dry run and prints a warning on failure."""
1705 # TODO(qyearsley): Either re-use this method in CMDset_commit
1706 # and CMDupload, or change CMDtry to trigger dry runs with
1707 # just SetCQState, and catch keyboard interrupt and other
1708 # errors in that method.
1709 try:
1710 self.SetCQState(_CQState.DRY_RUN)
1711 print('scheduled CQ Dry Run on %s' % self.GetIssueURL())
1712 return 0
1713 except KeyboardInterrupt:
1714 raise
1715 except:
1716 print('WARNING: failed to trigger CQ Dry Run.\n'
1717 'Either:\n'
1718 ' * your project has no CQ\n'
1719 ' * you don\'t have permission to trigger Dry Run\n'
1720 ' * bug in this code (see stack trace below).\n'
1721 'Consider specifying which bots to trigger manually '
1722 'or asking your project owners for permissions '
1723 'or contacting Chrome Infrastructure team at '
1724 'https://www.chromium.org/infra\n\n')
1725 # Still raise exception so that stack trace is printed.
1726 raise
1727
1585 # Forward methods to codereview specific implementation. 1728 # Forward methods to codereview specific implementation.
1586 1729
1587 def CloseIssue(self): 1730 def CloseIssue(self):
1588 return self._codereview_impl.CloseIssue() 1731 return self._codereview_impl.CloseIssue()
1589 1732
1590 def GetStatus(self): 1733 def GetStatus(self):
1591 return self._codereview_impl.GetStatus() 1734 return self._codereview_impl.GetStatus()
1592 1735
1593 def GetCodereviewServer(self): 1736 def GetCodereviewServer(self):
1594 return self._codereview_impl.GetCodereviewServer() 1737 return self._codereview_impl.GetCodereviewServer()
(...skipping 3046 matching lines...) Expand 10 before | Expand all | Expand 10 after
4641 """Fetches the tree status from a json url and returns the message 4784 """Fetches the tree status from a json url and returns the message
4642 with the reason for the tree to be opened or closed.""" 4785 with the reason for the tree to be opened or closed."""
4643 url = settings.GetTreeStatusUrl() 4786 url = settings.GetTreeStatusUrl()
4644 json_url = urlparse.urljoin(url, '/current?format=json') 4787 json_url = urlparse.urljoin(url, '/current?format=json')
4645 connection = urllib2.urlopen(json_url) 4788 connection = urllib2.urlopen(json_url)
4646 status = json.loads(connection.read()) 4789 status = json.loads(connection.read())
4647 connection.close() 4790 connection.close()
4648 return status['message'] 4791 return status['message']
4649 4792
4650 4793
4651 def GetBuilderMaster(bot_list):
4652 """For a given builder, fetch the master from AE if available."""
4653 map_url = 'https://builders-map.appspot.com/'
4654 try:
4655 master_map = json.load(urllib2.urlopen(map_url))
4656 except urllib2.URLError as e:
4657 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
4658 (map_url, e))
4659 except ValueError as e:
4660 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
4661 if not master_map:
4662 return None, 'Failed to build master map.'
4663
4664 result_master = ''
4665 for bot in bot_list:
4666 builder = bot.split(':', 1)[0]
4667 master_list = master_map.get(builder, [])
4668 if not master_list:
4669 return None, ('No matching master for builder %s.' % builder)
4670 elif len(master_list) > 1:
4671 return None, ('The builder name %s exists in multiple masters %s.' %
4672 (builder, master_list))
4673 else:
4674 cur_master = master_list[0]
4675 if not result_master:
4676 result_master = cur_master
4677 elif result_master != cur_master:
4678 return None, 'The builders do not belong to the same master.'
4679 return result_master, None
4680
4681
4682 def CMDtree(parser, args): 4794 def CMDtree(parser, args):
4683 """Shows the status of the tree.""" 4795 """Shows the status of the tree."""
4684 _, args = parser.parse_args(args) 4796 _, args = parser.parse_args(args)
4685 status = GetTreeStatus() 4797 status = GetTreeStatus()
4686 if 'unset' == status: 4798 if 'unset' == status:
4687 print('You must configure your tree status URL by running "git cl config".') 4799 print('You must configure your tree status URL by running "git cl config".')
4688 return 2 4800 return 2
4689 4801
4690 print('The tree is %s' % status) 4802 print('The tree is %s' % status)
4691 print() 4803 print()
4692 print(GetTreeStatusReason()) 4804 print(GetTreeStatusReason())
4693 if status != 'open': 4805 if status != 'open':
4694 return 1 4806 return 1
4695 return 0 4807 return 0
4696 4808
4697 4809
4698 def CMDtry(parser, args): 4810 def CMDtry(parser, args):
4699 """Triggers try jobs using CQ dry run or BuildBucket for individual builders. 4811 """Triggers try jobs using either BuildBucket or CQ dry run."""
4700 """
4701 group = optparse.OptionGroup(parser, 'Try job options') 4812 group = optparse.OptionGroup(parser, 'Try job options')
4702 group.add_option( 4813 group.add_option(
4703 '-b', '--bot', action='append', 4814 '-b', '--bot', action='append',
4704 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple ' 4815 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
4705 'times to specify multiple builders. ex: ' 4816 'times to specify multiple builders. ex: '
4706 '"-b win_rel -b win_layout". See ' 4817 '"-b win_rel -b win_layout". See '
4707 'the try server waterfall for the builders name and the tests ' 4818 'the try server waterfall for the builders name and the tests '
4708 'available.')) 4819 'available.'))
4709 group.add_option( 4820 group.add_option(
4710 '-B', '--bucket', default='', 4821 '-B', '--bucket', default='',
(...skipping 54 matching lines...) Expand 10 before | Expand all | Expand 10 after
4765 error_message = cl.CannotTriggerTryJobReason() 4876 error_message = cl.CannotTriggerTryJobReason()
4766 if error_message: 4877 if error_message:
4767 parser.error('Can\'t trigger try jobs: %s' % error_message) 4878 parser.error('Can\'t trigger try jobs: %s' % error_message)
4768 4879
4769 if not options.name: 4880 if not options.name:
4770 options.name = cl.GetBranch() 4881 options.name = cl.GetBranch()
4771 4882
4772 if options.bucket and options.master: 4883 if options.bucket and options.master:
4773 parser.error('Only one of --bucket and --master may be used.') 4884 parser.error('Only one of --bucket and --master may be used.')
4774 4885
4775 if options.bot and not options.master and not options.bucket: 4886 buckets = _get_bucket_map(cl, options, parser)
4776 options.master, err_msg = GetBuilderMaster(options.bot)
4777 if err_msg:
4778 parser.error('Tryserver master cannot be found because: %s\n'
4779 'Please manually specify the tryserver master'
4780 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
4781 4887
4782 def GetMasterMap(): 4888 if not buckets:
4783 """Returns {master: {builder_name: [test_names]}}. Not buckets!""" 4889 # Default to triggering Dry Run (see http://crbug.com/625697).
4784 # Process --bot. 4890 if options.verbose:
4785 if not options.bot: 4891 print('git cl try with no bots now defaults to CQ Dry Run.')
4786 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None) 4892 return cl.TriggerDryRun()
4787
4788 # Get try masters from PRESUBMIT.py files.
4789 masters = presubmit_support.DoGetTryMasters(
4790 change,
4791 change.LocalPaths(),
4792 settings.GetRoot(),
4793 None,
4794 None,
4795 options.verbose,
4796 sys.stdout)
4797 if masters:
4798 return masters
4799
4800 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
4801 options.bot = presubmit_support.DoGetTrySlaves(
4802 change,
4803 change.LocalPaths(),
4804 settings.GetRoot(),
4805 None,
4806 None,
4807 options.verbose,
4808 sys.stdout)
4809
4810 if not options.bot:
4811 return {}
4812
4813 builders_and_tests = {}
4814 # TODO(machenbach): The old style command-line options don't support
4815 # multiple try masters yet.
4816 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
4817 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
4818
4819 for bot in old_style:
4820 if ':' in bot:
4821 parser.error('Specifying testfilter is no longer supported')
4822 elif ',' in bot:
4823 parser.error('Specify one bot per --bot flag')
4824 else:
4825 builders_and_tests.setdefault(bot, [])
4826
4827 for bot, tests in new_style:
4828 builders_and_tests.setdefault(bot, []).extend(tests)
4829
4830 # Return a master map with one master to be backwards compatible. The
4831 # master name defaults to an empty string, which will cause the master
4832 # not to be set on rietveld (deprecated).
4833 return {options.master: builders_and_tests}
4834
4835 if options.bucket:
4836 buckets = {options.bucket: {b: [] for b in options.bot}}
4837 else:
4838 buckets = {}
4839 for master, data in GetMasterMap().iteritems():
4840 # Add the "master." prefix to the master name to obtain the bucket name.
4841 bucket = _prefix_master(master) if master else ''
4842 buckets[bucket] = data
4843
4844 if not buckets:
4845 # Default to triggering Dry Run (see http://crbug.com/625697).
4846 if options.verbose:
4847 print('git cl try with no bots now defaults to CQ Dry Run.')
4848 try:
4849 cl.SetCQState(_CQState.DRY_RUN)
4850 print('scheduled CQ Dry Run on %s' % cl.GetIssueURL())
4851 return 0
4852 except KeyboardInterrupt:
4853 raise
4854 except:
4855 print('WARNING: failed to trigger CQ Dry Run.\n'
4856 'Either:\n'
4857 ' * your project has no CQ\n'
4858 ' * you don\'t have permission to trigger Dry Run\n'
4859 ' * bug in this code (see stack trace below).\n'
4860 'Consider specifying which bots to trigger manually '
4861 'or asking your project owners for permissions '
4862 'or contacting Chrome Infrastructure team at '
4863 'https://www.chromium.org/infra\n\n')
4864 # Still raise exception so that stack trace is printed.
4865 raise
4866 4893
4867 for builders in buckets.itervalues(): 4894 for builders in buckets.itervalues():
4868 if any('triggered' in b for b in builders): 4895 if any('triggered' in b for b in builders):
4869 print('ERROR You are trying to send a job to a triggered bot. This type ' 4896 print('ERROR You are trying to send a job to a triggered bot. This type '
4870 'of bot requires an initial job from a parent (usually a builder). ' 4897 'of bot requires an initial job from a parent (usually a builder). '
4871 'Instead send your job to the parent.\n' 4898 'Instead send your job to the parent.\n'
4872 'Bot list: %s' % builders, file=sys.stderr) 4899 'Bot list: %s' % builders, file=sys.stderr)
4873 return 1 4900 return 1
4874 4901
4875 patchset = cl.GetMostRecentPatchset() 4902 patchset = cl.GetMostRecentPatchset()
4876 if patchset != cl.GetPatchset(): 4903 if patchset != cl.GetPatchset():
4877 print('Warning: Codereview server has newer patchsets (%s) than most ' 4904 print('Warning: Codereview server has newer patchsets (%s) than most '
4878 'recent upload from local checkout (%s). Did a previous upload ' 4905 'recent upload from local checkout (%s). Did a previous upload '
4879 'fail?\n' 4906 'fail?\n'
4880 'By default, git cl try uses the latest patchset from ' 4907 'By default, git cl try uses the latest patchset from '
4881 'codereview, continuing to use patchset %s.\n' % 4908 'codereview, continuing to use patchset %s.\n' %
4882 (patchset, cl.GetPatchset(), patchset)) 4909 (patchset, cl.GetPatchset(), patchset))
4910
4883 try: 4911 try:
4884 _trigger_try_jobs(auth_config, cl, buckets, options, 'git_cl_try', 4912 _trigger_try_jobs(auth_config, cl, buckets, options, 'git_cl_try',
4885 patchset) 4913 patchset)
4886 except BuildbucketResponseException as ex: 4914 except BuildbucketResponseException as ex:
4887 print('ERROR: %s' % ex) 4915 print('ERROR: %s' % ex)
4888 return 1 4916 return 1
4889 return 0 4917 return 0
4890 4918
4891 4919
4892 def CMDtry_results(parser, args): 4920 def CMDtry_results(parser, args):
(...skipping 101 matching lines...) Expand 10 before | Expand all | Expand 10 after
4994 if args: 5022 if args:
4995 parser.error('Unrecognized args: %s' % ' '.join(args)) 5023 parser.error('Unrecognized args: %s' % ' '.join(args))
4996 if options.dry_run and options.clear: 5024 if options.dry_run and options.clear:
4997 parser.error('Make up your mind: both --dry-run and --clear not allowed') 5025 parser.error('Make up your mind: both --dry-run and --clear not allowed')
4998 5026
4999 cl = Changelist(auth_config=auth_config, issue=options.issue, 5027 cl = Changelist(auth_config=auth_config, issue=options.issue,
5000 codereview=options.forced_codereview) 5028 codereview=options.forced_codereview)
5001 if options.clear: 5029 if options.clear:
5002 state = _CQState.NONE 5030 state = _CQState.NONE
5003 elif options.dry_run: 5031 elif options.dry_run:
5032 # TODO(qyearsley): Use cl.TriggerDryRun.
5004 state = _CQState.DRY_RUN 5033 state = _CQState.DRY_RUN
5005 else: 5034 else:
5006 state = _CQState.COMMIT 5035 state = _CQState.COMMIT
5007 if not cl.GetIssue(): 5036 if not cl.GetIssue():
5008 parser.error('Must upload the issue first') 5037 parser.error('Must upload the issue first')
5009 cl.SetCQState(state) 5038 cl.SetCQState(state)
5010 return 0 5039 return 0
5011 5040
5012 5041
5013 def CMDset_close(parser, args): 5042 def CMDset_close(parser, args):
(...skipping 367 matching lines...) Expand 10 before | Expand all | Expand 10 after
5381 if __name__ == '__main__': 5410 if __name__ == '__main__':
5382 # These affect sys.stdout so do it outside of main() to simplify mocks in 5411 # These affect sys.stdout so do it outside of main() to simplify mocks in
5383 # unit testing. 5412 # unit testing.
5384 fix_encoding.fix_encoding() 5413 fix_encoding.fix_encoding()
5385 setup_color.init() 5414 setup_color.init()
5386 try: 5415 try:
5387 sys.exit(main(sys.argv[1:])) 5416 sys.exit(main(sys.argv[1:]))
5388 except KeyboardInterrupt: 5417 except KeyboardInterrupt:
5389 sys.stderr.write('interrupted\n') 5418 sys.stderr.write('interrupted\n')
5390 sys.exit(1) 5419 sys.exit(1)
OLDNEW
« no previous file with comments | « no previous file | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698