| 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 GetIssueDescription(self): |
| 393 """Returns the issue description from Rietveld.""" | 378 """Returns the issue description from Rietveld.""" |
| 394 return self.SendToRietveld('/%d/description' % self.issue) | 379 return self.SendToRietveld('/%d/description' % self.issue) |
| 395 | 380 |
| 381 def UpdateDescriptionFromIssue(self): |
| 382 """Updates self.description with the issue description from Rietveld.""" |
| 383 self._desc = git_cl.ChangeDescription( |
| 384 self.SendToRietveld('/%d/description' % self.issue)) |
| 385 |
| 396 def AddComment(self, comment): | 386 def AddComment(self, comment): |
| 397 """Adds a comment for an issue on Rietveld. | 387 """Adds a comment for an issue on Rietveld. |
| 398 As a side effect, this will email everyone associated with the issue.""" | 388 As a side effect, this will email everyone associated with the issue.""" |
| 399 return self.RpcServer().add_comment(self.issue, comment) | 389 return self.RpcServer().add_comment(self.issue, comment) |
| 400 | 390 |
| 401 def PrimeLint(self): | 391 def PrimeLint(self): |
| 402 """Do background work on Rietveld to lint the file so that the results are | 392 """Do background work on Rietveld to lint the file so that the results are |
| 403 ready when the issue is viewed.""" | 393 ready when the issue is viewed.""" |
| 404 if self.issue and self.patchset: | 394 if self.issue and self.patchset: |
| 405 self.SendToRietveld('/lint/issue%s_%s' % (self.issue, self.patchset), | 395 self.SendToRietveld('/lint/issue%s_%s' % (self.issue, self.patchset), |
| (...skipping 438 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 844 args = map(replace_message, args) | 834 args = map(replace_message, args) |
| 845 if found_deprecated_arg[0]: | 835 if found_deprecated_arg[0]: |
| 846 print >> sys.stderr, ( | 836 print >> sys.stderr, ( |
| 847 '\nWARNING: Use -t or --title to set the title of the patchset.\n' | 837 '\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' | 838 'In the near future, -m or --message will send a message instead.\n' |
| 849 'See http://goo.gl/JGg0Z for details.\n') | 839 'See http://goo.gl/JGg0Z for details.\n') |
| 850 | 840 |
| 851 upload_arg = ["upload.py", "-y"] | 841 upload_arg = ["upload.py", "-y"] |
| 852 upload_arg.append("--server=%s" % change_info.rietveld) | 842 upload_arg.append("--server=%s" % change_info.rietveld) |
| 853 | 843 |
| 854 reviewers = change_info.reviewers or output.reviewers | 844 reviewers = change_info.get_reviewers() or output.reviewers |
| 855 if (reviewers and | 845 if (reviewers and |
| 856 not any(arg.startswith('-r') or arg.startswith('--reviewer') for | 846 not any(arg.startswith('-r') or arg.startswith('--reviewer') for |
| 857 arg in args)): | 847 arg in args)): |
| 858 upload_arg.append('--reviewers=%s' % ','.join(reviewers)) | 848 upload_arg.append('--reviewers=%s' % ','.join(reviewers)) |
| 859 | 849 |
| 860 upload_arg.extend(args) | 850 upload_arg.extend(args) |
| 861 | 851 |
| 862 desc_file = None | 852 desc_file = None |
| 863 try: | 853 try: |
| 864 if change_info.issue: | 854 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 | 986 # We face a problem with svn here: Let's say change 'bleh' modifies |
| 997 # svn:ignore on dir1\. but another unrelated change 'pouet' modifies | 987 # 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*. | 988 # 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: | 989 # 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 | 990 # 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 | 991 # you'll get "svn: Cannot non-recursively commit a directory deletion of a |
| 1002 # directory with child nodes". Yay... | 992 # directory with child nodes". Yay... |
| 1003 commit_cmd = ["svn", "commit"] | 993 commit_cmd = ["svn", "commit"] |
| 1004 if change_info.issue: | 994 if change_info.issue: |
| 1005 # Get the latest description from Rietveld. | 995 # Get the latest description from Rietveld. |
| 1006 change_info.description = change_info.GetIssueDescription() | 996 change_info.UpdateDescriptionFromIssue() |
| 1007 | 997 |
| 1008 commit_message = change_info.description.replace('\r\n', '\n') | 998 commit_desc = git_cl.ChangeDescription(change_info.description) |
| 1009 if change_info.issue: | 999 if change_info.issue: |
| 1010 server = change_info.rietveld | 1000 server = change_info.rietveld |
| 1011 if not server.startswith("http://") and not server.startswith("https://"): | 1001 if not server.startswith("http://") and not server.startswith("https://"): |
| 1012 server = "http://" + server | 1002 server = "http://" + server |
| 1013 commit_message += ('\nReview URL: %s/%d' % (server, change_info.issue)) | 1003 commit_desc.append_footer('Review URL: %s/%d' % (server, change_info.issue)) |
| 1014 | 1004 |
| 1015 handle, commit_filename = tempfile.mkstemp(text=True) | 1005 handle, commit_filename = tempfile.mkstemp(text=True) |
| 1016 os.write(handle, commit_message) | 1006 os.write(handle, commit_desc.description) |
| 1017 os.close(handle) | 1007 os.close(handle) |
| 1018 try: | 1008 try: |
| 1019 handle, targets_filename = tempfile.mkstemp(text=True) | 1009 handle, targets_filename = tempfile.mkstemp(text=True) |
| 1020 os.write(handle, "\n".join(change_info.GetFileNames())) | 1010 os.write(handle, "\n".join(change_info.GetFileNames())) |
| 1021 os.close(handle) | 1011 os.close(handle) |
| 1022 try: | 1012 try: |
| 1023 commit_cmd += ['--file=' + commit_filename] | 1013 commit_cmd += ['--file=' + commit_filename] |
| 1024 commit_cmd += ['--targets=' + targets_filename] | 1014 commit_cmd += ['--targets=' + targets_filename] |
| 1025 # Change the current working directory before calling commit. | 1015 # Change the current working directory before calling commit. |
| 1026 output = '' | 1016 output = '' |
| 1027 try: | 1017 try: |
| 1028 output = RunShell(commit_cmd, True) | 1018 output = RunShell(commit_cmd, True) |
| 1029 except subprocess2.CalledProcessError, e: | 1019 except subprocess2.CalledProcessError, e: |
| 1030 ErrorExit('Commit failed.\n%s' % e) | 1020 ErrorExit('Commit failed.\n%s' % e) |
| 1031 finally: | 1021 finally: |
| 1032 os.remove(commit_filename) | 1022 os.remove(commit_filename) |
| 1033 finally: | 1023 finally: |
| 1034 os.remove(targets_filename) | 1024 os.remove(targets_filename) |
| 1035 if output.find("Committed revision") != -1: | 1025 if output.find("Committed revision") != -1: |
| 1036 change_info.Delete() | 1026 change_info.Delete() |
| 1037 | 1027 |
| 1038 if change_info.issue: | 1028 if change_info.issue: |
| 1039 revision = re.compile(".*?\nCommitted revision (\d+)", | 1029 revision = re.compile(".*?\nCommitted revision (\d+)", |
| 1040 re.DOTALL).match(output).group(1) | 1030 re.DOTALL).match(output).group(1) |
| 1041 viewvc_url = GetCodeReviewSetting('VIEW_VC') | 1031 viewvc_url = GetCodeReviewSetting('VIEW_VC') |
| 1042 change_info.description += '\n' | |
| 1043 if viewvc_url and revision: | 1032 if viewvc_url and revision: |
| 1044 change_info.description += "\nCommitted: " + viewvc_url + revision | 1033 change_info.append_footer('Committed: ' + viewvc_url + revision) |
| 1045 elif revision: | 1034 elif revision: |
| 1046 change_info.description += "\nCommitted: " + revision | 1035 change_info.append_footer('Committed: ' + revision) |
| 1047 change_info.CloseIssue() | 1036 change_info.CloseIssue() |
| 1048 props = change_info.RpcServer().get_issue_properties( | 1037 props = change_info.RpcServer().get_issue_properties( |
| 1049 change_info.issue, False) | 1038 change_info.issue, False) |
| 1050 patch_num = len(props['patchsets']) | 1039 patch_num = len(props['patchsets']) |
| 1051 comment = "Committed patchset #%d manually as r%s" % (patch_num, revision) | 1040 comment = "Committed patchset #%d manually as r%s" % (patch_num, revision) |
| 1052 comment += ' (presubmit successful).' if not bypassed else '.' | 1041 comment += ' (presubmit successful).' if not bypassed else '.' |
| 1053 change_info.AddComment(comment) | 1042 change_info.AddComment(comment) |
| 1054 return 0 | 1043 return 0 |
| 1055 | 1044 |
| 1056 | 1045 |
| (...skipping 74 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 1131 ErrorExit('Running editor failed') | 1120 ErrorExit('Running editor failed') |
| 1132 | 1121 |
| 1133 split_result = result.split(separator1, 1) | 1122 split_result = result.split(separator1, 1) |
| 1134 if len(split_result) != 2: | 1123 if len(split_result) != 2: |
| 1135 ErrorExit("Don't modify the text starting with ---!\n\n%r" % result) | 1124 ErrorExit("Don't modify the text starting with ---!\n\n%r" % result) |
| 1136 | 1125 |
| 1137 # Update the CL description if it has changed. | 1126 # Update the CL description if it has changed. |
| 1138 new_description = split_result[0] | 1127 new_description = split_result[0] |
| 1139 cl_files_text = split_result[1] | 1128 cl_files_text = split_result[1] |
| 1140 if new_description != description or override_description: | 1129 if new_description != description or override_description: |
| 1141 change_info.description = new_description | 1130 change_info.force_description(new_description) |
| 1142 change_info.needs_upload = True | |
| 1143 | 1131 |
| 1144 new_cl_files = [] | 1132 new_cl_files = [] |
| 1145 for line in cl_files_text.splitlines(): | 1133 for line in cl_files_text.splitlines(): |
| 1146 if not len(line): | 1134 if not len(line): |
| 1147 continue | 1135 continue |
| 1148 if line.startswith("---"): | 1136 if line.startswith("---"): |
| 1149 break | 1137 break |
| 1150 status = line[:7] | 1138 status = line[:7] |
| 1151 filename = line[7:] | 1139 filename = line[7:] |
| 1152 new_cl_files.append((status, filename)) | 1140 new_cl_files.append((status, filename)) |
| 1153 | 1141 |
| 1154 if (not len(change_info.GetFiles()) and not change_info.issue and | 1142 if (not len(change_info.GetFiles()) and not change_info.issue and |
| 1155 not len(new_description) and not new_cl_files): | 1143 not len(new_description) and not new_cl_files): |
| 1156 ErrorExit("Empty changelist not saved") | 1144 ErrorExit("Empty changelist not saved") |
| 1157 | 1145 |
| 1158 change_info._files = new_cl_files | 1146 change_info._files = new_cl_files |
| 1159 change_info.Save() | 1147 change_info.Save() |
| 1160 if svn_info.get('URL', '').startswith('http:'): | 1148 if svn_info.get('URL', '').startswith('http:'): |
| 1161 Warn("WARNING: Creating CL in a read-only checkout. You will need to " | 1149 Warn("WARNING: Creating CL in a read-only checkout. You will need to " |
| 1162 "commit using a commit queue!") | 1150 "commit using a commit queue!") |
| 1163 | 1151 |
| 1164 print change_info.name + " changelist saved." | 1152 print change_info.name + " changelist saved." |
| 1165 if change_info.MissingTests(): | 1153 if change_info.MissingTests(): |
| 1166 Warn("WARNING: " + MISSING_TEST_MSG) | 1154 Warn("WARNING: " + MISSING_TEST_MSG) |
| 1167 | 1155 |
| 1168 # Update the Rietveld issue. | 1156 # Update the Rietveld issue. |
| 1169 if change_info.issue and change_info.NeedsUpload(): | 1157 if change_info.issue and change_info.NeedsUpload(): |
| 1170 change_info.UpdateRietveldDescription() | 1158 change_info.UpdateRietveldDescription() |
| 1171 change_info.needs_upload = False | |
| 1172 change_info.Save() | 1159 change_info.Save() |
| 1173 return 0 | 1160 return 0 |
| 1174 | 1161 |
| 1175 | 1162 |
| 1176 @need_change_and_args | 1163 @need_change_and_args |
| 1177 def CMDlint(change_info, args): | 1164 def CMDlint(change_info, args): |
| 1178 """Runs cpplint.py on all the files in the change list. | 1165 """Runs cpplint.py on all the files in the change list. |
| 1179 | 1166 |
| 1180 Checks all the files in the changelist for possible style violations. | 1167 Checks all the files in the changelist for possible style violations. |
| 1181 """ | 1168 """ |
| (...skipping 288 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 1470 raise | 1457 raise |
| 1471 print >> sys.stderr, ( | 1458 print >> sys.stderr, ( |
| 1472 'AppEngine is misbehaving and returned HTTP %d, again. Keep faith ' | 1459 'AppEngine is misbehaving and returned HTTP %d, again. Keep faith ' |
| 1473 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)) | 1460 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)) |
| 1474 return 1 | 1461 return 1 |
| 1475 | 1462 |
| 1476 | 1463 |
| 1477 if __name__ == "__main__": | 1464 if __name__ == "__main__": |
| 1478 fix_encoding.fix_encoding() | 1465 fix_encoding.fix_encoding() |
| 1479 sys.exit(main(sys.argv[1:])) | 1466 sys.exit(main(sys.argv[1:])) |
| OLD | NEW |