|
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 |