1# Licensed to the Apache Software Foundation (ASF) under one 2# or more contributor license agreements. See the NOTICE file 3# distributed with this work for additional information 4# regarding copyright ownership. The ASF licenses this file 5# to you under the Apache License, Version 2.0 (the 6# "License"); you may not use this file except in compliance 7# with the License. You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, 12# software distributed under the License is distributed on an 13# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14# KIND, either express or implied. See the License for the 15# specific language governing permissions and limitations 16# under the License. 17 18 19import csvn.core as svn 20from csvn.core import * 21import os 22 23class Txn(object): 24 def __init__(self, session): 25 self.pool = Pool() 26 self.iterpool = Pool() 27 self.session = session 28 self.root = _txn_operation(None, "OPEN", svn_node_dir) 29 self.commit_callback = None 30 self.ignore_func = None 31 self.autoprop_func = None 32 33 def ignore(self, ignore_func): 34 """Setup a callback function which decides whether a 35 new directory or path should be added to the repository. 36 37 IGNORE_FUNC must be a function which accepts two arguments: 38 (path, kind) 39 40 PATH is the path which is about to be added to the repository. 41 KIND is either svn_node_file or svn_node_dir, depending 42 on whether the proposed path is a file or a directory. 43 44 If IGNORE_FUNC returns True, the path will be ignored. Otherwise, 45 the path will be added. 46 47 Note that IGNORE_FUNC is only called when new files or 48 directories are added to the repository. It is not called, 49 for example, when directories within the repository are moved 50 or copied, since these copies are not new.""" 51 52 self.ignore_func = ignore_func 53 54 def autoprop(self, autoprop_func): 55 """Setup a callback function which automatically sets up 56 properties on new files or directories added to the 57 repository. 58 59 AUTOPROP_FUNC must be a function which accepts three 60 arguments: (txn, path, kind) 61 62 TXN is this transaction object. 63 PATH is the path which was just added to the repository. 64 KIND is either svn_node_file or svn_node_dir, depending 65 on whether the newly added path is a file or a directory. 66 67 If AUTOPROP_FUNC wants to set properties on PATH, it should 68 call TXN.propset with the appropriate arguments. 69 70 Note that AUTOPROP_FUNC is only called when new files or 71 directories are added to the repository. It is not called, 72 for example, when directories within the repository are moved 73 or copied, since these copies are not new.""" 74 75 self.autoprop_func = autoprop_func 76 77 def check_path(self, path, rev=None): 78 """Check the status of PATH@REV. If PATH or any of its 79 parents have been modified in this transaction, take this 80 into consideration.""" 81 path = self.session._relative_path(path) 82 return self._check_path(path, rev)[0] 83 84 def delete(self, path, base_rev=None): 85 """Delete PATH from the repository as of base_rev""" 86 87 path = self.session._relative_path(path) 88 89 kind, parent = self._check_path(path, base_rev) 90 91 if kind == svn_node_none: 92 if base_rev: 93 message = "'%s' not found in r%d" % (path, base_rev) 94 else: 95 message = "'%s' not found" % (path) 96 raise SubversionException(SVN_ERR_BAD_URL, message) 97 98 parent.open(path, "DELETE", kind) 99 100 def mkdir(self, path): 101 """Create a directory at PATH.""" 102 103 path = self.session._relative_path(path) 104 105 if self.ignore_func and self.ignore_func(path, svn_node_dir): 106 return 107 108 kind, parent = self._check_path(path) 109 110 if kind != svn_node_none: 111 if kind == svn_node_dir: 112 message = ("Can't create directory '%s': " 113 "Directory already exists" % path) 114 else: 115 message = ("Can't create directory '%s': " 116 "Path obstructed by file" % path) 117 raise SubversionException(SVN_ERR_BAD_URL, message) 118 119 parent.open(path, "ADD", svn_node_dir) 120 121 # Trigger autoprop_func on new directory adds 122 if self.autoprop_func: 123 self.autoprop_func(self, path, svn_node_dir) 124 125 def propset(self, path, key, value): 126 """Set the property named KEY to VALUE on the specified PATH""" 127 128 path = self.session._relative_path(path) 129 130 kind, parent = self._check_path(path) 131 132 if kind == svn_node_none: 133 message = ("Can't set property on '%s': " 134 "No such file or directory" % path) 135 raise SubversionException(SVN_ERR_BAD_URL, message) 136 137 node = parent.open(path, "OPEN", kind) 138 node.propset(key, value) 139 140 def propdel(self, path, key): 141 """Delete the property named KEY on the specified PATH""" 142 143 path = self.session._relative_path(path) 144 145 kind, parent = self._check_path(path) 146 147 if kind == svn_node_none: 148 message = ("Can't delete property on '%s': " 149 "No such file or directory" % path) 150 raise SubversionException(SVN_ERR_BAD_URL, message) 151 152 node = parent.open(path, "OPEN", kind) 153 node.propdel(key) 154 155 156 def copy(self, src_path, dest_path, src_rev=None, local_path=None): 157 """Copy a file or directory from SRC_PATH@SRC_REV to DEST_PATH. 158 If SRC_REV is not supplied, use the latest revision of SRC_PATH. 159 If LOCAL_PATH is supplied, update the new copy to match 160 LOCAL_PATH.""" 161 162 src_path = self.session._relative_path(src_path) 163 dest_path = self.session._relative_path(dest_path) 164 165 if not src_rev: 166 src_rev = self.session.latest_revnum() 167 168 kind = self.session.check_path(src_path, src_rev, encoded=False) 169 _, parent = self._check_path(dest_path) 170 171 if kind == svn_node_none: 172 message = ("Can't copy '%s': " 173 "No such file or directory" % src_path) 174 raise SubversionException(SVN_ERR_BAD_URL, message) 175 176 if kind == svn_node_file or local_path is None: 177 # Mark the file or directory as copied 178 parent.open(dest_path, "ADD", 179 kind, copyfrom_path=src_path, 180 copyfrom_rev=src_rev, 181 local_path=local_path) 182 else: 183 # Mark the directory as copied 184 parent.open(dest_path, "ADD", 185 kind, copyfrom_path=src_path, 186 copyfrom_rev=src_rev) 187 188 # Upload any changes from the supplied local path 189 # to the remote repository 190 self.upload(dest_path, local_path) 191 192 def upload(self, remote_path, local_path): 193 """Upload a local file or directory into the remote repository. 194 If the given file or directory already exists in the 195 repository, overwrite it. 196 197 This function does not add or update ignored files or 198 directories.""" 199 200 remote_path = self.session._relative_path(remote_path) 201 202 kind = svn_node_none 203 if os.path.isdir(local_path): 204 kind = svn_node_dir 205 elif os.path.exists(local_path): 206 kind = svn_node_file 207 208 # Don't add ignored files or directories 209 if self.ignore_func and self.ignore_func(remote_path, kind): 210 return 211 212 if (os.path.isdir(local_path) and 213 self.check_path(remote_path) != svn_node_dir): 214 self.mkdir(remote_path) 215 elif not os.path.isdir(local_path) and os.path.exists(local_path): 216 self._upload_file(remote_path, local_path) 217 218 ignores = [] 219 220 for root, dirs, files in os.walk(local_path): 221 222 # Convert the local root into a remote root 223 remote_root = root.replace(local_path.rstrip(os.path.sep), 224 remote_path.rstrip("/")) 225 remote_root = remote_root.replace(os.path.sep, "/").rstrip("/") 226 227 # Don't process ignored subdirectories 228 if (self.ignore_func and self.ignore_func(root, svn_node_dir) 229 or root in ignores): 230 231 # Ignore children too 232 for name in dirs: 233 ignores.append("%s/%s" % (remote_root, name)) 234 235 # Skip to the next tuple 236 continue 237 238 # Add all subdirectories 239 for name in dirs: 240 remote_dir = "%s/%s" % (remote_root, name) 241 self.mkdir(remote_dir) 242 243 # Add all files in this directory 244 for name in files: 245 remote_file = "%s/%s" % (remote_root, name) 246 local_file = os.path.join(root, name) 247 self._upload_file(remote_file, local_file) 248 249 def _txn_commit_callback(self, info, baton, pool): 250 self._txn_committed(info[0]) 251 252 def commit(self, message, base_rev = None): 253 """Commit all changes to the remote repository""" 254 255 if base_rev is None: 256 base_rev = self.session.latest_revnum() 257 258 commit_baton = c_void_p() 259 260 self.commit_callback = svn_commit_callback2_t(self._txn_commit_callback) 261 (editor, editor_baton) = self.session._get_commit_editor(message, 262 self.commit_callback, commit_baton, self.pool) 263 264 child_baton = c_void_p() 265 try: 266 self.root.replay(editor[0], self.session, base_rev, editor_baton) 267 except SubversionException: 268 try: 269 SVN_ERR(editor[0].abort_edit(editor_baton, self.pool)) 270 except SubversionException: 271 pass 272 raise 273 274 return self.committed_rev 275 276 # This private function handles commits and saves 277 # information about them in this object 278 def _txn_committed(self, info): 279 self.committed_rev = info.revision 280 self.committed_date = info.date 281 self.committed_author = info.author 282 self.post_commit_err = info.post_commit_err 283 284 # This private function uploads a single file to the 285 # remote repository. Don't use this function directly. 286 # Use 'upload' instead. 287 def _upload_file(self, remote_path, local_path): 288 289 if self.ignore_func and self.ignore_func(remote_path, svn_node_file): 290 return 291 292 kind, parent = self._check_path(remote_path) 293 if svn_node_none == kind: 294 mode = "ADD" 295 else: 296 mode = "OPEN" 297 298 parent.open(remote_path, mode, svn_node_file, 299 local_path=local_path) 300 301 # Trigger autoprop_func on new file adds 302 if mode == "ADD" and self.autoprop_func: 303 self.autoprop_func(self, remote_path, svn_node_file) 304 305 # Calculate the kind of the specified file, and open a handle 306 # to its parent operation. 307 def _check_path(self, path, rev=None): 308 path_components = path.split("/") 309 parent = self.root 310 copyfrom_path = None 311 total_path = path_components[0] 312 for path_component in path_components[1:]: 313 parent = parent.open(total_path, "OPEN") 314 if parent.copyfrom_path: 315 copyfrom_path = parent.copyfrom_path 316 rev = parent.copyfrom_rev 317 318 total_path = "%s/%s" % (total_path, path_component) 319 if copyfrom_path: 320 copyfrom_path = "%s/%s" % (copyfrom_path, path_component) 321 322 if path in parent.ops: 323 node = parent.open(path) 324 if node.action == "DELETE": 325 kind = svn_node_none 326 else: 327 kind = node.kind 328 else: 329 kind = self.session.check_path(copyfrom_path or total_path, rev, 330 encoded=False) 331 332 return (kind, parent) 333 334 335 336class _txn_operation(object): 337 def __init__(self, path, action, kind, copyfrom_path = None, 338 copyfrom_rev = -1, local_path = None): 339 self.path = path 340 self.action = action 341 self.kind = kind 342 self.copyfrom_path = copyfrom_path 343 self.copyfrom_rev = copyfrom_rev 344 self.local_path = local_path 345 self.ops = {} 346 self.properties = {} 347 348 def propset(self, key, value): 349 """Set the property named KEY to VALUE on this file/dir""" 350 self.properties[key] = value 351 352 def propdel(self, key): 353 """Delete the property named KEY on this file/dir""" 354 self.properties[key] = None 355 356 def open(self, path, action="OPEN", kind=svn_node_dir, 357 copyfrom_path = None, copyfrom_rev = -1, local_path = None): 358 if path in self.ops: 359 op = self.ops[path] 360 if action == "OPEN" and op.kind in (svn_node_dir, svn_node_file): 361 return op 362 elif action == "ADD" and op.action == "DELETE": 363 op.action = "REPLACE" 364 op.local_path = local_path 365 op.copyfrom_path = copyfrom_path 366 op.copyfrom_rev = copyfrom_rev 367 op.kind = kind 368 return op 369 elif (action == "DELETE" and op.action == "OPEN" and 370 kind == svn_node_dir): 371 op.action = action 372 return op 373 else: 374 # throw error 375 pass 376 else: 377 self.ops[path] = _txn_operation(path, action, kind, 378 copyfrom_path = copyfrom_path, 379 copyfrom_rev = copyfrom_rev, 380 local_path = local_path) 381 return self.ops[path] 382 383 def replay(self, editor, session, base_rev, baton): 384 subpool = Pool() 385 child_baton = c_void_p() 386 file_baton = c_void_p() 387 if self.path is None: 388 SVN_ERR(editor.open_root(baton, svn_revnum_t(base_rev), subpool, 389 byref(child_baton))) 390 else: 391 if self.action == "DELETE" or self.action == "REPLACE": 392 SVN_ERR(editor.delete_entry(self.path, base_rev, baton, 393 subpool)) 394 elif self.action == "OPEN": 395 if self.kind == svn_node_dir: 396 SVN_ERR(editor.open_directory(self.path, baton, 397 svn_revnum_t(base_rev), subpool, 398 byref(child_baton))) 399 else: 400 SVN_ERR(editor.open_file(self.path, baton, 401 svn_revnum_t(base_rev), subpool, 402 byref(file_baton))) 403 404 if self.action in ("ADD", "REPLACE"): 405 copyfrom_path = None 406 if self.copyfrom_path is not None: 407 copyfrom_path = session._abs_copyfrom_path( 408 self.copyfrom_path) 409 if self.kind == svn_node_dir: 410 SVN_ERR(editor.add_directory( 411 self.path, baton, copyfrom_path, 412 svn_revnum_t(self.copyfrom_rev), subpool, 413 byref(child_baton))) 414 else: 415 SVN_ERR(editor.add_file(self.path, baton, 416 copyfrom_path, svn_revnum_t(self.copyfrom_rev), 417 subpool, byref(file_baton))) 418 419 # Write out changes to properties 420 for (name, value) in self.properties.items(): 421 if value is None: 422 svn_value = POINTER(svn_string_t)() 423 else: 424 svn_value = svn_string_ncreate(value, len(value), 425 subpool) 426 if file_baton: 427 SVN_ERR(editor.change_file_prop(file_baton, name, 428 svn_value, subpool)) 429 elif child_baton: 430 SVN_ERR(editor.change_dir_prop(child_baton, name, 431 svn_value, subpool)) 432 433 # If there's a source file, and we opened a file to write, 434 # write out the contents 435 if self.local_path and file_baton: 436 handler = svn_txdelta_window_handler_t() 437 handler_baton = c_void_p() 438 f = POINTER(apr_file_t)() 439 SVN_ERR(editor.apply_textdelta(file_baton, NULL, subpool, 440 byref(handler), byref(handler_baton))) 441 442 svn_io_file_open(byref(f), self.local_path, APR_READ, 443 APR_OS_DEFAULT, subpool) 444 contents = svn_stream_from_aprfile(f, subpool) 445 svn_txdelta_send_stream(contents, handler, handler_baton, 446 NULL, subpool) 447 svn_io_file_close(f, subpool) 448 449 # If we opened a file, we need to close it 450 if file_baton: 451 SVN_ERR(editor.close_file(file_baton, NULL, subpool)) 452 453 if self.kind == svn_node_dir and self.action != "DELETE": 454 assert(child_baton) 455 456 # Look at the children 457 for op in self.ops.values(): 458 op.replay(editor, session, base_rev, child_baton) 459 460 if self.path: 461 # Close the directory 462 SVN_ERR(editor.close_directory(child_baton, subpool)) 463 else: 464 # Close the editor 465 SVN_ERR(editor.close_edit(baton, subpool)) 466 467