Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 #!/usr/bin/python | 1 #!/usr/bin/python |
| 2 # git-cl -- a git-command for integrating reviews on Rietveld | 2 # git-cl -- a git-command for integrating reviews on Rietveld |
| 3 # Copyright (C) 2008 Evan Martin <martine@danga.com> | 3 # Copyright (C) 2008 Evan Martin <martine@danga.com> |
| 4 | 4 |
| 5 import errno | 5 import errno |
| 6 import logging | 6 import logging |
| 7 import optparse | 7 import optparse |
| 8 import os | 8 import os |
| 9 import re | 9 import re |
| 10 import subprocess | 10 import subprocess |
| (...skipping 36 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 47 except OSError, e: | 47 except OSError, e: |
| 48 if e.errno == errno.EAGAIN and sys.platform == 'cygwin': | 48 if e.errno == errno.EAGAIN and sys.platform == 'cygwin': |
| 49 DieWithError( | 49 DieWithError( |
| 50 'Visit ' | 50 'Visit ' |
| 51 'http://code.google.com/p/chromium/wiki/CygwinDllRemappingFailure to ' | 51 'http://code.google.com/p/chromium/wiki/CygwinDllRemappingFailure to ' |
| 52 'learn how to fix this error; you need to rebase your cygwin dlls') | 52 'learn how to fix this error; you need to rebase your cygwin dlls') |
| 53 raise | 53 raise |
| 54 | 54 |
| 55 | 55 |
| 56 def RunCommand(cmd, error_ok=False, error_message=None, | 56 def RunCommand(cmd, error_ok=False, error_message=None, |
| 57 redirect_stdout=True, swallow_stderr=False, **kwargs): | 57 redirect_stdout=True, swallow_stderr=False, |
| 58 tee_filter=None, **kwargs): | |
| 58 if redirect_stdout: | 59 if redirect_stdout: |
| 59 stdout = subprocess.PIPE | 60 stdout = subprocess.PIPE |
| 60 else: | 61 else: |
| 61 stdout = None | 62 stdout = None |
| 62 if swallow_stderr: | 63 if swallow_stderr: |
| 63 stderr = subprocess.PIPE | 64 stderr = subprocess.PIPE |
| 64 else: | 65 else: |
| 65 stderr = None | 66 stderr = None |
| 66 proc = Popen(cmd, stdout=stdout, stderr=stderr, **kwargs) | 67 proc = Popen(cmd, stdout=stdout, stderr=stderr, **kwargs) |
| 67 output = proc.communicate()[0] | 68 if tee_filter: |
| 69 output = '' | |
| 70 while True: | |
| 71 line = proc.stdout.readline() | |
| 72 if line == '' and proc.poll() != None: | |
| 73 break | |
| 74 if not tee_filter(line): | |
| 75 print line | |
| 76 output += line + '\n' | |
| 77 else: | |
| 78 output = proc.communicate()[0] | |
| 79 | |
| 68 if not error_ok and proc.returncode != 0: | 80 if not error_ok and proc.returncode != 0: |
| 69 DieWithError('Command "%s" failed.\n' % (' '.join(cmd)) + | 81 DieWithError('Command "%s" failed.\n' % (' '.join(cmd)) + |
| 70 (error_message or output or '')) | 82 (error_message or output or '')) |
| 71 return output | 83 return output |
| 72 | 84 |
| 73 | 85 |
| 74 def RunGit(args, **kwargs): | 86 def RunGit(args, **kwargs): |
| 75 cmd = ['git'] + args | 87 cmd = ['git'] + args |
| 76 return RunCommand(cmd, **kwargs) | 88 return RunCommand(cmd, **kwargs) |
| 77 | 89 |
| (...skipping 394 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 472 | 484 |
| 473 SetProperty(settings.GetCCList(), 'CC list', 'cc') | 485 SetProperty(settings.GetCCList(), 'CC list', 'cc') |
| 474 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL', | 486 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL', |
| 475 'tree-status-url') | 487 'tree-status-url') |
| 476 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url') | 488 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url') |
| 477 | 489 |
| 478 # TODO: configure a default branch to diff against, rather than this | 490 # TODO: configure a default branch to diff against, rather than this |
| 479 # svn-based hackery. | 491 # svn-based hackery. |
| 480 | 492 |
| 481 | 493 |
| 494 class HookResults(object): | |
| 495 """Contains the parsed output of the presubmit hooks.""" | |
| 496 def __init__(self, output_from_hooks=None): | |
| 497 self.reviewers = [] | |
| 498 self.output = None | |
| 499 self._ParseOutputFromHooks(output_from_hooks) | |
| 500 | |
| 501 def print_output(self): | |
|
M-A Ruel
2011/03/08 22:40:59
Is this function useful? I don't see it being used
| |
| 502 if self.output: | |
| 503 print '\n'.join(self.output) | |
| 504 | |
| 505 def _ParseOutputFromHooks(self, output_from_hooks): | |
| 506 if not output_from_hooks: | |
| 507 return | |
| 508 lines = [] | |
| 509 reviewers = [] | |
| 510 reviewer_regexp = re.compile('ADD: R=(.+)') | |
| 511 for l in output_from_hooks.splitlines(): | |
| 512 m = reviewer_regexp.match(l) | |
| 513 if m: | |
| 514 reviewers.extend(m.group(1).split(',')) | |
| 515 else: | |
| 516 lines.append(l) | |
| 517 self.output = '\n'.join(lines) | |
| 518 self.reviewers = ','.join(reviewers) | |
| 519 | |
| 520 | |
| 521 class ChangeDescription(object): | |
| 522 """Contains a parsed form of the change description.""" | |
| 523 def __init__(self, subject, log_desc, reviewers): | |
| 524 self.subject = subject | |
| 525 self.log_desc = log_desc | |
| 526 self.reviewers = reviewers | |
| 527 self.description = self.log_desc | |
| 528 | |
| 529 def Update(self): | |
| 530 initial_text = """# Enter a description of the change. | |
| 531 # This will displayed on the codereview site. | |
| 532 # The first line will also be used as the subject of the review. | |
| 533 """ | |
| 534 initial_text += self.description | |
| 535 if 'R=' not in self.description and self.reviewers: | |
| 536 initial_text += '\nR=' + self.reviewers | |
| 537 if 'BUG=' not in self.description: | |
| 538 initial_text += '\nBUG=' | |
| 539 if 'TEST=' not in self.description: | |
| 540 initial_text += '\nTEST=' | |
| 541 self._ParseDescription(UserEditedLog(initial_text)) | |
| 542 | |
| 543 def _ParseDescription(self, description): | |
| 544 if not description: | |
| 545 self.description = description | |
| 546 return | |
| 547 | |
| 548 parsed_lines = [] | |
| 549 reviewers_regexp = re.compile('\s*R=(.+)') | |
| 550 reviewers = '' | |
| 551 subject = '' | |
| 552 for l in description.splitlines(): | |
| 553 if not subject: | |
| 554 subject = l | |
| 555 matched_reviewers = reviewers_regexp.match(l) | |
| 556 if matched_reviewers: | |
| 557 reviewers = matched_reviewers.group(1) | |
| 558 parsed_lines.append(l) | |
| 559 | |
| 560 self.description = '\n'.join(parsed_lines) + '\n' | |
| 561 self.subject = subject | |
| 562 self.reviewers = reviewers | |
| 563 | |
| 564 def IsEmpty(self): | |
| 565 return not self.description | |
| 566 | |
| 567 | |
| 482 def FindCodereviewSettingsFile(filename='codereview.settings'): | 568 def FindCodereviewSettingsFile(filename='codereview.settings'): |
| 483 """Finds the given file starting in the cwd and going up. | 569 """Finds the given file starting in the cwd and going up. |
| 484 | 570 |
| 485 Only looks up to the top of the repository unless an | 571 Only looks up to the top of the repository unless an |
| 486 'inherit-review-settings-ok' file exists in the root of the repository. | 572 'inherit-review-settings-ok' file exists in the root of the repository. |
| 487 """ | 573 """ |
| 488 inherit_ok_file = 'inherit-review-settings-ok' | 574 inherit_ok_file = 'inherit-review-settings-ok' |
| 489 cwd = os.getcwd() | 575 cwd = os.getcwd() |
| 490 root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip()) | 576 root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip()) |
| 491 if os.path.isfile(os.path.join(root, inherit_ok_file)): | 577 if os.path.isfile(os.path.join(root, inherit_ok_file)): |
| (...skipping 174 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 666 fileobj.close() | 752 fileobj.close() |
| 667 os.remove(filename) | 753 os.remove(filename) |
| 668 stripcomment_re = re.compile(r'^#.*$', re.MULTILINE) | 754 stripcomment_re = re.compile(r'^#.*$', re.MULTILINE) |
| 669 return stripcomment_re.sub('', text).strip() | 755 return stripcomment_re.sub('', text).strip() |
| 670 | 756 |
| 671 | 757 |
| 672 def RunHook(hook, upstream_branch, error_ok=False): | 758 def RunHook(hook, upstream_branch, error_ok=False): |
| 673 """Run a given hook if it exists. By default, we fail on errors.""" | 759 """Run a given hook if it exists. By default, we fail on errors.""" |
| 674 hook = '%s/%s' % (settings.GetRoot(), hook) | 760 hook = '%s/%s' % (settings.GetRoot(), hook) |
| 675 if not os.path.exists(hook): | 761 if not os.path.exists(hook): |
| 676 return | 762 return HookResults() |
| 677 return RunCommand([hook, upstream_branch], error_ok=error_ok, | 763 |
| 678 redirect_stdout=False) | 764 output = RunCommand([hook, upstream_branch], error_ok=error_ok, |
| 765 redirect_stdout=True, | |
| 766 tee_filter=lambda l: l.startswith('ADD:')) | |
| 767 return HookResults(output) | |
| 679 | 768 |
| 680 | 769 |
| 681 def CMDpresubmit(parser, args): | 770 def CMDpresubmit(parser, args): |
| 682 """run presubmit tests on the current changelist""" | 771 """run presubmit tests on the current changelist""" |
| 683 parser.add_option('--upload', action='store_true', | 772 parser.add_option('--upload', action='store_true', |
| 684 help='Run upload hook instead of the push/dcommit hook') | 773 help='Run upload hook instead of the push/dcommit hook') |
| 685 (options, args) = parser.parse_args(args) | 774 (options, args) = parser.parse_args(args) |
| 686 | 775 |
| 687 # Make sure index is up-to-date before running diff-index. | 776 # Make sure index is up-to-date before running diff-index. |
| 688 RunGit(['update-index', '--refresh', '-q'], error_ok=True) | 777 RunGit(['update-index', '--refresh', '-q'], error_ok=True) |
| 689 if RunGit(['diff-index', 'HEAD']): | 778 if RunGit(['diff-index', 'HEAD']): |
| 690 # TODO(maruel): Is this really necessary? | 779 # TODO(maruel): Is this really necessary? |
| 691 print 'Cannot presubmit with a dirty tree. You must commit locally first.' | 780 print 'Cannot presubmit with a dirty tree. You must commit locally first.' |
| 692 return 1 | 781 return 1 |
| 693 | 782 |
| 694 if args: | 783 if args: |
| 695 base_branch = args[0] | 784 base_branch = args[0] |
| 696 else: | 785 else: |
| 697 # Default to diffing against the "upstream" branch. | 786 # Default to diffing against the "upstream" branch. |
| 698 base_branch = Changelist().GetUpstreamBranch() | 787 base_branch = Changelist().GetUpstreamBranch() |
| 699 | 788 |
| 700 if options.upload: | 789 if options.upload: |
| 701 print '*** Presubmit checks for UPLOAD would report: ***' | 790 print '*** Presubmit checks for UPLOAD would report: ***' |
| 702 return not RunHook(PREUPLOAD_HOOK, upstream_branch=base_branch, | 791 return not RunHook(PREUPLOAD_HOOK, upstream_branch=base_branch, |
| 703 error_ok=True) | 792 error_ok=True).output |
| 704 else: | 793 else: |
| 705 print '*** Presubmit checks for DCOMMIT would report: ***' | 794 print '*** Presubmit checks for DCOMMIT would report: ***' |
| 706 return not RunHook(PREDCOMMIT_HOOK, upstream_branch=base_branch, | 795 return not RunHook(PREDCOMMIT_HOOK, upstream_branch=base_branch, |
| 707 error_ok=True) | 796 error_ok=True).output |
| 708 | 797 |
| 709 | 798 |
| 710 @usage('[args to "git diff"]') | 799 @usage('[args to "git diff"]') |
| 711 def CMDupload(parser, args): | 800 def CMDupload(parser, args): |
| 712 """upload the current changelist to codereview""" | 801 """upload the current changelist to codereview""" |
| 713 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks', | 802 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks', |
| 714 help='bypass upload presubmit hook') | 803 help='bypass upload presubmit hook') |
| 715 parser.add_option('-m', dest='message', help='message for patch') | 804 parser.add_option('-m', dest='message', help='message for patch') |
| 716 parser.add_option('-r', '--reviewers', | 805 parser.add_option('-r', '--reviewers', |
| 717 help='reviewer email addresses') | 806 help='reviewer email addresses') |
| (...skipping 18 matching lines...) Expand all Loading... | |
| 736 | 825 |
| 737 cl = Changelist() | 826 cl = Changelist() |
| 738 if args: | 827 if args: |
| 739 base_branch = args[0] | 828 base_branch = args[0] |
| 740 else: | 829 else: |
| 741 # Default to diffing against the "upstream" branch. | 830 # Default to diffing against the "upstream" branch. |
| 742 base_branch = cl.GetUpstreamBranch() | 831 base_branch = cl.GetUpstreamBranch() |
| 743 args = [base_branch + "..."] | 832 args = [base_branch + "..."] |
| 744 | 833 |
| 745 if not options.bypass_hooks: | 834 if not options.bypass_hooks: |
| 746 RunHook(PREUPLOAD_HOOK, upstream_branch=base_branch, error_ok=False) | 835 hook_results = RunHook(PREUPLOAD_HOOK, upstream_branch=base_branch, |
| 836 error_ok=False) | |
| 837 else: | |
| 838 hook_results = HookResults() | |
| 839 | |
| 840 if not options.reviewers and hook_results.reviewers: | |
| 841 options.reviewers = hook_results.reviewers | |
| 747 | 842 |
| 748 # --no-ext-diff is broken in some versions of Git, so try to work around | 843 # --no-ext-diff is broken in some versions of Git, so try to work around |
| 749 # this by overriding the environment (but there is still a problem if the | 844 # this by overriding the environment (but there is still a problem if the |
| 750 # git config key "diff.external" is used). | 845 # git config key "diff.external" is used). |
| 751 env = os.environ.copy() | 846 env = os.environ.copy() |
| 752 if 'GIT_EXTERNAL_DIFF' in env: | 847 if 'GIT_EXTERNAL_DIFF' in env: |
| 753 del env['GIT_EXTERNAL_DIFF'] | 848 del env['GIT_EXTERNAL_DIFF'] |
| 754 subprocess.call(['git', 'diff', '--no-ext-diff', '--stat', '-M'] + args, | 849 subprocess.call(['git', 'diff', '--no-ext-diff', '--stat', '-M'] + args, |
| 755 env=env) | 850 env=env) |
| 756 | 851 |
| 757 upload_args = ['--assume_yes'] # Don't ask about untracked files. | 852 upload_args = ['--assume_yes'] # Don't ask about untracked files. |
| 758 upload_args.extend(['--server', cl.GetRietveldServer()]) | 853 upload_args.extend(['--server', cl.GetRietveldServer()]) |
| 759 if options.reviewers: | |
| 760 upload_args.extend(['--reviewers', options.reviewers]) | |
| 761 if options.emulate_svn_auto_props: | 854 if options.emulate_svn_auto_props: |
| 762 upload_args.append('--emulate_svn_auto_props') | 855 upload_args.append('--emulate_svn_auto_props') |
| 763 if options.send_mail: | 856 if options.send_mail: |
| 764 if not options.reviewers: | 857 if not options.reviewers: |
| 765 DieWithError("Must specify reviewers to send email.") | 858 DieWithError("Must specify reviewers to send email.") |
| 766 upload_args.append('--send_mail') | 859 upload_args.append('--send_mail') |
| 767 if options.from_logs and not options.message: | 860 if options.from_logs and not options.message: |
| 768 print 'Must set message for subject line if using desc_from_logs' | 861 print 'Must set message for subject line if using desc_from_logs' |
| 769 return 1 | 862 return 1 |
| 770 | 863 |
| 771 change_desc = None | 864 change_desc = None |
| 772 | 865 |
| 773 if cl.GetIssue(): | 866 if cl.GetIssue(): |
| 774 if options.message: | 867 if options.message: |
| 775 upload_args.extend(['--message', options.message]) | 868 upload_args.extend(['--message', options.message]) |
| 776 upload_args.extend(['--issue', cl.GetIssue()]) | 869 upload_args.extend(['--issue', cl.GetIssue()]) |
| 777 print ("This branch is associated with issue %s. " | 870 print ("This branch is associated with issue %s. " |
| 778 "Adding patch to that issue." % cl.GetIssue()) | 871 "Adding patch to that issue." % cl.GetIssue()) |
| 779 else: | 872 else: |
| 780 log_desc = CreateDescriptionFromLog(args) | 873 log_desc = CreateDescriptionFromLog(args) |
| 781 if options.from_logs: | 874 change_desc = ChangeDescription(options.message, log_desc, |
| 782 # Uses logs as description and message as subject. | 875 options.reviewers) |
| 783 subject = options.message | 876 if not options.from_logs: |
| 784 change_desc = subject + '\n\n' + log_desc | 877 change_desc.Update() |
| 785 else: | 878 |
| 786 initial_text = """# Enter a description of the change. | 879 if change_desc.IsEmpty(): |
| 787 # This will displayed on the codereview site. | |
| 788 # The first line will also be used as the subject of the review. | |
| 789 """ | |
| 790 if 'BUG=' not in log_desc: | |
| 791 log_desc += '\nBUG=' | |
| 792 if 'TEST=' not in log_desc: | |
| 793 log_desc += '\nTEST=' | |
| 794 change_desc = UserEditedLog(initial_text + log_desc) | |
| 795 subject = '' | |
| 796 if change_desc: | |
| 797 subject = change_desc.splitlines()[0] | |
| 798 if not change_desc: | |
| 799 print "Description is empty; aborting." | 880 print "Description is empty; aborting." |
| 800 return 1 | 881 return 1 |
| 801 upload_args.extend(['--message', subject]) | 882 |
| 802 upload_args.extend(['--description', change_desc]) | 883 upload_args.extend(['--message', change_desc.subject]) |
| 884 upload_args.extend(['--description', change_desc.description]) | |
| 885 if change_desc.reviewers: | |
| 886 upload_args.extend(['--reviewers', change_desc.reviewers]) | |
| 803 cc = ','.join(filter(None, (settings.GetCCList(), options.cc))) | 887 cc = ','.join(filter(None, (settings.GetCCList(), options.cc))) |
| 804 if cc: | 888 if cc: |
| 805 upload_args.extend(['--cc', cc]) | 889 upload_args.extend(['--cc', cc]) |
| 806 | 890 |
| 807 # Include the upstream repo's URL in the change -- this is useful for | 891 # Include the upstream repo's URL in the change -- this is useful for |
| 808 # projects that have their source spread across multiple repos. | 892 # projects that have their source spread across multiple repos. |
| 809 remote_url = None | 893 remote_url = None |
| 810 if settings.GetIsGitSvn(): | 894 if settings.GetIsGitSvn(): |
| 811 # URL is dependent on the current directory. | 895 # URL is dependent on the current directory. |
| 812 data = RunGit(['svn', 'info'], cwd=settings.GetRoot()) | 896 data = RunGit(['svn', 'info'], cwd=settings.GetRoot()) |
| (...skipping 11 matching lines...) Expand all Loading... | |
| 824 try: | 908 try: |
| 825 issue, patchset = upload.RealMain(['upload'] + upload_args + args) | 909 issue, patchset = upload.RealMain(['upload'] + upload_args + args) |
| 826 except: | 910 except: |
| 827 # If we got an exception after the user typed a description for their | 911 # If we got an exception after the user typed a description for their |
| 828 # change, back up the description before re-raising. | 912 # change, back up the description before re-raising. |
| 829 if change_desc: | 913 if change_desc: |
| 830 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE) | 914 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE) |
| 831 print '\nGot exception while uploading -- saving description to %s\n' \ | 915 print '\nGot exception while uploading -- saving description to %s\n' \ |
| 832 % backup_path | 916 % backup_path |
| 833 backup_file = open(backup_path, 'w') | 917 backup_file = open(backup_path, 'w') |
| 834 backup_file.write(change_desc) | 918 backup_file.write(change_desc.description) |
| 835 backup_file.close() | 919 backup_file.close() |
| 836 raise | 920 raise |
| 837 | 921 |
| 838 if not cl.GetIssue(): | 922 if not cl.GetIssue(): |
| 839 cl.SetIssue(issue) | 923 cl.SetIssue(issue) |
| 840 cl.SetPatchset(patchset) | 924 cl.SetPatchset(patchset) |
| 841 return 0 | 925 return 0 |
| 842 | 926 |
| 843 | 927 |
| 844 def SendUpstream(parser, args, cmd): | 928 def SendUpstream(parser, args, cmd): |
| (...skipping 88 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 933 | 1017 |
| 934 description += "\n\nReview URL: %s" % cl.GetIssueURL() | 1018 description += "\n\nReview URL: %s" % cl.GetIssueURL() |
| 935 else: | 1019 else: |
| 936 if not description: | 1020 if not description: |
| 937 # Submitting TBR. See if there's already a description in Rietveld, else | 1021 # Submitting TBR. See if there's already a description in Rietveld, else |
| 938 # create a template description. Eitherway, give the user a chance to edit | 1022 # create a template description. Eitherway, give the user a chance to edit |
| 939 # it to fill in the TBR= field. | 1023 # it to fill in the TBR= field. |
| 940 if cl.GetIssue(): | 1024 if cl.GetIssue(): |
| 941 description = cl.GetDescription() | 1025 description = cl.GetDescription() |
| 942 | 1026 |
| 1027 # TODO(dpranke): Update to use ChangeDescription object. | |
| 943 if not description: | 1028 if not description: |
| 944 description = """# Enter a description of the change. | 1029 description = """# Enter a description of the change. |
| 945 # This will be used as the change log for the commit. | 1030 # This will be used as the change log for the commit. |
| 946 | 1031 |
| 947 """ | 1032 """ |
| 948 description += CreateDescriptionFromLog(args) | 1033 description += CreateDescriptionFromLog(args) |
| 949 | 1034 |
| 950 description = UserEditedLog(description + '\nTBR=') | 1035 description = UserEditedLog(description + '\nTBR=') |
| 951 | 1036 |
| 952 if not description: | 1037 if not description: |
| (...skipping 326 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 1279 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith ' | 1364 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith ' |
| 1280 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e))) | 1365 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e))) |
| 1281 | 1366 |
| 1282 # Not a known command. Default to help. | 1367 # Not a known command. Default to help. |
| 1283 GenUsage(parser, 'help') | 1368 GenUsage(parser, 'help') |
| 1284 return CMDhelp(parser, argv) | 1369 return CMDhelp(parser, argv) |
| 1285 | 1370 |
| 1286 | 1371 |
| 1287 if __name__ == '__main__': | 1372 if __name__ == '__main__': |
| 1288 sys.exit(main(sys.argv[1:])) | 1373 sys.exit(main(sys.argv[1:])) |
| OLD | NEW |