1""" 2svn-Command based Implementation of a Subversion WorkingCopy Path. 3 4 SvnWCCommandPath is the main class. 5 6""" 7 8import os, sys, time, re, calendar 9import py 10import subprocess 11from 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 20class cache: 21 proplist = {} 22 info = {} 23 entries = {} 24 prop = {} 25 26class 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 35class 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 75repositories = RepoCache() 76 77 78# svn support code 79 80ALLOWED_CHARS = "_ -/\\=$.~+%" #add characters as necessary when tested 81if sys.platform == "win32": 82 ALLOWED_CHARS += ":" 83ALLOWED_CHARS_HOST = ALLOWED_CHARS + '@:' 84 85def _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 95def _escape_helper(text): 96 text = str(text) 97 if sys.platform != 'win32': 98 text = str(text).replace('$', '\\$') 99 return text 100 101def _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 110def 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 123class 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 303def 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 311class 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 324def 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 330ILLEGAL_CHARS = '* | \\ / : < > ? \t \n \x0b \x0c \r'.split(' ') 331if os.sep in ILLEGAL_CHARS: 332 ILLEGAL_CHARS.remove(os.sep) 333ISWINDOWS = sys.platform == 'win32' 334_reg_allow_disk = re.compile(r'^([a-z]\:\\)?[^:]+$', re.I) 335def _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 346def 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 355def url_from_path(path): 356 fspath = path_to_fspath(path, False) 357 from urllib import 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 374class 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 399rex_blame = re.compile(r'\s*(\d+)\s*(\S+) (.*)') 400 401class 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 (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. 720If 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 733recursively. """ 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 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. 843rev_start is the starting revision (defaulting to the first one). 844rev_end is the last revision (defaulting to HEAD). 845if 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 895class 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 1021class 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 1129class 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 1174def 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 1185def 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 1207def 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 1215class 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 1241