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