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

Side by Side Diff: git_cl/git_cl.py

Issue 6674014: Make git-cl work with OWNERS files in a non .git/hooks/pre-cl-* world (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/tools/depot_tools
Patch Set: remove unnecessary code Created 9 years, 9 months 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 | Annotate | Revision Log
« no previous file with comments | « no previous file | git_cl/test/owners.sh » ('j') | 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/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 StringIO
10 import subprocess 11 import subprocess
11 import sys 12 import sys
12 import tempfile 13 import tempfile
13 import textwrap 14 import textwrap
14 import upload 15 import upload
15 import urlparse 16 import urlparse
16 import urllib2 17 import urllib2
17 18
18 try: 19 try:
19 import readline 20 import readline
(...skipping 39 matching lines...) Expand 10 before | Expand all | Expand 10 after
59 if redirect_stdout: 60 if redirect_stdout:
60 stdout = subprocess.PIPE 61 stdout = subprocess.PIPE
61 else: 62 else:
62 stdout = None 63 stdout = None
63 if swallow_stderr: 64 if swallow_stderr:
64 stderr = subprocess.PIPE 65 stderr = subprocess.PIPE
65 else: 66 else:
66 stderr = None 67 stderr = None
67 proc = Popen(cmd, stdout=stdout, stderr=stderr, **kwargs) 68 proc = Popen(cmd, stdout=stdout, stderr=stderr, **kwargs)
68 output = proc.communicate()[0] 69 output = proc.communicate()[0]
70
69 if not error_ok and proc.returncode != 0: 71 if not error_ok and proc.returncode != 0:
70 DieWithError('Command "%s" failed.\n' % (' '.join(cmd)) + 72 DieWithError('Command "%s" failed.\n' % (' '.join(cmd)) +
71 (error_message or output or '')) 73 (error_message or output or ''))
72 return output 74 return output
73 75
74 76
75 def RunGit(args, **kwargs): 77 def RunGit(args, **kwargs):
76 cmd = ['git'] + args 78 cmd = ['git'] + args
77 return RunCommand(cmd, **kwargs) 79 return RunCommand(cmd, **kwargs)
78 80
(...skipping 394 matching lines...) Expand 10 before | Expand all | Expand 10 after
473 475
474 SetProperty(settings.GetCCList(), 'CC list', 'cc') 476 SetProperty(settings.GetCCList(), 'CC list', 'cc')
475 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL', 477 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
476 'tree-status-url') 478 'tree-status-url')
477 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url') 479 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url')
478 480
479 # TODO: configure a default branch to diff against, rather than this 481 # TODO: configure a default branch to diff against, rather than this
480 # svn-based hackery. 482 # svn-based hackery.
481 483
482 484
485 class HookResults(object):
486 """Contains the parsed output of the presubmit hooks."""
487 def __init__(self, output_from_hooks=None):
488 self.reviewers = []
489 self.output = None
490 self._ParseOutputFromHooks(output_from_hooks)
491
492 def _ParseOutputFromHooks(self, output_from_hooks):
493 if not output_from_hooks:
494 return
495 lines = []
496 reviewers = []
497 reviewer_regexp = re.compile('ADD: R=(.+)')
M-A Ruel 2011/03/11 18:00:06 I wonder if instead it could be a more generic imp
Dirk Pranke 2011/03/11 21:06:31 Yeah, that would probably be a nice enhancement. H
498 for l in output_from_hooks.splitlines():
499 m = reviewer_regexp.match(l)
500 if m:
501 reviewers.extend(m.group(1).split(','))
502 else:
503 lines.append(l)
504 self.output = '\n'.join(lines)
505 self.reviewers = ','.join(reviewers)
506
507
508 class ChangeDescription(object):
509 """Contains a parsed form of the change description."""
510 def __init__(self, subject, log_desc, reviewers):
511 self.subject = subject
512 self.log_desc = log_desc
513 self.reviewers = reviewers
514 self.description = self.log_desc
515
516 def Update(self):
517 initial_text = """# Enter a description of the change.
518 # This will displayed on the codereview site.
519 # The first line will also be used as the subject of the review.
520 """
521 initial_text += self.description
522 if 'R=' not in self.description and self.reviewers:
523 initial_text += '\nR=' + self.reviewers
524 if 'BUG=' not in self.description:
525 initial_text += '\nBUG='
526 if 'TEST=' not in self.description:
527 initial_text += '\nTEST='
528 self._ParseDescription(UserEditedLog(initial_text))
529
530 def _ParseDescription(self, description):
531 if not description:
532 self.description = description
533 return
534
535 parsed_lines = []
536 reviewers_regexp = re.compile('\s*R=(.+)')
537 reviewers = ''
538 subject = ''
539 for l in description.splitlines():
540 if not subject:
541 subject = l
542 matched_reviewers = reviewers_regexp.match(l)
543 if matched_reviewers:
544 reviewers = matched_reviewers.group(1)
545 parsed_lines.append(l)
546
547 self.description = '\n'.join(parsed_lines) + '\n'
548 self.subject = subject
549 self.reviewers = reviewers
550
551 def IsEmpty(self):
552 return not self.description
553
554
483 def FindCodereviewSettingsFile(filename='codereview.settings'): 555 def FindCodereviewSettingsFile(filename='codereview.settings'):
484 """Finds the given file starting in the cwd and going up. 556 """Finds the given file starting in the cwd and going up.
485 557
486 Only looks up to the top of the repository unless an 558 Only looks up to the top of the repository unless an
487 'inherit-review-settings-ok' file exists in the root of the repository. 559 'inherit-review-settings-ok' file exists in the root of the repository.
488 """ 560 """
489 inherit_ok_file = 'inherit-review-settings-ok' 561 inherit_ok_file = 'inherit-review-settings-ok'
490 cwd = os.getcwd() 562 cwd = os.getcwd()
491 root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip()) 563 root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
492 if os.path.isfile(os.path.join(root, inherit_ok_file)): 564 if os.path.isfile(os.path.join(root, inherit_ok_file)):
(...skipping 171 matching lines...) Expand 10 before | Expand all | Expand 10 after
664 736
665 737
666 def ConvertToInteger(inputval): 738 def ConvertToInteger(inputval):
667 """Convert a string to integer, but returns either an int or None.""" 739 """Convert a string to integer, but returns either an int or None."""
668 try: 740 try:
669 return int(inputval) 741 return int(inputval)
670 except (TypeError, ValueError): 742 except (TypeError, ValueError):
671 return None 743 return None
672 744
673 745
674 def RunHook(committing, upstream_branch): 746 def RunHook(committing, upstream_branch, rietveld_server, tbr=False):
675 import presubmit_support 747 import presubmit_support
676 import scm 748 import scm
677 import watchlists 749 import watchlists
678 750
679 root = RunCommand(['git', 'rev-parse', '--show-cdup']).strip() 751 root = RunCommand(['git', 'rev-parse', '--show-cdup']).strip()
680 if not root: 752 if not root:
681 root = "." 753 root = "."
682 absroot = os.path.abspath(root) 754 absroot = os.path.abspath(root)
683 if not root: 755 if not root:
684 raise Exception("Could not get root directory.") 756 raise Exception("Could not get root directory.")
(...skipping 17 matching lines...) Expand all
702 issue, patchset) 774 issue, patchset)
703 775
704 # Apply watchlists on upload. 776 # Apply watchlists on upload.
705 if not committing: 777 if not committing:
706 watchlist = watchlists.Watchlists(change.RepositoryRoot()) 778 watchlist = watchlists.Watchlists(change.RepositoryRoot())
707 files = [f.LocalPath() for f in change.AffectedFiles()] 779 files = [f.LocalPath() for f in change.AffectedFiles()]
708 watchers = watchlist.GetWatchersForPaths(files) 780 watchers = watchlist.GetWatchersForPaths(files)
709 RunCommand(['git', 'config', '--replace-all', 781 RunCommand(['git', 'config', '--replace-all',
710 'rietveld.extracc', ','.join(watchers)]) 782 'rietveld.extracc', ','.join(watchers)])
711 783
712 return presubmit_support.DoPresubmitChecks(change, committing, 784 output = StringIO.StringIO()
713 verbose=None, output_stream=sys.stdout, input_stream=sys.stdin, 785 res = presubmit_support.DoPresubmitChecks(change, committing,
714 default_presubmit=None, may_prompt=None) 786 verbose=None, output_stream=output, input_stream=sys.stdin,
787 default_presubmit=None, may_prompt=None, tbr=tbr,
788 host_url=cl.GetRietveldServer())
789 if not res:
M-A Ruel 2011/03/11 21:21:55 What about PresubmitNotifyResult?
Dirk Pranke 2011/03/11 21:39:18 Good catch. It would get printed in some but not a
790 print output.getvalue()
791 sys.exit(1)
792 return HookResults(output.getvalue())
715 793
716 794
717 def CMDpresubmit(parser, args): 795 def CMDpresubmit(parser, args):
718 """run presubmit tests on the current changelist""" 796 """run presubmit tests on the current changelist"""
719 parser.add_option('--upload', action='store_true', 797 parser.add_option('--upload', action='store_true',
720 help='Run upload hook instead of the push/dcommit hook') 798 help='Run upload hook instead of the push/dcommit hook')
721 (options, args) = parser.parse_args(args) 799 (options, args) = parser.parse_args(args)
722 800
723 # Make sure index is up-to-date before running diff-index. 801 # Make sure index is up-to-date before running diff-index.
724 RunGit(['update-index', '--refresh', '-q'], error_ok=True) 802 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
725 if RunGit(['diff-index', 'HEAD']): 803 if RunGit(['diff-index', 'HEAD']):
726 # TODO(maruel): Is this really necessary? 804 # TODO(maruel): Is this really necessary?
727 print 'Cannot presubmit with a dirty tree. You must commit locally first.' 805 print 'Cannot presubmit with a dirty tree. You must commit locally first.'
728 return 1 806 return 1
729 807
808 cl = Changelist()
730 if args: 809 if args:
731 base_branch = args[0] 810 base_branch = args[0]
732 else: 811 else:
733 # Default to diffing against the "upstream" branch. 812 # Default to diffing against the "upstream" branch.
734 base_branch = Changelist().GetUpstreamBranch() 813 base_branch = cl.GetUpstreamBranch()
735 814
736 if options.upload: 815 if options.upload:
737 print '*** Presubmit checks for UPLOAD would report: ***' 816 print '*** Presubmit checks for UPLOAD would report: ***'
738 return RunHook(committing=False, upstream_branch=base_branch) 817 return RunHook(committing=False, upstream_branch=base_branch,
818 rietveld_server=cl.GetRietveldServer(), tbr=False).output
739 else: 819 else:
740 print '*** Presubmit checks for DCOMMIT would report: ***' 820 print '*** Presubmit checks for DCOMMIT would report: ***'
741 return RunHook(committing=True, upstream_branch=base_branch) 821 return RunHook(committing=True, upstream_branch=base_branch,
822 rietveld_server=cl.GetRietveldServer, tbr=False).output
742 823
743 824
744 @usage('[args to "git diff"]') 825 @usage('[args to "git diff"]')
745 def CMDupload(parser, args): 826 def CMDupload(parser, args):
746 """upload the current changelist to codereview""" 827 """upload the current changelist to codereview"""
747 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks', 828 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
748 help='bypass upload presubmit hook') 829 help='bypass upload presubmit hook')
749 parser.add_option('-m', dest='message', help='message for patch') 830 parser.add_option('-m', dest='message', help='message for patch')
750 parser.add_option('-r', '--reviewers', 831 parser.add_option('-r', '--reviewers',
751 help='reviewer email addresses') 832 help='reviewer email addresses')
(...skipping 18 matching lines...) Expand all
770 851
771 cl = Changelist() 852 cl = Changelist()
772 if args: 853 if args:
773 base_branch = args[0] 854 base_branch = args[0]
774 else: 855 else:
775 # Default to diffing against the "upstream" branch. 856 # Default to diffing against the "upstream" branch.
776 base_branch = cl.GetUpstreamBranch() 857 base_branch = cl.GetUpstreamBranch()
777 args = [base_branch + "..."] 858 args = [base_branch + "..."]
778 859
779 if not options.bypass_hooks: 860 if not options.bypass_hooks:
780 RunHook(committing=False, upstream_branch=base_branch) 861 hook_results = RunHook(committing=False, upstream_branch=base_branch,
862 rietveld_server=cl.GetRietveldServer(), tbr=False)
863 else:
864 hook_results = HookResults()
865
866 if not options.reviewers and hook_results.reviewers:
867 options.reviewers = hook_results.reviewers
781 868
782 # --no-ext-diff is broken in some versions of Git, so try to work around 869 # --no-ext-diff is broken in some versions of Git, so try to work around
783 # this by overriding the environment (but there is still a problem if the 870 # this by overriding the environment (but there is still a problem if the
784 # git config key "diff.external" is used). 871 # git config key "diff.external" is used).
785 env = os.environ.copy() 872 env = os.environ.copy()
786 if 'GIT_EXTERNAL_DIFF' in env: 873 if 'GIT_EXTERNAL_DIFF' in env:
787 del env['GIT_EXTERNAL_DIFF'] 874 del env['GIT_EXTERNAL_DIFF']
788 subprocess.call(['git', 'diff', '--no-ext-diff', '--stat', '-M'] + args, 875 subprocess.call(['git', 'diff', '--no-ext-diff', '--stat', '-M'] + args,
789 env=env) 876 env=env)
790 877
791 upload_args = ['--assume_yes'] # Don't ask about untracked files. 878 upload_args = ['--assume_yes'] # Don't ask about untracked files.
792 upload_args.extend(['--server', cl.GetRietveldServer()]) 879 upload_args.extend(['--server', cl.GetRietveldServer()])
793 if options.reviewers:
794 upload_args.extend(['--reviewers', options.reviewers])
795 if options.emulate_svn_auto_props: 880 if options.emulate_svn_auto_props:
796 upload_args.append('--emulate_svn_auto_props') 881 upload_args.append('--emulate_svn_auto_props')
797 if options.send_mail: 882 if options.send_mail:
798 if not options.reviewers: 883 if not options.reviewers:
799 DieWithError("Must specify reviewers to send email.") 884 DieWithError("Must specify reviewers to send email.")
800 upload_args.append('--send_mail') 885 upload_args.append('--send_mail')
801 if options.from_logs and not options.message: 886 if options.from_logs and not options.message:
802 print 'Must set message for subject line if using desc_from_logs' 887 print 'Must set message for subject line if using desc_from_logs'
803 return 1 888 return 1
804 889
805 change_desc = None 890 change_desc = None
806 891
807 if cl.GetIssue(): 892 if cl.GetIssue():
808 if options.message: 893 if options.message:
809 upload_args.extend(['--message', options.message]) 894 upload_args.extend(['--message', options.message])
810 upload_args.extend(['--issue', cl.GetIssue()]) 895 upload_args.extend(['--issue', cl.GetIssue()])
811 print ("This branch is associated with issue %s. " 896 print ("This branch is associated with issue %s. "
812 "Adding patch to that issue." % cl.GetIssue()) 897 "Adding patch to that issue." % cl.GetIssue())
813 else: 898 else:
814 log_desc = CreateDescriptionFromLog(args) 899 log_desc = CreateDescriptionFromLog(args)
815 if options.from_logs: 900 change_desc = ChangeDescription(options.message, log_desc,
816 # Uses logs as description and message as subject. 901 options.reviewers)
817 subject = options.message 902 if not options.from_logs:
818 change_desc = subject + '\n\n' + log_desc 903 change_desc.Update()
819 else: 904
820 initial_text = """# Enter a description of the change. 905 if change_desc.IsEmpty():
821 # This will displayed on the codereview site.
822 # The first line will also be used as the subject of the review.
823 """
824 if 'BUG=' not in log_desc:
825 log_desc += '\nBUG='
826 if 'TEST=' not in log_desc:
827 log_desc += '\nTEST='
828 change_desc = UserEditedLog(initial_text + log_desc)
829 subject = ''
830 if change_desc:
831 subject = change_desc.splitlines()[0]
832 if not change_desc:
833 print "Description is empty; aborting." 906 print "Description is empty; aborting."
834 return 1 907 return 1
835 upload_args.extend(['--message', subject]) 908
836 upload_args.extend(['--description', change_desc]) 909 upload_args.extend(['--message', change_desc.subject])
910 upload_args.extend(['--description', change_desc.description])
911 if change_desc.reviewers:
912 upload_args.extend(['--reviewers', change_desc.reviewers])
837 cc = ','.join(filter(None, (settings.GetCCList(), options.cc))) 913 cc = ','.join(filter(None, (settings.GetCCList(), options.cc)))
838 if cc: 914 if cc:
839 upload_args.extend(['--cc', cc]) 915 upload_args.extend(['--cc', cc])
840 916
841 # Include the upstream repo's URL in the change -- this is useful for 917 # Include the upstream repo's URL in the change -- this is useful for
842 # projects that have their source spread across multiple repos. 918 # projects that have their source spread across multiple repos.
843 remote_url = None 919 remote_url = None
844 if settings.GetIsGitSvn(): 920 if settings.GetIsGitSvn():
845 # URL is dependent on the current directory. 921 # URL is dependent on the current directory.
846 data = RunGit(['svn', 'info'], cwd=settings.GetRoot()) 922 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
(...skipping 11 matching lines...) Expand all
858 try: 934 try:
859 issue, patchset = upload.RealMain(['upload'] + upload_args + args) 935 issue, patchset = upload.RealMain(['upload'] + upload_args + args)
860 except: 936 except:
861 # If we got an exception after the user typed a description for their 937 # If we got an exception after the user typed a description for their
862 # change, back up the description before re-raising. 938 # change, back up the description before re-raising.
863 if change_desc: 939 if change_desc:
864 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE) 940 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
865 print '\nGot exception while uploading -- saving description to %s\n' \ 941 print '\nGot exception while uploading -- saving description to %s\n' \
866 % backup_path 942 % backup_path
867 backup_file = open(backup_path, 'w') 943 backup_file = open(backup_path, 'w')
868 backup_file.write(change_desc) 944 backup_file.write(change_desc.description)
869 backup_file.close() 945 backup_file.close()
870 raise 946 raise
871 947
872 if not cl.GetIssue(): 948 if not cl.GetIssue():
873 cl.SetIssue(issue) 949 cl.SetIssue(issue)
874 cl.SetPatchset(patchset) 950 cl.SetPatchset(patchset)
875 return 0 951 return 0
876 952
877 953
878 def SendUpstream(parser, args, cmd): 954 def SendUpstream(parser, args, cmd):
(...skipping 47 matching lines...) Expand 10 before | Expand all | Expand 10 after
926 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1', 1002 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
927 '--pretty=format:%H']) 1003 '--pretty=format:%H'])
928 extra_commits = RunGit(['rev-list', '^' + svn_head, base_branch]) 1004 extra_commits = RunGit(['rev-list', '^' + svn_head, base_branch])
929 if extra_commits: 1005 if extra_commits:
930 print ('This branch has %d additional commits not upstreamed yet.' 1006 print ('This branch has %d additional commits not upstreamed yet.'
931 % len(extra_commits.splitlines())) 1007 % len(extra_commits.splitlines()))
932 print ('Upstream "%s" or rebase this branch on top of the upstream trunk ' 1008 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
933 'before attempting to %s.' % (base_branch, cmd)) 1009 'before attempting to %s.' % (base_branch, cmd))
934 return 1 1010 return 1
935 1011
936 if not options.force and not options.bypass_hooks: 1012 if not options.bypass_hooks:
937 RunHook(committing=False, upstream_branch=base_branch) 1013 hook_results = RunHook(committing=True, upstream_branch=base_branch,
1014 rietveld_server=cl.GetRietveldServer(), tbr=options.tbr)
1015 print hook_results.output
938 1016
939 if cmd == 'dcommit': 1017 if cmd == 'dcommit':
940 # Check the tree status if the tree status URL is set. 1018 # Check the tree status if the tree status URL is set.
941 status = GetTreeStatus() 1019 status = GetTreeStatus()
942 if 'closed' == status: 1020 if 'closed' == status:
943 print ('The tree is closed. Please wait for it to reopen. Use ' 1021 print ('The tree is closed. Please wait for it to reopen. Use '
944 '"git cl dcommit -f" to commit on a closed tree.') 1022 '"git cl dcommit -f" to commit on a closed tree.')
945 return 1 1023 return 1
946 elif 'unknown' == status: 1024 elif 'unknown' == status:
947 print ('Unable to determine tree status. Please verify manually and ' 1025 print ('Unable to determine tree status. Please verify manually and '
(...skipping 19 matching lines...) Expand all
967 1045
968 description += "\n\nReview URL: %s" % cl.GetIssueURL() 1046 description += "\n\nReview URL: %s" % cl.GetIssueURL()
969 else: 1047 else:
970 if not description: 1048 if not description:
971 # Submitting TBR. See if there's already a description in Rietveld, else 1049 # Submitting TBR. See if there's already a description in Rietveld, else
972 # create a template description. Eitherway, give the user a chance to edit 1050 # create a template description. Eitherway, give the user a chance to edit
973 # it to fill in the TBR= field. 1051 # it to fill in the TBR= field.
974 if cl.GetIssue(): 1052 if cl.GetIssue():
975 description = cl.GetDescription() 1053 description = cl.GetDescription()
976 1054
1055 # TODO(dpranke): Update to use ChangeDescription object.
977 if not description: 1056 if not description:
978 description = """# Enter a description of the change. 1057 description = """# Enter a description of the change.
979 # This will be used as the change log for the commit. 1058 # This will be used as the change log for the commit.
980 1059
981 """ 1060 """
982 description += CreateDescriptionFromLog(args) 1061 description += CreateDescriptionFromLog(args)
983 1062
984 description = UserEditedLog(description + '\nTBR=') 1063 description = UserEditedLog(description + '\nTBR=')
985 1064
986 if not description: 1065 if not description:
(...skipping 321 matching lines...) Expand 10 before | Expand all | Expand 10 after
1308 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith ' 1387 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
1309 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e))) 1388 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
1310 1389
1311 # Not a known command. Default to help. 1390 # Not a known command. Default to help.
1312 GenUsage(parser, 'help') 1391 GenUsage(parser, 'help')
1313 return CMDhelp(parser, argv) 1392 return CMDhelp(parser, argv)
1314 1393
1315 1394
1316 if __name__ == '__main__': 1395 if __name__ == '__main__':
1317 sys.exit(main(sys.argv[1:])) 1396 sys.exit(main(sys.argv[1:]))
OLDNEW
« no previous file with comments | « no previous file | git_cl/test/owners.sh » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698