Trees Indices Help
Trac
Package trac :: Package versioncontrol :: Module svn_fs

Source Code for Module trac.versioncontrol.svn_fs

 1 # -*- coding: utf-8 -*- 
 2 # 
 3 # Copyright (C) 2005-2009 Edgewall Software 
 4 # Copyright (C) 2005 Christopher Lenz <cmlenz@gmx.de> 
 5 # Copyright (C) 2005-2007 Christian Boos <cboos@edgewall.org> 
 6 # All rights reserved. 
 7 # 
 8 # This software is licensed as described in the file COPYING, which 
 9 # you should have received as part of this distribution. The terms 
 10 # are also available at http://trac.edgewall.org/wiki/TracLicense. 
 11 # 
 12 # This software consists of voluntary contributions made by many 
 13 # individuals. For the exact contribution history, see the revision 
 14 # history and logs, available at http://trac.edgewall.org/log/. 
 15 # 
 16 # Author: Christopher Lenz <cmlenz@gmx.de> 
 17 # Christian Boos <cboos@edgewall.org> 
 18 
 19 """Filesystem access to Subversion repositories. 
 20  
 21 '''Note about Unicode:''' 
 22  
 23 The Subversion bindings are not unicode-aware and they expect to 
 24 receive UTF-8 encoded `string` parameters, 
 25  
 26 On the other hand, all paths manipulated by Trac are `unicode` objects. 
 27  
 28 Therefore: 
 29  
 30  * before being handed out to SVN, the Trac paths have to be encoded to 
 31  UTF-8, using `_to_svn()` 
 32  * before being handed out to Trac, a SVN path has to be decoded from 
 33  UTF-8, using `_from_svn()` 
 34  
 35 Whenever a value has to be stored as utf8, we explicitly mark the 
 36 variable name with "_utf8", in order to avoid any possible confusion. 
 37  
 38 Warning: 
 39  `SubversionNode.get_content()` returns an object from which one can read 
 40  a stream of bytes. NO guarantees can be given about what that stream of 
 41  bytes represents. It might be some text, encoded in some way or another. 
 42  SVN properties __might__ give some hints about the content, but they 
 43  actually only reflect the beliefs of whomever set those properties... 
 44 """ 
 45 
 46 import os.path  
 47 import weakref 
 48 import posixpath 
 49 from urllib import quote  
 50 
 51 from trac .config  import ListOption  
 52 from trac .core  import * 
 53 from trac .env  import ISystemInfoProvider 
 54 from trac .versioncontrol  import Changeset , Node , Repository , \ 
 55  IRepositoryConnector , \ 
 56  NoSuchChangeset , NoSuchNode  
 57 from trac .versioncontrol .cache  import CachedRepository  
 58 from trac .util  import embedded_numbers  
 59 from trac .util .concurrency  import threading 
 60 from trac .util .text  import exception_to_unicode , to_unicode  
 61 from trac .util .translation  import _  
 62 from trac .util .datefmt  import from_utimestamp  
 63 
 64 
 65 application_pool  = None 
 66 application_pool_lock  = threading.Lock() 
 67 
 68 
69 - def _import_svn ():
70 global fs, repos, core , delta, _kindmap , _svn_uri_canonicalize 71 from svn import fs, repos, core , delta 72 _kindmap = {core .svn_node_dir: Node .DIRECTORY , 73 core .svn_node_file: Node .FILE } 74 try: 75 _svn_uri_canonicalize = core .svn_uri_canonicalize # Subversion 1.7+ 76 except AttributeError: 77 _svn_uri_canonicalize = lambda v: v 78 # Protect svn.core methods from GC 79 Pool .apr_pool_clear = staticmethod(core .apr_pool_clear) 80 Pool .apr_pool_destroy = staticmethod(core .apr_pool_destroy)
81
82 - def _to_svn (pool, *args):
83 """Expect a pool and a list of `unicode` path components. 84 85 Returns an UTF-8 encoded string suitable for the Subversion python 86 bindings (the returned path never starts with a leading "/") 87 """ 88 return core .svn_path_canonicalize('/'.join (args ).lstrip('/') 89 .encode('utf-8'), 90 pool )
91
92 - def _from_svn (path):
93 """Expect an UTF-8 encoded string and transform it to an `unicode` object 94 95 But Subversion repositories built from conversion utilities can have 96 non-UTF-8 byte strings, so we have to convert using `to_unicode`. 97 """ 98 return path and to_unicode (path , 'utf-8')
99 100 # The following 3 helpers deal with unicode paths 101
102 - def _normalize_path (path):
103 """Remove leading "/", except for the root.""" 104 return path and path .strip('/') or '/'
105
106 - def _path_within_scope (scope, fullpath):
107 """Remove the leading scope from repository paths. 108 109 Return `None` if the path is not is scope. 110 """ 111 if fullpath is not None: 112 fullpath = fullpath.lstrip('/') 113 if scope == '/': 114 return _normalize_path(fullpath) 115 scope = scope .strip('/') 116 if (fullpath + '/').startswith(scope + '/'): 117 return fullpath[len(scope ) + 1:] or '/'
118
119 - def _is_path_within_scope (scope, fullpath):
120 """Check whether the given `fullpath` is within the given `scope`""" 121 if scope == '/': 122 return fullpath is not None 123 fullpath = fullpath and fullpath.lstrip('/') or '' 124 scope = scope .strip('/') 125 return (fullpath + '/').startswith(scope + '/')
126 127 # svn_opt_revision_t helpers 128
129 - def _svn_rev (num):
130 value = core .svn_opt_revision_value_t() 131 value.number = num 132 revision = core .svn_opt_revision_t() 133 revision.kind = core .svn_opt_revision_number 134 revision.value = value 135 return revision
136
137 - def _svn_head ():
138 revision = core .svn_opt_revision_t() 139 revision.kind = core .svn_opt_revision_head 140 return revision
141 142 # apr_pool_t helpers 143
144 - def _mark_weakpool_invalid (weakpool):
145 if weakpool(): 146 weakpool()._mark_invalid()
147 148
149 - class Pool (object):
150 """A Pythonic memory pool object""" 151
152 - def __init__ (self, parent_pool=None):
153 """Create a new memory pool""" 154 155 global application_pool 156 157 application_pool_lock .acquire() 158 try: 159 self._parent_pool = parent_pool or application_pool 160 161 # Create pool 162 if self._parent_pool: 163 self._pool = core .svn_pool_create(self._parent_pool()) 164 else: 165 # If we are an application-level pool, 166 # then initialize APR and set this pool 167 # to be the application-level pool 168 core .apr_initialize() 169 self._pool = core .svn_pool_create(None) 170 application_pool = self 171 finally: 172 application_pool_lock .release() 173 174 self._mark_valid()
175
176 - def __call__ (self):
177 return self._pool
178
179 - def valid (self):
180 """Check whether this memory pool and its parents 181 are still valid""" 182 return hasattr(self,"_is_valid")
183
184 - def assert_valid (self):
185 """Assert that this memory_pool is still valid.""" 186 assert self.valid ()
187
188 - def clear (self):
189 """Clear embedded memory pool. Invalidate all subpools.""" 190 self.apr_pool_clear(self._pool) 191 self._mark_valid()
192
193 - def destroy (self):
194 """Destroy embedded memory pool. If you do not destroy 195 the memory pool manually, Python will destroy it 196 automatically.""" 197 198 global application_pool 199 200 self.assert_valid () 201 202 # Destroy pool 203 self.apr_pool_destroy(self._pool) 204 205 # Clear application pool and terminate APR if necessary 206 if not self._parent_pool: 207 application_pool = None 208 209 self._mark_invalid()
210
211 - def __del__ (self):
212 """Automatically destroy memory pools, if necessary""" 213 if self.valid (): 214 self.destroy ()
215
216 - def _mark_valid (self):
217 """Mark pool as valid""" 218 if self._parent_pool: 219 # Refer to self using a weakreference so that we don't 220 # create a reference cycle 221 weakself = weakref.ref(self) 222 223 # Set up callbacks to mark pool as invalid when parents 224 # are destroyed 225 self._weakref = weakref.ref(self._parent_pool._is_valid, 226 lambda x : \ 227 _mark_weakpool_invalid(weakself)) 228 229 # mark pool as valid 230 self._is_valid = lambda: 1
231
232 - def _mark_invalid (self):
233 """Mark pool as invalid""" 234 if self.valid (): 235 # Mark invalid 236 del self._is_valid 237 238 # Free up memory 239 del self._parent_pool 240 if hasattr(self, "_weakref"): 241 del self._weakref
242 243
244 - class SvnCachedRepository (CachedRepository):
245 """Subversion-specific cached repository, zero-pads revision numbers 246 in the cache tables. 247 """
248 - def db_rev (self, rev):
249 return '%010d' % rev
250
251 - def rev_db (self, rev):
252 return int(rev or 0)
253 254
255 - class SubversionConnector (Component):
256 257 implements (ISystemInfoProvider, IRepositoryConnector ) 258 259 branches = ListOption ('svn', 'branches', 'trunk,branches/*', doc= 260 """Comma separated list of paths categorized as branches. 261 If a path ends with '*', then all the directory entries found below 262 that path will be included. 263 Example: `/trunk, /branches/*, /projectAlpha/trunk, /sandbox/*` 264 """) 265 266 tags = ListOption ('svn', 'tags', 'tags/*', doc= 267 """Comma separated list of paths categorized as tags. 268 269 If a path ends with '*', then all the directory entries found below 270 that path will be included. 271 Example: `/tags/*, /projectAlpha/tags/A-1.0, /projectAlpha/tags/A-v1.1` 272 """) 273 274 error = None 275
276 - def __init__ (self):
277 self._version = None 278 try: 279 _import_svn() 280 self.log .debug('Subversion bindings imported') 281 except ImportError, e : 282 self.error = e 283 self.log .info('Failed to load Subversion bindings', exc_info=True) 284 else: 285 version = (core .SVN_VER_MAJOR, core .SVN_VER_MINOR, 286 core .SVN_VER_MICRO) 287 self._version = '%d.%d.%d' % version + core .SVN_VER_TAG 288 if version [0] < 1: 289 self.error = _ ("Subversion >= 1.0 required, found %(version)s", 290 version =self._version) 291 Pool ()
292 293 # ISystemInfoProvider methods 294
295 - def get_system_info (self):
296 if self._version is not None: 297 yield 'Subversion', self._version
298 299 # IRepositoryConnector methods 300
301 - def get_supported_types (self):
302 prio = 1 303 if self.error : 304 prio = -1 305 yield ("direct-svnfs", prio*4) 306 yield ("svnfs", prio*4) 307 yield ("svn", prio*2)
308
309 - def get_repository (self, type, dir, params):
310 """Return a `SubversionRepository`. 311 312 The repository is wrapped in a `CachedRepository`, unless `type` is 313 'direct-svnfs'. 314 """ 315 params .update (tags =self.tags , branches =self.branches ) 316 fs_repos = SubversionRepository (dir, params , self.log ) 317 if type == 'direct-svnfs': 318 repos = fs_repos 319 else: 320 repos = SvnCachedRepository (self.env , fs_repos, self.log ) 321 repos.has_linear_changesets = True 322 return repos
323 324
325 - class SubversionRepository (Repository):
326 """Repository implementation based on the svn.fs API.""" 327
328 - def __init__ (self, path, params, log):
329 self.log = log 330 self.pool = Pool () 331 332 # Remove any trailing slash or else subversion might abort 333 if isinstance(path , unicode): 334 path_utf8 = path .encode('utf-8') 335 else: # note that this should usually not happen (unicode arg expected) 336 path_utf8 = to_unicode (path ).encode('utf-8') 337 338 path_utf8 = core .svn_path_canonicalize( 339 os.path .normpath(path_utf8).replace ('\\', '/')) 340 self.path = path_utf8.decode('utf-8') 341 342 root_path_utf8 = repos.svn_repos_find_root_path(path_utf8, self.pool ()) 343 if root_path_utf8 is None: 344 raise TracError (_ ("%(path)s does not appear to be a Subversion " 345 "repository.", path =to_unicode (path_utf8))) 346 347 try: 348 self.repos = repos.svn_repos_open(root_path_utf8, self.pool ()) 349 except core .SubversionException, e : 350 raise TracError (_ ("Couldn't open Subversion repository %(path)s: " 351 "%(svn_error)s", path =to_unicode (path_utf8), 352 svn_error=exception_to_unicode (e ))) 353 self.fs_ptr = repos.svn_repos_fs(self.repos) 354 355 self.uuid = fs.get_uuid(self.fs_ptr, self.pool ()) 356 self.base = 'svn:%s:%s' % (self.uuid, _from_svn(root_path_utf8)) 357 name = 'svn:%s:%s' % (self.uuid, self.path ) 358 359 Repository .__init__ (self, name , params , log ) 360 361 # if root_path_utf8 is shorter than the path_utf8, the difference is 362 # this scope (which always starts with a '/') 363 if root_path_utf8 != path_utf8: 364 self.scope = path_utf8[len(root_path_utf8):].decode('utf-8') 365 if not self.scope [-1] == '/': 366 self.scope += '/' 367 else: 368 self.scope = '/' 369 assert self.scope [0] == '/' 370 # we keep root_path_utf8 for RA 371 ra_prefix = os.name == 'nt' and 'file:///' or 'file://' 372 self.ra_url_utf8 = _svn_uri_canonicalize(ra_prefix + 373 quote (root_path_utf8)) 374 self.clear ()
375
376 - def clear (self, youngest_rev=None):
377 self.youngest = None 378 if youngest_rev is not None: 379 self.youngest = self.normalize_rev (youngest_rev ) 380 self.oldest = None
381
382 - def __del__ (self):
383 self.close ()
384
385 - def has_node (self, path, rev=None, pool=None):
386 if not pool : 387 pool = self.pool 388 rev = self.normalize_rev (rev) 389 rev_root = fs.revision_root(self.fs_ptr, rev, pool ()) 390 node_type = fs.check_path(rev_root, _to_svn(pool (), self.scope , path ), 391 pool ()) 392 return node_type in _kindmap
393
394 - def normalize_path (self, path):
395 return _normalize_path(path )
396
397 - def normalize_rev (self, rev):
398 if rev is None or isinstance(rev, basestring) and \ 399 rev.lower() in ('', 'head', 'latest', 'youngest'): 400 return self.youngest_rev 401 else: 402 try: 403 rev = int(rev) 404 if rev <= self.youngest_rev : 405 return rev 406 except (ValueError, TypeError): 407 pass 408 raise NoSuchChangeset (rev)
409
410 - def close (self):
411 if self.pool : 412 self.pool .destroy () 413 self.repos = self.fs_ptr = self.pool = None
414
415 - def get_base (self):
416 return self.base
417
418 - def _get_tags_or_branches (self, paths):
419 """Retrieve known branches or tags.""" 420 for path in self.params .get (paths, []): 421 if path .endswith('*'): 422 folder = posixpath.dirname(path ) 423 try: 424 entries = [n for n in self.get_node (folder).get_entries ()] 425 for node in sorted(entries, key=lambda n: 426 embedded_numbers (n.path .lower())): 427 if node.kind == Node .DIRECTORY : 428 yield node 429 except: # no right (TODO: should use a specific Exception here) 430 pass 431 else: 432 try: 433 yield self.get_node (path ) 434 except: # no right 435 pass
436
437 - def get_quickjump_entries (self, rev):
438 """Retrieve known branches, as (name, id) pairs. 439 440 Purposedly ignores `rev` and always takes the last revision. 441 """ 442 for n in self._get_tags_or_branches('branches'): 443 yield 'branches', n.path , n.path , None 444 for n in self._get_tags_or_branches('tags'): 445 yield 'tags', n.path , n.created_path , n.created_rev
446
447 - def get_path_url (self, path, rev):
448 url = self.params .get ('url', '').rstrip('/') 449 if url : 450 if not path or path == '/': 451 return url 452 return url + '/' + path .lstrip('/')
453
454 - def get_changeset (self, rev):
455 rev = self.normalize_rev (rev) 456 return SubversionChangeset (self, rev, self.scope , self.pool )
457
458 - def get_changeset_uid (self, rev):
459 return (self.uuid, rev)
460
461 - def get_node (self, path, rev=None):
462 path = path or '' 463 if path and path [-1] == '/': 464 path = path [:-1] 465 466 rev = self.normalize_rev (rev) or self.youngest_rev 467 468 return SubversionNode (path , rev, self, self.pool )
469
470 - def _get_node_revs (self, path, last=None, first=None):
471 """Return the revisions affecting `path` between `first` and `last` 472 revs. If `first` is not given, it goes down to the revision in which 473 the branch was created. 474 """ 475 node = self.get_node (path , last) 476 revs = [] 477 for (p, r, chg) in node.get_history (): 478 if p != path or (first and r < first): 479 break 480 revs.append(r) 481 return revs
482
483 - def _get_changed_revs (self, node_infos):
484 path_revs = {} 485 for node, first in node_infos: 486 path = node.path 487 revs = [] 488 for p, r, chg in node.get_history (): 489 if p != path or r < first: 490 break 491 revs.append(r) 492 path_revs[path ] = revs 493 return path_revs
494
495 - def _history (self, path, start, end, pool):
496 """`path` is a unicode path in the scope. 497 498 Generator yielding `(path, rev)` pairs, where `path` is an `unicode` 499 object. 500 Must start with `(path, created rev)`. 501 """ 502 path_utf8 = _to_svn(pool (), self.scope , path ) 503 if start < end: 504 start , end = end, start 505 if (start , end) == (1, 0): # only happens for empty repos 506 return 507 root = fs.revision_root(self.fs_ptr, start , pool ()) 508 # fs.node_history leaks when path doesn't exist (#6588) 509 if fs.check_path(root, path_utf8, pool ()) == core .svn_node_none: 510 return 511 tmp1 = Pool (pool ) 512 tmp2 = Pool (pool ) 513 history_ptr = fs.node_history(root, path_utf8, tmp1()) 514 cross_copies = 1 515 while history_ptr: 516 history_ptr = fs.history_prev(history_ptr, cross_copies, tmp2()) 517 tmp1.clear () 518 tmp1, tmp2 = tmp2, tmp1 519 if history_ptr: 520 path_utf8, rev = fs.history_location(history_ptr, tmp2()) 521 tmp2.clear () 522 if rev < end: 523 break 524 path = _from_svn(path_utf8) 525 yield path , rev 526 del tmp1 527 del tmp2
528
529 - def _previous_rev (self, rev, path='', pool=None):
530 if rev > 1: # don't use oldest here, as it's too expensive 531 for _ , prev in self._history(path , 1, rev-1, pool or self.pool ): 532 return prev 533 return None
534 535
536 - def get_oldest_rev (self):
537 if self.oldest is None: 538 self.oldest = 1 539 # trying to figure out the oldest rev for scoped repository 540 # is too expensive and uncovers a big memory leak (#5213) 541 # if self.scope != '/': 542 # self.oldest = self.next_rev(0, find_initial_rev=True) 543 return self.oldest
544
545 - def get_youngest_rev (self):
546 if not self.youngest: 547 self.youngest = fs.youngest_rev (self.fs_ptr, self.pool ()) 548 if self.scope != '/': 549 for path , rev in self._history('', 1, self.youngest, self.pool ): 550 self.youngest = rev 551 break 552 return self.youngest
553
554 - def previous_rev (self, rev, path=''):
555 rev = self.normalize_rev (rev) 556 return self._previous_rev(rev, path )
557
558 - def next_rev (self, rev, path='', find_initial_rev=False):
559 rev = self.normalize_rev (rev) 560 next = rev + 1 561 youngest = self.youngest_rev 562 subpool = Pool (self.pool ) 563 while next <= youngest: 564 subpool.clear () 565 for _ , next in self._history(path , rev+1, next , subpool): 566 return next 567 else: 568 if not find_initial_rev and \ 569 not self.has_node (path , next , subpool): 570 return next # a 'delete' event is also interesting... 571 next += 1 572 return None
573
574 - def rev_older_than (self, rev1, rev2):
575 return self.normalize_rev (rev1) < self.normalize_rev (rev2)
576
577 - def get_youngest_rev_in_cache (self, db):
578 """Get the latest stored revision by sorting the revision strings 579 numerically 580 581 (deprecated, only used for transparent migration to the new caching 582 scheme). 583 """ 584 cursor = db .cursor () 585 cursor .execute ("SELECT rev FROM revision " 586 "ORDER BY -LENGTH(rev), rev DESC LIMIT 1") 587 row = cursor .fetchone () 588 return row and row[0] or None
589
590 - def get_path_history (self, path, rev=None, limit=None):
591 path = self.normalize_path (path ) 592 rev = self.normalize_rev (rev) 593 expect_deletion = False 594 subpool = Pool (self.pool ) 595 numrevs = 0 596 while rev and (not limit or numrevs < limit): 597 subpool.clear () 598 if self.has_node (path , rev, subpool): 599 if expect_deletion: 600 # it was missing, now it's there again: 601 # rev+1 must be a delete 602 numrevs += 1 603 yield path , rev+1, Changeset .DELETE 604 newer = None # 'newer' is the previously seen history tuple 605 older = None # 'older' is the currently examined history tuple 606 for p, r in self._history(path , 1, rev, subpool): 607 older = (_path_within_scope(self.scope , p), r, 608 Changeset .ADD ) 609 rev = self._previous_rev(r, pool =subpool) 610 if newer: 611 numrevs += 1 612 if older[0] == path : 613 # still on the path: 'newer' was an edit 614 yield newer[0], newer[1], Changeset .EDIT 615 else: 616 # the path changed: 'newer' was a copy 617 rev = self._previous_rev(newer[1], pool =subpool) 618 # restart before the copy op 619 yield newer[0], newer[1], Changeset .COPY 620 older = (older[0], older[1], 'unknown') 621 break 622 newer = older 623 if older: 624 # either a real ADD or the source of a COPY 625 numrevs += 1 626 yield older 627 else: 628 expect_deletion = True 629 rev = self._previous_rev(rev, pool =subpool)
630
631 - def get_changes (self, old_path, old_rev, new_path, new_rev, 632 ignore_ancestry=0):
633 old_node = new_node = None 634 old_rev = self.normalize_rev (old_rev) 635 new_rev = self.normalize_rev (new_rev) 636 if self.has_node (old_path, old_rev): 637 old_node = self.get_node (old_path, old_rev) 638 else: 639 raise NoSuchNode (old_path, old_rev, 'The Base for Diff is invalid') 640 if self.has_node (new_path, new_rev): 641 new_node = self.get_node (new_path, new_rev) 642 else: 643 raise NoSuchNode (new_path, new_rev, 644 'The Target for Diff is invalid') 645 if new_node.kind != old_node.kind: 646 raise TracError (_ ('Diff mismatch: Base is a %(oldnode)s ' 647 '(%(oldpath)s in revision %(oldrev)s) and ' 648 'Target is a %(newnode)s (%(newpath)s in ' 649 'revision %(newrev)s).', oldnode=old_node.kind, 650 oldpath=old_path, oldrev=old_rev, 651 newnode=new_node.kind, newpath=new_path, 652 newrev=new_rev)) 653 subpool = Pool (self.pool ) 654 if new_node.isdir : 655 editor = DiffChangeEditor () 656 e_ptr, e_baton = delta.make_editor(editor, subpool()) 657 old_root = fs.revision_root(self.fs_ptr, old_rev, subpool()) 658 new_root = fs.revision_root(self.fs_ptr, new_rev, subpool()) 659 def authz_cb(root, path, pool): 660 return 1
661 text_deltas = 0 # as this is anyway re-done in Diff.py... 662 entry_props = 0 # "... typically used only for working copy updates" 663 repos.svn_repos_dir_delta(old_root, 664 _to_svn(subpool(), self.scope , old_path), 665 '', new_root, 666 _to_svn(subpool(), self.scope , new_path), 667 e_ptr, e_baton, authz_cb, 668 text_deltas, 669 1, # directory 670 entry_props, 671 ignore_ancestry, 672 subpool()) 673 # sort deltas by path before creating `SubversionNode`s to reduce 674 # memory usage (#10978) 675 deltas = sorted(((_from_svn(path ), kind, change) 676 for path , kind, change in editor.deltas), 677 key=lambda entry: entry[0]) 678 for path , kind, change in deltas: 679 old_node = new_node = None 680 if change != Changeset .ADD : 681 old_node = self.get_node (posixpath.join (old_path, path ), 682 old_rev) 683 if change != Changeset .DELETE : 684 new_node = self.get_node (posixpath.join (new_path, path ), 685 new_rev) 686 else: 687 kind = _kindmap [fs.check_path(old_root, 688 _to_svn(subpool(), 689 self.scope , 690 old_node.path ), 691 subpool())] 692 yield (old_node, new_node, kind, change) 693 else: 694 old_root = fs.revision_root(self.fs_ptr, old_rev, subpool()) 695 new_root = fs.revision_root(self.fs_ptr, new_rev, subpool()) 696 if fs.contents_changed(old_root, 697 _to_svn(subpool(), self.scope , old_path), 698 new_root, 699 _to_svn(subpool(), self.scope , new_path), 700 subpool()): 701 yield (old_node, new_node, Node .FILE , Changeset .EDIT )
702 703
704 - class SubversionNode (Node):
705
706 - def __init__ (self, path, rev, repos, pool=None, parent_root=None):
707 self.fs_ptr = repos.fs_ptr 708 self.scope = repos.scope 709 self.pool = Pool (pool ) 710 pool = self.pool () 711 self._scoped_path_utf8 = _to_svn(pool , self.scope , path ) 712 713 if parent_root: 714 self.root = parent_root 715 else: 716 self.root = fs.revision_root(self.fs_ptr, rev, pool ) 717 node_type = fs.check_path(self.root, self._scoped_path_utf8, pool ) 718 if not node_type in _kindmap : 719 raise NoSuchNode (path , rev) 720 cp_utf8 = fs.node_created_path(self.root, self._scoped_path_utf8, pool ) 721 cp = _from_svn(cp_utf8) 722 cr = fs.node_created_rev(self.root, self._scoped_path_utf8, pool ) 723 # Note: `cp` differs from `path` if the last change was a copy, 724 # In that case, `path` doesn't even exist at `cr`. 725 # The only guarantees are: 726 # * this node exists at (path,rev) 727 # * the node existed at (created_path,created_rev) 728 # Also, `cp` might well be out of the scope of the repository, 729 # in this case, we _don't_ use the ''create'' information. 730 if _is_path_within_scope(self.scope , cp): 731 self.created_rev = cr 732 self.created_path = _path_within_scope(self.scope , cp) 733 else: 734 self.created_rev , self.created_path = rev, path 735 # TODO: check node id 736 Node .__init__ (self, repos, path , rev, _kindmap [node_type])
737
738 - def get_content (self):
739 if self.isdir : 740 return None 741 s = core .Stream(fs.file_contents(self.root, self._scoped_path_utf8, 742 self.pool ())) 743 # Make sure the stream object references the pool to make sure the pool 744 # is not destroyed before the stream object. 745 s._pool = self.pool 746 return s
747
748 - def get_entries (self):
749 if self.isfile : 750 return 751 pool = Pool (self.pool ) 752 entries = fs.dir_entries(self.root, self._scoped_path_utf8, pool ()) 753 for item in entries.keys (): 754 path = posixpath.join (self.path , _from_svn(item)) 755 yield SubversionNode (path , self.rev, self.repos, self.pool , 756 self.root)
757
758 - def get_history (self, limit=None):
759 newer = None # 'newer' is the previously seen history tuple 760 older = None # 'older' is the currently examined history tuple 761 pool = Pool (self.pool ) 762 numrevs = 0 763 for path , rev in self.repos._history(self.path , 1, self.rev, pool ): 764 path = _path_within_scope(self.scope , path ) 765 if rev > 0 and path : 766 older = (path , rev, Changeset .ADD ) 767 if newer: 768 if newer[0] == older[0]: # stay on same path 769 change = Changeset .EDIT 770 else: 771 change = Changeset .COPY 772 newer = (newer[0], newer[1], change) 773 numrevs += 1 774 yield newer 775 newer = older 776 if limit and numrevs >= limit: 777 break 778 if newer and (not limit or numrevs < limit): 779 yield newer
780
781 - def get_annotations (self):
782 annotations = [] 783 if self.isfile : 784 def blame_receiver(line_no, revision, author, date, line, pool): 785 annotations.append(revision)
786 try: 787 rev = _svn_rev(self.rev) 788 start = _svn_rev(0) 789 file_url_utf8 = posixpath.join (self.repos.ra_url_utf8, 790 quote (self._scoped_path_utf8)) 791 # svn_client_blame2() requires a canonical uri since 792 # Subversion 1.7 (#11167) 793 file_url_utf8 = _svn_uri_canonicalize(file_url_utf8) 794 self.repos.log .info('opening ra_local session to %r', 795 file_url_utf8) 796 from svn import client 797 client.blame2(file_url_utf8, rev, start , rev, blame_receiver, 798 client.create_context(), self.pool ()) 799 except (core .SubversionException, AttributeError), e : 800 # svn thinks file is a binary or blame not supported 801 raise TracError (_ ('svn blame failed on %(path)s: %(error)s', 802 path =self.path , error =to_unicode (e ))) 803 return annotations
804 805 # def get_previous(self): 806 # # FIXME: redo it with fs.node_history 807
808 - def get_properties (self):
809 props = fs.node_proplist(self.root, self._scoped_path_utf8, self.pool ()) 810 for name , value in props.items(): 811 # Note that property values can be arbitrary binary values 812 # so we can't assume they are UTF-8 strings... 813 props[_from_svn(name )] = to_unicode (value) 814 return props
815
816 - def get_content_length (self):
817 if self.isdir : 818 return None 819 return fs.file_length(self.root, self._scoped_path_utf8, self.pool ())
820
821 - def get_content_type (self):
822 if self.isdir : 823 return None 824 return self._get_prop(core .SVN_PROP_MIME_TYPE)
825
826 - def get_last_modified (self):
827 _date = fs.revision_prop(self.fs_ptr, self.created_rev , 828 core .SVN_PROP_REVISION_DATE, self.pool ()) 829 if not _date: 830 return None 831 return from_utimestamp (core .svn_time_from_cstring(_date, self.pool ()))
832
833 - def _get_prop (self, name):
834 return fs.node_prop(self.root, self._scoped_path_utf8, name , 835 self.pool ())
836
837 - def get_branch_origin (self):
838 """Return the revision in which the node's path was created""" 839 root_and_path = fs.closest_copy(self.root, self._scoped_path_utf8) 840 if root_and_path: 841 return fs.revision_root_revision(root_and_path[0])
842
843 - def get_copy_ancestry (self):
844 """Retrieve the list of `(path,rev)` copy ancestors of this node. 845 Most recent ancestor first. Each ancestor `(path, rev)` corresponds 846 to the path and revision of the source at the time the copy or move 847 operation was performed. 848 """ 849 ancestors = [] 850 previous = (self._scoped_path_utf8, self.rev, self.root) 851 while previous: 852 (previous_path, previous_rev , previous_root) = previous 853 previous = None 854 root_path = fs.closest_copy(previous_root, previous_path) 855 if root_path: 856 (root, path ) = root_path 857 path = path .lstrip('/') 858 rev = fs.revision_root_revision(root) 859 relpath = None 860 if path != previous_path: 861 # `previous_path` is a subfolder of `path` and didn't 862 # change since `path` was copied 863 relpath = previous_path[len(path ):].strip('/') 864 copied_from = fs.copied_from(root, path ) 865 if copied_from: 866 (rev, path ) = copied_from 867 path = path .lstrip('/') 868 root = fs.revision_root(self.fs_ptr, rev, self.pool ()) 869 if relpath: 870 path += '/' + relpath 871 ui_path = _path_within_scope(self.scope , _from_svn(path )) 872 if ui_path: 873 ancestors.append((ui_path, rev)) 874 previous = (path , rev, root) 875 return ancestors
876 877
878 - class SubversionChangeset (Changeset):
879
880 - def __init__ (self, repos, rev, scope, pool=None):
881 self.rev = rev 882 self.scope = scope 883 self.fs_ptr = repos.fs_ptr 884 self.pool = Pool (pool ) 885 try: 886 message = self._get_prop(core .SVN_PROP_REVISION_LOG) 887 except core .SubversionException: 888 raise NoSuchChangeset (rev) 889 author = self._get_prop(core .SVN_PROP_REVISION_AUTHOR) 890 # we _hope_ it's UTF-8, but can't be 100% sure (#4321) 891 message = message and to_unicode (message , 'utf-8') 892 author = author and to_unicode (author, 'utf-8') 893 _date = self._get_prop(core .SVN_PROP_REVISION_DATE) 894 if _date: 895 ts = core .svn_time_from_cstring(_date, self.pool ()) 896 date = from_utimestamp (ts) 897 else: 898 date = None 899 Changeset .__init__ (self, repos, rev, message , author, date)
900
901 - def get_properties (self):
902 props = fs.revision_proplist(self.fs_ptr, self.rev, self.pool ()) 903 properties = {} 904 for k, v in props.iteritems(): 905 if k not in (core .SVN_PROP_REVISION_LOG, 906 core .SVN_PROP_REVISION_AUTHOR, 907 core .SVN_PROP_REVISION_DATE): 908 properties[k] = to_unicode (v) 909 # Note: the above `to_unicode` has a small probability 910 # to mess-up binary properties, like icons. 911 return properties
912
913 - def get_changes (self):
914 pool = Pool (self.pool ) 915 tmp = Pool (pool ) 916 root = fs.revision_root(self.fs_ptr, self.rev, pool ()) 917 editor = repos.RevisionChangeCollector(self.fs_ptr, self.rev, pool ()) 918 e_ptr, e_baton = delta.make_editor(editor, pool ()) 919 repos.svn_repos_replay(root, e_ptr, e_baton, pool ()) 920 921 idx = 0 922 copies, deletions = {}, {} 923 changes = [] 924 revroots = {} 925 for path_utf8, change in editor.changes.items(): 926 new_path = _from_svn(path_utf8) 927 928 # Filtering on `path` 929 if not _is_path_within_scope(self.scope , new_path): 930 continue 931 932 path_utf8 = change.path 933 base_path_utf8 = change.base_path 934 path = _from_svn(path_utf8) 935 base_path = _from_svn(base_path_utf8) 936 base_rev = change.base_rev 937 change_action = getattr(change, 'action', None) 938 939 # Ensure `base_path` is within the scope 940 if not _is_path_within_scope(self.scope , base_path ): 941 base_path , base_rev = None, -1 942 943 # Determine the action 944 if not path and not new_path and self.scope == '/': 945 action = Changeset .EDIT # root property change 946 elif not path or (change_action is not None 947 and change_action == repos.CHANGE_ACTION_DELETE): 948 if new_path: # deletion 949 action = Changeset .DELETE 950 deletions[new_path.lstrip('/')] = idx 951 else: # deletion outside of scope, ignore 952 continue 953 elif change.added or not base_path : # add or copy 954 action = Changeset .ADD 955 if base_path and base_rev: 956 action = Changeset .COPY 957 copies[base_path .lstrip('/')] = idx 958 else: 959 action = Changeset .EDIT 960 # identify the most interesting base_path/base_rev 961 # in terms of last changed information (see r2562) 962 if revroots.has_key(base_rev): 963 b_root = revroots[base_rev] 964 else: 965 b_root = fs.revision_root(self.fs_ptr, base_rev, pool ()) 966 revroots[base_rev] = b_root 967 tmp.clear () 968 cbase_path_utf8 = fs.node_created_path(b_root, base_path_utf8, 969 tmp()) 970 cbase_path = _from_svn(cbase_path_utf8) 971 cbase_rev = fs.node_created_rev(b_root, base_path_utf8, tmp()) 972 # give up if the created path is outside the scope 973 if _is_path_within_scope(self.scope , cbase_path): 974 base_path , base_rev = cbase_path, cbase_rev 975 976 kind = _kindmap [change.item_kind] 977 path = _path_within_scope(self.scope , new_path or base_path ) 978 base_path = _path_within_scope(self.scope , base_path ) 979 changes.append([path , kind, action, base_path , base_rev]) 980 idx += 1 981 982 moves = [] 983 # a MOVE is a COPY whose `base_path` corresponds to a `new_path` 984 # which has been deleted 985 for k, v in copies.items(): 986 if k in deletions: 987 changes[v][2] = Changeset .MOVE 988 moves.append(deletions[k]) 989 offset = 0 990 moves.sort() 991 for i in moves: 992 del changes[i - offset] 993 offset += 1 994 995 changes.sort() 996 for change in changes: 997 yield tuple(change)
998
999 - def _get_prop (self, name):
1000 return fs.revision_prop(self.fs_ptr, self.rev, name , self.pool ())
1001 1002 1003 # 1004 # Delta editor for diffs between arbitrary nodes 1005 # 1006 # Note 1: the 'copyfrom_path' and 'copyfrom_rev' information is not used 1007 # because 'repos.svn_repos_dir_delta' *doesn't* provide it. 1008 # 1009 # Note 2: the 'dir_baton' is the path of the parent directory 1010 # 1011 1012
1013 - def DiffChangeEditor ():
1014 1015 class DiffChangeEditor(delta.Editor): 1016 1017 def __init__(self): 1018 self.deltas = []
1019 1020 # -- svn.delta.Editor callbacks 1021 1022 def open_root(self, base_revision, dir_pool): 1023 return ('/', Changeset .EDIT ) 1024 1025 def add_directory(self, path, dir_baton, copyfrom_path, copyfrom_rev, 1026 dir_pool): 1027 self.deltas.append((path , Node .DIRECTORY , Changeset .ADD )) 1028 return (path , Changeset .ADD ) 1029 1030 def open_directory(self, path, dir_baton, base_revision, dir_pool): 1031 return (path , dir_baton[1]) 1032 1033 def change_dir_prop(self, dir_baton, name, value, pool): 1034 path , change = dir_baton 1035 if change != Changeset .ADD : 1036 self.deltas.append((path , Node .DIRECTORY , change)) 1037 1038 def delete_entry(self, path, revision, dir_baton, pool): 1039 self.deltas.append((path , None, Changeset .DELETE )) 1040 1041 def add_file(self, path, dir_baton, copyfrom_path, copyfrom_revision, 1042 dir_pool): 1043 self.deltas.append((path , Node .FILE , Changeset .ADD )) 1044 1045 def open_file(self, path, dir_baton, dummy_rev, file_pool): 1046 self.deltas.append((path , Node .FILE , Changeset .EDIT )) 1047 1048 return DiffChangeEditor () 1049
Trees Indices Help
Trac
Generated by Epydoc 3.0.1 on Mon Feb 13 23:37:26 2023 http://epydoc.sourceforge.net

AltStyle によって変換されたページ (->オリジナル) /