~ibmcharmers/charms/xenial/ibm-cinder-storwize-svc/trunk

« back to all changes in this revision

Viewing changes to .tox/py35/lib/python3.5/site-packages/py/_path/svnwc.py

  • Committer: Ankammarao
  • Date: 2017-03-06 05:11:42 UTC
  • Revision ID: achittet@in.ibm.com-20170306051142-dpg27z4es1k56hfn
Marked tests folder executable

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
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('not found') != -1:
 
486
                raise py.error.ENOENT(self)
 
487
            elif strerr.find("E200009:") != -1:
 
488
                raise py.error.ENOENT(self)
 
489
            if (strerr.find('file exists') != -1 or
 
490
                strerr.find('file already exists') != -1 or
 
491
                strerr.find('w150002:') != -1 or
 
492
                strerr.find("can't create directory") != -1):
 
493
                raise py.error.EEXIST(strerr) #self)
 
494
            raise
 
495
        return out
 
496
 
 
497
    def switch(self, url):
 
498
        """ switch to given URL. """
 
499
        self._authsvn('switch', [url])
 
500
 
 
501
    def checkout(self, url=None, rev=None):
 
502
        """ checkout from url to local wcpath. """
 
503
        args = []
 
504
        if url is None:
 
505
            url = self.url
 
506
        if rev is None or rev == -1:
 
507
            if (py.std.sys.platform != 'win32' and
 
508
                    _getsvnversion() == '1.3'):
 
509
                url += "@HEAD"
 
510
        else:
 
511
            if _getsvnversion() == '1.3':
 
512
                url += "@%d" % rev
 
513
            else:
 
514
                args.append('-r' + str(rev))
 
515
        args.append(url)
 
516
        self._authsvn('co', args)
 
517
 
 
518
    def update(self, rev='HEAD', interactive=True):
 
519
        """ update working copy item to given revision. (None -> HEAD). """
 
520
        opts = ['-r', rev]
 
521
        if not interactive:
 
522
            opts.append("--non-interactive")
 
523
        self._authsvn('up', opts)
 
524
 
 
525
    def write(self, content, mode='w'):
 
526
        """ write content into local filesystem wc. """
 
527
        self.localpath.write(content, mode)
 
528
 
 
529
    def dirpath(self, *args):
 
530
        """ return the directory Path of the current Path. """
 
531
        return self.__class__(self.localpath.dirpath(*args), auth=self.auth)
 
532
 
 
533
    def _ensuredirs(self):
 
534
        parent = self.dirpath()
 
535
        if parent.check(dir=0):
 
536
            parent._ensuredirs()
 
537
        if self.check(dir=0):
 
538
            self.mkdir()
 
539
        return self
 
540
 
 
541
    def ensure(self, *args, **kwargs):
 
542
        """ ensure that an args-joined path exists (by default as
 
543
            a file). if you specify a keyword argument 'directory=True'
 
544
            then the path is forced  to be a directory path.
 
545
        """
 
546
        p = self.join(*args)
 
547
        if p.check():
 
548
            if p.check(versioned=False):
 
549
                p.add()
 
550
            return p
 
551
        if kwargs.get('dir', 0):
 
552
            return p._ensuredirs()
 
553
        parent = p.dirpath()
 
554
        parent._ensuredirs()
 
555
        p.write("")
 
556
        p.add()
 
557
        return p
 
558
 
 
559
    def mkdir(self, *args):
 
560
        """ create & return the directory joined with args. """
 
561
        if args:
 
562
            return self.join(*args).mkdir()
 
563
        else:
 
564
            self._svn('mkdir')
 
565
            return self
 
566
 
 
567
    def add(self):
 
568
        """ add ourself to svn """
 
569
        self._svn('add')
 
570
 
 
571
    def remove(self, rec=1, force=1):
 
572
        """ remove a file or a directory tree. 'rec'ursive is
 
573
            ignored and considered always true (because of
 
574
            underlying svn semantics.
 
575
        """
 
576
        assert rec, "svn cannot remove non-recursively"
 
577
        if not self.check(versioned=True):
 
578
            # not added to svn (anymore?), just remove
 
579
            py.path.local(self).remove()
 
580
            return
 
581
        flags = []
 
582
        if force:
 
583
            flags.append('--force')
 
584
        self._svn('remove', *flags)
 
585
 
 
586
    def copy(self, target):
 
587
        """ copy path to target."""
 
588
        py.process.cmdexec("svn copy %s %s" %(str(self), str(target)))
 
589
 
 
590
    def rename(self, target):
 
591
        """ rename this path to target. """
 
592
        py.process.cmdexec("svn move --force %s %s" %(str(self), str(target)))
 
593
 
 
594
    def lock(self):
 
595
        """ set a lock (exclusive) on the resource """
 
596
        out = self._authsvn('lock').strip()
 
597
        if not out:
 
598
            # warning or error, raise exception
 
599
            raise ValueError("unknown error in svn lock command")
 
600
 
 
601
    def unlock(self):
 
602
        """ unset a previously set lock """
 
603
        out = self._authsvn('unlock').strip()
 
604
        if out.startswith('svn:'):
 
605
            # warning or error, raise exception
 
606
            raise Exception(out[4:])
 
607
 
 
608
    def cleanup(self):
 
609
        """ remove any locks from the resource """
 
610
        # XXX should be fixed properly!!!
 
611
        try:
 
612
            self.unlock()
 
613
        except:
 
614
            pass
 
615
 
 
616
    def status(self, updates=0, rec=0, externals=0):
 
617
        """ return (collective) Status object for this file. """
 
618
        # http://svnbook.red-bean.com/book.html#svn-ch-3-sect-4.3.1
 
619
        #             2201     2192        jum   test
 
620
        # XXX
 
621
        if externals:
 
622
            raise ValueError("XXX cannot perform status() "
 
623
                             "on external items yet")
 
624
        else:
 
625
            #1.2 supports: externals = '--ignore-externals'
 
626
            externals = ''
 
627
        if rec:
 
628
            rec= ''
 
629
        else:
 
630
            rec = '--non-recursive'
 
631
 
 
632
        # XXX does not work on all subversion versions
 
633
        #if not externals:
 
634
        #    externals = '--ignore-externals'
 
635
 
 
636
        if updates:
 
637
            updates = '-u'
 
638
        else:
 
639
            updates = ''
 
640
 
 
641
        try:
 
642
            cmd = 'status -v --xml --no-ignore %s %s %s' % (
 
643
                    updates, rec, externals)
 
644
            out = self._authsvn(cmd)
 
645
        except py.process.cmdexec.Error:
 
646
            cmd = 'status -v --no-ignore %s %s %s' % (
 
647
                    updates, rec, externals)
 
648
            out = self._authsvn(cmd)
 
649
            rootstatus = WCStatus(self).fromstring(out, self)
 
650
        else:
 
651
            rootstatus = XMLWCStatus(self).fromstring(out, self)
 
652
        return rootstatus
 
653
 
 
654
    def diff(self, rev=None):
 
655
        """ return a diff of the current path against revision rev (defaulting
 
656
            to the last one).
 
657
        """
 
658
        args = []
 
659
        if rev is not None:
 
660
            args.append("-r %d" % rev)
 
661
        out = self._authsvn('diff', args)
 
662
        return out
 
663
 
 
664
    def blame(self):
 
665
        """ return a list of tuples of three elements:
 
666
            (revision, commiter, line)
 
667
        """
 
668
        out = self._svn('blame')
 
669
        result = []
 
670
        blamelines = out.splitlines()
 
671
        reallines = py.path.svnurl(self.url).readlines()
 
672
        for i, (blameline, line) in enumerate(
 
673
                zip(blamelines, reallines)):
 
674
            m = rex_blame.match(blameline)
 
675
            if not m:
 
676
                raise ValueError("output line %r of svn blame does not match "
 
677
                                 "expected format" % (line, ))
 
678
            rev, name, _ = m.groups()
 
679
            result.append((int(rev), name, line))
 
680
        return result
 
681
 
 
682
    _rex_commit = re.compile(r'.*Committed revision (\d+)\.$', re.DOTALL)
 
683
    def commit(self, msg='', rec=1):
 
684
        """ commit with support for non-recursive commits """
 
685
        # XXX i guess escaping should be done better here?!?
 
686
        cmd = 'commit -m "%s" --force-log' % (msg.replace('"', '\\"'),)
 
687
        if not rec:
 
688
            cmd += ' -N'
 
689
        out = self._authsvn(cmd)
 
690
        try:
 
691
            del cache.info[self]
 
692
        except KeyError:
 
693
            pass
 
694
        if out:
 
695
            m = self._rex_commit.match(out)
 
696
            return int(m.group(1))
 
697
 
 
698
    def propset(self, name, value, *args):
 
699
        """ set property name to value on this path. """
 
700
        d = py.path.local.mkdtemp()
 
701
        try:
 
702
            p = d.join('value')
 
703
            p.write(value)
 
704
            self._svn('propset', name, '--file', str(p), *args)
 
705
        finally:
 
706
            d.remove()
 
707
 
 
708
    def propget(self, name):
 
709
        """ get property name on this path. """
 
710
        res = self._svn('propget', name)
 
711
        return res[:-1] # strip trailing newline
 
712
 
 
713
    def propdel(self, name):
 
714
        """ delete property name on this path. """
 
715
        res = self._svn('propdel', name)
 
716
        return res[:-1] # strip trailing newline
 
717
 
 
718
    def proplist(self, rec=0):
 
719
        """ return a mapping of property names to property values.
 
720
If rec is True, then return a dictionary mapping sub-paths to such mappings.
 
721
"""
 
722
        if rec:
 
723
            res = self._svn('proplist -R')
 
724
            return make_recursive_propdict(self, res)
 
725
        else:
 
726
            res = self._svn('proplist')
 
727
            lines = res.split('\n')
 
728
            lines = [x.strip() for x in lines[1:]]
 
729
            return PropListDict(self, lines)
 
730
 
 
731
    def revert(self, rec=0):
 
732
        """ revert the local changes of this path. if rec is True, do so
 
733
recursively. """
 
734
        if rec:
 
735
            result = self._svn('revert -R')
 
736
        else:
 
737
            result = self._svn('revert')
 
738
        return result
 
739
 
 
740
    def new(self, **kw):
 
741
        """ create a modified version of this path. A 'rev' argument
 
742
            indicates a new revision.
 
743
            the following keyword arguments modify various path parts:
 
744
 
 
745
              http://host.com/repo/path/file.ext
 
746
              |-----------------------|          dirname
 
747
                                        |------| basename
 
748
                                        |--|     purebasename
 
749
                                            |--| ext
 
750
        """
 
751
        if kw:
 
752
            localpath = self.localpath.new(**kw)
 
753
        else:
 
754
            localpath = self.localpath
 
755
        return self.__class__(localpath, auth=self.auth)
 
756
 
 
757
    def join(self, *args, **kwargs):
 
758
        """ return a new Path (with the same revision) which is composed
 
759
            of the self Path followed by 'args' path components.
 
760
        """
 
761
        if not args:
 
762
            return self
 
763
        localpath = self.localpath.join(*args, **kwargs)
 
764
        return self.__class__(localpath, auth=self.auth)
 
765
 
 
766
    def info(self, usecache=1):
 
767
        """ return an Info structure with svn-provided information. """
 
768
        info = usecache and cache.info.get(self)
 
769
        if not info:
 
770
            try:
 
771
                output = self._svn('info')
 
772
            except py.process.cmdexec.Error:
 
773
                e = sys.exc_info()[1]
 
774
                if e.err.find('Path is not a working copy directory') != -1:
 
775
                    raise py.error.ENOENT(self, e.err)
 
776
                elif e.err.find("is not under version control") != -1:
 
777
                    raise py.error.ENOENT(self, e.err)
 
778
                raise
 
779
            # XXX SVN 1.3 has output on stderr instead of stdout (while it does
 
780
            # return 0!), so a bit nasty, but we assume no output is output
 
781
            # to stderr...
 
782
            if (output.strip() == '' or
 
783
                    output.lower().find('not a versioned resource') != -1):
 
784
                raise py.error.ENOENT(self, output)
 
785
            info = InfoSvnWCCommand(output)
 
786
 
 
787
            # Can't reliably compare on Windows without access to win32api
 
788
            if py.std.sys.platform != 'win32':
 
789
                if info.path != self.localpath:
 
790
                    raise py.error.ENOENT(self, "not a versioned resource:" +
 
791
                            " %s != %s" % (info.path, self.localpath))
 
792
            cache.info[self] = info
 
793
        return info
 
794
 
 
795
    def listdir(self, fil=None, sort=None):
 
796
        """ return a sequence of Paths.
 
797
 
 
798
        listdir will return either a tuple or a list of paths
 
799
        depending on implementation choices.
 
800
        """
 
801
        if isinstance(fil, str):
 
802
            fil = common.FNMatcher(fil)
 
803
        # XXX unify argument naming with LocalPath.listdir
 
804
        def notsvn(path):
 
805
            return path.basename != '.svn'
 
806
 
 
807
        paths = []
 
808
        for localpath in self.localpath.listdir(notsvn):
 
809
            p = self.__class__(localpath, auth=self.auth)
 
810
            if notsvn(p) and (not fil or fil(p)):
 
811
                paths.append(p)
 
812
        self._sortlist(paths, sort)
 
813
        return paths
 
814
 
 
815
    def open(self, mode='r'):
 
816
        """ return an opened file with the given mode. """
 
817
        return open(self.strpath, mode)
 
818
 
 
819
    def _getbyspec(self, spec):
 
820
        return self.localpath._getbyspec(spec)
 
821
 
 
822
    class Checkers(py.path.local.Checkers):
 
823
        def __init__(self, path):
 
824
            self.svnwcpath = path
 
825
            self.path = path.localpath
 
826
        def versioned(self):
 
827
            try:
 
828
                s = self.svnwcpath.info()
 
829
            except (py.error.ENOENT, py.error.EEXIST):
 
830
                return False
 
831
            except py.process.cmdexec.Error:
 
832
                e = sys.exc_info()[1]
 
833
                if e.err.find('is not a working copy')!=-1:
 
834
                    return False
 
835
                if e.err.lower().find('not a versioned resource') != -1:
 
836
                    return False
 
837
                raise
 
838
            else:
 
839
                return True
 
840
 
 
841
    def log(self, rev_start=None, rev_end=1, verbose=False):
 
842
        """ return a list of LogEntry instances for this path.
 
843
rev_start is the starting revision (defaulting to the first one).
 
844
rev_end is the last revision (defaulting to HEAD).
 
845
if verbose is True, then the LogEntry instances also know which files changed.
 
846
"""
 
847
        assert self.check()   # make it simpler for the pipe
 
848
        rev_start = rev_start is None and "HEAD" or rev_start
 
849
        rev_end = rev_end is None and "HEAD" or rev_end
 
850
        if rev_start == "HEAD" and rev_end == 1:
 
851
                rev_opt = ""
 
852
        else:
 
853
            rev_opt = "-r %s:%s" % (rev_start, rev_end)
 
854
        verbose_opt = verbose and "-v" or ""
 
855
        locale_env = fixlocale()
 
856
        # some blather on stderr
 
857
        auth_opt = self._makeauthoptions()
 
858
        #stdin, stdout, stderr  = os.popen3(locale_env +
 
859
        #                                   'svn log --xml %s %s %s "%s"' % (
 
860
        #                                    rev_opt, verbose_opt, auth_opt,
 
861
        #                                    self.strpath))
 
862
        cmd = locale_env + 'svn log --xml %s %s %s "%s"' % (
 
863
            rev_opt, verbose_opt, auth_opt, self.strpath)
 
864
 
 
865
        popen = subprocess.Popen(cmd,
 
866
                    stdout=subprocess.PIPE,
 
867
                    stderr=subprocess.PIPE,
 
868
                    shell=True,
 
869
        )
 
870
        stdout, stderr = popen.communicate()
 
871
        stdout = py.builtin._totext(stdout, sys.getdefaultencoding())
 
872
        minidom,ExpatError = importxml()
 
873
        try:
 
874
            tree = minidom.parseString(stdout)
 
875
        except ExpatError:
 
876
            raise ValueError('no such revision')
 
877
        result = []
 
878
        for logentry in filter(None, tree.firstChild.childNodes):
 
879
            if logentry.nodeType == logentry.ELEMENT_NODE:
 
880
                result.append(LogEntry(logentry))
 
881
        return result
 
882
 
 
883
    def size(self):
 
884
        """ Return the size of the file content of the Path. """
 
885
        return self.info().size
 
886
 
 
887
    def mtime(self):
 
888
        """ Return the last modification time of the file. """
 
889
        return self.info().mtime
 
890
 
 
891
    def __hash__(self):
 
892
        return hash((self.strpath, self.__class__, self.auth))
 
893
 
 
894
 
 
895
class WCStatus:
 
896
    attrnames = ('modified','added', 'conflict', 'unchanged', 'external',
 
897
                'deleted', 'prop_modified', 'unknown', 'update_available',
 
898
                'incomplete', 'kindmismatch', 'ignored', 'locked', 'replaced'
 
899
                )
 
900
 
 
901
    def __init__(self, wcpath, rev=None, modrev=None, author=None):
 
902
        self.wcpath = wcpath
 
903
        self.rev = rev
 
904
        self.modrev = modrev
 
905
        self.author = author
 
906
 
 
907
        for name in self.attrnames:
 
908
            setattr(self, name, [])
 
909
 
 
910
    def allpath(self, sort=True, **kw):
 
911
        d = {}
 
912
        for name in self.attrnames:
 
913
            if name not in kw or kw[name]:
 
914
                for path in getattr(self, name):
 
915
                    d[path] = 1
 
916
        l = d.keys()
 
917
        if sort:
 
918
            l.sort()
 
919
        return l
 
920
 
 
921
    # XXX a bit scary to assume there's always 2 spaces between username and
 
922
    # path, however with win32 allowing spaces in user names there doesn't
 
923
    # seem to be a more solid approach :(
 
924
    _rex_status = re.compile(r'\s+(\d+|-)\s+(\S+)\s+(.+?)\s{2,}(.*)')
 
925
 
 
926
    def fromstring(data, rootwcpath, rev=None, modrev=None, author=None):
 
927
        """ return a new WCStatus object from data 's'
 
928
        """
 
929
        rootstatus = WCStatus(rootwcpath, rev, modrev, author)
 
930
        update_rev = None
 
931
        for line in data.split('\n'):
 
932
            if not line.strip():
 
933
                continue
 
934
            #print "processing %r" % line
 
935
            flags, rest = line[:8], line[8:]
 
936
            # first column
 
937
            c0,c1,c2,c3,c4,c5,x6,c7 = flags
 
938
            #if '*' in line:
 
939
            #    print "flags", repr(flags), "rest", repr(rest)
 
940
 
 
941
            if c0 in '?XI':
 
942
                fn = line.split(None, 1)[1]
 
943
                if c0 == '?':
 
944
                    wcpath = rootwcpath.join(fn, abs=1)
 
945
                    rootstatus.unknown.append(wcpath)
 
946
                elif c0 == 'X':
 
947
                    wcpath = rootwcpath.__class__(
 
948
                        rootwcpath.localpath.join(fn, abs=1),
 
949
                        auth=rootwcpath.auth)
 
950
                    rootstatus.external.append(wcpath)
 
951
                elif c0 == 'I':
 
952
                    wcpath = rootwcpath.join(fn, abs=1)
 
953
                    rootstatus.ignored.append(wcpath)
 
954
 
 
955
                continue
 
956
 
 
957
            #elif c0 in '~!' or c4 == 'S':
 
958
            #    raise NotImplementedError("received flag %r" % c0)
 
959
 
 
960
            m = WCStatus._rex_status.match(rest)
 
961
            if not m:
 
962
                if c7 == '*':
 
963
                    fn = rest.strip()
 
964
                    wcpath = rootwcpath.join(fn, abs=1)
 
965
                    rootstatus.update_available.append(wcpath)
 
966
                    continue
 
967
                if line.lower().find('against revision:')!=-1:
 
968
                    update_rev = int(rest.split(':')[1].strip())
 
969
                    continue
 
970
                if line.lower().find('status on external') > -1:
 
971
                    # XXX not sure what to do here... perhaps we want to
 
972
                    # store some state instead of just continuing, as right
 
973
                    # now it makes the top-level external get added twice
 
974
                    # (once as external, once as 'normal' unchanged item)
 
975
                    # because of the way SVN presents external items
 
976
                    continue
 
977
                # keep trying
 
978
                raise ValueError("could not parse line %r" % line)
 
979
            else:
 
980
                rev, modrev, author, fn = m.groups()
 
981
            wcpath = rootwcpath.join(fn, abs=1)
 
982
            #assert wcpath.check()
 
983
            if c0 == 'M':
 
984
                assert wcpath.check(file=1), "didn't expect a directory with changed content here"
 
985
                rootstatus.modified.append(wcpath)
 
986
            elif c0 == 'A' or c3 == '+' :
 
987
                rootstatus.added.append(wcpath)
 
988
            elif c0 == 'D':
 
989
                rootstatus.deleted.append(wcpath)
 
990
            elif c0 == 'C':
 
991
                rootstatus.conflict.append(wcpath)
 
992
            elif c0 == '~':
 
993
                rootstatus.kindmismatch.append(wcpath)
 
994
            elif c0 == '!':
 
995
                rootstatus.incomplete.append(wcpath)
 
996
            elif c0 == 'R':
 
997
                rootstatus.replaced.append(wcpath)
 
998
            elif not c0.strip():
 
999
                rootstatus.unchanged.append(wcpath)
 
1000
            else:
 
1001
                raise NotImplementedError("received flag %r" % c0)
 
1002
 
 
1003
            if c1 == 'M':
 
1004
                rootstatus.prop_modified.append(wcpath)
 
1005
            # XXX do we cover all client versions here?
 
1006
            if c2 == 'L' or c5 == 'K':
 
1007
                rootstatus.locked.append(wcpath)
 
1008
            if c7 == '*':
 
1009
                rootstatus.update_available.append(wcpath)
 
1010
 
 
1011
            if wcpath == rootwcpath:
 
1012
                rootstatus.rev = rev
 
1013
                rootstatus.modrev = modrev
 
1014
                rootstatus.author = author
 
1015
                if update_rev:
 
1016
                    rootstatus.update_rev = update_rev
 
1017
                continue
 
1018
        return rootstatus
 
1019
    fromstring = staticmethod(fromstring)
 
1020
 
 
1021
class XMLWCStatus(WCStatus):
 
1022
    def fromstring(data, rootwcpath, rev=None, modrev=None, author=None):
 
1023
        """ parse 'data' (XML string as outputted by svn st) into a status obj
 
1024
        """
 
1025
        # XXX for externals, the path is shown twice: once
 
1026
        # with external information, and once with full info as if
 
1027
        # the item was a normal non-external... the current way of
 
1028
        # dealing with this issue is by ignoring it - this does make
 
1029
        # externals appear as external items as well as 'normal',
 
1030
        # unchanged ones in the status object so this is far from ideal
 
1031
        rootstatus = WCStatus(rootwcpath, rev, modrev, author)
 
1032
        update_rev = None
 
1033
        minidom, ExpatError = importxml()
 
1034
        try:
 
1035
            doc = minidom.parseString(data)
 
1036
        except ExpatError:
 
1037
            e = sys.exc_info()[1]
 
1038
            raise ValueError(str(e))
 
1039
        urevels = doc.getElementsByTagName('against')
 
1040
        if urevels:
 
1041
            rootstatus.update_rev = urevels[-1].getAttribute('revision')
 
1042
        for entryel in doc.getElementsByTagName('entry'):
 
1043
            path = entryel.getAttribute('path')
 
1044
            statusel = entryel.getElementsByTagName('wc-status')[0]
 
1045
            itemstatus = statusel.getAttribute('item')
 
1046
 
 
1047
            if itemstatus == 'unversioned':
 
1048
                wcpath = rootwcpath.join(path, abs=1)
 
1049
                rootstatus.unknown.append(wcpath)
 
1050
                continue
 
1051
            elif itemstatus == 'external':
 
1052
                wcpath = rootwcpath.__class__(
 
1053
                    rootwcpath.localpath.join(path, abs=1),
 
1054
                    auth=rootwcpath.auth)
 
1055
                rootstatus.external.append(wcpath)
 
1056
                continue
 
1057
            elif itemstatus == 'ignored':
 
1058
                wcpath = rootwcpath.join(path, abs=1)
 
1059
                rootstatus.ignored.append(wcpath)
 
1060
                continue
 
1061
            elif itemstatus == 'incomplete':
 
1062
                wcpath = rootwcpath.join(path, abs=1)
 
1063
                rootstatus.incomplete.append(wcpath)
 
1064
                continue
 
1065
 
 
1066
            rev = statusel.getAttribute('revision')
 
1067
            if itemstatus == 'added' or itemstatus == 'none':
 
1068
                rev = '0'
 
1069
                modrev = '?'
 
1070
                author = '?'
 
1071
                date = ''
 
1072
            elif itemstatus == "replaced":
 
1073
                pass
 
1074
            else:
 
1075
                #print entryel.toxml()
 
1076
                commitel = entryel.getElementsByTagName('commit')[0]
 
1077
                if commitel:
 
1078
                    modrev = commitel.getAttribute('revision')
 
1079
                    author = ''
 
1080
                    author_els = commitel.getElementsByTagName('author')
 
1081
                    if author_els:
 
1082
                        for c in author_els[0].childNodes:
 
1083
                            author += c.nodeValue
 
1084
                    date = ''
 
1085
                    for c in commitel.getElementsByTagName('date')[0]\
 
1086
                            .childNodes:
 
1087
                        date += c.nodeValue
 
1088
 
 
1089
            wcpath = rootwcpath.join(path, abs=1)
 
1090
 
 
1091
            assert itemstatus != 'modified' or wcpath.check(file=1), (
 
1092
                'did\'t expect a directory with changed content here')
 
1093
 
 
1094
            itemattrname = {
 
1095
                'normal': 'unchanged',
 
1096
                'unversioned': 'unknown',
 
1097
                'conflicted': 'conflict',
 
1098
                'none': 'added',
 
1099
            }.get(itemstatus, itemstatus)
 
1100
 
 
1101
            attr = getattr(rootstatus, itemattrname)
 
1102
            attr.append(wcpath)
 
1103
 
 
1104
            propsstatus = statusel.getAttribute('props')
 
1105
            if propsstatus not in ('none', 'normal'):
 
1106
                rootstatus.prop_modified.append(wcpath)
 
1107
 
 
1108
            if wcpath == rootwcpath:
 
1109
                rootstatus.rev = rev
 
1110
                rootstatus.modrev = modrev
 
1111
                rootstatus.author = author
 
1112
                rootstatus.date = date
 
1113
 
 
1114
            # handle repos-status element (remote info)
 
1115
            rstatusels = entryel.getElementsByTagName('repos-status')
 
1116
            if rstatusels:
 
1117
                rstatusel = rstatusels[0]
 
1118
                ritemstatus = rstatusel.getAttribute('item')
 
1119
                if ritemstatus in ('added', 'modified'):
 
1120
                    rootstatus.update_available.append(wcpath)
 
1121
 
 
1122
            lockels = entryel.getElementsByTagName('lock')
 
1123
            if len(lockels):
 
1124
                rootstatus.locked.append(wcpath)
 
1125
 
 
1126
        return rootstatus
 
1127
    fromstring = staticmethod(fromstring)
 
1128
 
 
1129
class InfoSvnWCCommand:
 
1130
    def __init__(self, output):
 
1131
        # Path: test
 
1132
        # URL: http://codespeak.net/svn/std.path/trunk/dist/std.path/test
 
1133
        # Repository UUID: fd0d7bf2-dfb6-0310-8d31-b7ecfe96aada
 
1134
        # Revision: 2151
 
1135
        # Node Kind: directory
 
1136
        # Schedule: normal
 
1137
        # Last Changed Author: hpk
 
1138
        # Last Changed Rev: 2100
 
1139
        # Last Changed Date: 2003-10-27 20:43:14 +0100 (Mon, 27 Oct 2003)
 
1140
        # Properties Last Updated: 2003-11-03 14:47:48 +0100 (Mon, 03 Nov 2003)
 
1141
 
 
1142
        d = {}
 
1143
        for line in output.split('\n'):
 
1144
            if not line.strip():
 
1145
                continue
 
1146
            key, value = line.split(':', 1)
 
1147
            key = key.lower().replace(' ', '')
 
1148
            value = value.strip()
 
1149
            d[key] = value
 
1150
        try:
 
1151
            self.url = d['url']
 
1152
        except KeyError:
 
1153
            raise  ValueError("Not a versioned resource")
 
1154
            #raise ValueError, "Not a versioned resource %r" % path
 
1155
        self.kind = d['nodekind'] == 'directory' and 'dir' or d['nodekind']
 
1156
        try:
 
1157
            self.rev = int(d['revision'])
 
1158
        except KeyError:
 
1159
            self.rev = None
 
1160
 
 
1161
        self.path = py.path.local(d['path'])
 
1162
        self.size = self.path.size()
 
1163
        if 'lastchangedrev' in d:
 
1164
            self.created_rev = int(d['lastchangedrev'])
 
1165
        if 'lastchangedauthor' in d:
 
1166
            self.last_author = d['lastchangedauthor']
 
1167
        if 'lastchangeddate' in d:
 
1168
            self.mtime = parse_wcinfotime(d['lastchangeddate'])
 
1169
            self.time = self.mtime * 1000000
 
1170
 
 
1171
    def __eq__(self, other):
 
1172
        return self.__dict__ == other.__dict__
 
1173
 
 
1174
def parse_wcinfotime(timestr):
 
1175
    """ Returns seconds since epoch, UTC. """
 
1176
    # example: 2003-10-27 20:43:14 +0100 (Mon, 27 Oct 2003)
 
1177
    m = re.match(r'(\d+-\d+-\d+ \d+:\d+:\d+) ([+-]\d+) .*', timestr)
 
1178
    if not m:
 
1179
        raise ValueError("timestring %r does not match" % timestr)
 
1180
    timestr, timezone = m.groups()
 
1181
    # do not handle timezone specially, return value should be UTC
 
1182
    parsedtime = time.strptime(timestr, "%Y-%m-%d %H:%M:%S")
 
1183
    return calendar.timegm(parsedtime)
 
1184
 
 
1185
def make_recursive_propdict(wcroot,
 
1186
                            output,
 
1187
                            rex = re.compile("Properties on '(.*)':")):
 
1188
    """ Return a dictionary of path->PropListDict mappings. """
 
1189
    lines = [x for x in output.split('\n') if x]
 
1190
    pdict = {}
 
1191
    while lines:
 
1192
        line = lines.pop(0)
 
1193
        m = rex.match(line)
 
1194
        if not m:
 
1195
            raise ValueError("could not parse propget-line: %r" % line)
 
1196
        path = m.groups()[0]
 
1197
        wcpath = wcroot.join(path, abs=1)
 
1198
        propnames = []
 
1199
        while lines and lines[0].startswith('  '):
 
1200
            propname = lines.pop(0).strip()
 
1201
            propnames.append(propname)
 
1202
        assert propnames, "must have found properties!"
 
1203
        pdict[wcpath] = PropListDict(wcpath, propnames)
 
1204
    return pdict
 
1205
 
 
1206
 
 
1207
def importxml(cache=[]):
 
1208
    if cache:
 
1209
        return cache
 
1210
    from xml.dom import minidom
 
1211
    from xml.parsers.expat import ExpatError
 
1212
    cache.extend([minidom, ExpatError])
 
1213
    return cache
 
1214
 
 
1215
class LogEntry:
 
1216
    def __init__(self, logentry):
 
1217
        self.rev = int(logentry.getAttribute('revision'))
 
1218
        for lpart in filter(None, logentry.childNodes):
 
1219
            if lpart.nodeType == lpart.ELEMENT_NODE:
 
1220
                if lpart.nodeName == 'author':
 
1221
                    self.author = lpart.firstChild.nodeValue
 
1222
                elif lpart.nodeName == 'msg':
 
1223
                    if lpart.firstChild:
 
1224
                        self.msg = lpart.firstChild.nodeValue
 
1225
                    else:
 
1226
                        self.msg = ''
 
1227
                elif lpart.nodeName == 'date':
 
1228
                    #2003-07-29T20:05:11.598637Z
 
1229
                    timestr = lpart.firstChild.nodeValue
 
1230
                    self.date = parse_apr_time(timestr)
 
1231
                elif lpart.nodeName == 'paths':
 
1232
                    self.strpaths = []
 
1233
                    for ppart in filter(None, lpart.childNodes):
 
1234
                        if ppart.nodeType == ppart.ELEMENT_NODE:
 
1235
                            self.strpaths.append(PathEntry(ppart))
 
1236
    def __repr__(self):
 
1237
        return '<Logentry rev=%d author=%s date=%s>' % (
 
1238
            self.rev, self.author, self.date)
 
1239
 
 
1240