OLD | NEW |
1 #!/usr/bin/python | 1 #!/usr/bin/python |
2 # Copyright (c) 2010 The Chromium Authors. All rights reserved. | 2 # Copyright (c) 2010 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 """\ | 6 """\ |
7 Wrapper script around Rietveld's upload.py that simplifies working with groups | 7 Wrapper script around Rietveld's upload.py that simplifies working with groups |
8 of files. | 8 of files. |
9 """ | 9 """ |
10 | 10 |
(...skipping 21 matching lines...) Expand all Loading... |
32 json.loads | 32 json.loads |
33 except (ImportError, AttributeError): | 33 except (ImportError, AttributeError): |
34 # Import the one included in depot_tools. | 34 # Import the one included in depot_tools. |
35 sys.path.append(os.path.join(os.path.dirname(__file__), 'third_party')) | 35 sys.path.append(os.path.join(os.path.dirname(__file__), 'third_party')) |
36 import simplejson as json # pylint: disable=F0401 | 36 import simplejson as json # pylint: disable=F0401 |
37 | 37 |
38 import breakpad # pylint: disable=W0611 | 38 import breakpad # pylint: disable=W0611 |
39 | 39 |
40 # gcl now depends on gclient. | 40 # gcl now depends on gclient. |
41 from scm import SVN | 41 from scm import SVN |
42 | |
43 import gclient_utils | 42 import gclient_utils |
44 import owners | |
45 import presubmit_support | |
46 | 43 |
47 __version__ = '1.2' | 44 __version__ = '1.2' |
48 | 45 |
49 | 46 |
50 CODEREVIEW_SETTINGS = { | 47 CODEREVIEW_SETTINGS = { |
51 # To make gcl send reviews to a server, check in a file named | 48 # To make gcl send reviews to a server, check in a file named |
52 # "codereview.settings" (see |CODEREVIEW_SETTINGS_FILE| below) to your | 49 # "codereview.settings" (see |CODEREVIEW_SETTINGS_FILE| below) to your |
53 # project's base directory and add the following line to codereview.settings: | 50 # project's base directory and add the following line to codereview.settings: |
54 # CODE_REVIEW_SERVER: codereview.yourserver.org | 51 # CODE_REVIEW_SERVER: codereview.yourserver.org |
55 } | 52 } |
(...skipping 10 matching lines...) Expand all Loading... |
66 # Warning message when the change appears to be missing tests. | 63 # Warning message when the change appears to be missing tests. |
67 MISSING_TEST_MSG = "Change contains new or modified methods, but no new tests!" | 64 MISSING_TEST_MSG = "Change contains new or modified methods, but no new tests!" |
68 | 65 |
69 # Global cache of files cached in GetCacheDir(). | 66 # Global cache of files cached in GetCacheDir(). |
70 FILES_CACHE = {} | 67 FILES_CACHE = {} |
71 | 68 |
72 # Valid extensions for files we want to lint. | 69 # Valid extensions for files we want to lint. |
73 DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)" | 70 DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)" |
74 DEFAULT_LINT_IGNORE_REGEX = r"$^" | 71 DEFAULT_LINT_IGNORE_REGEX = r"$^" |
75 | 72 |
76 REVIEWERS_REGEX = r'\s*R=(.+)' | |
77 | 73 |
78 def CheckHomeForFile(filename): | 74 def CheckHomeForFile(filename): |
79 """Checks the users home dir for the existence of the given file. Returns | 75 """Checks the users home dir for the existence of the given file. Returns |
80 the path to the file if it's there, or None if it is not. | 76 the path to the file if it's there, or None if it is not. |
81 """ | 77 """ |
82 home_vars = ['HOME'] | 78 home_vars = ['HOME'] |
83 if sys.platform in ('cygwin', 'win32'): | 79 if sys.platform in ('cygwin', 'win32'): |
84 home_vars.append('USERPROFILE') | 80 home_vars.append('USERPROFILE') |
85 for home_var in home_vars: | 81 for home_var in home_vars: |
86 home = os.getenv(home_var) | 82 home = os.getenv(home_var) |
(...skipping 196 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
283 rietveld: rietveld server for this change | 279 rietveld: rietveld server for this change |
284 """ | 280 """ |
285 # Kept for unit test support. This is for the old format, it's deprecated. | 281 # Kept for unit test support. This is for the old format, it's deprecated. |
286 _SEPARATOR = "\n-----\n" | 282 _SEPARATOR = "\n-----\n" |
287 | 283 |
288 def __init__(self, name, issue, patchset, description, files, local_root, | 284 def __init__(self, name, issue, patchset, description, files, local_root, |
289 rietveld, needs_upload=False): | 285 rietveld, needs_upload=False): |
290 self.name = name | 286 self.name = name |
291 self.issue = int(issue) | 287 self.issue = int(issue) |
292 self.patchset = int(patchset) | 288 self.patchset = int(patchset) |
293 self._description = None | 289 self.description = description |
294 self._subject = None | |
295 self._reviewers = None | |
296 self._set_description(description) | |
297 if files is None: | 290 if files is None: |
298 files = [] | 291 files = [] |
299 self._files = files | 292 self._files = files |
300 self.patch = None | 293 self.patch = None |
301 self._local_root = local_root | 294 self._local_root = local_root |
302 self.needs_upload = needs_upload | 295 self.needs_upload = needs_upload |
303 self.rietveld = rietveld | 296 self.rietveld = rietveld |
304 if not self.rietveld: | 297 if not self.rietveld: |
305 # Set the default value. | 298 # Set the default value. |
306 self.rietveld = GetCodeReviewSetting('CODE_REVIEW_SERVER') | 299 self.rietveld = GetCodeReviewSetting('CODE_REVIEW_SERVER') |
307 | 300 |
308 def _get_description(self): | |
309 return self._description | |
310 | |
311 def _set_description(self, description): | |
312 # TODO(dpranke): Cloned from git_cl.py. These should be shared. | |
313 if not description: | |
314 self._description = description | |
315 return | |
316 | |
317 parsed_lines = [] | |
318 reviewers_re = re.compile(REVIEWERS_REGEX) | |
319 reviewers = '' | |
320 subject = '' | |
321 for l in description.splitlines(): | |
322 if not subject: | |
323 subject = l | |
324 matched_reviewers = reviewers_re.match(l) | |
325 if matched_reviewers: | |
326 reviewers = matched_reviewers.group(1) | |
327 parsed_lines.append(l) | |
328 | |
329 if len(subject) > 100: | |
330 subject = subject[:97] + '...' | |
331 | |
332 self._subject = subject | |
333 self._reviewers = reviewers | |
334 self._description = '\n'.join(parsed_lines) | |
335 | |
336 description = property(_get_description, _set_description) | |
337 | |
338 @property | |
339 def reviewers(self): | |
340 return self._reviewers | |
341 | |
342 @property | |
343 def subject(self): | |
344 return self._subject | |
345 | |
346 def NeedsUpload(self): | 301 def NeedsUpload(self): |
347 return self.needs_upload | 302 return self.needs_upload |
348 | 303 |
349 def GetFileNames(self): | 304 def GetFileNames(self): |
350 """Returns the list of file names included in this change.""" | 305 """Returns the list of file names included in this change.""" |
351 return [f[1] for f in self._files] | 306 return [f[1] for f in self._files] |
352 | 307 |
353 def GetFiles(self): | 308 def GetFiles(self): |
354 """Returns the list of files included in this change with their status.""" | 309 """Returns the list of files included in this change with their status.""" |
355 return self._files | 310 return self._files |
(...skipping 396 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
752 | 707 |
753 return editor | 708 return editor |
754 | 709 |
755 | 710 |
756 def GenerateDiff(files, root=None): | 711 def GenerateDiff(files, root=None): |
757 return SVN.GenerateDiff(files, root=root) | 712 return SVN.GenerateDiff(files, root=root) |
758 | 713 |
759 | 714 |
760 def OptionallyDoPresubmitChecks(change_info, committing, args): | 715 def OptionallyDoPresubmitChecks(change_info, committing, args): |
761 if FilterFlag(args, "--no_presubmit") or FilterFlag(args, "--force"): | 716 if FilterFlag(args, "--no_presubmit") or FilterFlag(args, "--force"): |
762 return presubmit_support.PresubmitOutput() | 717 return True |
763 return DoPresubmitChecks(change_info, committing, True) | 718 return DoPresubmitChecks(change_info, committing, True) |
764 | 719 |
765 | 720 |
766 def suggest_reviewers(change_info, affected_files): | |
767 owners_db = owners.Database(change_info.GetLocalRoot(), fopen=file, | |
768 os_path=os.path) | |
769 return owners_db.reviewers_for(affected_files) | |
770 | |
771 | |
772 def defer_attributes(a, b): | 721 def defer_attributes(a, b): |
773 """Copy attributes from an object (like a function) to another.""" | 722 """Copy attributes from an object (like a function) to another.""" |
774 for x in dir(a): | 723 for x in dir(a): |
775 if not getattr(b, x, None): | 724 if not getattr(b, x, None): |
776 setattr(b, x, getattr(a, x)) | 725 setattr(b, x, getattr(a, x)) |
777 | 726 |
778 | 727 |
779 def need_change(function): | 728 def need_change(function): |
780 """Converts args -> change_info.""" | 729 """Converts args -> change_info.""" |
781 # pylint: disable=W0612,W0621 | 730 # pylint: disable=W0612,W0621 |
(...skipping 59 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
841 def CMDupload(change_info, args): | 790 def CMDupload(change_info, args): |
842 """Uploads the changelist to the server for review. | 791 """Uploads the changelist to the server for review. |
843 | 792 |
844 This does not submit a try job; use gcl try to submit a try job. | 793 This does not submit a try job; use gcl try to submit a try job. |
845 """ | 794 """ |
846 if '-s' in args or '--server' in args: | 795 if '-s' in args or '--server' in args: |
847 ErrorExit('Don\'t use the -s flag, fix codereview.settings instead') | 796 ErrorExit('Don\'t use the -s flag, fix codereview.settings instead') |
848 if not change_info.GetFiles(): | 797 if not change_info.GetFiles(): |
849 print "Nothing to upload, changelist is empty." | 798 print "Nothing to upload, changelist is empty." |
850 return 0 | 799 return 0 |
851 | 800 if not OptionallyDoPresubmitChecks(change_info, False, args): |
852 output = OptionallyDoPresubmitChecks(change_info, False, args) | |
853 if not output.should_continue(): | |
854 return 1 | 801 return 1 |
855 no_watchlists = (FilterFlag(args, "--no_watchlists") or | 802 no_watchlists = (FilterFlag(args, "--no_watchlists") or |
856 FilterFlag(args, "--no-watchlists")) | 803 FilterFlag(args, "--no-watchlists")) |
857 | 804 |
858 # Map --send-mail to --send_mail | 805 # Map --send-mail to --send_mail |
859 if FilterFlag(args, "--send-mail"): | 806 if FilterFlag(args, "--send-mail"): |
860 args.append("--send_mail") | 807 args.append("--send_mail") |
861 | 808 |
862 upload_arg = ["upload.py", "-y"] | 809 upload_arg = ["upload.py", "-y"] |
863 upload_arg.append("--server=%s" % change_info.rietveld) | 810 upload_arg.append("--server=%s" % change_info.rietveld) |
864 | |
865 reviewers = change_info.reviewers or output.reviewers | |
866 if (reviewers and | |
867 not any(arg.startswith('-r') or arg.startswith('--reviewer') for | |
868 arg in args)): | |
869 upload_arg.append('--reviewers=%s' % ','.join(reviewers)) | |
870 | |
871 upload_arg.extend(args) | 811 upload_arg.extend(args) |
872 | 812 |
873 desc_file = "" | 813 desc_file = "" |
874 if change_info.issue: # Uploading a new patchset. | 814 if change_info.issue: # Uploading a new patchset. |
875 found_message = False | 815 found_message = False |
876 for arg in args: | 816 for arg in args: |
877 if arg.startswith("--message") or arg.startswith("-m"): | 817 if arg.startswith("--message") or arg.startswith("-m"): |
878 found_message = True | 818 found_message = True |
879 break | 819 break |
880 | 820 |
(...skipping 13 matching lines...) Expand all Loading... |
894 watchlist = watchlists.Watchlists(change_info.GetLocalRoot()) | 834 watchlist = watchlists.Watchlists(change_info.GetLocalRoot()) |
895 watchers = watchlist.GetWatchersForPaths(change_info.GetFileNames()) | 835 watchers = watchlist.GetWatchersForPaths(change_info.GetFileNames()) |
896 | 836 |
897 cc_list = GetCodeReviewSetting("CC_LIST") | 837 cc_list = GetCodeReviewSetting("CC_LIST") |
898 if not no_watchlists and watchers: | 838 if not no_watchlists and watchers: |
899 # Filter out all empty elements and join by ',' | 839 # Filter out all empty elements and join by ',' |
900 cc_list = ','.join(filter(None, [cc_list] + watchers)) | 840 cc_list = ','.join(filter(None, [cc_list] + watchers)) |
901 if cc_list: | 841 if cc_list: |
902 upload_arg.append("--cc=" + cc_list) | 842 upload_arg.append("--cc=" + cc_list) |
903 upload_arg.append("--description_file=" + desc_file + "") | 843 upload_arg.append("--description_file=" + desc_file + "") |
904 if change_info.subject: | 844 if change_info.description: |
905 upload_arg.append("--message=" + change_info.subject) | 845 subject = change_info.description[:77] |
| 846 if subject.find("\r\n") != -1: |
| 847 subject = subject[:subject.find("\r\n")] |
| 848 if subject.find("\n") != -1: |
| 849 subject = subject[:subject.find("\n")] |
| 850 if len(change_info.description) > 77: |
| 851 subject = subject + "..." |
| 852 upload_arg.append("--message=" + subject) |
906 | 853 |
907 if GetCodeReviewSetting("PRIVATE") == "True": | 854 if GetCodeReviewSetting("PRIVATE") == "True": |
908 upload_arg.append("--private") | 855 upload_arg.append("--private") |
909 | 856 |
910 # Change the current working directory before calling upload.py so that it | 857 # Change the current working directory before calling upload.py so that it |
911 # shows the correct base. | 858 # shows the correct base. |
912 previous_cwd = os.getcwd() | 859 previous_cwd = os.getcwd() |
913 os.chdir(change_info.GetLocalRoot()) | 860 os.chdir(change_info.GetLocalRoot()) |
914 # If we have a lot of files with long paths, then we won't be able to fit | 861 # If we have a lot of files with long paths, then we won't be able to fit |
915 # the command to "svn diff". Instead, we generate the diff manually for | 862 # the command to "svn diff". Instead, we generate the diff manually for |
(...skipping 111 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
1027 output = RunShell(commit_cmd, True) | 974 output = RunShell(commit_cmd, True) |
1028 os.remove(commit_filename) | 975 os.remove(commit_filename) |
1029 os.remove(targets_filename) | 976 os.remove(targets_filename) |
1030 if output.find("Committed revision") != -1: | 977 if output.find("Committed revision") != -1: |
1031 change_info.Delete() | 978 change_info.Delete() |
1032 | 979 |
1033 if change_info.issue: | 980 if change_info.issue: |
1034 revision = re.compile(".*?\nCommitted revision (\d+)", | 981 revision = re.compile(".*?\nCommitted revision (\d+)", |
1035 re.DOTALL).match(output).group(1) | 982 re.DOTALL).match(output).group(1) |
1036 viewvc_url = GetCodeReviewSetting("VIEW_VC") | 983 viewvc_url = GetCodeReviewSetting("VIEW_VC") |
1037 change_info.description += '\n' | 984 change_info.description = change_info.description + '\n' |
1038 if viewvc_url: | 985 if viewvc_url: |
1039 change_info.description += "\nCommitted: " + viewvc_url + revision | 986 change_info.description += "\nCommitted: " + viewvc_url + revision |
1040 change_info.CloseIssue() | 987 change_info.CloseIssue() |
1041 os.chdir(previous_cwd) | 988 os.chdir(previous_cwd) |
1042 return 0 | 989 return 0 |
1043 | 990 |
1044 | 991 |
1045 def CMDchange(args): | 992 def CMDchange(args): |
1046 """Creates or edits a changelist. | 993 """Creates or edits a changelist. |
1047 | 994 |
(...skipping 41 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
1089 description = change_info.description | 1036 description = change_info.description |
1090 | 1037 |
1091 other_files = GetFilesNotInCL() | 1038 other_files = GetFilesNotInCL() |
1092 | 1039 |
1093 # Edited files (as opposed to files with only changed properties) will have | 1040 # Edited files (as opposed to files with only changed properties) will have |
1094 # a letter for the first character in the status string. | 1041 # a letter for the first character in the status string. |
1095 file_re = re.compile(r"^[a-z].+\Z", re.IGNORECASE) | 1042 file_re = re.compile(r"^[a-z].+\Z", re.IGNORECASE) |
1096 affected_files = [x for x in other_files if file_re.match(x[0])] | 1043 affected_files = [x for x in other_files if file_re.match(x[0])] |
1097 unaffected_files = [x for x in other_files if not file_re.match(x[0])] | 1044 unaffected_files = [x for x in other_files if not file_re.match(x[0])] |
1098 | 1045 |
1099 suggested_reviewers = suggest_reviewers(change_info, affected_files) | |
1100 if suggested_reviewers: | |
1101 reviewers_re = re.compile(REVIEWERS_REGEX) | |
1102 if not any( | |
1103 reviewers_re.match(l) for l in description.splitlines()): | |
1104 description += '\nR=' + ','.join(suggested_reviewers) + '\n' | |
1105 | |
1106 separator1 = ("\n---All lines above this line become the description.\n" | 1046 separator1 = ("\n---All lines above this line become the description.\n" |
1107 "---Repository Root: " + change_info.GetLocalRoot() + "\n" | 1047 "---Repository Root: " + change_info.GetLocalRoot() + "\n" |
1108 "---Paths in this changelist (" + change_info.name + "):\n") | 1048 "---Paths in this changelist (" + change_info.name + "):\n") |
1109 separator2 = "\n\n---Paths modified but not in any changelist:\n\n" | 1049 separator2 = "\n\n---Paths modified but not in any changelist:\n\n" |
1110 text = (description + separator1 + '\n' + | 1050 text = (description + separator1 + '\n' + |
1111 '\n'.join([f[0] + f[1] for f in change_info.GetFiles()])) | 1051 '\n'.join([f[0] + f[1] for f in change_info.GetFiles()])) |
1112 | 1052 |
1113 if change_info.Exists(): | 1053 if change_info.Exists(): |
1114 text += (separator2 + | 1054 text += (separator2 + |
1115 '\n'.join([f[0] + f[1] for f in affected_files]) + '\n') | 1055 '\n'.join([f[0] + f[1] for f in affected_files]) + '\n') |
(...skipping 101 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
1217 else: | 1157 else: |
1218 print "Skipping file %s" % filename | 1158 print "Skipping file %s" % filename |
1219 | 1159 |
1220 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count | 1160 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count |
1221 os.chdir(previous_cwd) | 1161 os.chdir(previous_cwd) |
1222 return 1 | 1162 return 1 |
1223 | 1163 |
1224 | 1164 |
1225 def DoPresubmitChecks(change_info, committing, may_prompt): | 1165 def DoPresubmitChecks(change_info, committing, may_prompt): |
1226 """Imports presubmit, then calls presubmit.DoPresubmitChecks.""" | 1166 """Imports presubmit, then calls presubmit.DoPresubmitChecks.""" |
| 1167 # Need to import here to avoid circular dependency. |
| 1168 import presubmit_support |
1227 root_presubmit = GetCachedFile('PRESUBMIT.py', use_root=True) | 1169 root_presubmit = GetCachedFile('PRESUBMIT.py', use_root=True) |
1228 change = presubmit_support.SvnChange(change_info.name, | 1170 change = presubmit_support.SvnChange(change_info.name, |
1229 change_info.description, | 1171 change_info.description, |
1230 change_info.GetLocalRoot(), | 1172 change_info.GetLocalRoot(), |
1231 change_info.GetFiles(), | 1173 change_info.GetFiles(), |
1232 change_info.issue, | 1174 change_info.issue, |
1233 change_info.patchset) | 1175 change_info.patchset) |
1234 output = presubmit_support.DoPresubmitChecks(change=change, | 1176 output = presubmit_support.DoPresubmitChecks(change=change, |
1235 committing=committing, | 1177 committing=committing, |
1236 verbose=False, | 1178 verbose=False, |
1237 output_stream=sys.stdout, | 1179 output_stream=sys.stdout, |
1238 input_stream=sys.stdin, | 1180 input_stream=sys.stdin, |
1239 default_presubmit=root_presubmit, | 1181 default_presubmit=root_presubmit, |
1240 may_prompt=may_prompt, | 1182 may_prompt=may_prompt) |
1241 tbr=False, | |
1242 host_url=change_info.rietveld) | |
1243 if not output.should_continue() and may_prompt: | 1183 if not output.should_continue() and may_prompt: |
1244 # TODO(dpranke): move into DoPresubmitChecks(), unify cmd line args. | 1184 # TODO(dpranke): move into DoPresubmitChecks(), unify cmd line args. |
1245 print "\nPresubmit errors, can't continue (use --no_presubmit to bypass)" | 1185 print "\nPresubmit errors, can't continue (use --no_presubmit to bypass)" |
1246 | 1186 |
1247 return output | 1187 # TODO(dpranke): Return the output object and make use of it. |
| 1188 return output.should_continue() |
1248 | 1189 |
1249 | 1190 |
1250 @no_args | 1191 @no_args |
1251 def CMDchanges(): | 1192 def CMDchanges(): |
1252 """Lists all the changelists and their files.""" | 1193 """Lists all the changelists and their files.""" |
1253 for cl in GetCLs(): | 1194 for cl in GetCLs(): |
1254 change_info = ChangeInfo.Load(cl, GetRepositoryRoot(), True, True) | 1195 change_info = ChangeInfo.Load(cl, GetRepositoryRoot(), True, True) |
1255 print "\n--- Changelist " + change_info.name + ":" | 1196 print "\n--- Changelist " + change_info.name + ":" |
1256 for filename in change_info.GetFiles(): | 1197 for filename in change_info.GetFiles(): |
1257 print "".join(filename) | 1198 print "".join(filename) |
(...skipping 203 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
1461 if e.code != 500: | 1402 if e.code != 500: |
1462 raise | 1403 raise |
1463 print >> sys.stderr, ( | 1404 print >> sys.stderr, ( |
1464 'AppEngine is misbehaving and returned HTTP %d, again. Keep faith ' | 1405 'AppEngine is misbehaving and returned HTTP %d, again. Keep faith ' |
1465 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)) | 1406 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)) |
1466 return 1 | 1407 return 1 |
1467 | 1408 |
1468 | 1409 |
1469 if __name__ == "__main__": | 1410 if __name__ == "__main__": |
1470 sys.exit(main(sys.argv[1:])) | 1411 sys.exit(main(sys.argv[1:])) |
OLD | NEW |