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 """\ | 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 |
11 import json | 11 import json |
12 import optparse | 12 import optparse |
13 import os | 13 import os |
14 import random | 14 import random |
15 import re | 15 import re |
16 import string | 16 import string |
17 import sys | 17 import sys |
18 import tempfile | 18 import tempfile |
19 import time | 19 import time |
20 import urllib2 | 20 import urllib2 |
21 | 21 |
22 import breakpad # pylint: disable=W0611 | 22 import breakpad # pylint: disable=W0611 |
23 | 23 |
24 | 24 |
25 import fix_encoding | 25 import fix_encoding |
26 import gclient_utils | 26 import gclient_utils |
| 27 import git_cl |
27 import presubmit_support | 28 import presubmit_support |
28 import rietveld | 29 import rietveld |
29 from scm import SVN | 30 from scm import SVN |
30 import subprocess2 | 31 import subprocess2 |
31 from third_party import upload | 32 from third_party import upload |
32 | 33 |
33 __version__ = '1.2.1' | 34 __version__ = '1.2.1' |
34 | 35 |
35 | 36 |
36 CODEREVIEW_SETTINGS = { | 37 CODEREVIEW_SETTINGS = { |
(...skipping 15 matching lines...) Expand all Loading... |
52 # Warning message when the change appears to be missing tests. | 53 # Warning message when the change appears to be missing tests. |
53 MISSING_TEST_MSG = "Change contains new or modified methods, but no new tests!" | 54 MISSING_TEST_MSG = "Change contains new or modified methods, but no new tests!" |
54 | 55 |
55 # Global cache of files cached in GetCacheDir(). | 56 # Global cache of files cached in GetCacheDir(). |
56 FILES_CACHE = {} | 57 FILES_CACHE = {} |
57 | 58 |
58 # Valid extensions for files we want to lint. | 59 # Valid extensions for files we want to lint. |
59 DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)" | 60 DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)" |
60 DEFAULT_LINT_IGNORE_REGEX = r"$^" | 61 DEFAULT_LINT_IGNORE_REGEX = r"$^" |
61 | 62 |
62 REVIEWERS_REGEX = r'\s*R=(.+)' | |
63 | |
64 def CheckHomeForFile(filename): | 63 def CheckHomeForFile(filename): |
65 """Checks the users home dir for the existence of the given file. Returns | 64 """Checks the users home dir for the existence of the given file. Returns |
66 the path to the file if it's there, or None if it is not. | 65 the path to the file if it's there, or None if it is not. |
67 """ | 66 """ |
68 home_vars = ['HOME'] | 67 home_vars = ['HOME'] |
69 if sys.platform in ('cygwin', 'win32'): | 68 if sys.platform in ('cygwin', 'win32'): |
70 home_vars.append('USERPROFILE') | 69 home_vars.append('USERPROFILE') |
71 for home_var in home_vars: | 70 for home_var in home_vars: |
72 home = os.getenv(home_var) | 71 home = os.getenv(home_var) |
73 if home != None: | 72 if home != None: |
(...skipping 192 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
266 files: a list of 2 tuple containing (status, filename) of changed files, | 265 files: a list of 2 tuple containing (status, filename) of changed files, |
267 with paths being relative to the top repository directory. | 266 with paths being relative to the top repository directory. |
268 local_root: Local root directory | 267 local_root: Local root directory |
269 rietveld: rietveld server for this change | 268 rietveld: rietveld server for this change |
270 """ | 269 """ |
271 # Kept for unit test support. This is for the old format, it's deprecated. | 270 # Kept for unit test support. This is for the old format, it's deprecated. |
272 SEPARATOR = "\n-----\n" | 271 SEPARATOR = "\n-----\n" |
273 | 272 |
274 def __init__(self, name, issue, patchset, description, files, local_root, | 273 def __init__(self, name, issue, patchset, description, files, local_root, |
275 rietveld_url, needs_upload): | 274 rietveld_url, needs_upload): |
| 275 # Defer the description processing to git_cl.ChangeDescription. |
| 276 self._desc = git_cl.ChangeDescription(description) |
276 self.name = name | 277 self.name = name |
277 self.issue = int(issue) | 278 self.issue = int(issue) |
278 self.patchset = int(patchset) | 279 self.patchset = int(patchset) |
279 self._description = None | 280 self._files = files or [] |
280 self._reviewers = None | |
281 self._set_description(description) | |
282 if files is None: | |
283 files = [] | |
284 self._files = files | |
285 self.patch = None | 281 self.patch = None |
286 self._local_root = local_root | 282 self._local_root = local_root |
287 self.needs_upload = needs_upload | 283 self.needs_upload = needs_upload |
288 self.rietveld = gclient_utils.UpgradeToHttps( | 284 self.rietveld = gclient_utils.UpgradeToHttps( |
289 rietveld_url or GetCodeReviewSetting('CODE_REVIEW_SERVER')) | 285 rietveld_url or GetCodeReviewSetting('CODE_REVIEW_SERVER')) |
290 self._rpc_server = None | 286 self._rpc_server = None |
291 | 287 |
292 def _get_description(self): | 288 @property |
293 return self._description | 289 def description(self): |
| 290 return self._desc.description |
294 | 291 |
295 def _set_description(self, description): | 292 def force_description(self, new_description): |
296 # TODO(dpranke): Cloned from git_cl.py. These should be shared. | 293 self._desc = git_cl.ChangeDescription(new_description) |
297 if not description: | 294 self.needs_upload = True |
298 self._description = description | |
299 return | |
300 | 295 |
301 parsed_lines = [] | 296 def append_footer(self, line): |
302 reviewers_re = re.compile(REVIEWERS_REGEX) | 297 self._desc.append_footer(line) |
303 reviewers = '' | |
304 for l in description.splitlines(): | |
305 matched_reviewers = reviewers_re.match(l) | |
306 if matched_reviewers: | |
307 reviewers = matched_reviewers.group(1).split(',') | |
308 parsed_lines.append(l) | |
309 self._reviewers = reviewers | |
310 self._description = '\n'.join(parsed_lines) | |
311 | 298 |
312 description = property(_get_description, _set_description) | 299 def get_reviewers(self): |
313 | 300 return self._desc.get_reviewers() |
314 @property | |
315 def reviewers(self): | |
316 return self._reviewers | |
317 | 301 |
318 def NeedsUpload(self): | 302 def NeedsUpload(self): |
319 return self.needs_upload | 303 return self.needs_upload |
320 | 304 |
321 def GetFileNames(self): | 305 def GetFileNames(self): |
322 """Returns the list of file names included in this change.""" | 306 """Returns the list of file names included in this change.""" |
323 return [f[1] for f in self._files] | 307 return [f[1] for f in self._files] |
324 | 308 |
325 def GetFiles(self): | 309 def GetFiles(self): |
326 """Returns the list of files included in this change with their status.""" | 310 """Returns the list of files included in this change with their status.""" |
(...skipping 54 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
381 ctype, body = upload.EncodeMultipartFormData(data, []) | 365 ctype, body = upload.EncodeMultipartFormData(data, []) |
382 self.SendToRietveld('/%d/close' % self.issue, payload=body, | 366 self.SendToRietveld('/%d/close' % self.issue, payload=body, |
383 content_type=ctype) | 367 content_type=ctype) |
384 | 368 |
385 def UpdateRietveldDescription(self): | 369 def UpdateRietveldDescription(self): |
386 """Sets the description for an issue on Rietveld.""" | 370 """Sets the description for an issue on Rietveld.""" |
387 data = [("description", self.description),] | 371 data = [("description", self.description),] |
388 ctype, body = upload.EncodeMultipartFormData(data, []) | 372 ctype, body = upload.EncodeMultipartFormData(data, []) |
389 self.SendToRietveld('/%d/description' % self.issue, payload=body, | 373 self.SendToRietveld('/%d/description' % self.issue, payload=body, |
390 content_type=ctype) | 374 content_type=ctype) |
| 375 self.needs_upload = False |
391 | 376 |
392 def GetIssueDescription(self): | 377 def UpdateDescriptionFromIssue(self): |
393 """Returns the issue description from Rietveld.""" | 378 """Updates self.description with the issue description from Rietveld.""" |
394 return self.SendToRietveld('/%d/description' % self.issue) | 379 self._desc = git_cl.ChangeDescription( |
| 380 self.SendToRietveld('/%d/description' % self.issue)) |
395 | 381 |
396 def AddComment(self, comment): | 382 def AddComment(self, comment): |
397 """Adds a comment for an issue on Rietveld. | 383 """Adds a comment for an issue on Rietveld. |
398 As a side effect, this will email everyone associated with the issue.""" | 384 As a side effect, this will email everyone associated with the issue.""" |
399 return self.RpcServer().add_comment(self.issue, comment) | 385 return self.RpcServer().add_comment(self.issue, comment) |
400 | 386 |
401 def PrimeLint(self): | 387 def PrimeLint(self): |
402 """Do background work on Rietveld to lint the file so that the results are | 388 """Do background work on Rietveld to lint the file so that the results are |
403 ready when the issue is viewed.""" | 389 ready when the issue is viewed.""" |
404 if self.issue and self.patchset: | 390 if self.issue and self.patchset: |
(...skipping 439 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
844 args = map(replace_message, args) | 830 args = map(replace_message, args) |
845 if found_deprecated_arg[0]: | 831 if found_deprecated_arg[0]: |
846 print >> sys.stderr, ( | 832 print >> sys.stderr, ( |
847 '\nWARNING: Use -t or --title to set the title of the patchset.\n' | 833 '\nWARNING: Use -t or --title to set the title of the patchset.\n' |
848 'In the near future, -m or --message will send a message instead.\n' | 834 'In the near future, -m or --message will send a message instead.\n' |
849 'See http://goo.gl/JGg0Z for details.\n') | 835 'See http://goo.gl/JGg0Z for details.\n') |
850 | 836 |
851 upload_arg = ["upload.py", "-y"] | 837 upload_arg = ["upload.py", "-y"] |
852 upload_arg.append("--server=%s" % change_info.rietveld) | 838 upload_arg.append("--server=%s" % change_info.rietveld) |
853 | 839 |
854 reviewers = change_info.reviewers or output.reviewers | 840 reviewers = change_info.get_reviewers() or output.reviewers |
855 if (reviewers and | 841 if (reviewers and |
856 not any(arg.startswith('-r') or arg.startswith('--reviewer') for | 842 not any(arg.startswith('-r') or arg.startswith('--reviewer') for |
857 arg in args)): | 843 arg in args)): |
858 upload_arg.append('--reviewers=%s' % ','.join(reviewers)) | 844 upload_arg.append('--reviewers=%s' % ','.join(reviewers)) |
859 | 845 |
860 upload_arg.extend(args) | 846 upload_arg.extend(args) |
861 | 847 |
862 desc_file = None | 848 desc_file = None |
863 try: | 849 try: |
864 if change_info.issue: | 850 if change_info.issue: |
(...skipping 131 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
996 # We face a problem with svn here: Let's say change 'bleh' modifies | 982 # We face a problem with svn here: Let's say change 'bleh' modifies |
997 # svn:ignore on dir1\. but another unrelated change 'pouet' modifies | 983 # svn:ignore on dir1\. but another unrelated change 'pouet' modifies |
998 # dir1\foo.cc. When the user `gcl commit bleh`, foo.cc is *also committed*. | 984 # dir1\foo.cc. When the user `gcl commit bleh`, foo.cc is *also committed*. |
999 # The only fix is to use --non-recursive but that has its issues too: | 985 # The only fix is to use --non-recursive but that has its issues too: |
1000 # Let's say if dir1 is deleted, --non-recursive must *not* be used otherwise | 986 # Let's say if dir1 is deleted, --non-recursive must *not* be used otherwise |
1001 # you'll get "svn: Cannot non-recursively commit a directory deletion of a | 987 # you'll get "svn: Cannot non-recursively commit a directory deletion of a |
1002 # directory with child nodes". Yay... | 988 # directory with child nodes". Yay... |
1003 commit_cmd = ["svn", "commit"] | 989 commit_cmd = ["svn", "commit"] |
1004 if change_info.issue: | 990 if change_info.issue: |
1005 # Get the latest description from Rietveld. | 991 # Get the latest description from Rietveld. |
1006 change_info.description = change_info.GetIssueDescription() | 992 change_info.UpdateDescriptionFromIssue() |
1007 | 993 |
1008 commit_message = change_info.description.replace('\r\n', '\n') | 994 commit_desc = git_cl.ChangeDescription(change_info.description) |
1009 if change_info.issue: | 995 if change_info.issue: |
1010 server = change_info.rietveld | 996 server = change_info.rietveld |
1011 if not server.startswith("http://") and not server.startswith("https://"): | 997 if not server.startswith("http://") and not server.startswith("https://"): |
1012 server = "http://" + server | 998 server = "http://" + server |
1013 commit_message += ('\nReview URL: %s/%d' % (server, change_info.issue)) | 999 commit_desc.append_footer('Review URL: %s/%d' % (server, change_info.issue)) |
1014 | 1000 |
1015 handle, commit_filename = tempfile.mkstemp(text=True) | 1001 handle, commit_filename = tempfile.mkstemp(text=True) |
1016 os.write(handle, commit_message) | 1002 os.write(handle, commit_desc.description) |
1017 os.close(handle) | 1003 os.close(handle) |
1018 try: | 1004 try: |
1019 handle, targets_filename = tempfile.mkstemp(text=True) | 1005 handle, targets_filename = tempfile.mkstemp(text=True) |
1020 os.write(handle, "\n".join(change_info.GetFileNames())) | 1006 os.write(handle, "\n".join(change_info.GetFileNames())) |
1021 os.close(handle) | 1007 os.close(handle) |
1022 try: | 1008 try: |
1023 commit_cmd += ['--file=' + commit_filename] | 1009 commit_cmd += ['--file=' + commit_filename] |
1024 commit_cmd += ['--targets=' + targets_filename] | 1010 commit_cmd += ['--targets=' + targets_filename] |
1025 # Change the current working directory before calling commit. | 1011 # Change the current working directory before calling commit. |
1026 output = '' | 1012 output = '' |
1027 try: | 1013 try: |
1028 output = RunShell(commit_cmd, True) | 1014 output = RunShell(commit_cmd, True) |
1029 except subprocess2.CalledProcessError, e: | 1015 except subprocess2.CalledProcessError, e: |
1030 ErrorExit('Commit failed.\n%s' % e) | 1016 ErrorExit('Commit failed.\n%s' % e) |
1031 finally: | 1017 finally: |
1032 os.remove(commit_filename) | 1018 os.remove(commit_filename) |
1033 finally: | 1019 finally: |
1034 os.remove(targets_filename) | 1020 os.remove(targets_filename) |
1035 if output.find("Committed revision") != -1: | 1021 if output.find("Committed revision") != -1: |
1036 change_info.Delete() | 1022 change_info.Delete() |
1037 | 1023 |
1038 if change_info.issue: | 1024 if change_info.issue: |
1039 revision = re.compile(".*?\nCommitted revision (\d+)", | 1025 revision = re.compile(".*?\nCommitted revision (\d+)", |
1040 re.DOTALL).match(output).group(1) | 1026 re.DOTALL).match(output).group(1) |
1041 viewvc_url = GetCodeReviewSetting('VIEW_VC') | 1027 viewvc_url = GetCodeReviewSetting('VIEW_VC') |
1042 change_info.description += '\n' | |
1043 if viewvc_url and revision: | 1028 if viewvc_url and revision: |
1044 change_info.description += "\nCommitted: " + viewvc_url + revision | 1029 change_info.append_footer('Committed: ' + viewvc_url + revision) |
1045 elif revision: | 1030 elif revision: |
1046 change_info.description += "\nCommitted: " + revision | 1031 change_info.append_footer('Committed: ' + revision) |
1047 change_info.CloseIssue() | 1032 change_info.CloseIssue() |
1048 props = change_info.RpcServer().get_issue_properties( | 1033 props = change_info.RpcServer().get_issue_properties( |
1049 change_info.issue, False) | 1034 change_info.issue, False) |
1050 patch_num = len(props['patchsets']) | 1035 patch_num = len(props['patchsets']) |
1051 comment = "Committed patchset #%d manually as r%s" % (patch_num, revision) | 1036 comment = "Committed patchset #%d manually as r%s" % (patch_num, revision) |
1052 comment += ' (presubmit successful).' if not bypassed else '.' | 1037 comment += ' (presubmit successful).' if not bypassed else '.' |
1053 change_info.AddComment(comment) | 1038 change_info.AddComment(comment) |
1054 return 0 | 1039 return 0 |
1055 | 1040 |
1056 | 1041 |
(...skipping 74 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
1131 ErrorExit('Running editor failed') | 1116 ErrorExit('Running editor failed') |
1132 | 1117 |
1133 split_result = result.split(separator1, 1) | 1118 split_result = result.split(separator1, 1) |
1134 if len(split_result) != 2: | 1119 if len(split_result) != 2: |
1135 ErrorExit("Don't modify the text starting with ---!\n\n%r" % result) | 1120 ErrorExit("Don't modify the text starting with ---!\n\n%r" % result) |
1136 | 1121 |
1137 # Update the CL description if it has changed. | 1122 # Update the CL description if it has changed. |
1138 new_description = split_result[0] | 1123 new_description = split_result[0] |
1139 cl_files_text = split_result[1] | 1124 cl_files_text = split_result[1] |
1140 if new_description != description or override_description: | 1125 if new_description != description or override_description: |
1141 change_info.description = new_description | 1126 change_info.force_description(new_description) |
1142 change_info.needs_upload = True | |
1143 | 1127 |
1144 new_cl_files = [] | 1128 new_cl_files = [] |
1145 for line in cl_files_text.splitlines(): | 1129 for line in cl_files_text.splitlines(): |
1146 if not len(line): | 1130 if not len(line): |
1147 continue | 1131 continue |
1148 if line.startswith("---"): | 1132 if line.startswith("---"): |
1149 break | 1133 break |
1150 status = line[:7] | 1134 status = line[:7] |
1151 filename = line[7:] | 1135 filename = line[7:] |
1152 new_cl_files.append((status, filename)) | 1136 new_cl_files.append((status, filename)) |
1153 | 1137 |
1154 if (not len(change_info.GetFiles()) and not change_info.issue and | 1138 if (not len(change_info.GetFiles()) and not change_info.issue and |
1155 not len(new_description) and not new_cl_files): | 1139 not len(new_description) and not new_cl_files): |
1156 ErrorExit("Empty changelist not saved") | 1140 ErrorExit("Empty changelist not saved") |
1157 | 1141 |
1158 change_info._files = new_cl_files | 1142 change_info._files = new_cl_files |
1159 change_info.Save() | 1143 change_info.Save() |
1160 if svn_info.get('URL', '').startswith('http:'): | 1144 if svn_info.get('URL', '').startswith('http:'): |
1161 Warn("WARNING: Creating CL in a read-only checkout. You will need to " | 1145 Warn("WARNING: Creating CL in a read-only checkout. You will need to " |
1162 "commit using a commit queue!") | 1146 "commit using a commit queue!") |
1163 | 1147 |
1164 print change_info.name + " changelist saved." | 1148 print change_info.name + " changelist saved." |
1165 if change_info.MissingTests(): | 1149 if change_info.MissingTests(): |
1166 Warn("WARNING: " + MISSING_TEST_MSG) | 1150 Warn("WARNING: " + MISSING_TEST_MSG) |
1167 | 1151 |
1168 # Update the Rietveld issue. | 1152 # Update the Rietveld issue. |
1169 if change_info.issue and change_info.NeedsUpload(): | 1153 if change_info.issue and change_info.NeedsUpload(): |
1170 change_info.UpdateRietveldDescription() | 1154 change_info.UpdateRietveldDescription() |
1171 change_info.needs_upload = False | |
1172 change_info.Save() | 1155 change_info.Save() |
1173 return 0 | 1156 return 0 |
1174 | 1157 |
1175 | 1158 |
1176 @need_change_and_args | 1159 @need_change_and_args |
1177 def CMDlint(change_info, args): | 1160 def CMDlint(change_info, args): |
1178 """Runs cpplint.py on all the files in the change list. | 1161 """Runs cpplint.py on all the files in the change list. |
1179 | 1162 |
1180 Checks all the files in the changelist for possible style violations. | 1163 Checks all the files in the changelist for possible style violations. |
1181 """ | 1164 """ |
(...skipping 288 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
1470 raise | 1453 raise |
1471 print >> sys.stderr, ( | 1454 print >> sys.stderr, ( |
1472 'AppEngine is misbehaving and returned HTTP %d, again. Keep faith ' | 1455 'AppEngine is misbehaving and returned HTTP %d, again. Keep faith ' |
1473 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)) | 1456 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)) |
1474 return 1 | 1457 return 1 |
1475 | 1458 |
1476 | 1459 |
1477 if __name__ == "__main__": | 1460 if __name__ == "__main__": |
1478 fix_encoding.fix_encoding() | 1461 fix_encoding.fix_encoding() |
1479 sys.exit(main(sys.argv[1:])) | 1462 sys.exit(main(sys.argv[1:])) |
OLD | NEW |