 Chromium Code Reviews
 Chromium Code Reviews Issue 2419113002:
  Add -B/--bucket flag to git-cl try  (Closed)
    
  
    Issue 2419113002:
  Add -B/--bucket flag to git-cl try  (Closed) 
  | 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 58 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 69 GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingGit' | 69 GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingGit' | 
| 70 REFS_THAT_ALIAS_TO_OTHER_REFS = { | 70 REFS_THAT_ALIAS_TO_OTHER_REFS = { | 
| 71 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master', | 71 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master', | 
| 72 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master', | 72 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master', | 
| 73 } | 73 } | 
| 74 | 74 | 
| 75 # Valid extensions for files we want to lint. | 75 # Valid extensions for files we want to lint. | 
| 76 DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)" | 76 DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)" | 
| 77 DEFAULT_LINT_IGNORE_REGEX = r"$^" | 77 DEFAULT_LINT_IGNORE_REGEX = r"$^" | 
| 78 | 78 | 
| 79 # Buildbucket master name prefix. | |
| 80 MASTER_PREFIX = 'master.' | |
| 81 | |
| 79 # Shortcut since it quickly becomes redundant. | 82 # Shortcut since it quickly becomes redundant. | 
| 80 Fore = colorama.Fore | 83 Fore = colorama.Fore | 
| 81 | 84 | 
| 82 # Initialized in main() | 85 # Initialized in main() | 
| 83 settings = None | 86 settings = None | 
| 84 | 87 | 
| 85 | 88 | 
| 86 def DieWithError(message): | 89 def DieWithError(message): | 
| 87 print(message, file=sys.stderr) | 90 print(message, file=sys.stderr) | 
| 88 sys.exit(1) | 91 sys.exit(1) | 
| (...skipping 179 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 268 | 271 | 
| 269 | 272 | 
| 270 def _prefix_master(master): | 273 def _prefix_master(master): | 
| 271 """Convert user-specified master name to full master name. | 274 """Convert user-specified master name to full master name. | 
| 272 | 275 | 
| 273 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket | 276 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket | 
| 274 name, while the developers always use shortened master name | 277 name, while the developers always use shortened master name | 
| 275 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This | 278 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This | 
| 276 function does the conversion for buildbucket migration. | 279 function does the conversion for buildbucket migration. | 
| 277 """ | 280 """ | 
| 278 prefix = 'master.' | 281 if master.startswith(MASTER_PREFIX): | 
| 279 if master.startswith(prefix): | |
| 280 return master | 282 return master | 
| 281 return '%s%s' % (prefix, master) | 283 return '%s%s' % (MASTER_PREFIX, master) | 
| 284 | |
| 285 | |
| 286 def _unprefix_master(bucket): | |
| 287 """Convert bucket name to shortened master name. | |
| 288 | |
| 289 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket | |
| 290 name, while the developers always use shortened master name | |
| 291 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This | |
| 292 function does the conversion for buildbucket migration. | |
| 293 """ | |
| 294 if bucket.startswith(MASTER_PREFIX): | |
| 295 return bucket[len(MASTER_PREFIX):] | |
| 296 return bucket | |
| 282 | 297 | 
| 283 | 298 | 
| 284 def _buildbucket_retry(operation_name, http, *args, **kwargs): | 299 def _buildbucket_retry(operation_name, http, *args, **kwargs): | 
| 285 """Retries requests to buildbucket service and returns parsed json content.""" | 300 """Retries requests to buildbucket service and returns parsed json content.""" | 
| 286 try_count = 0 | 301 try_count = 0 | 
| 287 while True: | 302 while True: | 
| 288 response, content = http.request(*args, **kwargs) | 303 response, content = http.request(*args, **kwargs) | 
| 289 try: | 304 try: | 
| 290 content_json = json.loads(content) | 305 content_json = json.loads(content) | 
| 291 except ValueError: | 306 except ValueError: | 
| (...skipping 19 matching lines...) Expand all Loading... | |
| 311 if response.status < 500 or try_count >= 2: | 326 if response.status < 500 or try_count >= 2: | 
| 312 raise httplib2.HttpLib2Error(content) | 327 raise httplib2.HttpLib2Error(content) | 
| 313 | 328 | 
| 314 # status >= 500 means transient failures. | 329 # status >= 500 means transient failures. | 
| 315 logging.debug('Transient errors when %s. Will retry.', operation_name) | 330 logging.debug('Transient errors when %s. Will retry.', operation_name) | 
| 316 time.sleep(0.5 + 1.5*try_count) | 331 time.sleep(0.5 + 1.5*try_count) | 
| 317 try_count += 1 | 332 try_count += 1 | 
| 318 assert False, 'unreachable' | 333 assert False, 'unreachable' | 
| 319 | 334 | 
| 320 | 335 | 
| 321 def _trigger_try_jobs(auth_config, changelist, masters, options, | 336 def _trigger_try_jobs(auth_config, changelist, buckets, options, | 
| 322 category='git_cl_try', patchset=None): | 337 category='git_cl_try', patchset=None): | 
| 323 assert changelist.GetIssue(), 'CL must be uploaded first' | 338 assert changelist.GetIssue(), 'CL must be uploaded first' | 
| 324 codereview_url = changelist.GetCodereviewServer() | 339 codereview_url = changelist.GetCodereviewServer() | 
| 325 assert codereview_url, 'CL must be uploaded first' | 340 assert codereview_url, 'CL must be uploaded first' | 
| 326 patchset = patchset or changelist.GetMostRecentPatchset() | 341 patchset = patchset or changelist.GetMostRecentPatchset() | 
| 327 assert patchset, 'CL must be uploaded first' | 342 assert patchset, 'CL must be uploaded first' | 
| 328 | 343 | 
| 329 codereview_host = urlparse.urlparse(codereview_url).hostname | 344 codereview_host = urlparse.urlparse(codereview_url).hostname | 
| 330 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config) | 345 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config) | 
| 331 http = authenticator.authorize(httplib2.Http()) | 346 http = authenticator.authorize(httplib2.Http()) | 
| (...skipping 11 matching lines...) Expand all Loading... | |
| 343 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format( | 358 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format( | 
| 344 codereview='gerrit' if changelist.IsGerrit() else 'rietveld', | 359 codereview='gerrit' if changelist.IsGerrit() else 'rietveld', | 
| 345 hostname=codereview_host, | 360 hostname=codereview_host, | 
| 346 issue=changelist.GetIssue(), | 361 issue=changelist.GetIssue(), | 
| 347 patch=patchset) | 362 patch=patchset) | 
| 348 extra_properties = _get_properties_from_options(options) | 363 extra_properties = _get_properties_from_options(options) | 
| 349 | 364 | 
| 350 batch_req_body = {'builds': []} | 365 batch_req_body = {'builds': []} | 
| 351 print_text = [] | 366 print_text = [] | 
| 352 print_text.append('Tried jobs on:') | 367 print_text.append('Tried jobs on:') | 
| 353 for master, builders_and_tests in sorted(masters.iteritems()): | 368 for bucket, builders_and_tests in sorted(buckets.iteritems()): | 
| 354 print_text.append('Master: %s' % master) | 369 print_text.append('Bucket: %s' % bucket) | 
| 355 bucket = _prefix_master(master) | 370 master = _unprefix_master(bucket) | 
| 356 for builder, tests in sorted(builders_and_tests.iteritems()): | 371 for builder, tests in sorted(builders_and_tests.iteritems()): | 
| 357 print_text.append(' %s: %s' % (builder, tests)) | 372 print_text.append(' %s: %s' % (builder, tests)) | 
| 358 parameters = { | 373 parameters = { | 
| 359 'builder_name': builder, | 374 'builder_name': builder, | 
| 360 'changes': [{ | 375 'changes': [{ | 
| 361 'author': {'email': owner_email}, | 376 'author': {'email': owner_email}, | 
| 362 'revision': options.revision, | 377 'revision': options.revision, | 
| 363 }], | 378 }], | 
| 364 'properties': { | 379 'properties': { | 
| 365 'category': category, | 380 'category': category, | 
| (...skipping 4332 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 4698 """ | 4713 """ | 
| 4699 group = optparse.OptionGroup(parser, 'Try job options') | 4714 group = optparse.OptionGroup(parser, 'Try job options') | 
| 4700 group.add_option( | 4715 group.add_option( | 
| 4701 '-b', '--bot', action='append', | 4716 '-b', '--bot', action='append', | 
| 4702 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple ' | 4717 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple ' | 
| 4703 'times to specify multiple builders. ex: ' | 4718 'times to specify multiple builders. ex: ' | 
| 4704 '"-b win_rel -b win_layout". See ' | 4719 '"-b win_rel -b win_layout". See ' | 
| 4705 'the try server waterfall for the builders name and the tests ' | 4720 'the try server waterfall for the builders name and the tests ' | 
| 4706 'available.')) | 4721 'available.')) | 
| 4707 group.add_option( | 4722 group.add_option( | 
| 4723 '-B', '--bucket', default='', | |
| 4724 help=('Buildbucket bucket to send the try requests.')) | |
| 4725 group.add_option( | |
| 4708 '-m', '--master', default='', | 4726 '-m', '--master', default='', | 
| 4709 help=('Specify a try master where to run the tries.')) | 4727 help=('Specify a try master where to run the tries.')) | 
| 4710 # TODO(tandrii,nodir): add -B --bucket flag. | |
| 4711 group.add_option( | 4728 group.add_option( | 
| 4712 '-r', '--revision', | 4729 '-r', '--revision', | 
| 4713 help='Revision to use for the try job; default: the revision will ' | 4730 help='Revision to use for the try job; default: the revision will ' | 
| 4714 'be determined by the try recipe that builder runs, which usually ' | 4731 'be determined by the try recipe that builder runs, which usually ' | 
| 4715 'defaults to HEAD of origin/master') | 4732 'defaults to HEAD of origin/master') | 
| 4716 group.add_option( | 4733 group.add_option( | 
| 4717 '-c', '--clobber', action='store_true', default=False, | 4734 '-c', '--clobber', action='store_true', default=False, | 
| 4718 help='Force a clobber before building; that is don\'t do an ' | 4735 help='Force a clobber before building; that is don\'t do an ' | 
| 4719 'incremental build') | 4736 'incremental build') | 
| 4720 group.add_option( | 4737 group.add_option( | 
| (...skipping 37 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 4758 'If your project has Commit Queue, dry run is a workaround:\n' | 4775 'If your project has Commit Queue, dry run is a workaround:\n' | 
| 4759 ' git cl set-commit --dry-run') | 4776 ' git cl set-commit --dry-run') | 
| 4760 | 4777 | 
| 4761 error_message = cl.CannotTriggerTryJobReason() | 4778 error_message = cl.CannotTriggerTryJobReason() | 
| 4762 if error_message: | 4779 if error_message: | 
| 4763 parser.error('Can\'t trigger try jobs: %s') | 4780 parser.error('Can\'t trigger try jobs: %s') | 
| 4764 | 4781 | 
| 4765 if not options.name: | 4782 if not options.name: | 
| 4766 options.name = cl.GetBranch() | 4783 options.name = cl.GetBranch() | 
| 4767 | 4784 | 
| 4768 if options.bot and not options.master: | 4785 if options.bucket and options.master: | 
| 4786 parser.error('Only one of --bucket and --master may be used.') | |
| 4787 | |
| 4788 if options.bot and not options.master and not options.bucket: | |
| 4769 options.master, err_msg = GetBuilderMaster(options.bot) | 4789 options.master, err_msg = GetBuilderMaster(options.bot) | 
| 4770 if err_msg: | 4790 if err_msg: | 
| 4771 parser.error('Tryserver master cannot be found because: %s\n' | 4791 parser.error('Tryserver master cannot be found because: %s\n' | 
| 4772 'Please manually specify the tryserver master' | 4792 'Please manually specify the tryserver master' | 
| 4773 ', e.g. "-m tryserver.chromium.linux".' % err_msg) | 4793 ', e.g. "-m tryserver.chromium.linux".' % err_msg) | 
| 4774 | 4794 | 
| 4795 def GetBotsMap(bots): | |
| 4796 builders_and_tests = {} | |
| 4797 | |
| 4798 # TODO(machenbach): The old style command-line options don't support | |
| 4799 # multiple try masters yet. | |
| 4800 old_style = filter(lambda x: isinstance(x, basestring), bots) | |
| 4801 new_style = filter(lambda x: isinstance(x, tuple), bots) | |
| 4802 | |
| 4803 for bot in old_style: | |
| 
nodir
2016/10/14 18:41:18
let's not support old style stuff in new code path
 
borenet
2016/10/14 18:46:42
Unfortunately the old code path goes through here
 
nodir
2016/10/14 18:54:16
I think it is better copy-paste code that is neede
 
borenet
2016/10/17 14:25:00
Done. It seems that when using "-b <bot>" we get t
 | |
| 4804 if ':' in bot: | |
| 4805 parser.error('Specifying testfilter is no longer supported') | |
| 4806 elif ',' in bot: | |
| 4807 parser.error('Specify one bot per --bot flag') | |
| 4808 else: | |
| 4809 builders_and_tests.setdefault(bot, []) | |
| 4810 | |
| 4811 for bot, tests in new_style: | |
| 4812 builders_and_tests.setdefault(bot, []).extend(tests) | |
| 4813 return builders_and_tests | |
| 4814 | |
| 4775 def GetMasterMap(): | 4815 def GetMasterMap(): | 
| 4776 # Process --bot. | 4816 # Process --bot. | 
| 4777 if not options.bot: | 4817 if not options.bot: | 
| 4778 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None) | 4818 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None) | 
| 4779 | 4819 | 
| 4780 # Get try masters from PRESUBMIT.py files. | 4820 # Get try masters from PRESUBMIT.py files. | 
| 4781 masters = presubmit_support.DoGetTryMasters( | 4821 masters = presubmit_support.DoGetTryMasters( | 
| 4782 change, | 4822 change, | 
| 4783 change.LocalPaths(), | 4823 change.LocalPaths(), | 
| 4784 settings.GetRoot(), | 4824 settings.GetRoot(), | 
| (...skipping 10 matching lines...) Expand all Loading... | |
| 4795 change.LocalPaths(), | 4835 change.LocalPaths(), | 
| 4796 settings.GetRoot(), | 4836 settings.GetRoot(), | 
| 4797 None, | 4837 None, | 
| 4798 None, | 4838 None, | 
| 4799 options.verbose, | 4839 options.verbose, | 
| 4800 sys.stdout) | 4840 sys.stdout) | 
| 4801 | 4841 | 
| 4802 if not options.bot: | 4842 if not options.bot: | 
| 4803 return {} | 4843 return {} | 
| 4804 | 4844 | 
| 4805 builders_and_tests = {} | 4845 builders_and_tests = GetBotsMap(options.bot) | 
| 4806 # TODO(machenbach): The old style command-line options don't support | |
| 4807 # multiple try masters yet. | |
| 4808 old_style = filter(lambda x: isinstance(x, basestring), options.bot) | |
| 4809 new_style = filter(lambda x: isinstance(x, tuple), options.bot) | |
| 4810 | |
| 4811 for bot in old_style: | |
| 4812 if ':' in bot: | |
| 4813 parser.error('Specifying testfilter is no longer supported') | |
| 4814 elif ',' in bot: | |
| 4815 parser.error('Specify one bot per --bot flag') | |
| 4816 else: | |
| 4817 builders_and_tests.setdefault(bot, []) | |
| 4818 | |
| 4819 for bot, tests in new_style: | |
| 4820 builders_and_tests.setdefault(bot, []).extend(tests) | |
| 4821 | 4846 | 
| 4822 # Return a master map with one master to be backwards compatible. The | 4847 # Return a master map with one master to be backwards compatible. The | 
| 4823 # master name defaults to an empty string, which will cause the master | 4848 # master name defaults to an empty string, which will cause the master | 
| 4824 # not to be set on rietveld (deprecated). | 4849 # not to be set on rietveld (deprecated). | 
| 4825 return {options.master: builders_and_tests} | 4850 bucket = "" | 
| 
nodir
2016/10/14 18:41:17
use ''
 
borenet
2016/10/14 18:46:42
Done.
 | |
| 4851 if options.master: | |
| 4852 # Add the "master." prefix to the master name to obtain the bucket name. | |
| 4853 bucket = _prefix_master(options.master) | |
| 4854 return {bucket: builders_and_tests} | |
| 4826 | 4855 | 
| 4827 masters = GetMasterMap() | 4856 if options.bucket: | 
| 4828 if not masters: | 4857 buckets = {options.bucket: GetBotsMap(options.bot or [])} | 
| 4829 # Default to triggering Dry Run (see http://crbug.com/625697). | 4858 else: | 
| 4830 if options.verbose: | 4859 buckets = GetMasterMap() | 
| 4831 print('git cl try with no bots now defaults to CQ Dry Run.') | 4860 if not buckets: | 
| 4832 try: | 4861 # Default to triggering Dry Run (see http://crbug.com/625697). | 
| 4833 cl.SetCQState(_CQState.DRY_RUN) | 4862 if options.verbose: | 
| 4834 print('scheduled CQ Dry Run on %s' % cl.GetIssueURL()) | 4863 print('git cl try with no bots now defaults to CQ Dry Run.') | 
| 4835 return 0 | 4864 try: | 
| 4836 except KeyboardInterrupt: | 4865 cl.SetCQState(_CQState.DRY_RUN) | 
| 4837 raise | 4866 print('scheduled CQ Dry Run on %s' % cl.GetIssueURL()) | 
| 4838 except: | 4867 return 0 | 
| 4839 print('WARNING: failed to trigger CQ Dry Run.\n' | 4868 except KeyboardInterrupt: | 
| 4840 'Either:\n' | 4869 raise | 
| 4841 ' * your project has no CQ\n' | 4870 except: | 
| 4842 ' * you don\'t have permission to trigger Dry Run\n' | 4871 print('WARNING: failed to trigger CQ Dry Run.\n' | 
| 4843 ' * bug in this code (see stack trace below).\n' | 4872 'Either:\n' | 
| 4844 'Consider specifying which bots to trigger manually ' | 4873 ' * your project has no CQ\n' | 
| 4845 'or asking your project owners for permissions ' | 4874 ' * you don\'t have permission to trigger Dry Run\n' | 
| 4846 'or contacting Chrome Infrastructure team at ' | 4875 ' * bug in this code (see stack trace below).\n' | 
| 4847 'https://www.chromium.org/infra\n\n') | 4876 'Consider specifying which bots to trigger manually ' | 
| 4848 # Still raise exception so that stack trace is printed. | 4877 'or asking your project owners for permissions ' | 
| 4849 raise | 4878 'or contacting Chrome Infrastructure team at ' | 
| 4879 'https://www.chromium.org/infra\n\n') | |
| 4880 # Still raise exception so that stack trace is printed. | |
| 4881 raise | |
| 4850 | 4882 | 
| 4851 for builders in masters.itervalues(): | 4883 for builders in buckets.itervalues(): | 
| 4852 if any('triggered' in b for b in builders): | 4884 if any('triggered' in b for b in builders): | 
| 4853 print('ERROR You are trying to send a job to a triggered bot. This type ' | 4885 print('ERROR You are trying to send a job to a triggered bot. This type ' | 
| 4854 'of bot requires an initial job from a parent (usually a builder). ' | 4886 'of bot requires an initial job from a parent (usually a builder). ' | 
| 4855 'Instead send your job to the parent.\n' | 4887 'Instead send your job to the parent.\n' | 
| 4856 'Bot list: %s' % builders, file=sys.stderr) | 4888 'Bot list: %s' % builders, file=sys.stderr) | 
| 4857 return 1 | 4889 return 1 | 
| 4858 | 4890 | 
| 4859 patchset = cl.GetMostRecentPatchset() | 4891 patchset = cl.GetMostRecentPatchset() | 
| 4860 if patchset != cl.GetPatchset(): | 4892 if patchset != cl.GetPatchset(): | 
| 4861 print('Warning: Codereview server has newer patchsets (%s) than most ' | 4893 print('Warning: Codereview server has newer patchsets (%s) than most ' | 
| 4862 'recent upload from local checkout (%s). Did a previous upload ' | 4894 'recent upload from local checkout (%s). Did a previous upload ' | 
| 4863 'fail?\n' | 4895 'fail?\n' | 
| 4864 'By default, git cl try uses the latest patchset from ' | 4896 'By default, git cl try uses the latest patchset from ' | 
| 4865 'codereview, continuing to use patchset %s.\n' % | 4897 'codereview, continuing to use patchset %s.\n' % | 
| 4866 (patchset, cl.GetPatchset(), patchset)) | 4898 (patchset, cl.GetPatchset(), patchset)) | 
| 4867 try: | 4899 try: | 
| 4868 _trigger_try_jobs(auth_config, cl, masters, options, 'git_cl_try', | 4900 _trigger_try_jobs(auth_config, cl, buckets, options, 'git_cl_try', | 
| 4869 patchset) | 4901 patchset) | 
| 4870 except BuildbucketResponseException as ex: | 4902 except BuildbucketResponseException as ex: | 
| 4871 print('ERROR: %s' % ex) | 4903 print('ERROR: %s' % ex) | 
| 4872 return 1 | 4904 return 1 | 
| 4873 return 0 | 4905 return 0 | 
| 4874 | 4906 | 
| 4875 | 4907 | 
| 4876 def CMDtry_results(parser, args): | 4908 def CMDtry_results(parser, args): | 
| 4877 """Prints info about try jobs associated with current CL.""" | 4909 """Prints info about try jobs associated with current CL.""" | 
| 4878 group = optparse.OptionGroup(parser, 'Try job results options') | 4910 group = optparse.OptionGroup(parser, 'Try job results options') | 
| 4879 group.add_option( | 4911 group.add_option( | 
| (...skipping 485 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 5365 if __name__ == '__main__': | 5397 if __name__ == '__main__': | 
| 5366 # These affect sys.stdout so do it outside of main() to simplify mocks in | 5398 # These affect sys.stdout so do it outside of main() to simplify mocks in | 
| 5367 # unit testing. | 5399 # unit testing. | 
| 5368 fix_encoding.fix_encoding() | 5400 fix_encoding.fix_encoding() | 
| 5369 setup_color.init() | 5401 setup_color.init() | 
| 5370 try: | 5402 try: | 
| 5371 sys.exit(main(sys.argv[1:])) | 5403 sys.exit(main(sys.argv[1:])) | 
| 5372 except KeyboardInterrupt: | 5404 except KeyboardInterrupt: | 
| 5373 sys.stderr.write('interrupted\n') | 5405 sys.stderr.write('interrupted\n') | 
| 5374 sys.exit(1) | 5406 sys.exit(1) | 
| OLD | NEW |