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

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: clean up logging 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=(.+)')
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):
747 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
675 import presubmit_support 748 import presubmit_support
676 import scm 749 import scm
677 import watchlists 750 import watchlists
678 751
679 root = RunCommand(['git', 'rev-parse', '--show-cdup']).strip() 752 root = RunCommand(['git', 'rev-parse', '--show-cdup']).strip()
680 if not root: 753 if not root:
681 root = "." 754 root = "."
682 absroot = os.path.abspath(root) 755 absroot = os.path.abspath(root)
683 if not root: 756 if not root:
684 raise Exception("Could not get root directory.") 757 raise Exception("Could not get root directory.")
(...skipping 17 matching lines...) Expand all
702 issue, patchset) 775 issue, patchset)
703 776
704 # Apply watchlists on upload. 777 # Apply watchlists on upload.
705 if not committing: 778 if not committing:
706 watchlist = watchlists.Watchlists(change.RepositoryRoot()) 779 watchlist = watchlists.Watchlists(change.RepositoryRoot())
707 files = [f.LocalPath() for f in change.AffectedFiles()] 780 files = [f.LocalPath() for f in change.AffectedFiles()]
708 watchers = watchlist.GetWatchersForPaths(files) 781 watchers = watchlist.GetWatchersForPaths(files)
709 RunCommand(['git', 'config', '--replace-all', 782 RunCommand(['git', 'config', '--replace-all',
710 'rietveld.extracc', ','.join(watchers)]) 783 'rietveld.extracc', ','.join(watchers)])
711 784
712 return presubmit_support.DoPresubmitChecks(change, committing, 785 output = StringIO.StringIO()
713 verbose=None, output_stream=sys.stdout, input_stream=sys.stdin, 786 res = presubmit_support.DoPresubmitChecks(change, committing,
714 default_presubmit=None, may_prompt=None) 787 verbose=None, output_stream=output, input_stream=sys.stdin,
788 default_presubmit=None, may_prompt=None, tbr=tbr,
M-A Ruel 2011/03/11 23:11:14 You disable may_prompt so that converts all warnin
789 host_url=cl.GetRietveldServer())
790 hook_results = HookResults(output.getvalue())
791 if hook_results.output:
792 print hook_results.output
793
794 # TODO(dpranke): We should propagate the error out instead of calling exit().
795 if not res:
796 sys.exit(1)
797 return hook_results
715 798
716 799
717 def CMDpresubmit(parser, args): 800 def CMDpresubmit(parser, args):
718 """run presubmit tests on the current changelist""" 801 """run presubmit tests on the current changelist"""
719 parser.add_option('--upload', action='store_true', 802 parser.add_option('--upload', action='store_true',
720 help='Run upload hook instead of the push/dcommit hook') 803 help='Run upload hook instead of the push/dcommit hook')
721 (options, args) = parser.parse_args(args) 804 (options, args) = parser.parse_args(args)
722 805
723 # Make sure index is up-to-date before running diff-index. 806 # Make sure index is up-to-date before running diff-index.
724 RunGit(['update-index', '--refresh', '-q'], error_ok=True) 807 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
725 if RunGit(['diff-index', 'HEAD']): 808 if RunGit(['diff-index', 'HEAD']):
726 # TODO(maruel): Is this really necessary? 809 # TODO(maruel): Is this really necessary?
727 print 'Cannot presubmit with a dirty tree. You must commit locally first.' 810 print 'Cannot presubmit with a dirty tree. You must commit locally first.'
728 return 1 811 return 1
729 812
813 cl = Changelist()
730 if args: 814 if args:
731 base_branch = args[0] 815 base_branch = args[0]
732 else: 816 else:
733 # Default to diffing against the "upstream" branch. 817 # Default to diffing against the "upstream" branch.
734 base_branch = Changelist().GetUpstreamBranch() 818 base_branch = cl.GetUpstreamBranch()
735 819
736 if options.upload: 820 if options.upload:
737 print '*** Presubmit checks for UPLOAD would report: ***' 821 print '*** Presubmit checks for UPLOAD would report: ***'
738 return RunHook(committing=False, upstream_branch=base_branch) 822 RunHook(committing=False, upstream_branch=base_branch,
823 rietveld_server=cl.GetRietveldServer(), tbr=False)
824 return 0
739 else: 825 else:
740 print '*** Presubmit checks for DCOMMIT would report: ***' 826 print '*** Presubmit checks for DCOMMIT would report: ***'
741 return RunHook(committing=True, upstream_branch=base_branch) 827 RunHook(committing=True, upstream_branch=base_branch,
828 rietveld_server=cl.GetRietveldServer, tbr=False)
829 return 0
742 830
743 831
744 @usage('[args to "git diff"]') 832 @usage('[args to "git diff"]')
745 def CMDupload(parser, args): 833 def CMDupload(parser, args):
746 """upload the current changelist to codereview""" 834 """upload the current changelist to codereview"""
747 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks', 835 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
748 help='bypass upload presubmit hook') 836 help='bypass upload presubmit hook')
749 parser.add_option('-m', dest='message', help='message for patch') 837 parser.add_option('-m', dest='message', help='message for patch')
750 parser.add_option('-r', '--reviewers', 838 parser.add_option('-r', '--reviewers',
751 help='reviewer email addresses') 839 help='reviewer email addresses')
(...skipping 18 matching lines...) Expand all
770 858
771 cl = Changelist() 859 cl = Changelist()
772 if args: 860 if args:
773 base_branch = args[0] 861 base_branch = args[0]
774 else: 862 else:
775 # Default to diffing against the "upstream" branch. 863 # Default to diffing against the "upstream" branch.
776 base_branch = cl.GetUpstreamBranch() 864 base_branch = cl.GetUpstreamBranch()
777 args = [base_branch + "..."] 865 args = [base_branch + "..."]
778 866
779 if not options.bypass_hooks: 867 if not options.bypass_hooks:
780 RunHook(committing=False, upstream_branch=base_branch) 868 hook_results = RunHook(committing=False, upstream_branch=base_branch,
869 rietveld_server=cl.GetRietveldServer(), tbr=False)
870 else:
871 hook_results = HookResults()
872
873 if not options.reviewers and hook_results.reviewers:
874 options.reviewers = hook_results.reviewers
781 875
782 # --no-ext-diff is broken in some versions of Git, so try to work around 876 # --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 877 # this by overriding the environment (but there is still a problem if the
784 # git config key "diff.external" is used). 878 # git config key "diff.external" is used).
785 env = os.environ.copy() 879 env = os.environ.copy()
786 if 'GIT_EXTERNAL_DIFF' in env: 880 if 'GIT_EXTERNAL_DIFF' in env:
787 del env['GIT_EXTERNAL_DIFF'] 881 del env['GIT_EXTERNAL_DIFF']
788 subprocess.call(['git', 'diff', '--no-ext-diff', '--stat', '-M'] + args, 882 subprocess.call(['git', 'diff', '--no-ext-diff', '--stat', '-M'] + args,
789 env=env) 883 env=env)
790 884
791 upload_args = ['--assume_yes'] # Don't ask about untracked files. 885 upload_args = ['--assume_yes'] # Don't ask about untracked files.
792 upload_args.extend(['--server', cl.GetRietveldServer()]) 886 upload_args.extend(['--server', cl.GetRietveldServer()])
793 if options.reviewers:
794 upload_args.extend(['--reviewers', options.reviewers])
795 if options.emulate_svn_auto_props: 887 if options.emulate_svn_auto_props:
796 upload_args.append('--emulate_svn_auto_props') 888 upload_args.append('--emulate_svn_auto_props')
797 if options.send_mail: 889 if options.send_mail:
798 if not options.reviewers: 890 if not options.reviewers:
799 DieWithError("Must specify reviewers to send email.") 891 DieWithError("Must specify reviewers to send email.")
800 upload_args.append('--send_mail') 892 upload_args.append('--send_mail')
801 if options.from_logs and not options.message: 893 if options.from_logs and not options.message:
802 print 'Must set message for subject line if using desc_from_logs' 894 print 'Must set message for subject line if using desc_from_logs'
803 return 1 895 return 1
804 896
805 change_desc = None 897 change_desc = None
806 898
807 if cl.GetIssue(): 899 if cl.GetIssue():
808 if options.message: 900 if options.message:
809 upload_args.extend(['--message', options.message]) 901 upload_args.extend(['--message', options.message])
810 upload_args.extend(['--issue', cl.GetIssue()]) 902 upload_args.extend(['--issue', cl.GetIssue()])
811 print ("This branch is associated with issue %s. " 903 print ("This branch is associated with issue %s. "
812 "Adding patch to that issue." % cl.GetIssue()) 904 "Adding patch to that issue." % cl.GetIssue())
813 else: 905 else:
814 log_desc = CreateDescriptionFromLog(args) 906 log_desc = CreateDescriptionFromLog(args)
815 if options.from_logs: 907 change_desc = ChangeDescription(options.message, log_desc,
816 # Uses logs as description and message as subject. 908 options.reviewers)
817 subject = options.message 909 if not options.from_logs:
818 change_desc = subject + '\n\n' + log_desc 910 change_desc.Update()
819 else: 911
820 initial_text = """# Enter a description of the change. 912 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." 913 print "Description is empty; aborting."
834 return 1 914 return 1
835 upload_args.extend(['--message', subject]) 915
836 upload_args.extend(['--description', change_desc]) 916 upload_args.extend(['--message', change_desc.subject])
917 upload_args.extend(['--description', change_desc.description])
918 if change_desc.reviewers:
919 upload_args.extend(['--reviewers', change_desc.reviewers])
837 cc = ','.join(filter(None, (settings.GetCCList(), options.cc))) 920 cc = ','.join(filter(None, (settings.GetCCList(), options.cc)))
838 if cc: 921 if cc:
839 upload_args.extend(['--cc', cc]) 922 upload_args.extend(['--cc', cc])
840 923
841 # Include the upstream repo's URL in the change -- this is useful for 924 # Include the upstream repo's URL in the change -- this is useful for
842 # projects that have their source spread across multiple repos. 925 # projects that have their source spread across multiple repos.
843 remote_url = None 926 remote_url = None
844 if settings.GetIsGitSvn(): 927 if settings.GetIsGitSvn():
845 # URL is dependent on the current directory. 928 # URL is dependent on the current directory.
846 data = RunGit(['svn', 'info'], cwd=settings.GetRoot()) 929 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
(...skipping 11 matching lines...) Expand all
858 try: 941 try:
859 issue, patchset = upload.RealMain(['upload'] + upload_args + args) 942 issue, patchset = upload.RealMain(['upload'] + upload_args + args)
860 except: 943 except:
861 # If we got an exception after the user typed a description for their 944 # If we got an exception after the user typed a description for their
862 # change, back up the description before re-raising. 945 # change, back up the description before re-raising.
863 if change_desc: 946 if change_desc:
864 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE) 947 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
865 print '\nGot exception while uploading -- saving description to %s\n' \ 948 print '\nGot exception while uploading -- saving description to %s\n' \
866 % backup_path 949 % backup_path
867 backup_file = open(backup_path, 'w') 950 backup_file = open(backup_path, 'w')
868 backup_file.write(change_desc) 951 backup_file.write(change_desc.description)
869 backup_file.close() 952 backup_file.close()
870 raise 953 raise
871 954
872 if not cl.GetIssue(): 955 if not cl.GetIssue():
873 cl.SetIssue(issue) 956 cl.SetIssue(issue)
874 cl.SetPatchset(patchset) 957 cl.SetPatchset(patchset)
875 return 0 958 return 0
876 959
877 960
878 def SendUpstream(parser, args, cmd): 961 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', 1009 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
927 '--pretty=format:%H']) 1010 '--pretty=format:%H'])
928 extra_commits = RunGit(['rev-list', '^' + svn_head, base_branch]) 1011 extra_commits = RunGit(['rev-list', '^' + svn_head, base_branch])
929 if extra_commits: 1012 if extra_commits:
930 print ('This branch has %d additional commits not upstreamed yet.' 1013 print ('This branch has %d additional commits not upstreamed yet.'
931 % len(extra_commits.splitlines())) 1014 % len(extra_commits.splitlines()))
932 print ('Upstream "%s" or rebase this branch on top of the upstream trunk ' 1015 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
933 'before attempting to %s.' % (base_branch, cmd)) 1016 'before attempting to %s.' % (base_branch, cmd))
934 return 1 1017 return 1
935 1018
936 if not options.force and not options.bypass_hooks: 1019 if not options.bypass_hooks:
937 RunHook(committing=False, upstream_branch=base_branch) 1020 hook_results = RunHook(committing=True, upstream_branch=base_branch,
1021 rietveld_server=cl.GetRietveldServer(), tbr=options.tbr)
938 1022
939 if cmd == 'dcommit': 1023 if cmd == 'dcommit':
940 # Check the tree status if the tree status URL is set. 1024 # Check the tree status if the tree status URL is set.
941 status = GetTreeStatus() 1025 status = GetTreeStatus()
942 if 'closed' == status: 1026 if 'closed' == status:
943 print ('The tree is closed. Please wait for it to reopen. Use ' 1027 print ('The tree is closed. Please wait for it to reopen. Use '
944 '"git cl dcommit -f" to commit on a closed tree.') 1028 '"git cl dcommit -f" to commit on a closed tree.')
945 return 1 1029 return 1
946 elif 'unknown' == status: 1030 elif 'unknown' == status:
947 print ('Unable to determine tree status. Please verify manually and ' 1031 print ('Unable to determine tree status. Please verify manually and '
(...skipping 19 matching lines...) Expand all
967 1051
968 description += "\n\nReview URL: %s" % cl.GetIssueURL() 1052 description += "\n\nReview URL: %s" % cl.GetIssueURL()
969 else: 1053 else:
970 if not description: 1054 if not description:
971 # Submitting TBR. See if there's already a description in Rietveld, else 1055 # 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 1056 # create a template description. Eitherway, give the user a chance to edit
973 # it to fill in the TBR= field. 1057 # it to fill in the TBR= field.
974 if cl.GetIssue(): 1058 if cl.GetIssue():
975 description = cl.GetDescription() 1059 description = cl.GetDescription()
976 1060
1061 # TODO(dpranke): Update to use ChangeDescription object.
977 if not description: 1062 if not description:
978 description = """# Enter a description of the change. 1063 description = """# Enter a description of the change.
979 # This will be used as the change log for the commit. 1064 # This will be used as the change log for the commit.
980 1065
981 """ 1066 """
982 description += CreateDescriptionFromLog(args) 1067 description += CreateDescriptionFromLog(args)
983 1068
984 description = UserEditedLog(description + '\nTBR=') 1069 description = UserEditedLog(description + '\nTBR=')
985 1070
986 if not description: 1071 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 ' 1393 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
1309 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e))) 1394 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
1310 1395
1311 # Not a known command. Default to help. 1396 # Not a known command. Default to help.
1312 GenUsage(parser, 'help') 1397 GenUsage(parser, 'help')
1313 return CMDhelp(parser, argv) 1398 return CMDhelp(parser, argv)
1314 1399
1315 1400
1316 if __name__ == '__main__': 1401 if __name__ == '__main__':
1317 sys.exit(main(sys.argv[1:])) 1402 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