eggs/py-1.4.0-py2.6.egg/py/_path/svnwc.py
changeset 69 c6bca38c1cbf
equal deleted inserted replaced
68:5ff1fc726848 69:c6bca38c1cbf
       
     1 """
       
     2 svn-Command based Implementation of a Subversion WorkingCopy Path.
       
     3 
       
     4   SvnWCCommandPath  is the main class.
       
     5 
       
     6 """
       
     7 
       
     8 import os, sys, time, re, calendar
       
     9 import py
       
    10 import subprocess
       
    11 from py._path import common
       
    12 
       
    13 #-----------------------------------------------------------
       
    14 # Caching latest repository revision and repo-paths
       
    15 # (getting them is slow with the current implementations)
       
    16 #
       
    17 # XXX make mt-safe
       
    18 #-----------------------------------------------------------
       
    19 
       
    20 class cache:
       
    21     proplist = {}
       
    22     info = {}
       
    23     entries = {}
       
    24     prop = {}
       
    25 
       
    26 class RepoEntry:
       
    27     def __init__(self, url, rev, timestamp):
       
    28         self.url = url
       
    29         self.rev = rev
       
    30         self.timestamp = timestamp
       
    31 
       
    32     def __str__(self):
       
    33         return "repo: %s;%s  %s" %(self.url, self.rev, self.timestamp)
       
    34 
       
    35 class RepoCache:
       
    36     """ The Repocache manages discovered repository paths
       
    37     and their revisions.  If inside a timeout the cache
       
    38     will even return the revision of the root.
       
    39     """
       
    40     timeout = 20 # seconds after which we forget that we know the last revision
       
    41 
       
    42     def __init__(self):
       
    43         self.repos = []
       
    44 
       
    45     def clear(self):
       
    46         self.repos = []
       
    47 
       
    48     def put(self, url, rev, timestamp=None):
       
    49         if rev is None:
       
    50             return
       
    51         if timestamp is None:
       
    52             timestamp = time.time()
       
    53 
       
    54         for entry in self.repos:
       
    55             if url == entry.url:
       
    56                 entry.timestamp = timestamp
       
    57                 entry.rev = rev
       
    58                 #print "set repo", entry
       
    59                 break
       
    60         else:
       
    61             entry = RepoEntry(url, rev, timestamp)
       
    62             self.repos.append(entry)
       
    63             #print "appended repo", entry
       
    64 
       
    65     def get(self, url):
       
    66         now = time.time()
       
    67         for entry in self.repos:
       
    68             if url.startswith(entry.url):
       
    69                 if now < entry.timestamp + self.timeout:
       
    70                     #print "returning immediate Etrny", entry
       
    71                     return entry.url, entry.rev
       
    72                 return entry.url, -1
       
    73         return url, -1
       
    74 
       
    75 repositories = RepoCache()
       
    76 
       
    77 
       
    78 # svn support code
       
    79 
       
    80 ALLOWED_CHARS = "_ -/\\=$.~+%" #add characters as necessary when tested
       
    81 if sys.platform == "win32":
       
    82     ALLOWED_CHARS += ":"
       
    83 ALLOWED_CHARS_HOST = ALLOWED_CHARS + '@:'
       
    84 
       
    85 def _getsvnversion(ver=[]):
       
    86     try:
       
    87         return ver[0]
       
    88     except IndexError:
       
    89         v = py.process.cmdexec("svn -q --version")
       
    90         v.strip()
       
    91         v = '.'.join(v.split('.')[:2])
       
    92         ver.append(v)
       
    93         return v
       
    94 
       
    95 def _escape_helper(text):
       
    96     text = str(text)
       
    97     if py.std.sys.platform != 'win32':
       
    98         text = str(text).replace('$', '\\$')
       
    99     return text
       
   100 
       
   101 def _check_for_bad_chars(text, allowed_chars=ALLOWED_CHARS):
       
   102     for c in str(text):
       
   103         if c.isalnum():
       
   104             continue
       
   105         if c in allowed_chars:
       
   106             continue
       
   107         return True
       
   108     return False
       
   109 
       
   110 def checkbadchars(url):
       
   111     # (hpk) not quite sure about the exact purpose, guido w.?
       
   112     proto, uri = url.split("://", 1)
       
   113     if proto != "file":
       
   114         host, uripath = uri.split('/', 1)
       
   115         # only check for bad chars in the non-protocol parts
       
   116         if (_check_for_bad_chars(host, ALLOWED_CHARS_HOST) \
       
   117             or _check_for_bad_chars(uripath, ALLOWED_CHARS)):
       
   118             raise ValueError("bad char in %r" % (url, ))
       
   119 
       
   120 
       
   121 #_______________________________________________________________
       
   122 
       
   123 class SvnPathBase(common.PathBase):
       
   124     """ Base implementation for SvnPath implementations. """
       
   125     sep = '/'
       
   126 
       
   127     def _geturl(self):
       
   128         return self.strpath
       
   129     url = property(_geturl, None, None, "url of this svn-path.")
       
   130 
       
   131     def __str__(self):
       
   132         """ return a string representation (including rev-number) """
       
   133         return self.strpath
       
   134 
       
   135     def __hash__(self):
       
   136         return hash(self.strpath)
       
   137 
       
   138     def new(self, **kw):
       
   139         """ create a modified version of this path. A 'rev' argument
       
   140             indicates a new revision.
       
   141             the following keyword arguments modify various path parts::
       
   142 
       
   143               http://host.com/repo/path/file.ext
       
   144               |-----------------------|          dirname
       
   145                                         |------| basename
       
   146                                         |--|     purebasename
       
   147                                             |--| ext
       
   148         """
       
   149         obj = object.__new__(self.__class__)
       
   150         obj.rev = kw.get('rev', self.rev)
       
   151         obj.auth = kw.get('auth', self.auth)
       
   152         dirname, basename, purebasename, ext = self._getbyspec(
       
   153              "dirname,basename,purebasename,ext")
       
   154         if 'basename' in kw:
       
   155             if 'purebasename' in kw or 'ext' in kw:
       
   156                 raise ValueError("invalid specification %r" % kw)
       
   157         else:
       
   158             pb = kw.setdefault('purebasename', purebasename)
       
   159             ext = kw.setdefault('ext', ext)
       
   160             if ext and not ext.startswith('.'):
       
   161                 ext = '.' + ext
       
   162             kw['basename'] = pb + ext
       
   163 
       
   164         kw.setdefault('dirname', dirname)
       
   165         kw.setdefault('sep', self.sep)
       
   166         if kw['basename']:
       
   167             obj.strpath = "%(dirname)s%(sep)s%(basename)s" % kw
       
   168         else:
       
   169             obj.strpath = "%(dirname)s" % kw
       
   170         return obj
       
   171 
       
   172     def _getbyspec(self, spec):
       
   173         """ get specified parts of the path.  'arg' is a string
       
   174             with comma separated path parts. The parts are returned
       
   175             in exactly the order of the specification.
       
   176 
       
   177             you may specify the following parts:
       
   178 
       
   179             http://host.com/repo/path/file.ext
       
   180             |-----------------------|          dirname
       
   181                                       |------| basename
       
   182                                       |--|     purebasename
       
   183                                           |--| ext
       
   184         """
       
   185         res = []
       
   186         parts = self.strpath.split(self.sep)
       
   187         for name in spec.split(','):
       
   188             name = name.strip()
       
   189             if name == 'dirname':
       
   190                 res.append(self.sep.join(parts[:-1]))
       
   191             elif name == 'basename':
       
   192                 res.append(parts[-1])
       
   193             else:
       
   194                 basename = parts[-1]
       
   195                 i = basename.rfind('.')
       
   196                 if i == -1:
       
   197                     purebasename, ext = basename, ''
       
   198                 else:
       
   199                     purebasename, ext = basename[:i], basename[i:]
       
   200                 if name == 'purebasename':
       
   201                     res.append(purebasename)
       
   202                 elif name == 'ext':
       
   203                     res.append(ext)
       
   204                 else:
       
   205                     raise NameError("Don't know part %r" % name)
       
   206         return res
       
   207 
       
   208     def __eq__(self, other):
       
   209         """ return true if path and rev attributes each match """
       
   210         return (str(self) == str(other) and
       
   211                (self.rev == other.rev or self.rev == other.rev))
       
   212 
       
   213     def __ne__(self, other):
       
   214         return not self == other
       
   215 
       
   216     def join(self, *args):
       
   217         """ return a new Path (with the same revision) which is composed
       
   218             of the self Path followed by 'args' path components.
       
   219         """
       
   220         if not args:
       
   221             return self
       
   222 
       
   223         args = tuple([arg.strip(self.sep) for arg in args])
       
   224         parts = (self.strpath, ) + args
       
   225         newpath = self.__class__(self.sep.join(parts), self.rev, self.auth)
       
   226         return newpath
       
   227 
       
   228     def propget(self, name):
       
   229         """ return the content of the given property. """
       
   230         value = self._propget(name)
       
   231         return value
       
   232 
       
   233     def proplist(self):
       
   234         """ list all property names. """
       
   235         content = self._proplist()
       
   236         return content
       
   237 
       
   238     def size(self):
       
   239         """ Return the size of the file content of the Path. """
       
   240         return self.info().size
       
   241 
       
   242     def mtime(self):
       
   243         """ Return the last modification time of the file. """
       
   244         return self.info().mtime
       
   245 
       
   246     # shared help methods
       
   247 
       
   248     def _escape(self, cmd):
       
   249         return _escape_helper(cmd)
       
   250 
       
   251 
       
   252     #def _childmaxrev(self):
       
   253     #    """ return maximum revision number of childs (or self.rev if no childs) """
       
   254     #    rev = self.rev
       
   255     #    for name, info in self._listdir_nameinfo():
       
   256     #        rev = max(rev, info.created_rev)
       
   257     #    return rev
       
   258 
       
   259     #def _getlatestrevision(self):
       
   260     #    """ return latest repo-revision for this path. """
       
   261     #    url = self.strpath
       
   262     #    path = self.__class__(url, None)
       
   263     #
       
   264     #    # we need a long walk to find the root-repo and revision
       
   265     #    while 1:
       
   266     #        try:
       
   267     #            rev = max(rev, path._childmaxrev())
       
   268     #            previous = path
       
   269     #            path = path.dirpath()
       
   270     #        except (IOError, process.cmdexec.Error):
       
   271     #            break
       
   272     #    if rev is None:
       
   273     #        raise IOError, "could not determine newest repo revision for %s" % self
       
   274     #    return rev
       
   275 
       
   276     class Checkers(common.Checkers):
       
   277         def dir(self):
       
   278             try:
       
   279                 return self.path.info().kind == 'dir'
       
   280             except py.error.Error:
       
   281                 return self._listdirworks()
       
   282 
       
   283         def _listdirworks(self):
       
   284             try:
       
   285                 self.path.listdir()
       
   286             except py.error.ENOENT:
       
   287                 return False
       
   288             else:
       
   289                 return True
       
   290 
       
   291         def file(self):
       
   292             try:
       
   293                 return self.path.info().kind == 'file'
       
   294             except py.error.ENOENT:
       
   295                 return False
       
   296 
       
   297         def exists(self):
       
   298             try:
       
   299                 return self.path.info()
       
   300             except py.error.ENOENT:
       
   301                 return self._listdirworks()
       
   302 
       
   303 def parse_apr_time(timestr):
       
   304     i = timestr.rfind('.')
       
   305     if i == -1:
       
   306         raise ValueError("could not parse %s" % timestr)
       
   307     timestr = timestr[:i]
       
   308     parsedtime = time.strptime(timestr, "%Y-%m-%dT%H:%M:%S")
       
   309     return time.mktime(parsedtime)
       
   310 
       
   311 class PropListDict(dict):
       
   312     """ a Dictionary which fetches values (InfoSvnCommand instances) lazily"""
       
   313     def __init__(self, path, keynames):
       
   314         dict.__init__(self, [(x, None) for x in keynames])
       
   315         self.path = path
       
   316 
       
   317     def __getitem__(self, key):
       
   318         value = dict.__getitem__(self, key)
       
   319         if value is None:
       
   320             value = self.path.propget(key)
       
   321             dict.__setitem__(self, key, value)
       
   322         return value
       
   323 
       
   324 def fixlocale():
       
   325     if sys.platform != 'win32':
       
   326         return 'LC_ALL=C '
       
   327     return ''
       
   328 
       
   329 # some nasty chunk of code to solve path and url conversion and quoting issues
       
   330 ILLEGAL_CHARS = '* | \ / : < > ? \t \n \x0b \x0c \r'.split(' ')
       
   331 if os.sep in ILLEGAL_CHARS:
       
   332     ILLEGAL_CHARS.remove(os.sep)
       
   333 ISWINDOWS = sys.platform == 'win32'
       
   334 _reg_allow_disk = re.compile(r'^([a-z]\:\\)?[^:]+$', re.I)
       
   335 def _check_path(path):
       
   336     illegal = ILLEGAL_CHARS[:]
       
   337     sp = path.strpath
       
   338     if ISWINDOWS:
       
   339         illegal.remove(':')
       
   340         if not _reg_allow_disk.match(sp):
       
   341             raise ValueError('path may not contain a colon (:)')
       
   342     for char in sp:
       
   343         if char not in string.printable or char in illegal:
       
   344             raise ValueError('illegal character %r in path' % (char,))
       
   345 
       
   346 def path_to_fspath(path, addat=True):
       
   347     _check_path(path)
       
   348     sp = path.strpath
       
   349     if addat and path.rev != -1:
       
   350         sp = '%s@%s' % (sp, path.rev)
       
   351     elif addat:
       
   352         sp = '%s@HEAD' % (sp,)
       
   353     return sp
       
   354 
       
   355 def url_from_path(path):
       
   356     fspath = path_to_fspath(path, False)
       
   357     quote = py.std.urllib.quote
       
   358     if ISWINDOWS:
       
   359         match = _reg_allow_disk.match(fspath)
       
   360         fspath = fspath.replace('\\', '/')
       
   361         if match.group(1):
       
   362             fspath = '/%s%s' % (match.group(1).replace('\\', '/'),
       
   363                                 quote(fspath[len(match.group(1)):]))
       
   364         else:
       
   365             fspath = quote(fspath)
       
   366     else:
       
   367         fspath = quote(fspath)
       
   368     if path.rev != -1:
       
   369         fspath = '%s@%s' % (fspath, path.rev)
       
   370     else:
       
   371         fspath = '%s@HEAD' % (fspath,)
       
   372     return 'file://%s' % (fspath,)
       
   373 
       
   374 class SvnAuth(object):
       
   375     """ container for auth information for Subversion """
       
   376     def __init__(self, username, password, cache_auth=True, interactive=True):
       
   377         self.username = username
       
   378         self.password = password
       
   379         self.cache_auth = cache_auth
       
   380         self.interactive = interactive
       
   381 
       
   382     def makecmdoptions(self):
       
   383         uname = self.username.replace('"', '\\"')
       
   384         passwd = self.password.replace('"', '\\"')
       
   385         ret = []
       
   386         if uname:
       
   387             ret.append('--username="%s"' % (uname,))
       
   388         if passwd:
       
   389             ret.append('--password="%s"' % (passwd,))
       
   390         if not self.cache_auth:
       
   391             ret.append('--no-auth-cache')
       
   392         if not self.interactive:
       
   393             ret.append('--non-interactive')
       
   394         return ' '.join(ret)
       
   395 
       
   396     def __str__(self):
       
   397         return "<SvnAuth username=%s ...>" %(self.username,)
       
   398 
       
   399 rex_blame = re.compile(r'\s*(\d+)\s*(\S+) (.*)')
       
   400 
       
   401 class SvnWCCommandPath(common.PathBase):
       
   402     """ path implementation offering access/modification to svn working copies.
       
   403         It has methods similar to the functions in os.path and similar to the
       
   404         commands of the svn client.
       
   405     """
       
   406     sep = os.sep
       
   407 
       
   408     def __new__(cls, wcpath=None, auth=None):
       
   409         self = object.__new__(cls)
       
   410         if isinstance(wcpath, cls):
       
   411             if wcpath.__class__ == cls:
       
   412                 return wcpath
       
   413             wcpath = wcpath.localpath
       
   414         if _check_for_bad_chars(str(wcpath),
       
   415                                           ALLOWED_CHARS):
       
   416             raise ValueError("bad char in wcpath %s" % (wcpath, ))
       
   417         self.localpath = py.path.local(wcpath)
       
   418         self.auth = auth
       
   419         return self
       
   420 
       
   421     strpath = property(lambda x: str(x.localpath), None, None, "string path")
       
   422     rev = property(lambda x: x.info(usecache=0).rev, None, None, "revision")
       
   423 
       
   424     def __eq__(self, other):
       
   425         return self.localpath == getattr(other, 'localpath', None)
       
   426 
       
   427     def _geturl(self):
       
   428         if getattr(self, '_url', None) is None:
       
   429             info = self.info()
       
   430             self._url = info.url #SvnPath(info.url, info.rev)
       
   431         assert isinstance(self._url, py.builtin._basestring)
       
   432         return self._url
       
   433 
       
   434     url = property(_geturl, None, None, "url of this WC item")
       
   435 
       
   436     def _escape(self, cmd):
       
   437         return _escape_helper(cmd)
       
   438 
       
   439     def dump(self, obj):
       
   440         """ pickle object into path location"""
       
   441         return self.localpath.dump(obj)
       
   442 
       
   443     def svnurl(self):
       
   444         """ return current SvnPath for this WC-item. """
       
   445         info = self.info()
       
   446         return py.path.svnurl(info.url)
       
   447 
       
   448     def __repr__(self):
       
   449         return "svnwc(%r)" % (self.strpath) # , self._url)
       
   450 
       
   451     def __str__(self):
       
   452         return str(self.localpath)
       
   453 
       
   454     def _makeauthoptions(self):
       
   455         if self.auth is None:
       
   456             return ''
       
   457         return self.auth.makecmdoptions()
       
   458 
       
   459     def _authsvn(self, cmd, args=None):
       
   460         args = args and list(args) or []
       
   461         args.append(self._makeauthoptions())
       
   462         return self._svn(cmd, *args)
       
   463 
       
   464     def _svn(self, cmd, *args):
       
   465         l = ['svn %s' % cmd]
       
   466         args = [self._escape(item) for item in args]
       
   467         l.extend(args)
       
   468         l.append('"%s"' % self._escape(self.strpath))
       
   469         # try fixing the locale because we can't otherwise parse
       
   470         string = fixlocale() + " ".join(l)
       
   471         try:
       
   472             try:
       
   473                 key = 'LC_MESSAGES'
       
   474                 hold = os.environ.get(key)
       
   475                 os.environ[key] = 'C'
       
   476                 out = py.process.cmdexec(string)
       
   477             finally:
       
   478                 if hold:
       
   479                     os.environ[key] = hold
       
   480                 else:
       
   481                     del os.environ[key]
       
   482         except py.process.cmdexec.Error:
       
   483             e = sys.exc_info()[1]
       
   484             strerr = e.err.lower()
       
   485             if strerr.find('file not found') != -1:
       
   486                 raise py.error.ENOENT(self)
       
   487             if (strerr.find('file exists') != -1 or
       
   488                 strerr.find('file already exists') != -1 or
       
   489                 strerr.find("can't create directory") != -1):
       
   490                 raise py.error.EEXIST(self)
       
   491             raise
       
   492         return out
       
   493 
       
   494     def switch(self, url):
       
   495         """ switch to given URL. """
       
   496         self._authsvn('switch', [url])
       
   497 
       
   498     def checkout(self, url=None, rev=None):
       
   499         """ checkout from url to local wcpath. """
       
   500         args = []
       
   501         if url is None:
       
   502             url = self.url
       
   503         if rev is None or rev == -1:
       
   504             if (py.std.sys.platform != 'win32' and
       
   505                     _getsvnversion() == '1.3'):
       
   506                 url += "@HEAD"
       
   507         else:
       
   508             if _getsvnversion() == '1.3':
       
   509                 url += "@%d" % rev
       
   510             else:
       
   511                 args.append('-r' + str(rev))
       
   512         args.append(url)
       
   513         self._authsvn('co', args)
       
   514 
       
   515     def update(self, rev='HEAD', interactive=True):
       
   516         """ update working copy item to given revision. (None -> HEAD). """
       
   517         opts = ['-r', rev]
       
   518         if not interactive:
       
   519             opts.append("--non-interactive")
       
   520         self._authsvn('up', opts)
       
   521 
       
   522     def write(self, content, mode='w'):
       
   523         """ write content into local filesystem wc. """
       
   524         self.localpath.write(content, mode)
       
   525 
       
   526     def dirpath(self, *args):
       
   527         """ return the directory Path of the current Path. """
       
   528         return self.__class__(self.localpath.dirpath(*args), auth=self.auth)
       
   529 
       
   530     def _ensuredirs(self):
       
   531         parent = self.dirpath()
       
   532         if parent.check(dir=0):
       
   533             parent._ensuredirs()
       
   534         if self.check(dir=0):
       
   535             self.mkdir()
       
   536         return self
       
   537 
       
   538     def ensure(self, *args, **kwargs):
       
   539         """ ensure that an args-joined path exists (by default as
       
   540             a file). if you specify a keyword argument 'directory=True'
       
   541             then the path is forced  to be a directory path.
       
   542         """
       
   543         p = self.join(*args)
       
   544         if p.check():
       
   545             if p.check(versioned=False):
       
   546                 p.add()
       
   547             return p
       
   548         if kwargs.get('dir', 0):
       
   549             return p._ensuredirs()
       
   550         parent = p.dirpath()
       
   551         parent._ensuredirs()
       
   552         p.write("")
       
   553         p.add()
       
   554         return p
       
   555 
       
   556     def mkdir(self, *args):
       
   557         """ create & return the directory joined with args. """
       
   558         if args:
       
   559             return self.join(*args).mkdir()
       
   560         else:
       
   561             self._svn('mkdir')
       
   562             return self
       
   563 
       
   564     def add(self):
       
   565         """ add ourself to svn """
       
   566         self._svn('add')
       
   567 
       
   568     def remove(self, rec=1, force=1):
       
   569         """ remove a file or a directory tree. 'rec'ursive is
       
   570             ignored and considered always true (because of
       
   571             underlying svn semantics.
       
   572         """
       
   573         assert rec, "svn cannot remove non-recursively"
       
   574         if not self.check(versioned=True):
       
   575             # not added to svn (anymore?), just remove
       
   576             py.path.local(self).remove()
       
   577             return
       
   578         flags = []
       
   579         if force:
       
   580             flags.append('--force')
       
   581         self._svn('remove', *flags)
       
   582 
       
   583     def copy(self, target):
       
   584         """ copy path to target."""
       
   585         py.process.cmdexec("svn copy %s %s" %(str(self), str(target)))
       
   586 
       
   587     def rename(self, target):
       
   588         """ rename this path to target. """
       
   589         py.process.cmdexec("svn move --force %s %s" %(str(self), str(target)))
       
   590 
       
   591     def lock(self):
       
   592         """ set a lock (exclusive) on the resource """
       
   593         out = self._authsvn('lock').strip()
       
   594         if not out:
       
   595             # warning or error, raise exception
       
   596             raise Exception(out[4:])
       
   597 
       
   598     def unlock(self):
       
   599         """ unset a previously set lock """
       
   600         out = self._authsvn('unlock').strip()
       
   601         if out.startswith('svn:'):
       
   602             # warning or error, raise exception
       
   603             raise Exception(out[4:])
       
   604 
       
   605     def cleanup(self):
       
   606         """ remove any locks from the resource """
       
   607         # XXX should be fixed properly!!!
       
   608         try:
       
   609             self.unlock()
       
   610         except:
       
   611             pass
       
   612 
       
   613     def status(self, updates=0, rec=0, externals=0):
       
   614         """ return (collective) Status object for this file. """
       
   615         # http://svnbook.red-bean.com/book.html#svn-ch-3-sect-4.3.1
       
   616         #             2201     2192        jum   test
       
   617         # XXX
       
   618         if externals:
       
   619             raise ValueError("XXX cannot perform status() "
       
   620                              "on external items yet")
       
   621         else:
       
   622             #1.2 supports: externals = '--ignore-externals'
       
   623             externals = ''
       
   624         if rec:
       
   625             rec= ''
       
   626         else:
       
   627             rec = '--non-recursive'
       
   628 
       
   629         # XXX does not work on all subversion versions
       
   630         #if not externals:
       
   631         #    externals = '--ignore-externals'
       
   632 
       
   633         if updates:
       
   634             updates = '-u'
       
   635         else:
       
   636             updates = ''
       
   637 
       
   638         try:
       
   639             cmd = 'status -v --xml --no-ignore %s %s %s' % (
       
   640                     updates, rec, externals)
       
   641             out = self._authsvn(cmd)
       
   642         except py.process.cmdexec.Error:
       
   643             cmd = 'status -v --no-ignore %s %s %s' % (
       
   644                     updates, rec, externals)
       
   645             out = self._authsvn(cmd)
       
   646             rootstatus = WCStatus(self).fromstring(out, self)
       
   647         else:
       
   648             rootstatus = XMLWCStatus(self).fromstring(out, self)
       
   649         return rootstatus
       
   650 
       
   651     def diff(self, rev=None):
       
   652         """ return a diff of the current path against revision rev (defaulting
       
   653             to the last one).
       
   654         """
       
   655         args = []
       
   656         if rev is not None:
       
   657             args.append("-r %d" % rev)
       
   658         out = self._authsvn('diff', args)
       
   659         return out
       
   660 
       
   661     def blame(self):
       
   662         """ return a list of tuples of three elements:
       
   663             (revision, commiter, line)
       
   664         """
       
   665         out = self._svn('blame')
       
   666         result = []
       
   667         blamelines = out.splitlines()
       
   668         reallines = py.path.svnurl(self.url).readlines()
       
   669         for i, (blameline, line) in enumerate(
       
   670                 zip(blamelines, reallines)):
       
   671             m = rex_blame.match(blameline)
       
   672             if not m:
       
   673                 raise ValueError("output line %r of svn blame does not match "
       
   674                                  "expected format" % (line, ))
       
   675             rev, name, _ = m.groups()
       
   676             result.append((int(rev), name, line))
       
   677         return result
       
   678 
       
   679     _rex_commit = re.compile(r'.*Committed revision (\d+)\.$', re.DOTALL)
       
   680     def commit(self, msg='', rec=1):
       
   681         """ commit with support for non-recursive commits """
       
   682         # XXX i guess escaping should be done better here?!?
       
   683         cmd = 'commit -m "%s" --force-log' % (msg.replace('"', '\\"'),)
       
   684         if not rec:
       
   685             cmd += ' -N'
       
   686         out = self._authsvn(cmd)
       
   687         try:
       
   688             del cache.info[self]
       
   689         except KeyError:
       
   690             pass
       
   691         if out:
       
   692             m = self._rex_commit.match(out)
       
   693             return int(m.group(1))
       
   694 
       
   695     def propset(self, name, value, *args):
       
   696         """ set property name to value on this path. """
       
   697         d = py.path.local.mkdtemp()
       
   698         try:
       
   699             p = d.join('value')
       
   700             p.write(value)
       
   701             self._svn('propset', name, '--file', str(p), *args)
       
   702         finally:
       
   703             d.remove()
       
   704 
       
   705     def propget(self, name):
       
   706         """ get property name on this path. """
       
   707         res = self._svn('propget', name)
       
   708         return res[:-1] # strip trailing newline
       
   709 
       
   710     def propdel(self, name):
       
   711         """ delete property name on this path. """
       
   712         res = self._svn('propdel', name)
       
   713         return res[:-1] # strip trailing newline
       
   714 
       
   715     def proplist(self, rec=0):
       
   716         """ return a mapping of property names to property values.
       
   717 If rec is True, then return a dictionary mapping sub-paths to such mappings.
       
   718 """
       
   719         if rec:
       
   720             res = self._svn('proplist -R')
       
   721             return make_recursive_propdict(self, res)
       
   722         else:
       
   723             res = self._svn('proplist')
       
   724             lines = res.split('\n')
       
   725             lines = [x.strip() for x in lines[1:]]
       
   726             return PropListDict(self, lines)
       
   727 
       
   728     def revert(self, rec=0):
       
   729         """ revert the local changes of this path. if rec is True, do so
       
   730 recursively. """
       
   731         if rec:
       
   732             result = self._svn('revert -R')
       
   733         else:
       
   734             result = self._svn('revert')
       
   735         return result
       
   736 
       
   737     def new(self, **kw):
       
   738         """ create a modified version of this path. A 'rev' argument
       
   739             indicates a new revision.
       
   740             the following keyword arguments modify various path parts:
       
   741 
       
   742               http://host.com/repo/path/file.ext
       
   743               |-----------------------|          dirname
       
   744                                         |------| basename
       
   745                                         |--|     purebasename
       
   746                                             |--| ext
       
   747         """
       
   748         if kw:
       
   749             localpath = self.localpath.new(**kw)
       
   750         else:
       
   751             localpath = self.localpath
       
   752         return self.__class__(localpath, auth=self.auth)
       
   753 
       
   754     def join(self, *args, **kwargs):
       
   755         """ return a new Path (with the same revision) which is composed
       
   756             of the self Path followed by 'args' path components.
       
   757         """
       
   758         if not args:
       
   759             return self
       
   760         localpath = self.localpath.join(*args, **kwargs)
       
   761         return self.__class__(localpath, auth=self.auth)
       
   762 
       
   763     def info(self, usecache=1):
       
   764         """ return an Info structure with svn-provided information. """
       
   765         info = usecache and cache.info.get(self)
       
   766         if not info:
       
   767             try:
       
   768                 output = self._svn('info')
       
   769             except py.process.cmdexec.Error:
       
   770                 e = sys.exc_info()[1]
       
   771                 if e.err.find('Path is not a working copy directory') != -1:
       
   772                     raise py.error.ENOENT(self, e.err)
       
   773                 elif e.err.find("is not under version control") != -1:
       
   774                     raise py.error.ENOENT(self, e.err)
       
   775                 raise
       
   776             # XXX SVN 1.3 has output on stderr instead of stdout (while it does
       
   777             # return 0!), so a bit nasty, but we assume no output is output
       
   778             # to stderr...
       
   779             if (output.strip() == '' or
       
   780                     output.lower().find('not a versioned resource') != -1):
       
   781                 raise py.error.ENOENT(self, output)
       
   782             info = InfoSvnWCCommand(output)
       
   783 
       
   784             # Can't reliably compare on Windows without access to win32api
       
   785             if py.std.sys.platform != 'win32':
       
   786                 if info.path != self.localpath:
       
   787                     raise py.error.ENOENT(self, "not a versioned resource:" +
       
   788                             " %s != %s" % (info.path, self.localpath))
       
   789             cache.info[self] = info
       
   790         return info
       
   791 
       
   792     def listdir(self, fil=None, sort=None):
       
   793         """ return a sequence of Paths.
       
   794 
       
   795         listdir will return either a tuple or a list of paths
       
   796         depending on implementation choices.
       
   797         """
       
   798         if isinstance(fil, str):
       
   799             fil = common.FNMatcher(fil)
       
   800         # XXX unify argument naming with LocalPath.listdir
       
   801         def notsvn(path):
       
   802             return path.basename != '.svn'
       
   803 
       
   804         paths = []
       
   805         for localpath in self.localpath.listdir(notsvn):
       
   806             p = self.__class__(localpath, auth=self.auth)
       
   807             if notsvn(p) and (not fil or fil(p)):
       
   808                 paths.append(p)
       
   809         self._sortlist(paths, sort)
       
   810         return paths
       
   811 
       
   812     def open(self, mode='r'):
       
   813         """ return an opened file with the given mode. """
       
   814         return open(self.strpath, mode)
       
   815 
       
   816     def _getbyspec(self, spec):
       
   817         return self.localpath._getbyspec(spec)
       
   818 
       
   819     class Checkers(py.path.local.Checkers):
       
   820         def __init__(self, path):
       
   821             self.svnwcpath = path
       
   822             self.path = path.localpath
       
   823         def versioned(self):
       
   824             try:
       
   825                 s = self.svnwcpath.info()
       
   826             except (py.error.ENOENT, py.error.EEXIST):
       
   827                 return False
       
   828             except py.process.cmdexec.Error:
       
   829                 e = sys.exc_info()[1]
       
   830                 if e.err.find('is not a working copy')!=-1:
       
   831                     return False
       
   832                 if e.err.lower().find('not a versioned resource') != -1:
       
   833                     return False
       
   834                 raise
       
   835             else:
       
   836                 return True
       
   837 
       
   838     def log(self, rev_start=None, rev_end=1, verbose=False):
       
   839         """ return a list of LogEntry instances for this path.
       
   840 rev_start is the starting revision (defaulting to the first one).
       
   841 rev_end is the last revision (defaulting to HEAD).
       
   842 if verbose is True, then the LogEntry instances also know which files changed.
       
   843 """
       
   844         assert self.check()   # make it simpler for the pipe
       
   845         rev_start = rev_start is None and "HEAD" or rev_start
       
   846         rev_end = rev_end is None and "HEAD" or rev_end
       
   847         if rev_start == "HEAD" and rev_end == 1:
       
   848                 rev_opt = ""
       
   849         else:
       
   850             rev_opt = "-r %s:%s" % (rev_start, rev_end)
       
   851         verbose_opt = verbose and "-v" or ""
       
   852         locale_env = fixlocale()
       
   853         # some blather on stderr
       
   854         auth_opt = self._makeauthoptions()
       
   855         #stdin, stdout, stderr  = os.popen3(locale_env +
       
   856         #                                   'svn log --xml %s %s %s "%s"' % (
       
   857         #                                    rev_opt, verbose_opt, auth_opt,
       
   858         #                                    self.strpath))
       
   859         cmd = locale_env + 'svn log --xml %s %s %s "%s"' % (
       
   860             rev_opt, verbose_opt, auth_opt, self.strpath)
       
   861 
       
   862         popen = subprocess.Popen(cmd,
       
   863                     stdout=subprocess.PIPE,
       
   864                     stderr=subprocess.PIPE,
       
   865                     shell=True,
       
   866         )
       
   867         stdout, stderr = popen.communicate()
       
   868         stdout = py.builtin._totext(stdout, sys.getdefaultencoding())
       
   869         minidom,ExpatError = importxml()
       
   870         try:
       
   871             tree = minidom.parseString(stdout)
       
   872         except ExpatError:
       
   873             raise ValueError('no such revision')
       
   874         result = []
       
   875         for logentry in filter(None, tree.firstChild.childNodes):
       
   876             if logentry.nodeType == logentry.ELEMENT_NODE:
       
   877                 result.append(LogEntry(logentry))
       
   878         return result
       
   879 
       
   880     def size(self):
       
   881         """ Return the size of the file content of the Path. """
       
   882         return self.info().size
       
   883 
       
   884     def mtime(self):
       
   885         """ Return the last modification time of the file. """
       
   886         return self.info().mtime
       
   887 
       
   888     def __hash__(self):
       
   889         return hash((self.strpath, self.__class__, self.auth))
       
   890 
       
   891 
       
   892 class WCStatus:
       
   893     attrnames = ('modified','added', 'conflict', 'unchanged', 'external',
       
   894                 'deleted', 'prop_modified', 'unknown', 'update_available',
       
   895                 'incomplete', 'kindmismatch', 'ignored', 'locked', 'replaced'
       
   896                 )
       
   897 
       
   898     def __init__(self, wcpath, rev=None, modrev=None, author=None):
       
   899         self.wcpath = wcpath
       
   900         self.rev = rev
       
   901         self.modrev = modrev
       
   902         self.author = author
       
   903 
       
   904         for name in self.attrnames:
       
   905             setattr(self, name, [])
       
   906 
       
   907     def allpath(self, sort=True, **kw):
       
   908         d = {}
       
   909         for name in self.attrnames:
       
   910             if name not in kw or kw[name]:
       
   911                 for path in getattr(self, name):
       
   912                     d[path] = 1
       
   913         l = d.keys()
       
   914         if sort:
       
   915             l.sort()
       
   916         return l
       
   917 
       
   918     # XXX a bit scary to assume there's always 2 spaces between username and
       
   919     # path, however with win32 allowing spaces in user names there doesn't
       
   920     # seem to be a more solid approach :(
       
   921     _rex_status = re.compile(r'\s+(\d+|-)\s+(\S+)\s+(.+?)\s{2,}(.*)')
       
   922 
       
   923     def fromstring(data, rootwcpath, rev=None, modrev=None, author=None):
       
   924         """ return a new WCStatus object from data 's'
       
   925         """
       
   926         rootstatus = WCStatus(rootwcpath, rev, modrev, author)
       
   927         update_rev = None
       
   928         for line in data.split('\n'):
       
   929             if not line.strip():
       
   930                 continue
       
   931             #print "processing %r" % line
       
   932             flags, rest = line[:8], line[8:]
       
   933             # first column
       
   934             c0,c1,c2,c3,c4,c5,x6,c7 = flags
       
   935             #if '*' in line:
       
   936             #    print "flags", repr(flags), "rest", repr(rest)
       
   937 
       
   938             if c0 in '?XI':
       
   939                 fn = line.split(None, 1)[1]
       
   940                 if c0 == '?':
       
   941                     wcpath = rootwcpath.join(fn, abs=1)
       
   942                     rootstatus.unknown.append(wcpath)
       
   943                 elif c0 == 'X':
       
   944                     wcpath = rootwcpath.__class__(
       
   945                         rootwcpath.localpath.join(fn, abs=1),
       
   946                         auth=rootwcpath.auth)
       
   947                     rootstatus.external.append(wcpath)
       
   948                 elif c0 == 'I':
       
   949                     wcpath = rootwcpath.join(fn, abs=1)
       
   950                     rootstatus.ignored.append(wcpath)
       
   951 
       
   952                 continue
       
   953 
       
   954             #elif c0 in '~!' or c4 == 'S':
       
   955             #    raise NotImplementedError("received flag %r" % c0)
       
   956 
       
   957             m = WCStatus._rex_status.match(rest)
       
   958             if not m:
       
   959                 if c7 == '*':
       
   960                     fn = rest.strip()
       
   961                     wcpath = rootwcpath.join(fn, abs=1)
       
   962                     rootstatus.update_available.append(wcpath)
       
   963                     continue
       
   964                 if line.lower().find('against revision:')!=-1:
       
   965                     update_rev = int(rest.split(':')[1].strip())
       
   966                     continue
       
   967                 if line.lower().find('status on external') > -1:
       
   968                     # XXX not sure what to do here... perhaps we want to
       
   969                     # store some state instead of just continuing, as right
       
   970                     # now it makes the top-level external get added twice
       
   971                     # (once as external, once as 'normal' unchanged item)
       
   972                     # because of the way SVN presents external items
       
   973                     continue
       
   974                 # keep trying
       
   975                 raise ValueError("could not parse line %r" % line)
       
   976             else:
       
   977                 rev, modrev, author, fn = m.groups()
       
   978             wcpath = rootwcpath.join(fn, abs=1)
       
   979             #assert wcpath.check()
       
   980             if c0 == 'M':
       
   981                 assert wcpath.check(file=1), "didn't expect a directory with changed content here"
       
   982                 rootstatus.modified.append(wcpath)
       
   983             elif c0 == 'A' or c3 == '+' :
       
   984                 rootstatus.added.append(wcpath)
       
   985             elif c0 == 'D':
       
   986                 rootstatus.deleted.append(wcpath)
       
   987             elif c0 == 'C':
       
   988                 rootstatus.conflict.append(wcpath)
       
   989             elif c0 == '~':
       
   990                 rootstatus.kindmismatch.append(wcpath)
       
   991             elif c0 == '!':
       
   992                 rootstatus.incomplete.append(wcpath)
       
   993             elif c0 == 'R':
       
   994                 rootstatus.replaced.append(wcpath)
       
   995             elif not c0.strip():
       
   996                 rootstatus.unchanged.append(wcpath)
       
   997             else:
       
   998                 raise NotImplementedError("received flag %r" % c0)
       
   999 
       
  1000             if c1 == 'M':
       
  1001                 rootstatus.prop_modified.append(wcpath)
       
  1002             # XXX do we cover all client versions here?
       
  1003             if c2 == 'L' or c5 == 'K':
       
  1004                 rootstatus.locked.append(wcpath)
       
  1005             if c7 == '*':
       
  1006                 rootstatus.update_available.append(wcpath)
       
  1007 
       
  1008             if wcpath == rootwcpath:
       
  1009                 rootstatus.rev = rev
       
  1010                 rootstatus.modrev = modrev
       
  1011                 rootstatus.author = author
       
  1012                 if update_rev:
       
  1013                     rootstatus.update_rev = update_rev
       
  1014                 continue
       
  1015         return rootstatus
       
  1016     fromstring = staticmethod(fromstring)
       
  1017 
       
  1018 class XMLWCStatus(WCStatus):
       
  1019     def fromstring(data, rootwcpath, rev=None, modrev=None, author=None):
       
  1020         """ parse 'data' (XML string as outputted by svn st) into a status obj
       
  1021         """
       
  1022         # XXX for externals, the path is shown twice: once
       
  1023         # with external information, and once with full info as if
       
  1024         # the item was a normal non-external... the current way of
       
  1025         # dealing with this issue is by ignoring it - this does make
       
  1026         # externals appear as external items as well as 'normal',
       
  1027         # unchanged ones in the status object so this is far from ideal
       
  1028         rootstatus = WCStatus(rootwcpath, rev, modrev, author)
       
  1029         update_rev = None
       
  1030         minidom, ExpatError = importxml()
       
  1031         try:
       
  1032             doc = minidom.parseString(data)
       
  1033         except ExpatError:
       
  1034             e = sys.exc_info()[1]
       
  1035             raise ValueError(str(e))
       
  1036         urevels = doc.getElementsByTagName('against')
       
  1037         if urevels:
       
  1038             rootstatus.update_rev = urevels[-1].getAttribute('revision')
       
  1039         for entryel in doc.getElementsByTagName('entry'):
       
  1040             path = entryel.getAttribute('path')
       
  1041             statusel = entryel.getElementsByTagName('wc-status')[0]
       
  1042             itemstatus = statusel.getAttribute('item')
       
  1043 
       
  1044             if itemstatus == 'unversioned':
       
  1045                 wcpath = rootwcpath.join(path, abs=1)
       
  1046                 rootstatus.unknown.append(wcpath)
       
  1047                 continue
       
  1048             elif itemstatus == 'external':
       
  1049                 wcpath = rootwcpath.__class__(
       
  1050                     rootwcpath.localpath.join(path, abs=1),
       
  1051                     auth=rootwcpath.auth)
       
  1052                 rootstatus.external.append(wcpath)
       
  1053                 continue
       
  1054             elif itemstatus == 'ignored':
       
  1055                 wcpath = rootwcpath.join(path, abs=1)
       
  1056                 rootstatus.ignored.append(wcpath)
       
  1057                 continue
       
  1058             elif itemstatus == 'incomplete':
       
  1059                 wcpath = rootwcpath.join(path, abs=1)
       
  1060                 rootstatus.incomplete.append(wcpath)
       
  1061                 continue
       
  1062 
       
  1063             rev = statusel.getAttribute('revision')
       
  1064             if itemstatus == 'added' or itemstatus == 'none':
       
  1065                 rev = '0'
       
  1066                 modrev = '?'
       
  1067                 author = '?'
       
  1068                 date = ''
       
  1069             else:
       
  1070                 #print entryel.toxml()
       
  1071                 commitel = entryel.getElementsByTagName('commit')[0]
       
  1072                 if commitel:
       
  1073                     modrev = commitel.getAttribute('revision')
       
  1074                     author = ''
       
  1075                     author_els = commitel.getElementsByTagName('author')
       
  1076                     if author_els:
       
  1077                         for c in author_els[0].childNodes:
       
  1078                             author += c.nodeValue
       
  1079                     date = ''
       
  1080                     for c in commitel.getElementsByTagName('date')[0]\
       
  1081                             .childNodes:
       
  1082                         date += c.nodeValue
       
  1083 
       
  1084             wcpath = rootwcpath.join(path, abs=1)
       
  1085 
       
  1086             assert itemstatus != 'modified' or wcpath.check(file=1), (
       
  1087                 'did\'t expect a directory with changed content here')
       
  1088 
       
  1089             itemattrname = {
       
  1090                 'normal': 'unchanged',
       
  1091                 'unversioned': 'unknown',
       
  1092                 'conflicted': 'conflict',
       
  1093                 'none': 'added',
       
  1094             }.get(itemstatus, itemstatus)
       
  1095 
       
  1096             attr = getattr(rootstatus, itemattrname)
       
  1097             attr.append(wcpath)
       
  1098 
       
  1099             propsstatus = statusel.getAttribute('props')
       
  1100             if propsstatus not in ('none', 'normal'):
       
  1101                 rootstatus.prop_modified.append(wcpath)
       
  1102 
       
  1103             if wcpath == rootwcpath:
       
  1104                 rootstatus.rev = rev
       
  1105                 rootstatus.modrev = modrev
       
  1106                 rootstatus.author = author
       
  1107                 rootstatus.date = date
       
  1108 
       
  1109             # handle repos-status element (remote info)
       
  1110             rstatusels = entryel.getElementsByTagName('repos-status')
       
  1111             if rstatusels:
       
  1112                 rstatusel = rstatusels[0]
       
  1113                 ritemstatus = rstatusel.getAttribute('item')
       
  1114                 if ritemstatus in ('added', 'modified'):
       
  1115                     rootstatus.update_available.append(wcpath)
       
  1116 
       
  1117             lockels = entryel.getElementsByTagName('lock')
       
  1118             if len(lockels):
       
  1119                 rootstatus.locked.append(wcpath)
       
  1120 
       
  1121         return rootstatus
       
  1122     fromstring = staticmethod(fromstring)
       
  1123 
       
  1124 class InfoSvnWCCommand:
       
  1125     def __init__(self, output):
       
  1126         # Path: test
       
  1127         # URL: http://codespeak.net/svn/std.path/trunk/dist/std.path/test
       
  1128         # Repository UUID: fd0d7bf2-dfb6-0310-8d31-b7ecfe96aada
       
  1129         # Revision: 2151
       
  1130         # Node Kind: directory
       
  1131         # Schedule: normal
       
  1132         # Last Changed Author: hpk
       
  1133         # Last Changed Rev: 2100
       
  1134         # Last Changed Date: 2003-10-27 20:43:14 +0100 (Mon, 27 Oct 2003)
       
  1135         # Properties Last Updated: 2003-11-03 14:47:48 +0100 (Mon, 03 Nov 2003)
       
  1136 
       
  1137         d = {}
       
  1138         for line in output.split('\n'):
       
  1139             if not line.strip():
       
  1140                 continue
       
  1141             key, value = line.split(':', 1)
       
  1142             key = key.lower().replace(' ', '')
       
  1143             value = value.strip()
       
  1144             d[key] = value
       
  1145         try:
       
  1146             self.url = d['url']
       
  1147         except KeyError:
       
  1148             raise  ValueError("Not a versioned resource")
       
  1149             #raise ValueError, "Not a versioned resource %r" % path
       
  1150         self.kind = d['nodekind'] == 'directory' and 'dir' or d['nodekind']
       
  1151         self.rev = int(d['revision'])
       
  1152         self.path = py.path.local(d['path'])
       
  1153         self.size = self.path.size()
       
  1154         if 'lastchangedrev' in d:
       
  1155             self.created_rev = int(d['lastchangedrev'])
       
  1156         if 'lastchangedauthor' in d:
       
  1157             self.last_author = d['lastchangedauthor']
       
  1158         if 'lastchangeddate' in d:
       
  1159             self.mtime = parse_wcinfotime(d['lastchangeddate'])
       
  1160             self.time = self.mtime * 1000000
       
  1161 
       
  1162     def __eq__(self, other):
       
  1163         return self.__dict__ == other.__dict__
       
  1164 
       
  1165 def parse_wcinfotime(timestr):
       
  1166     """ Returns seconds since epoch, UTC. """
       
  1167     # example: 2003-10-27 20:43:14 +0100 (Mon, 27 Oct 2003)
       
  1168     m = re.match(r'(\d+-\d+-\d+ \d+:\d+:\d+) ([+-]\d+) .*', timestr)
       
  1169     if not m:
       
  1170         raise ValueError("timestring %r does not match" % timestr)
       
  1171     timestr, timezone = m.groups()
       
  1172     # do not handle timezone specially, return value should be UTC
       
  1173     parsedtime = time.strptime(timestr, "%Y-%m-%d %H:%M:%S")
       
  1174     return calendar.timegm(parsedtime)
       
  1175 
       
  1176 def make_recursive_propdict(wcroot,
       
  1177                             output,
       
  1178                             rex = re.compile("Properties on '(.*)':")):
       
  1179     """ Return a dictionary of path->PropListDict mappings. """
       
  1180     lines = [x for x in output.split('\n') if x]
       
  1181     pdict = {}
       
  1182     while lines:
       
  1183         line = lines.pop(0)
       
  1184         m = rex.match(line)
       
  1185         if not m:
       
  1186             raise ValueError("could not parse propget-line: %r" % line)
       
  1187         path = m.groups()[0]
       
  1188         wcpath = wcroot.join(path, abs=1)
       
  1189         propnames = []
       
  1190         while lines and lines[0].startswith('  '):
       
  1191             propname = lines.pop(0).strip()
       
  1192             propnames.append(propname)
       
  1193         assert propnames, "must have found properties!"
       
  1194         pdict[wcpath] = PropListDict(wcpath, propnames)
       
  1195     return pdict
       
  1196 
       
  1197 
       
  1198 def importxml(cache=[]):
       
  1199     if cache:
       
  1200         return cache
       
  1201     from xml.dom import minidom
       
  1202     from xml.parsers.expat import ExpatError
       
  1203     cache.extend([minidom, ExpatError])
       
  1204     return cache
       
  1205 
       
  1206 class LogEntry:
       
  1207     def __init__(self, logentry):
       
  1208         self.rev = int(logentry.getAttribute('revision'))
       
  1209         for lpart in filter(None, logentry.childNodes):
       
  1210             if lpart.nodeType == lpart.ELEMENT_NODE:
       
  1211                 if lpart.nodeName == 'author':
       
  1212                     self.author = lpart.firstChild.nodeValue
       
  1213                 elif lpart.nodeName == 'msg':
       
  1214                     if lpart.firstChild:
       
  1215                         self.msg = lpart.firstChild.nodeValue
       
  1216                     else:
       
  1217                         self.msg = ''
       
  1218                 elif lpart.nodeName == 'date':
       
  1219                     #2003-07-29T20:05:11.598637Z
       
  1220                     timestr = lpart.firstChild.nodeValue
       
  1221                     self.date = parse_apr_time(timestr)
       
  1222                 elif lpart.nodeName == 'paths':
       
  1223                     self.strpaths = []
       
  1224                     for ppart in filter(None, lpart.childNodes):
       
  1225                         if ppart.nodeType == ppart.ELEMENT_NODE:
       
  1226                             self.strpaths.append(PathEntry(ppart))
       
  1227     def __repr__(self):
       
  1228         return '<Logentry rev=%d author=%s date=%s>' % (
       
  1229             self.rev, self.author, self.date)
       
  1230 
       
  1231