| OLD | NEW |
| 1 #!/usr/bin/python | 1 #!/usr/bin/python |
| 2 # Copyright (c) 2006-2009 The Chromium Authors. All rights reserved. | 2 # Copyright (c) 2006-2009 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 # Wrapper script around Rietveld's upload.py that groups files into | 6 # Wrapper script around Rietveld's upload.py that groups files into |
| 7 # changelists. | 7 # changelists. |
| 8 | 8 |
| 9 import getpass | 9 import getpass |
| 10 import os | 10 import os |
| 11 import random | 11 import random |
| 12 import re | 12 import re |
| 13 import string | 13 import string |
| 14 import subprocess | 14 import subprocess |
| 15 import sys | 15 import sys |
| 16 import tempfile | 16 import tempfile |
| 17 import upload | 17 import upload |
| 18 import urllib2 | 18 import urllib2 |
| 19 import xml.dom.minidom | 19 import xml.dom.minidom |
| 20 | 20 |
| 21 # gcl now depends on gclient. | |
| 22 import gclient | |
| 23 | 21 |
| 24 __version__ = '1.0' | 22 __version__ = '1.0' |
| 25 | 23 |
| 26 | 24 |
| 27 CODEREVIEW_SETTINGS = { | 25 CODEREVIEW_SETTINGS = { |
| 28 # Default values. | 26 # Default values. |
| 29 "CODE_REVIEW_SERVER": "codereview.chromium.org", | 27 "CODE_REVIEW_SERVER": "codereview.chromium.org", |
| 30 "CC_LIST": "chromium-reviews@googlegroups.com", | 28 "CC_LIST": "chromium-reviews@googlegroups.com", |
| 31 "VIEW_VC": "http://src.chromium.org/viewvc/chrome?view=rev&revision=", | 29 "VIEW_VC": "http://src.chromium.org/viewvc/chrome?view=rev&revision=", |
| 32 } | 30 } |
| (...skipping 10 matching lines...) Expand all Loading... |
| 43 # Filename where we store repository specific information for gcl. | 41 # Filename where we store repository specific information for gcl. |
| 44 CODEREVIEW_SETTINGS_FILE = "codereview.settings" | 42 CODEREVIEW_SETTINGS_FILE = "codereview.settings" |
| 45 | 43 |
| 46 # Warning message when the change appears to be missing tests. | 44 # Warning message when the change appears to be missing tests. |
| 47 MISSING_TEST_MSG = "Change contains new or modified methods, but no new tests!" | 45 MISSING_TEST_MSG = "Change contains new or modified methods, but no new tests!" |
| 48 | 46 |
| 49 # Caches whether we read the codereview.settings file yet or not. | 47 # Caches whether we read the codereview.settings file yet or not. |
| 50 read_gcl_info = False | 48 read_gcl_info = False |
| 51 | 49 |
| 52 | 50 |
| 51 ### Simplified XML processing functions. |
| 52 |
| 53 def ParseXML(output): |
| 54 try: |
| 55 return xml.dom.minidom.parseString(output) |
| 56 except xml.parsers.expat.ExpatError: |
| 57 return None |
| 58 |
| 59 def GetNamedNodeText(node, node_name): |
| 60 child_nodes = node.getElementsByTagName(node_name) |
| 61 if not child_nodes: |
| 62 return None |
| 63 assert len(child_nodes) == 1 and child_nodes[0].childNodes.length == 1 |
| 64 return child_nodes[0].firstChild.nodeValue |
| 65 |
| 66 |
| 67 def GetNodeNamedAttributeText(node, node_name, attribute_name): |
| 68 child_nodes = node.getElementsByTagName(node_name) |
| 69 if not child_nodes: |
| 70 return None |
| 71 assert len(child_nodes) == 1 |
| 72 return child_nodes[0].getAttribute(attribute_name) |
| 73 |
| 74 |
| 53 ### SVN Functions | 75 ### SVN Functions |
| 54 | 76 |
| 55 def IsSVNMoved(filename): | 77 def IsSVNMoved(filename): |
| 56 """Determine if a file has been added through svn mv""" | 78 """Determine if a file has been added through svn mv""" |
| 57 info = gclient.CaptureSVNInfo(filename) | 79 info = GetSVNFileInfo(filename) |
| 58 return (info.get('Copied From URL') and | 80 return (info.get('Copied From URL') and |
| 59 info.get('Copied From Rev') and | 81 info.get('Copied From Rev') and |
| 60 info.get('Schedule') == 'add') | 82 info.get('Schedule') == 'add') |
| 61 | 83 |
| 62 | 84 |
| 85 def GetSVNFileInfo(file): |
| 86 """Returns a dictionary from the svn info output for the given file.""" |
| 87 dom = ParseXML(RunShell(["svn", "info", "--xml", file])) |
| 88 result = {} |
| 89 if dom: |
| 90 # /info/entry/ |
| 91 # url |
| 92 # reposityory/(root|uuid) |
| 93 # wc-info/(schedule|depth) |
| 94 # commit/(author|date) |
| 95 result['Node Kind'] = GetNodeNamedAttributeText(dom, 'entry', 'kind') |
| 96 result['Repository Root'] = GetNamedNodeText(dom, 'root') |
| 97 result['Schedule'] = GetNamedNodeText(dom, 'schedule') |
| 98 result['URL'] = GetNamedNodeText(dom, 'url') |
| 99 result['Path'] = GetNodeNamedAttributeText(dom, 'entry', 'path') |
| 100 result['Copied From URL'] = GetNamedNodeText(dom, 'copy-from-url') |
| 101 result['Copied From Rev'] = GetNamedNodeText(dom, 'copy-from-rev') |
| 102 return result |
| 103 |
| 104 |
| 63 def GetSVNFileProperty(file, property_name): | 105 def GetSVNFileProperty(file, property_name): |
| 64 """Returns the value of an SVN property for the given file. | 106 """Returns the value of an SVN property for the given file. |
| 65 | 107 |
| 66 Args: | 108 Args: |
| 67 file: The file to check | 109 file: The file to check |
| 68 property_name: The name of the SVN property, e.g. "svn:mime-type" | 110 property_name: The name of the SVN property, e.g. "svn:mime-type" |
| 69 | 111 |
| 70 Returns: | 112 Returns: |
| 71 The value of the property, which will be the empty string if the property | 113 The value of the property, which will be the empty string if the property |
| 72 is not set on the file. If the file is not under version control, the | 114 is not set on the file. If the file is not under version control, the |
| (...skipping 25 matching lines...) Expand all Loading... |
| 98 'conflicted': 'C', | 140 'conflicted': 'C', |
| 99 'deleted': 'D', | 141 'deleted': 'D', |
| 100 'ignored': 'I', | 142 'ignored': 'I', |
| 101 'missing': '!', | 143 'missing': '!', |
| 102 'modified': 'M', | 144 'modified': 'M', |
| 103 'normal': ' ', | 145 'normal': ' ', |
| 104 'replaced': 'R', | 146 'replaced': 'R', |
| 105 'unversioned': '?', | 147 'unversioned': '?', |
| 106 # TODO(maruel): Find the corresponding strings for X, ~ | 148 # TODO(maruel): Find the corresponding strings for X, ~ |
| 107 } | 149 } |
| 108 dom = gclient.ParseXML(RunShell(command)) | 150 dom = ParseXML(RunShell(command)) |
| 109 results = [] | 151 results = [] |
| 110 if dom: | 152 if dom: |
| 111 # /status/target/entry/(wc-status|commit|author|date) | 153 # /status/target/entry/(wc-status|commit|author|date) |
| 112 for target in dom.getElementsByTagName('target'): | 154 for target in dom.getElementsByTagName('target'): |
| 113 base_path = target.getAttribute('path') | 155 base_path = target.getAttribute('path') |
| 114 for entry in target.getElementsByTagName('entry'): | 156 for entry in target.getElementsByTagName('entry'): |
| 115 file = entry.getAttribute('path') | 157 file = entry.getAttribute('path') |
| 116 wc_status = entry.getElementsByTagName('wc-status') | 158 wc_status = entry.getElementsByTagName('wc-status') |
| 117 assert len(wc_status) == 1 | 159 assert len(wc_status) == 1 |
| 118 # Emulate svn 1.5 status ouput... | 160 # Emulate svn 1.5 status ouput... |
| (...skipping 34 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 153 return [item[1] for item in GetSVNStatus(extra_args) if item[0][0] == '?'] | 195 return [item[1] for item in GetSVNStatus(extra_args) if item[0][0] == '?'] |
| 154 | 196 |
| 155 | 197 |
| 156 def GetRepositoryRoot(): | 198 def GetRepositoryRoot(): |
| 157 """Returns the top level directory of the current repository. | 199 """Returns the top level directory of the current repository. |
| 158 | 200 |
| 159 The directory is returned as an absolute path. | 201 The directory is returned as an absolute path. |
| 160 """ | 202 """ |
| 161 global repository_root | 203 global repository_root |
| 162 if not repository_root: | 204 if not repository_root: |
| 163 infos = gclient.CaptureSVNInfo(os.getcwd(), print_error=False) | 205 cur_dir_repo_root = GetSVNFileInfo(os.getcwd()).get("Repository Root") |
| 164 cur_dir_repo_root = infos.get("Repository Root") | |
| 165 if not cur_dir_repo_root: | 206 if not cur_dir_repo_root: |
| 166 raise Exception("gcl run outside of repository") | 207 raise Exception("gcl run outside of repository") |
| 167 | 208 |
| 168 repository_root = os.getcwd() | 209 repository_root = os.getcwd() |
| 169 while True: | 210 while True: |
| 170 parent = os.path.dirname(repository_root) | 211 parent = os.path.dirname(repository_root) |
| 171 if (gclient.CaptureSVNInfo(parent).get("Repository Root") != | 212 if GetSVNFileInfo(parent).get("Repository Root") != cur_dir_repo_root: |
| 172 cur_dir_repo_root): | |
| 173 break | 213 break |
| 174 repository_root = parent | 214 repository_root = parent |
| 175 return repository_root | 215 return repository_root |
| 176 | 216 |
| 177 | 217 |
| 178 def GetInfoDir(): | 218 def GetInfoDir(): |
| 179 """Returns the directory where gcl info files are stored.""" | 219 """Returns the directory where gcl info files are stored.""" |
| 180 global gcl_info_dir | 220 global gcl_info_dir |
| 181 if not gcl_info_dir: | 221 if not gcl_info_dir: |
| 182 gcl_info_dir = os.path.join(GetRepositoryRoot(), '.svn', 'gcl_info') | 222 gcl_info_dir = os.path.join(GetRepositoryRoot(), '.svn', 'gcl_info') |
| 183 return gcl_info_dir | 223 return gcl_info_dir |
| 184 | 224 |
| 185 | 225 |
| 186 def GetCodeReviewSetting(key): | 226 def GetCodeReviewSetting(key): |
| 187 """Returns a value for the given key for this repository.""" | 227 """Returns a value for the given key for this repository.""" |
| 188 global read_gcl_info | 228 global read_gcl_info |
| 189 if not read_gcl_info: | 229 if not read_gcl_info: |
| 190 read_gcl_info = True | 230 read_gcl_info = True |
| 191 # First we check if we have a cached version. | 231 # First we check if we have a cached version. |
| 192 cached_settings_file = os.path.join(GetInfoDir(), CODEREVIEW_SETTINGS_FILE) | 232 cached_settings_file = os.path.join(GetInfoDir(), CODEREVIEW_SETTINGS_FILE) |
| 193 if (not os.path.exists(cached_settings_file) or | 233 if (not os.path.exists(cached_settings_file) or |
| 194 os.stat(cached_settings_file).st_mtime > 60*60*24*3): | 234 os.stat(cached_settings_file).st_mtime > 60*60*24*3): |
| 195 dir_info = gclient.CaptureSVNInfo(".") | 235 dir_info = GetSVNFileInfo(".") |
| 196 repo_root = dir_info["Repository Root"] | 236 repo_root = dir_info["Repository Root"] |
| 197 url_path = dir_info["URL"] | 237 url_path = dir_info["URL"] |
| 198 settings = "" | 238 settings = "" |
| 199 while True: | 239 while True: |
| 200 # Look for the codereview.settings file at the current level. | 240 # Look for the codereview.settings file at the current level. |
| 201 svn_path = url_path + "/" + CODEREVIEW_SETTINGS_FILE | 241 svn_path = url_path + "/" + CODEREVIEW_SETTINGS_FILE |
| 202 settings, rc = RunShellWithReturnCode(["svn", "cat", svn_path]) | 242 settings, rc = RunShellWithReturnCode(["svn", "cat", svn_path]) |
| 203 if not rc: | 243 if not rc: |
| 204 # Exit the loop if the file was found. | 244 # Exit the loop if the file was found. |
| 205 break | 245 break |
| (...skipping 81 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 287 | 327 |
| 288 | 328 |
| 289 class ChangeInfo: | 329 class ChangeInfo: |
| 290 """Holds information about a changelist. | 330 """Holds information about a changelist. |
| 291 | 331 |
| 292 issue: the Rietveld issue number, of "" if it hasn't been uploaded yet. | 332 issue: the Rietveld issue number, of "" if it hasn't been uploaded yet. |
| 293 description: the description. | 333 description: the description. |
| 294 files: a list of 2 tuple containing (status, filename) of changed files, | 334 files: a list of 2 tuple containing (status, filename) of changed files, |
| 295 with paths being relative to the top repository directory. | 335 with paths being relative to the top repository directory. |
| 296 """ | 336 """ |
| 297 def __init__(self, name="", issue="", description="", files=None): | 337 def __init__(self, name="", issue="", description="", files=[]): |
| 298 self.name = name | 338 self.name = name |
| 299 self.issue = issue | 339 self.issue = issue |
| 300 self.description = description | 340 self.description = description |
| 301 self.files = files | 341 self.files = files |
| 302 if self.files is None: | |
| 303 self.files = [] | |
| 304 self.patch = None | 342 self.patch = None |
| 305 | 343 |
| 306 def FileList(self): | 344 def FileList(self): |
| 307 """Returns a list of files.""" | 345 """Returns a list of files.""" |
| 308 return [file[1] for file in self.files] | 346 return [file[1] for file in self.files] |
| 309 | 347 |
| 310 def _NonDeletedFileList(self): | 348 def _NonDeletedFileList(self): |
| 311 """Returns a list of files in this change, not including deleted files.""" | 349 """Returns a list of files in this change, not including deleted files.""" |
| 312 return [file[1] for file in self.files if not file[0].startswith("D")] | 350 return [file[1] for file in self.files if not file[0].startswith("D")] |
| 313 | 351 |
| (...skipping 391 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 705 previous_cwd = os.getcwd() | 743 previous_cwd = os.getcwd() |
| 706 if root is None: | 744 if root is None: |
| 707 os.chdir(GetRepositoryRoot()) | 745 os.chdir(GetRepositoryRoot()) |
| 708 else: | 746 else: |
| 709 os.chdir(root) | 747 os.chdir(root) |
| 710 | 748 |
| 711 diff = [] | 749 diff = [] |
| 712 for file in files: | 750 for file in files: |
| 713 # Use svn info output instead of os.path.isdir because the latter fails | 751 # Use svn info output instead of os.path.isdir because the latter fails |
| 714 # when the file is deleted. | 752 # when the file is deleted. |
| 715 if gclient.CaptureSVNInfo(file).get("Node Kind") in ("dir", "directory"): | 753 if GetSVNFileInfo(file).get("Node Kind") in ("dir", "directory"): |
| 716 continue | 754 continue |
| 717 # If the user specified a custom diff command in their svn config file, | 755 # If the user specified a custom diff command in their svn config file, |
| 718 # then it'll be used when we do svn diff, which we don't want to happen | 756 # then it'll be used when we do svn diff, which we don't want to happen |
| 719 # since we want the unified diff. Using --diff-cmd=diff doesn't always | 757 # since we want the unified diff. Using --diff-cmd=diff doesn't always |
| 720 # work, since they can have another diff executable in their path that | 758 # work, since they can have another diff executable in their path that |
| 721 # gives different line endings. So we use a bogus temp directory as the | 759 # gives different line endings. So we use a bogus temp directory as the |
| 722 # config directory, which gets around these problems. | 760 # config directory, which gets around these problems. |
| 723 if sys.platform.startswith("win"): | 761 if sys.platform.startswith("win"): |
| 724 parent_dir = tempfile.gettempdir() | 762 parent_dir = tempfile.gettempdir() |
| 725 else: | 763 else: |
| (...skipping 420 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 1146 # the files. This allows commands such as 'gcl diff xxx' to work. | 1184 # the files. This allows commands such as 'gcl diff xxx' to work. |
| 1147 args =["svn", command] | 1185 args =["svn", command] |
| 1148 root = GetRepositoryRoot() | 1186 root = GetRepositoryRoot() |
| 1149 args.extend([os.path.join(root, x) for x in change_info.FileList()]) | 1187 args.extend([os.path.join(root, x) for x in change_info.FileList()]) |
| 1150 RunShell(args, True) | 1188 RunShell(args, True) |
| 1151 return 0 | 1189 return 0 |
| 1152 | 1190 |
| 1153 | 1191 |
| 1154 if __name__ == "__main__": | 1192 if __name__ == "__main__": |
| 1155 sys.exit(main()) | 1193 sys.exit(main()) |
| OLD | NEW |