Package svnmailer :: Module subversion
[hide private]

Source Code for Module svnmailer.subversion

  1  # -*- coding: utf-8 -*- 
  2  # pylint: disable-msg = C0103, R0901, W0201 
  3  # 
  4  # Copyright 2004-2006 André Malo or his licensors, as applicable 
  5  # 
  6  # Licensed under the Apache License, Version 2.0 (the "License"); 
  7  # you may not use this file except in compliance with the License. 
  8  # You may obtain a copy of the License at 
  9  # 
 10  #     http://www.apache.org/licenses/LICENSE-2.0 
 11  # 
 12  # Unless required by applicable law or agreed to in writing, software 
 13  # distributed under the License is distributed on an "AS IS" BASIS, 
 14  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
 15  # See the License for the specific language governing permissions and 
 16  # limitations under the License. 
 17  """ 
 18  Access to the subversion respository 
 19   
 20  @var version: The version of the subversion library (maj, min, pat, rev) 
 21  @type version: C{_Version} 
 22   
 23  @var _SVN_INVALID_REVNUM: the invalid revision number 
 24  @type _SVN_INVALID_REVNUM: C{int} 
 25  """ 
 26  __author__    = "André Malo" 
 27  __docformat__ = "epytext en" 
 28  __all__       = [ 
 29      'version', 'Repository', 'Error', 'RepositoryError', 'isUnicodeProperty' 
 30      'VersionedPathDescriptor', 'LockedPathDescriptor', 
 31  ] 
 32   
 33   
 34  # global imports 
 35  import os 
 36  from svn import core as svn_core 
 37  from svn import repos as svn_repos 
 38  from svn import fs as svn_fs 
 39  from svn import delta as svn_delta 
 40   
 41  _SVN_INVALID_REVNUM = svn_core.SWIG_SVN_INVALID_REVNUM 
 42   
 43  # Exceptions 
 44  Error = svn_core.SubversionException 
 45   
 46   
47 -class _Version(object):
48 """ SVN version container class 49 50 @ivar major: Major version 51 @type major: C{int} 52 53 @ivar minor: Minor version 54 @type minor: C{int} 55 56 @ivar patch: Patch level 57 @type patch: C{int} 58 59 @ivar revision: Revision number 60 @type revision: C{int} 61 62 @ivar tag: Additional tag 63 @type tag: C{str} 64 65 @ivar min_1_2: SVN >= 1.2? 66 @type min_1_2: C{bool} 67 """ 68
69 - def __init__(self):
70 """ Initialization """ 71 self.major = svn_core.SVN_VER_MAJOR 72 self.minor = svn_core.SVN_VER_MINOR 73 self.revision = svn_core.SVN_VER_REVISION 74 self.tag = svn_core.SVN_VER_TAG 75 try: 76 self.patch = svn_core.SVN_VER_PATCH # 1.1 77 except AttributeError: 78 self.patch = svn_core.SVN_VER_MICRO # 1.0 79 80 self.min_1_2 = bool( 81 self.major > 1 or (self.major == 1 and self.minor >= 2) 82 ) 83 self.min_1_7 = bool( 84 self.major > 1 or (self.major == 1 and self.minor >= 7) 85 )
86 87 version = _Version() 88 89
90 -def isUnicodeProperty(name):
91 """ Returns if the supplied name represents a translated property 92 93 @param name: The property name 94 @type name: C{str} 95 96 @return: The decision 97 @rtype: C{bool} 98 """ 99 return bool(svn_core.svn_prop_needs_translation(name))
100 101
102 -def isBinary(mtype):
103 """ Returns True if the supplied mime type represents a binary 104 105 @param mtype: The mime type 106 @type mtype: C{str} 107 108 @return: The decision 109 @rtype: C{bool} 110 """ 111 return bool(mtype and 112 svn_core.svn_mime_type_is_binary(mtype) 113 )
114 115
116 -class RepositoryError(Exception):
117 """ A repository error occured 118 119 @ivar svn_err_code: The SVN error code 120 @type svn_err_code: C{int} 121 122 @ivar svn_err_name: The name of the SVN error (if it could be mapped) 123 @type svn_err_name: C{str} 124 125 @ivar svn_err_str: The SVN error description 126 @type svn_err_str: C{str} 127 """ 128
129 - def __init__(self, sexc):
130 """ Initialization 131 132 @param sexc: C{svnmailer.subversion.Error} 133 @type sexc: C{svnmailer.subversion.Error} 134 """ 135 Exception.__init__(self) 136 self.svn_err_str, self.svn_err_code = sexc.args 137 self.svn_err_name = dict([(getattr(svn_core, var), var) 138 for var in vars(svn_core) if var.startswith('SVN_ERR') 139 ]).get(self.svn_err_code, 'unknown APR code')
140 141
142 - def __str__(self):
143 """ Human readable representation 144 145 @return: The string representation 146 @rtype: C{str} 147 """ 148 return str((self.svn_err_code, self.svn_err_name, self.svn_err_str))
149 150 151 # Main repository access class
152 -class Repository(object):
153 """ Access to the subversion repository 154 155 @cvar _pool: C{None} 156 @ivar _pool: main APR pool 157 @type _pool: swig object 158 159 @cvar _apr_initialized: C{False} 160 @ivar _apr_initialized: is APR initialized? 161 @type _apr_initialized: C{bool} 162 163 @ivar _repos: Reference to the open repository 164 @type _repos: swig object 165 166 @ivar _fs: Reference to the repos filesystem 167 @type _fs: swig object 168 169 @ivar _revRoots: Cached revision root objects 170 @type _revRoots: C{dict} 171 172 @ivar _revChanges: Cached revision change lists 173 @type _revChanges: C{dict} 174 175 @ivar _revProps: Cached revision properties 176 @type _revProps: C{dict} 177 178 @ivar _revTimes: Cached revision times 179 @type _revTimes: C{dict} 180 181 @ivar _pathProps: Cached path properties 182 @type _pathProps: C{dict} 183 184 @ivar _pathPropLists: Cached path propery lists 185 @type _pathPropLists: C{dict} 186 187 @ivar path: The path to the repository 188 @type path: C{unicode} 189 """ 190 _pool = None 191 _apr_initialized = False 192 193
194 - def __init__(self, repos_path):
195 """ Open the repository 196 197 @param repos_path: The repository path as unicode 198 @type repos_path: C{unicode} 199 """ 200 # init APR 201 svn_core.apr_initialize() 202 self._apr_initialized = True 203 204 # get ourself a pool 205 self._pool = svn_core.svn_pool_create(None) 206 207 # normalize the repos path 208 repos_path = repos_path.encode("utf-8", "strict") 209 repos_path = os.path.normpath(os.path.abspath(repos_path)) 210 211 # open the repos 212 self._repos = svn_repos.svn_repos_open(repos_path, self._pool) 213 self._fs = svn_repos.svn_repos_fs(self._repos) 214 215 # init the rest 216 self._revRoots = {} 217 self._revChanges = {} 218 self._revProps = {} 219 self._revTimes = {} 220 self._pathProps = {} 221 self._pathPropLists = {} 222 self.path = repos_path
223 224
225 - def close(self):
226 """ Destroy the pool and release the shared lock """ 227 try: 228 if self._pool: 229 pool = self._pool 230 self._pool = None 231 svn_core.svn_pool_destroy(pool) 232 finally: 233 if self._apr_initialized: 234 self._apr_initialized = False 235 svn_core.apr_terminate()
236 237
238 - def getChangesList(self, revision):
239 """ Return the list of changes of a revisions sorted by path 240 241 @param revision: The revision 242 @type revision: C{int} 243 244 @return: The Changes list 245 @rtype: C{list} 246 """ 247 try: 248 changelist = self._revChanges[revision] 249 except KeyError: 250 editor = self._getChangeCollector(revision) 251 e_pool = editor.getPool() 252 e_ptr, e_baton = svn_delta.make_editor(editor, e_pool) 253 svn_repos.svn_repos_replay( 254 self._getRevisionRoot(revision), e_ptr, e_baton, e_pool 255 ) 256 257 e_changes = (version.min_1_2 and 258 [editor.get_changes()] or [editor.changes])[0] 259 changelist = [VersionedPathDescriptor(self, path, revision, change) 260 for path, change in e_changes.items() 261 ] 262 changelist.sort() 263 264 # store in the cache 265 self._revChanges[revision] = changelist 266 267 del editor # destroy any subpool 268 269 return changelist
270 271
272 - def getPathProperties(self, path, revision):
273 """ Get a dict of properties for a particular path/revision 274 275 @param path: The path 276 @type path: C{str} 277 278 @param revision: The revision number 279 @type revision: C{int} 280 281 @return: The dict of properties 282 @rtype: C{dict} 283 """ 284 try: 285 plist = self._pathPropLists[(path, revision)] 286 except KeyError: 287 rev_root = self._getRevisionRoot(revision) 288 plist = self._pathPropLists[(path, revision)] = dict([ 289 (key, str(value)) for key, value in 290 svn_fs.node_proplist(rev_root, path, self._pool).items() 291 ]) 292 293 return plist
294 295
296 - def getPathProperty(self, name, path, revision):
297 """ Get the value of a particular property 298 299 @param name: The name of the property 300 @type name: C{str} 301 302 @param path: The path the property is attached to 303 @type path: C{str} 304 305 @param revision: The revision number 306 @type revision: C{int} 307 308 @return: The property value or C{None} if the property 309 doesn't exist. 310 @rtype: C{str} 311 """ 312 try: 313 value = self._pathProps[(name, path, revision)] 314 except KeyError: 315 root = self._getRevisionRoot(revision) 316 value = self._pathProps[(name, path, revision)] = svn_fs.node_prop( 317 root, path, name, self._pool 318 ) 319 320 return value
321 322
323 - def getPathMimeType(self, path, revision):
324 """ Get the MIME type of a particular path 325 326 @param path: The path 327 @type path: C{str} 328 329 @param revision: The revision number 330 @type revision: C{int} 331 332 @return: The mime type or C{None} 333 @rtype: C{str} 334 """ 335 return self.getPathProperty( 336 svn_core.SVN_PROP_MIME_TYPE, path, revision 337 )
338 339
340 - def dumpPathContent(self, fp, path, revision):
341 """ Dump the contents of a particular path into a file 342 343 @param fp: The file descriptor 344 @type fp: file like object 345 346 @param path: The path to process 347 @type path: C{str} 348 349 @param revision: The revision number 350 @type revision: C{int} 351 """ 352 pool = svn_core.svn_pool_create(self._pool) 353 354 try: 355 root = self._getRevisionRoot(revision) 356 stream = svn_fs.file_contents(root, path, pool) 357 358 try: 359 while True: 360 chunk = svn_core.svn_stream_read( 361 stream, svn_core.SVN_STREAM_CHUNK_SIZE 362 ) 363 if not chunk: 364 break 365 366 fp.write(chunk) 367 finally: 368 svn_core.svn_stream_close(stream) 369 finally: 370 svn_core.svn_pool_destroy(pool)
371 372
373 - def getRevisionTime(self, revision):
374 """ Returns the time of a particular rev. in seconds since epoch 375 376 @param revision: The revision number 377 @type revision: C{int} 378 379 @return: The time 380 @rtype: C{int} 381 """ 382 try: 383 rtime = self._revTimes[revision] 384 except KeyError: 385 svndate = self.getRevisionProperty( 386 revision, svn_core.SVN_PROP_REVISION_DATE 387 ) 388 rtime = self._revTimes[revision] = svn_core.secs_from_timestr( 389 svndate, self._pool 390 ) 391 392 return rtime
393 394
395 - def getRevisionAuthor(self, revision):
396 """ Returns the author of a particular revision 397 398 @param revision: The revision number 399 @type revision: C{int} 400 401 @return: The author 402 @rtype: C{str} 403 """ 404 return self.getRevisionProperty( 405 revision, svn_core.SVN_PROP_REVISION_AUTHOR 406 )
407 408
409 - def getRevisionLog(self, revision):
410 """ Returns the log entry of a particular revision 411 412 @param revision: The revision number 413 @type revision: C{int} 414 415 @return: The log entry or C{None} 416 @rtype: C{str} 417 """ 418 return self.getRevisionProperty( 419 revision, svn_core.SVN_PROP_REVISION_LOG 420 )
421 422
423 - def getRevisionProperty(self, revision, propname):
424 """ Returns the value of a revision property 425 426 @param propname: The property name 427 @type propname: C{str} 428 429 @param revision: The revision number 430 @type revision: C{int} 431 432 @return: The property value 433 @rtype: C{str} 434 """ 435 try: 436 value = self._revProps[(revision, propname)] 437 except KeyError: 438 value = self._revProps[(revision, propname)] = \ 439 svn_fs.revision_prop(self._fs, revision, propname, self._pool) 440 441 return value
442 443
444 - def _getChangeCollector(self, revision):
445 """ Return the RevisionChangeCollector instance 446 447 @param revision: The revision 448 @type revision: C{int} 449 450 @return: The Collector instance 451 @rtype: C{_RevisionChangeCollector} 452 """ 453 return _RevisionChangeCollector(self, revision)
454 455
456 - def _getRevisionRoot(self, revision):
457 """ Return the root object of a particular revision 458 459 @note: The root objects are cached 460 461 @param revision: The revision number 462 @type revision: C{int} 463 464 @return: The revision root 465 @rtype: swig object 466 """ 467 try: 468 root = self._revRoots[revision] 469 except KeyError: 470 root = self._revRoots[revision] = svn_fs.revision_root( 471 self._fs, revision, self._pool 472 ) 473 474 return root
475 476
477 -class PathDescriptor(object):
478 """ Describes the basic information of a particular path 479 480 @ivar path: The path, we're talking about 481 @type path: C{str} 482 483 @ivar repos: The repository this change belongs to 484 @type repos: C{Repository} 485 """ 486
487 - def __init__(self, repos, path, *args, **kwargs):
488 """ Initialization 489 490 @note: Don't override this method, override L{init} 491 instead. 492 493 @param repos: The repository reference 494 @type repos: C{Repository} 495 496 @param path: The path 497 @type path: C{str} 498 """ 499 self.repos = repos 500 self.path = (path[:1] == '/' and [path[1:]] or [path])[0] 501 502 self.init(*args, **kwargs)
503 504
505 - def init(self, *args, **kwargs):
506 """ Custom initialization """ 507 pass
508 509
510 - def __cmp__(self, other):
511 """ Compares two change objects by path 512 513 @param other: The object compared to 514 @type other: hopefully C{VersionedPathDescriptor} 515 516 @return: Returns -1, 0 or 1 517 @rtype: C{int} 518 """ 519 return cmp(self.path, other.path)
520 521
522 - def isDirectory(self):
523 """ Returns whether the path is a directory 524 525 @return: is a directory? 526 @rtype: C{bool} 527 """ 528 raise NotImplementedError()
529 530
531 -class LockedPathDescriptor(PathDescriptor):
532 """ Describes the lock status of a particular path 533 534 @ivar is_locked: is locked? 535 @type is_locked: C{bool} 536 """
537 - def init(self, *args, **kwargs):
538 """ Custom initialization """ 539 self._init(*args, **kwargs)
540 541
542 - def _init(self, is_locked):
543 """ Initialization """ 544 self.is_locked = is_locked 545 self._cache = None
546 547
548 - def isDirectory(self):
549 """ Returns whether the path is a directory """ 550 # currently there's no way to lock directories 551 return False
552 553
554 - def _getLockDescription(self):
555 """ Returns the lock description object """ 556 if self._cache is None: 557 self._cache = svn_fs.get_lock( 558 self.repos._fs, self.path, self.repos._pool 559 ) 560 561 return self._cache
562 563
564 - def getComment(self):
565 """ Returns the lock comment """ 566 desc = self._getLockDescription() 567 if desc: 568 return str(desc.comment or "") 569 570 return ""
571 572
573 -class VersionedPathDescriptor(PathDescriptor):
574 """ Describes the changes of a particular path 575 576 This is a wrapper around svn_repos.ChangedPath instances. 577 outside of this module one shouldn't need to deal with these. 578 579 @ivar revision: The revision number 580 @type revision: C{int} 581 582 @ivar _change: The change 583 @type _change: C{svn_repos.ChangedPath} 584 """
585 - def init(self, *args, **kwargs):
586 """ Custom initialization """ 587 self._init(*args, **kwargs)
588 589
590 - def _init(self, revision, change):
591 """ Initialization 592 593 @param revision: The revision number 594 @type revision: C{int} 595 596 @param change: The change specification 597 @type change: C{svn_repos.ChangedPath} 598 """ 599 self.revision = revision 600 self._change = change
601 602
603 - def getBaseRevision(self):
604 """ Returns the revision number of the original path 605 606 @return: The revision number 607 @rtype: C{int} 608 """ 609 return self._change.base_rev
610 611
612 - def getBasePath(self):
613 """ Returns the original path 614 615 @return: The path 616 @rtype: C{str} 617 """ 618 # check the difference between 1.1 and 1.2 bindings... 619 return (self._change.base_path[:1] == '/' and 620 [self._change.base_path[1:]] or [self._change.base_path])[0]
621 622
623 - def getModifiedProperties(self):
624 """ Returns the dict of modified properties 625 626 The dict contains the property names as keys and 627 a 2-tuple as value where the first element contains the 628 old property value and second element the new one. 629 630 @return: The dict of changed properties 631 @rtype: C{dict} 632 """ 633 if type(self._change.prop_changes) == type({}): 634 return self._change.prop_changes 635 636 if not self._change.prop_changes: 637 return {} 638 639 # get the property dicts 640 if self.wasAdded(): 641 propdict1 = {} 642 else: 643 propdict1 = self.repos.getPathProperties( 644 self.getBasePath(), self.getBaseRevision() 645 ) 646 647 if self.wasDeleted(): 648 propdict2 = {} 649 else: 650 propdict2 = self.repos.getPathProperties( 651 self.path, self.revision 652 ) 653 654 # compute diff dict 655 # non-existant properties in either version get None as value 656 self._change.prop_changes = {} 657 for name, value1, value2 in [ 658 (key, propdict1.get(key), propdict2.get(key)) for key in 659 dict.fromkeys(propdict1.keys() + propdict2.keys()).keys()]: 660 661 if value1 != value2: 662 self._change.prop_changes[name] = (value1, value2) 663 664 return self._change.prop_changes
665 666
667 - def isDirectory(self):
668 """ Returns whether the path is a directory """ 669 return bool(self._change.item_kind == svn_core.svn_node_dir)
670 671
672 - def isBinary(self):
673 """ Returns whether one of the revisions is a binary file 674 675 @return: is binary? 676 @rtype: C{bool} 677 """ 678 if not self.wasDeleted(): 679 if isBinary(self.repos.getPathMimeType( 680 self.path, self.revision 681 )): 682 return True 683 684 if not self.wasAdded() or self.wasCopied(): 685 return isBinary(self.repos.getPathMimeType( 686 self.getBasePath(), self.getBaseRevision() 687 )) 688 689 return False
690 691
692 - def hasPropertyChanges(self):
693 """ Returns whether the path has property changes 694 695 @return: has property changes? 696 @rtype: C{bool} 697 """ 698 return bool(self._change.prop_changes)
699 700
701 - def hasContentChanges(self):
702 """ Returns whether the path has content changes 703 704 @return: has content changes? 705 @rtype: C{bool} 706 """ 707 return bool(self._change.text_changed)
708 709
710 - def wasDeleted(self):
711 """ Returns whether the path was deleted 712 713 @return: was deleted? 714 @rtype: C{bool} 715 """ 716 if version.min_1_7: 717 return self._change.action == svn_repos.CHANGE_ACTION_DELETE 718 return bool(self._change.path is None)
719 720
721 - def wasAdded(self):
722 """ Returns whether the path was added 723 724 @return: was added? 725 @rtype: C{bool} 726 """ 727 return bool(self._change.added)
728 729
730 - def wasModified(self):
731 """ Returns whether the path was just modified 732 733 @return: was modified? 734 @rtype: C{bool} 735 """ 736 return bool(not self._change.added and not self.wasDeleted())
737 738
739 - def wasCopied(self):
740 """ Returns whether the path was copied 741 742 @return: was copied? 743 @rtype: C{bool} 744 """ 745 return bool( 746 self._change.added and self._change.base_path and 747 self._change.base_rev != _SVN_INVALID_REVNUM 748 )
749 750 751 if version.min_1_2:
752 - class Collector(svn_repos.ChangeCollector, object):
753 """ svn 1.2 collector """ 754 pass
755 else:
756 - class Collector(svn_repos.RevisionChangeCollector, object):
757 """ svn 1.[01] collector """ 758 pass
759 760
761 -class _RevisionChangeCollector(Collector):
762 """ Collect all changes between two particular revisions 763 764 @cvar __pool: C{None} 765 @ivar __pool: The APR subpool 766 @type __pool: swig object 767 """ 768 __pool = None 769
770 - def __init__(self, repos, revision):
771 """ Initialization 772 773 @param repos: Reference to the repository object 774 @type repos: C{Repository} 775 776 @param revision: The revision 777 @type revision: C{int} 778 """ 779 self.__repos = repos 780 self.__pool = svn_core.svn_pool_create(repos._pool) 781 782 super(_RevisionChangeCollector, self).__init__( 783 repos._fs, 784 version.min_1_2 and 785 repos._getRevisionRoot(revision) or revision, 786 self.__pool, 787 )
788 789
790 - def __del__(self):
791 """ Destroy the subpool """ 792 if self.__pool: 793 pool = self.__pool 794 self.__pool = None 795 svn_core.svn_pool_destroy(pool)
796 797
798 - def getPool(self):
799 """ Returns the subpool 800 801 @return: the pool 802 @rtype: swig object 803 """ 804 return self.__pool
805 806
807 - def _get_root(self, rev):
808 """ Return the root of a particular revision 809 810 @note: The root objects are cached 811 812 @param rev: The revision number 813 @type rev: C{int} 814 815 @return: The revision root 816 @rtype: swig object 817 """ 818 return self.__repos._getRevisionRoot(rev)
819