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