1 # Copyright (c) 2009, Google Inc. All rights reserved. 2 # Copyright (c) 2009 Apple Inc. All rights reserved. 3 # 4 # Redistribution and use in source and binary forms, with or without 5 # modification, are permitted provided that the following conditions are 6 # met: 7 # 8 # * Redistributions of source code must retain the above copyright 9 # notice, this list of conditions and the following disclaimer. 10 # * Redistributions in binary form must reproduce the above 11 # copyright notice, this list of conditions and the following disclaimer 12 # in the documentation and/or other materials provided with the 13 # distribution. 14 # * Neither the name of Google Inc. nor the names of its 15 # contributors may be used to endorse or promote products derived from 16 # this software without specific prior written permission. 17 # 18 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 # 30 # Python module for interacting with an SCM system (like SVN or Git) 31 32 import logging 33 import re 34 import sys 35 36 from webkitpy.common.system.executive import Executive, ScriptError 37 from webkitpy.common.system.filesystem import FileSystem 38 39 _log = logging.getLogger(__name__) 40 41 42 class CheckoutNeedsUpdate(ScriptError): 43 def __init__(self, script_args, exit_code, output, cwd): 44 ScriptError.__init__(self, script_args=script_args, exit_code=exit_code, output=output, cwd=cwd) 45 46 47 # FIXME: Should be moved onto SCM 48 def commit_error_handler(error): 49 if re.search("resource out of date", error.output): 50 raise CheckoutNeedsUpdate(script_args=error.script_args, exit_code=error.exit_code, output=error.output, cwd=error.cwd) 51 Executive.default_error_handler(error) 52 53 54 class AuthenticationError(Exception): 55 def __init__(self, server_host, prompt_for_password=False): 56 self.server_host = server_host 57 self.prompt_for_password = prompt_for_password 58 59 60 61 # SCM methods are expected to return paths relative to self.checkout_root. 62 class SCM: 63 def __init__(self, cwd, executive=None, filesystem=None): 64 self.cwd = cwd 65 self._executive = executive or Executive() 66 self._filesystem = filesystem or FileSystem() 67 self.checkout_root = self.find_checkout_root(self.cwd) 68 69 # A wrapper used by subclasses to create processes. 70 def run(self, args, cwd=None, input=None, error_handler=None, return_exit_code=False, return_stderr=True, decode_output=True): 71 # FIXME: We should set cwd appropriately. 72 return self._executive.run_command(args, 73 cwd=cwd, 74 input=input, 75 error_handler=error_handler, 76 return_exit_code=return_exit_code, 77 return_stderr=return_stderr, 78 decode_output=decode_output) 79 80 # SCM always returns repository relative path, but sometimes we need 81 # absolute paths to pass to rm, etc. 82 def absolute_path(self, repository_relative_path): 83 return self._filesystem.join(self.checkout_root, repository_relative_path) 84 85 # FIXME: This belongs in Checkout, not SCM. 86 def scripts_directory(self): 87 return self._filesystem.join(self.checkout_root, "Tools", "Scripts") 88 89 # FIXME: This belongs in Checkout, not SCM. 90 def script_path(self, script_name): 91 return self._filesystem.join(self.scripts_directory(), script_name) 92 93 def run_status_and_extract_filenames(self, status_command, status_regexp): 94 filenames = [] 95 # We run with cwd=self.checkout_root so that returned-paths are root-relative. 96 for line in self.run(status_command, cwd=self.checkout_root).splitlines(): 97 match = re.search(status_regexp, line) 98 if not match: 99 continue 100 # status = match.group('status') 101 filename = match.group('filename') 102 filenames.append(filename) 103 return filenames 104 105 def strip_r_from_svn_revision(self, svn_revision): 106 match = re.match("^r(?P<svn_revision>\d+)", unicode(svn_revision)) 107 if (match): 108 return match.group('svn_revision') 109 return svn_revision 110 111 def svn_revision_from_commit_text(self, commit_text): 112 match = re.search(self.commit_success_regexp(), commit_text, re.MULTILINE) 113 return match.group('svn_revision') 114 115 @staticmethod 116 def _subclass_must_implement(): 117 raise NotImplementedError("subclasses must implement") 118 119 @classmethod 120 def in_working_directory(cls, path, executive=None): 121 SCM._subclass_must_implement() 122 123 def find_checkout_root(self, path): 124 SCM._subclass_must_implement() 125 126 @staticmethod 127 def commit_success_regexp(): 128 SCM._subclass_must_implement() 129 130 def status_command(self): 131 self._subclass_must_implement() 132 133 def add(self, path, return_exit_code=False): 134 self.add_list([path], return_exit_code) 135 136 def add_list(self, paths, return_exit_code=False): 137 self._subclass_must_implement() 138 139 def delete(self, path): 140 self.delete_list([path]) 141 142 def delete_list(self, paths): 143 self._subclass_must_implement() 144 145 def move(self, origin, destination): 146 self._subclass_must_implement() 147 148 def exists(self, path): 149 self._subclass_must_implement() 150 151 def changed_files(self, git_commit=None): 152 self._subclass_must_implement() 153 154 def changed_files_for_revision(self, revision): 155 self._subclass_must_implement() 156 157 def revisions_changing_file(self, path, limit=5): 158 self._subclass_must_implement() 159 160 def added_files(self): 161 self._subclass_must_implement() 162 163 def conflicted_files(self): 164 self._subclass_must_implement() 165 166 def display_name(self): 167 self._subclass_must_implement() 168 169 def head_svn_revision(self): 170 return self.svn_revision(self.checkout_root) 171 172 def svn_revision(self, path): 173 """Returns the latest svn revision found in the checkout.""" 174 self._subclass_must_implement() 175 176 def timestamp_of_revision(self, path, revision): 177 self._subclass_must_implement() 178 179 def create_patch(self, git_commit=None, changed_files=None): 180 self._subclass_must_implement() 181 182 def committer_email_for_revision(self, revision): 183 self._subclass_must_implement() 184 185 def contents_at_revision(self, path, revision): 186 self._subclass_must_implement() 187 188 def diff_for_revision(self, revision): 189 self._subclass_must_implement() 190 191 def diff_for_file(self, path, log=None): 192 self._subclass_must_implement() 193 194 def show_head(self, path): 195 self._subclass_must_implement() 196 197 def apply_reverse_diff(self, revision): 198 self._subclass_must_implement() 199 200 def revert_files(self, file_paths): 201 self._subclass_must_implement() 202 203 def commit_with_message(self, message, username=None, password=None, git_commit=None, force_squash=False, changed_files=None): 204 self._subclass_must_implement() 205 206 def svn_commit_log(self, svn_revision): 207 self._subclass_must_implement() 208 209 def last_svn_commit_log(self): 210 self._subclass_must_implement() 211 212 def blame(self, path): 213 self._subclass_must_implement() 214 215 def svn_blame(self, path): 216 self._subclass_must_implement() 217 218 def has_working_directory_changes(self): 219 self._subclass_must_implement() 220 221 def discard_working_directory_changes(self): 222 self._subclass_must_implement() 223 224 #-------------------------------------------------------------------------- 225 # Subclasses must indicate if they support local commits, 226 # but the SCM baseclass will only call local_commits methods when this is true. 227 @staticmethod 228 def supports_local_commits(): 229 SCM._subclass_must_implement() 230 231 def local_commits(self): 232 return [] 233 234 def has_local_commits(self): 235 return len(self.local_commits()) > 0 236 237 def discard_local_commits(self): 238 return 239 240 def remote_merge_base(self): 241 SCM._subclass_must_implement() 242 243 def commit_locally_with_message(self, message, commit_all_working_directory_changes=True): 244 _log.error("Your source control manager does not support local commits.") 245 sys.exit(1) 246 247 def local_changes_exist(self): 248 return (self.supports_local_commits() and self.has_local_commits()) or self.has_working_directory_changes() 249 250 def discard_local_changes(self): 251 if self.has_working_directory_changes(): 252 self.discard_working_directory_changes() 253 254 if self.has_local_commits(): 255 self.discard_local_commits() 256