text: """
  svn-Command based Implementation of a Subversion WorkingCopy Path.
  
    SvnWCCommandPath  is the main class.
  
  """
  
  import os, sys, time, re, calendar
  import py
  import subprocess
  from py._path import common
  
  #-----------------------------------------------------------
  # Caching latest repository revision and repo-paths
  # (getting them is slow with the current implementations)
  #
  # XXX make mt-safe
  #-----------------------------------------------------------
  
  class cache:
      proplist = {}
      info = {}
      entries = {}
      prop = {}
  
  class RepoEntry:
      def __init__(self, url, rev, timestamp):
          self.url = url
          self.rev = rev
          self.timestamp = timestamp
  
      def __str__(self):
          return "repo: %s;%s  %s" %(self.url, self.rev, self.timestamp)
  
  class RepoCache:
      """ The Repocache manages discovered repository paths
      and their revisions.  If inside a timeout the cache
      will even return the revision of the root.
      """
      timeout = 20 # seconds after which we forget that we know the last revision
  
      def __init__(self):
          self.repos = []
  
      def clear(self):
          self.repos = []
  
      def put(self, url, rev, timestamp=None):
          if rev is None:
              return
          if timestamp is None:
              timestamp = time.time()
  
          for entry in self.repos:
              if url == entry.url:
                  entry.timestamp = timestamp
                  entry.rev = rev
                  #print "set repo", entry
                  break
          else:
              entry = RepoEntry(url, rev, timestamp)
              self.repos.append(entry)
              #print "appended repo", entry
  
      def get(self, url):
          now = time.time()
          for entry in self.repos:
              if url.startswith(entry.url):
                  if now < entry.timestamp + self.timeout:
                      #print "returning immediate Etrny", entry
                      return entry.url, entry.rev
                  return entry.url, -1
          return url, -1
  
  repositories = RepoCache()
  
  
  # svn support code
  
  ALLOWED_CHARS = "_ -/\\=$.~+%" #add characters as necessary when tested
  if sys.platform == "win32":
      ALLOWED_CHARS += ":"
  ALLOWED_CHARS_HOST = ALLOWED_CHARS + '@:'
  
  def _getsvnversion(ver=[]):
      try:
          return ver[0]
      except IndexError:
          v = py.process.cmdexec("svn -q --version")
          v.strip()
          v = '.'.join(v.split('.')[:2])
          ver.append(v)
          return v
  
  def _escape_helper(text):
      text = str(text)
      if py.std.sys.platform != 'win32':
          text = str(text).replace('$', '\\$')
      return text
  
  def _check_for_bad_chars(text, allowed_chars=ALLOWED_CHARS):
      for c in str(text):
          if c.isalnum():
              continue
          if c in allowed_chars:
              continue
          return True
      return False
  
  def checkbadchars(url):
      # (hpk) not quite sure about the exact purpose, guido w.?
      proto, uri = url.split("://", 1)
      if proto != "file":
          host, uripath = uri.split('/', 1)
          # only check for bad chars in the non-protocol parts
          if (_check_for_bad_chars(host, ALLOWED_CHARS_HOST) \
              or _check_for_bad_chars(uripath, ALLOWED_CHARS)):
              raise ValueError("bad char in %r" % (url, ))
  
  
  #_______________________________________________________________
  
  class SvnPathBase(common.PathBase):
      """ Base implementation for SvnPath implementations. """
      sep = '/'
  
      def _geturl(self):
          return self.strpath
      url = property(_geturl, None, None, "url of this svn-path.")
  
      def __str__(self):
          """ return a string representation (including rev-number) """
          return self.strpath
  
      def __hash__(self):
          return hash(self.strpath)
  
      def new(self, **kw):
          """ create a modified version of this path. A 'rev' argument
              indicates a new revision.
              the following keyword arguments modify various path parts::
  
                http://host.com/repo/path/file.ext
                |-----------------------|          dirname
                                          |------| basename
                                          |--|     purebasename
                                              |--| ext
          """
          obj = object.__new__(self.__class__)
          obj.rev = kw.get('rev', self.rev)
          obj.auth = kw.get('auth', self.auth)
          dirname, basename, purebasename, ext = self._getbyspec(
               "dirname,basename,purebasename,ext")
          if 'basename' in kw:
              if 'purebasename' in kw or 'ext' in kw:
                  raise ValueError("invalid specification %r" % kw)
          else:
              pb = kw.setdefault('purebasename', purebasename)
              ext = kw.setdefault('ext', ext)
              if ext and not ext.startswith('.'):
                  ext = '.' + ext
              kw['basename'] = pb + ext
  
          kw.setdefault('dirname', dirname)
          kw.setdefault('sep', self.sep)
          if kw['basename']:
              obj.strpath = "%(dirname)s%(sep)s%(basename)s" % kw
          else:
              obj.strpath = "%(dirname)s" % kw
          return obj
  
      def _getbyspec(self, spec):
          """ get specified parts of the path.  'arg' is a string
              with comma separated path parts. The parts are returned
              in exactly the order of the specification.
  
              you may specify the following parts:
  
              http://host.com/repo/path/file.ext
              |-----------------------|          dirname
                                        |------| basename
                                        |--|     purebasename
                                            |--| ext
          """
          res = []
          parts = self.strpath.split(self.sep)
          for name in spec.split(','):
              name = name.strip()
              if name == 'dirname':
                  res.append(self.sep.join(parts[:-1]))
              elif name == 'basename':
                  res.append(parts[-1])
              else:
                  basename = parts[-1]
                  i = basename.rfind('.')
                  if i == -1:
                      purebasename, ext = basename, ''
                  else:
                      purebasename, ext = basename[:i], basename[i:]
                  if name == 'purebasename':
                      res.append(purebasename)
                  elif name == 'ext':
                      res.append(ext)
                  else:
                      raise NameError("Don't know part %r" % name)
          return res
  
      def __eq__(self, other):
          """ return true if path and rev attributes each match """
          return (str(self) == str(other) and
                 (self.rev == other.rev or self.rev == other.rev))
  
      def __ne__(self, other):
          return not self == other
  
      def join(self, *args):
          """ return a new Path (with the same revision) which is composed
              of the self Path followed by 'args' path components.
          """
          if not args:
              return self
  
          args = tuple([arg.strip(self.sep) for arg in args])
          parts = (self.strpath, ) + args
          newpath = self.__class__(self.sep.join(parts), self.rev, self.auth)
          return newpath
  
      def propget(self, name):
          """ return the content of the given property. """
          value = self._propget(name)
          return value
  
      def proplist(self):
          """ list all property names. """
          content = self._proplist()
          return content
  
      def size(self):
          """ Return the size of the file content of the Path. """
          return self.info().size
  
      def mtime(self):
          """ Return the last modification time of the file. """
          return self.info().mtime
  
      # shared help methods
  
      def _escape(self, cmd):
          return _escape_helper(cmd)
  
  
      #def _childmaxrev(self):
      #    """ return maximum revision number of childs (or self.rev if no childs) """
      #    rev = self.rev
      #    for name, info in self._listdir_nameinfo():
      #        rev = max(rev, info.created_rev)
      #    return rev
  
      #def _getlatestrevision(self):
      #    """ return latest repo-revision for this path. """
      #    url = self.strpath
      #    path = self.__class__(url, None)
      #
      #    # we need a long walk to find the root-repo and revision
      #    while 1:
      #        try:
      #            rev = max(rev, path._childmaxrev())
      #            previous = path
      #            path = path.dirpath()
      #        except (IOError, process.cmdexec.Error):
      #            break
      #    if rev is None:
      #        raise IOError, "could not determine newest repo revision for %s" % self
      #    return rev
  
      class Checkers(common.Checkers):
          def dir(self):
              try:
                  return self.path.info().kind == 'dir'
              except py.error.Error:
                  return self._listdirworks()
  
          def _listdirworks(self):
              try:
                  self.path.listdir()
              except py.error.ENOENT:
                  return False
              else:
                  return True
  
          def file(self):
              try:
                  return self.path.info().kind == 'file'
              except py.error.ENOENT:
                  return False
  
          def exists(self):
              try:
                  return self.path.info()
              except py.error.ENOENT:
                  return self._listdirworks()
  
  def parse_apr_time(timestr):
      i = timestr.rfind('.')
      if i == -1:
          raise ValueError("could not parse %s" % timestr)
      timestr = timestr[:i]
      parsedtime = time.strptime(timestr, "%Y-%m-%dT%H:%M:%S")
      return time.mktime(parsedtime)
  
  class PropListDict(dict):
      """ a Dictionary which fetches values (InfoSvnCommand instances) lazily"""
      def __init__(self, path, keynames):
          dict.__init__(self, [(x, None) for x in keynames])
          self.path = path
  
      def __getitem__(self, key):
          value = dict.__getitem__(self, key)
          if value is None:
              value = self.path.propget(key)
              dict.__setitem__(self, key, value)
          return value
  
  def fixlocale():
      if sys.platform != 'win32':
          return 'LC_ALL=C '
      return ''
  
  # some nasty chunk of code to solve path and url conversion and quoting issues
  ILLEGAL_CHARS = '* | \\ / : < > ? \t \n \x0b \x0c \r'.split(' ')
  if os.sep in ILLEGAL_CHARS:
      ILLEGAL_CHARS.remove(os.sep)
  ISWINDOWS = sys.platform == 'win32'
  _reg_allow_disk = re.compile(r'^([a-z]\:\\)?[^:]+$', re.I)
  def _check_path(path):
      illegal = ILLEGAL_CHARS[:]
      sp = path.strpath
      if ISWINDOWS:
          illegal.remove(':')
          if not _reg_allow_disk.match(sp):
              raise ValueError('path may not contain a colon (:)')
      for char in sp:
          if char not in string.printable or char in illegal:
              raise ValueError('illegal character %r in path' % (char,))
  
  def path_to_fspath(path, addat=True):
      _check_path(path)
      sp = path.strpath
      if addat and path.rev != -1:
          sp = '%s@%s' % (sp, path.rev)
      elif addat:
          sp = '%s@HEAD' % (sp,)
      return sp
  
  def url_from_path(path):
      fspath = path_to_fspath(path, False)
      quote = py.std.urllib.quote
      if ISWINDOWS:
          match = _reg_allow_disk.match(fspath)
          fspath = fspath.replace('\\', '/')
          if match.group(1):
              fspath = '/%s%s' % (match.group(1).replace('\\', '/'),
                                  quote(fspath[len(match.group(1)):]))
          else:
              fspath = quote(fspath)
      else:
          fspath = quote(fspath)
      if path.rev != -1:
          fspath = '%s@%s' % (fspath, path.rev)
      else:
          fspath = '%s@HEAD' % (fspath,)
      return 'file://%s' % (fspath,)
  
  class SvnAuth(object):
      """ container for auth information for Subversion """
      def __init__(self, username, password, cache_auth=True, interactive=True):
          self.username = username
          self.password = password
          self.cache_auth = cache_auth
          self.interactive = interactive
  
      def makecmdoptions(self):
          uname = self.username.replace('"', '\\"')
          passwd = self.password.replace('"', '\\"')
          ret = []
          if uname:
              ret.append('--username="%s"' % (uname,))
          if passwd:
              ret.append('--password="%s"' % (passwd,))
          if not self.cache_auth:
              ret.append('--no-auth-cache')
          if not self.interactive:
              ret.append('--non-interactive')
          return ' '.join(ret)
  
      def __str__(self):
          return "<SvnAuth username=%s ...>" %(self.username,)
  
  rex_blame = re.compile(r'\s*(\d+)\s*(\S+) (.*)')
  
  class SvnWCCommandPath(common.PathBase):
      """ path implementation offering access/modification to svn working copies.
          It has methods similar to the functions in os.path and similar to the
          commands of the svn client.
      """
      sep = os.sep
  
      def __new__(cls, wcpath=None, auth=None):
          self = object.__new__(cls)
          if isinstance(wcpath, cls):
              if wcpath.__class__ == cls:
                  return wcpath
              wcpath = wcpath.localpath
          if _check_for_bad_chars(str(wcpath),
                                            ALLOWED_CHARS):
              raise ValueError("bad char in wcpath %s" % (wcpath, ))
          self.localpath = py.path.local(wcpath)
          self.auth = auth
          return self
  
      strpath = property(lambda x: str(x.localpath), None, None, "string path")
      rev = property(lambda x: x.info(usecache=0).rev, None, None, "revision")
  
      def __eq__(self, other):
          return self.localpath == getattr(other, 'localpath', None)
  
      def _geturl(self):
          if getattr(self, '_url', None) is None:
              info = self.info()
              self._url = info.url #SvnPath(info.url, info.rev)
          assert isinstance(self._url, py.builtin._basestring)
          return self._url
  
      url = property(_geturl, None, None, "url of this WC item")
  
      def _escape(self, cmd):
          return _escape_helper(cmd)
  
      def dump(self, obj):
          """ pickle object into path location"""
          return self.localpath.dump(obj)
  
      def svnurl(self):
          """ return current SvnPath for this WC-item. """
          info = self.info()
          return py.path.svnurl(info.url)
  
      def __repr__(self):
          return "svnwc(%r)" % (self.strpath) # , self._url)
  
      def __str__(self):
          return str(self.localpath)
  
      def _makeauthoptions(self):
          if self.auth is None:
              return ''
          return self.auth.makecmdoptions()
  
      def _authsvn(self, cmd, args=None):
          args = args and list(args) or []
          args.append(self._makeauthoptions())
          return self._svn(cmd, *args)
  
      def _svn(self, cmd, *args):
          l = ['svn %s' % cmd]
          args = [self._escape(item) for item in args]
          l.extend(args)
          l.append('"%s"' % self._escape(self.strpath))
          # try fixing the locale because we can't otherwise parse
          string = fixlocale() + " ".join(l)
          try:
              try:
                  key = 'LC_MESSAGES'
                  hold = os.environ.get(key)
                  os.environ[key] = 'C'
                  out = py.process.cmdexec(string)
              finally:
                  if hold:
                      os.environ[key] = hold
                  else:
                      del os.environ[key]
          except py.process.cmdexec.Error:
              e = sys.exc_info()[1]
              strerr = e.err.lower()
              if strerr.find('not found') != -1:
                  raise py.error.ENOENT(self)
              elif strerr.find("E200009:") != -1:
                  raise py.error.ENOENT(self)
              if (strerr.find('file exists') != -1 or
                  strerr.find('file already exists') != -1 or
                  strerr.find('w150002:') != -1 or
                  strerr.find("can't create directory") != -1):
                  raise py.error.EEXIST(strerr) #self)
              raise
          return out
  
      def switch(self, url):
          """ switch to given URL. """
          self._authsvn('switch', [url])
  
      def checkout(self, url=None, rev=None):
          """ checkout from url to local wcpath. """
          args = []
          if url is None:
              url = self.url
          if rev is None or rev == -1:
              if (py.std.sys.platform != 'win32' and
                      _getsvnversion() == '1.3'):
                  url += "@HEAD"
          else:
              if _getsvnversion() == '1.3':
                  url += "@%d" % rev
              else:
                  args.append('-r' + str(rev))
          args.append(url)
          self._authsvn('co', args)
  
      def update(self, rev='HEAD', interactive=True):
          """ update working copy item to given revision. (None -> HEAD). """
          opts = ['-r', rev]
          if not interactive:
              opts.append("--non-interactive")
          self._authsvn('up', opts)
  
      def write(self, content, mode='w'):
          """ write content into local filesystem wc. """
          self.localpath.write(content, mode)
  
      def dirpath(self, *args):
          """ return the directory Path of the current Path. """
          return self.__class__(self.localpath.dirpath(*args), auth=self.auth)
  
      def _ensuredirs(self):
          parent = self.dirpath()
          if parent.check(dir=0):
              parent._ensuredirs()
          if self.check(dir=0):
              self.mkdir()
          return self
  
      def ensure(self, *args, **kwargs):
          """ ensure that an args-joined path exists (by default as
              a file). if you specify a keyword argument 'directory=True'
              then the path is forced  to be a directory path.
          """
          p = self.join(*args)
          if p.check():
              if p.check(versioned=False):
                  p.add()
              return p
          if kwargs.get('dir', 0):
              return p._ensuredirs()
          parent = p.dirpath()
          parent._ensuredirs()
          p.write("")
          p.add()
          return p
  
      def mkdir(self, *args):
          """ create & return the directory joined with args. """
          if args:
              return self.join(*args).mkdir()
          else:
              self._svn('mkdir')
              return self
  
      def add(self):
          """ add ourself to svn """
          self._svn('add')
  
      def remove(self, rec=1, force=1):
          """ remove a file or a directory tree. 'rec'ursive is
              ignored and considered always true (because of
              underlying svn semantics.
          """
          assert rec, "svn cannot remove non-recursively"
          if not self.check(versioned=True):
              # not added to svn (anymore?), just remove
              py.path.local(self).remove()
              return
          flags = []
          if force:
              flags.append('--force')
          self._svn('remove', *flags)
  
      def copy(self, target):
          """ copy path to target."""
          py.process.cmdexec("svn copy %s %s" %(str(self), str(target)))
  
      def rename(self, target):
          """ rename this path to target. """
          py.process.cmdexec("svn move --force %s %s" %(str(self), str(target)))
  
      def lock(self):
          """ set a lock (exclusive) on the resource """
          out = self._authsvn('lock').strip()
          if not out:
              # warning or error, raise exception
              raise ValueError("unknown error in svn lock command")
  
      def unlock(self):
          """ unset a previously set lock """
          out = self._authsvn('unlock').strip()
          if out.startswith('svn:'):
              # warning or error, raise exception
              raise Exception(out[4:])
  
      def cleanup(self):
          """ remove any locks from the resource """
          # XXX should be fixed properly!!!
          try:
              self.unlock()
          except:
              pass
  
      def status(self, updates=0, rec=0, externals=0):
          """ return (collective) Status object for this file. """
          # http://svnbook.red-bean.com/book.html#svn-ch-3-sect-4.3.1
          #             2201     2192        jum   test
          # XXX
          if externals:
              raise ValueError("XXX cannot perform status() "
                               "on external items yet")
          else:
              #1.2 supports: externals = '--ignore-externals'
              externals = ''
          if rec:
              rec= ''
          else:
              rec = '--non-recursive'
  
          # XXX does not work on all subversion versions
          #if not externals:
          #    externals = '--ignore-externals'
  
          if updates:
              updates = '-u'
          else:
              updates = ''
  
          try:
              cmd = 'status -v --xml --no-ignore %s %s %s' % (
                      updates, rec, externals)
              out = self._authsvn(cmd)
          except py.process.cmdexec.Error:
              cmd = 'status -v --no-ignore %s %s %s' % (
                      updates, rec, externals)
              out = self._authsvn(cmd)
              rootstatus = WCStatus(self).fromstring(out, self)
          else:
              rootstatus = XMLWCStatus(self).fromstring(out, self)
          return rootstatus
  
      def diff(self, rev=None):
          """ return a diff of the current path against revision rev (defaulting
              to the last one).
          """
          args = []
          if rev is not None:
              args.append("-r %d" % rev)
          out = self._authsvn('diff', args)
          return out
  
      def blame(self):
          """ return a list of tuples of three elements:
              (revision, commiter, line)
          """
          out = self._svn('blame')
          result = []
          blamelines = out.splitlines()
          reallines = py.path.svnurl(self.url).readlines()
          for i, (blameline, line) in enumerate(
                  zip(blamelines, reallines)):
              m = rex_blame.match(blameline)
              if not m:
                  raise ValueError("output line %r of svn blame does not match "
                                   "expected format" % (line, ))
              rev, name, _ = m.groups()
              result.append((int(rev), name, line))
          return result
  
      _rex_commit = re.compile(r'.*Committed revision (\d+)\.$', re.DOTALL)
      def commit(self, msg='', rec=1):
          """ commit with support for non-recursive commits """
          # XXX i guess escaping should be done better here?!?
          cmd = 'commit -m "%s" --force-log' % (msg.replace('"', '\\"'),)
          if not rec:
              cmd += ' -N'
          out = self._authsvn(cmd)
          try:
              del cache.info[self]
          except KeyError:
              pass
          if out:
              m = self._rex_commit.match(out)
              return int(m.group(1))
  
      def propset(self, name, value, *args):
          """ set property name to value on this path. """
          d = py.path.local.mkdtemp()
          try:
              p = d.join('value')
              p.write(value)
              self._svn('propset', name, '--file', str(p), *args)
          finally:
              d.remove()
  
      def propget(self, name):
          """ get property name on this path. """
          res = self._svn('propget', name)
          return res[:-1] # strip trailing newline
  
      def propdel(self, name):
          """ delete property name on this path. """
          res = self._svn('propdel', name)
          return res[:-1] # strip trailing newline
  
      def proplist(self, rec=0):
          """ return a mapping of property names to property values.
  If rec is True, then return a dictionary mapping sub-paths to such mappings.
  """
          if rec:
              res = self._svn('proplist -R')
              return make_recursive_propdict(self, res)
          else:
              res = self._svn('proplist')
              lines = res.split('\n')
              lines = [x.strip() for x in lines[1:]]
              return PropListDict(self, lines)
  
      def revert(self, rec=0):
          """ revert the local changes of this path. if rec is True, do so
  recursively. """
          if rec:
              result = self._svn('revert -R')
          else:
              result = self._svn('revert')
          return result
  
      def new(self, **kw):
          """ create a modified version of this path. A 'rev' argument
              indicates a new revision.
              the following keyword arguments modify various path parts:
  
                http://host.com/repo/path/file.ext
                |-----------------------|          dirname
                                          |------| basename
                                          |--|     purebasename
                                              |--| ext
          """
          if kw:
              localpath = self.localpath.new(**kw)
          else:
              localpath = self.localpath
          return self.__class__(localpath, auth=self.auth)
  
      def join(self, *args, **kwargs):
          """ return a new Path (with the same revision) which is composed
              of the self Path followed by 'args' path components.
          """
          if not args:
              return self
          localpath = self.localpath.join(*args, **kwargs)
          return self.__class__(localpath, auth=self.auth)
  
      def info(self, usecache=1):
          """ return an Info structure with svn-provided information. """
          info = usecache and cache.info.get(self)
          if not info:
              try:
                  output = self._svn('info')
              except py.process.cmdexec.Error:
                  e = sys.exc_info()[1]
                  if e.err.find('Path is not a working copy directory') != -1:
                      raise py.error.ENOENT(self, e.err)
                  elif e.err.find("is not under version control") != -1:
                      raise py.error.ENOENT(self, e.err)
                  raise
              # XXX SVN 1.3 has output on stderr instead of stdout (while it does
              # return 0!), so a bit nasty, but we assume no output is output
              # to stderr...
              if (output.strip() == '' or
                      output.lower().find('not a versioned resource') != -1):
                  raise py.error.ENOENT(self, output)
              info = InfoSvnWCCommand(output)
  
              # Can't reliably compare on Windows without access to win32api
              if py.std.sys.platform != 'win32':
                  if info.path != self.localpath:
                      raise py.error.ENOENT(self, "not a versioned resource:" +
                              " %s != %s" % (info.path, self.localpath))
              cache.info[self] = info
          return info
  
      def listdir(self, fil=None, sort=None):
          """ return a sequence of Paths.
  
          listdir will return either a tuple or a list of paths
          depending on implementation choices.
          """
          if isinstance(fil, str):
              fil = common.FNMatcher(fil)
          # XXX unify argument naming with LocalPath.listdir
          def notsvn(path):
              return path.basename != '.svn'
  
          paths = []
          for localpath in self.localpath.listdir(notsvn):
              p = self.__class__(localpath, auth=self.auth)
              if notsvn(p) and (not fil or fil(p)):
                  paths.append(p)
          self._sortlist(paths, sort)
          return paths
  
      def open(self, mode='r'):
          """ return an opened file with the given mode. """
          return open(self.strpath, mode)
  
      def _getbyspec(self, spec):
          return self.localpath._getbyspec(spec)
  
      class Checkers(py.path.local.Checkers):
          def __init__(self, path):
              self.svnwcpath = path
              self.path = path.localpath
          def versioned(self):
              try:
                  s = self.svnwcpath.info()
              except (py.error.ENOENT, py.error.EEXIST):
                  return False
              except py.process.cmdexec.Error:
                  e = sys.exc_info()[1]
                  if e.err.find('is not a working copy')!=-1:
                      return False
                  if e.err.lower().find('not a versioned resource') != -1:
                      return False
                  raise
              else:
                  return True
  
      def log(self, rev_start=None, rev_end=1, verbose=False):
          """ return a list of LogEntry instances for this path.
  rev_start is the starting revision (defaulting to the first one).
  rev_end is the last revision (defaulting to HEAD).
  if verbose is True, then the LogEntry instances also know which files changed.
  """
          assert self.check()   # make it simpler for the pipe
          rev_start = rev_start is None and "HEAD" or rev_start
          rev_end = rev_end is None and "HEAD" or rev_end
          if rev_start == "HEAD" and rev_end == 1:
                  rev_opt = ""
          else:
              rev_opt = "-r %s:%s" % (rev_start, rev_end)
          verbose_opt = verbose and "-v" or ""
          locale_env = fixlocale()
          # some blather on stderr
          auth_opt = self._makeauthoptions()
          #stdin, stdout, stderr  = os.popen3(locale_env +
          #                                   'svn log --xml %s %s %s "%s"' % (
          #                                    rev_opt, verbose_opt, auth_opt,
          #                                    self.strpath))
          cmd = locale_env + 'svn log --xml %s %s %s "%s"' % (
              rev_opt, verbose_opt, auth_opt, self.strpath)
  
          popen = subprocess.Popen(cmd,
                      stdout=subprocess.PIPE,
                      stderr=subprocess.PIPE,
                      shell=True,
          )
          stdout, stderr = popen.communicate()
          stdout = py.builtin._totext(stdout, sys.getdefaultencoding())
          minidom,ExpatError = importxml()
          try:
              tree = minidom.parseString(stdout)
          except ExpatError:
              raise ValueError('no such revision')
          result = []
          for logentry in filter(None, tree.firstChild.childNodes):
              if logentry.nodeType == logentry.ELEMENT_NODE:
                  result.append(LogEntry(logentry))
          return result
  
      def size(self):
          """ Return the size of the file content of the Path. """
          return self.info().size
  
      def mtime(self):
          """ Return the last modification time of the file. """
          return self.info().mtime
  
      def __hash__(self):
          return hash((self.strpath, self.__class__, self.auth))
  
  
  class WCStatus:
      attrnames = ('modified','added', 'conflict', 'unchanged', 'external',
                  'deleted', 'prop_modified', 'unknown', 'update_available',
                  'incomplete', 'kindmismatch', 'ignored', 'locked', 'replaced'
                  )
  
      def __init__(self, wcpath, rev=None, modrev=None, author=None):
          self.wcpath = wcpath
          self.rev = rev
          self.modrev = modrev
          self.author = author
  
          for name in self.attrnames:
              setattr(self, name, [])
  
      def allpath(self, sort=True, **kw):
          d = {}
          for name in self.attrnames:
              if name not in kw or kw[name]:
                  for path in getattr(self, name):
                      d[path] = 1
          l = d.keys()
          if sort:
              l.sort()
          return l
  
      # XXX a bit scary to assume there's always 2 spaces between username and
      # path, however with win32 allowing spaces in user names there doesn't
      # seem to be a more solid approach :(
      _rex_status = re.compile(r'\s+(\d+|-)\s+(\S+)\s+(.+?)\s{2,}(.*)')
  
      def fromstring(data, rootwcpath, rev=None, modrev=None, author=None):
          """ return a new WCStatus object from data 's'
          """
          rootstatus = WCStatus(rootwcpath, rev, modrev, author)
          update_rev = None
          for line in data.split('\n'):
              if not line.strip():
                  continue
              #print "processing %r" % line
              flags, rest = line[:8], line[8:]
              # first column
              c0,c1,c2,c3,c4,c5,x6,c7 = flags
              #if '*' in line:
              #    print "flags", repr(flags), "rest", repr(rest)
  
              if c0 in '?XI':
                  fn = line.split(None, 1)[1]
                  if c0 == '?':
                      wcpath = rootwcpath.join(fn, abs=1)
                      rootstatus.unknown.append(wcpath)
                  elif c0 == 'X':
                      wcpath = rootwcpath.__class__(
                          rootwcpath.localpath.join(fn, abs=1),
                          auth=rootwcpath.auth)
                      rootstatus.external.append(wcpath)
                  elif c0 == 'I':
                      wcpath = rootwcpath.join(fn, abs=1)
                      rootstatus.ignored.append(wcpath)
  
                  continue
  
              #elif c0 in '~!' or c4 == 'S':
              #    raise NotImplementedError("received flag %r" % c0)
  
              m = WCStatus._rex_status.match(rest)
              if not m:
                  if c7 == '*':
                      fn = rest.strip()
                      wcpath = rootwcpath.join(fn, abs=1)
                      rootstatus.update_available.append(wcpath)
                      continue
                  if line.lower().find('against revision:')!=-1:
                      update_rev = int(rest.split(':')[1].strip())
                      continue
                  if line.lower().find('status on external') > -1:
                      # XXX not sure what to do here... perhaps we want to
                      # store some state instead of just continuing, as right
                      # now it makes the top-level external get added twice
                      # (once as external, once as 'normal' unchanged item)
                      # because of the way SVN presents external items
                      continue
                  # keep trying
                  raise ValueError("could not parse line %r" % line)
              else:
                  rev, modrev, author, fn = m.groups()
              wcpath = rootwcpath.join(fn, abs=1)
              #assert wcpath.check()
              if c0 == 'M':
                  assert wcpath.check(file=1), "didn't expect a directory with changed content here"
                  rootstatus.modified.append(wcpath)
              elif c0 == 'A' or c3 == '+' :
                  rootstatus.added.append(wcpath)
              elif c0 == 'D':
                  rootstatus.deleted.append(wcpath)
              elif c0 == 'C':
                  rootstatus.conflict.append(wcpath)
              elif c0 == '~':
                  rootstatus.kindmismatch.append(wcpath)
              elif c0 == '!':
                  rootstatus.incomplete.append(wcpath)
              elif c0 == 'R':
                  rootstatus.replaced.append(wcpath)
              elif not c0.strip():
                  rootstatus.unchanged.append(wcpath)
              else:
                  raise NotImplementedError("received flag %r" % c0)
  
              if c1 == 'M':
                  rootstatus.prop_modified.append(wcpath)
              # XXX do we cover all client versions here?
              if c2 == 'L' or c5 == 'K':
                  rootstatus.locked.append(wcpath)
              if c7 == '*':
                  rootstatus.update_available.append(wcpath)
  
              if wcpath == rootwcpath:
                  rootstatus.rev = rev
                  rootstatus.modrev = modrev
                  rootstatus.author = author
                  if update_rev:
                      rootstatus.update_rev = update_rev
                  continue
          return rootstatus
      fromstring = staticmethod(fromstring)
  
  class XMLWCStatus(WCStatus):
      def fromstring(data, rootwcpath, rev=None, modrev=None, author=None):
          """ parse 'data' (XML string as outputted by svn st) into a status obj
          """
          # XXX for externals, the path is shown twice: once
          # with external information, and once with full info as if
          # the item was a normal non-external... the current way of
          # dealing with this issue is by ignoring it - this does make
          # externals appear as external items as well as 'normal',
          # unchanged ones in the status object so this is far from ideal
          rootstatus = WCStatus(rootwcpath, rev, modrev, author)
          update_rev = None
          minidom, ExpatError = importxml()
          try:
              doc = minidom.parseString(data)
          except ExpatError:
              e = sys.exc_info()[1]
              raise ValueError(str(e))
          urevels = doc.getElementsByTagName('against')
          if urevels:
              rootstatus.update_rev = urevels[-1].getAttribute('revision')
          for entryel in doc.getElementsByTagName('entry'):
              path = entryel.getAttribute('path')
              statusel = entryel.getElementsByTagName('wc-status')[0]
              itemstatus = statusel.getAttribute('item')
  
              if itemstatus == 'unversioned':
                  wcpath = rootwcpath.join(path, abs=1)
                  rootstatus.unknown.append(wcpath)
                  continue
              elif itemstatus == 'external':
                  wcpath = rootwcpath.__class__(
                      rootwcpath.localpath.join(path, abs=1),
                      auth=rootwcpath.auth)
                  rootstatus.external.append(wcpath)
                  continue
              elif itemstatus == 'ignored':
                  wcpath = rootwcpath.join(path, abs=1)
                  rootstatus.ignored.append(wcpath)
                  continue
              elif itemstatus == 'incomplete':
                  wcpath = rootwcpath.join(path, abs=1)
                  rootstatus.incomplete.append(wcpath)
                  continue
  
              rev = statusel.getAttribute('revision')
              if itemstatus == 'added' or itemstatus == 'none':
                  rev = '0'
                  modrev = '?'
                  author = '?'
                  date = ''
              elif itemstatus == "replaced":
                  pass
              else:
                  #print entryel.toxml()
                  commitel = entryel.getElementsByTagName('commit')[0]
                  if commitel:
                      modrev = commitel.getAttribute('revision')
                      author = ''
                      author_els = commitel.getElementsByTagName('author')
                      if author_els:
                          for c in author_els[0].childNodes:
                              author += c.nodeValue
                      date = ''
                      for c in commitel.getElementsByTagName('date')[0]\
                              .childNodes:
                          date += c.nodeValue
  
              wcpath = rootwcpath.join(path, abs=1)
  
              assert itemstatus != 'modified' or wcpath.check(file=1), (
                  'did\'t expect a directory with changed content here')
  
              itemattrname = {
                  'normal': 'unchanged',
                  'unversioned': 'unknown',
                  'conflicted': 'conflict',
                  'none': 'added',
              }.get(itemstatus, itemstatus)
  
              attr = getattr(rootstatus, itemattrname)
              attr.append(wcpath)
  
              propsstatus = statusel.getAttribute('props')
              if propsstatus not in ('none', 'normal'):
                  rootstatus.prop_modified.append(wcpath)
  
              if wcpath == rootwcpath:
                  rootstatus.rev = rev
                  rootstatus.modrev = modrev
                  rootstatus.author = author
                  rootstatus.date = date
  
              # handle repos-status element (remote info)
              rstatusels = entryel.getElementsByTagName('repos-status')
              if rstatusels:
                  rstatusel = rstatusels[0]
                  ritemstatus = rstatusel.getAttribute('item')
                  if ritemstatus in ('added', 'modified'):
                      rootstatus.update_available.append(wcpath)
  
              lockels = entryel.getElementsByTagName('lock')
              if len(lockels):
                  rootstatus.locked.append(wcpath)
  
          return rootstatus
      fromstring = staticmethod(fromstring)
  
  class InfoSvnWCCommand:
      def __init__(self, output):
          # Path: test
          # URL: http://codespeak.net/svn/std.path/trunk/dist/std.path/test
          # Repository UUID: fd0d7bf2-dfb6-0310-8d31-b7ecfe96aada
          # Revision: 2151
          # Node Kind: directory
          # Schedule: normal
          # Last Changed Author: hpk
          # Last Changed Rev: 2100
          # Last Changed Date: 2003-10-27 20:43:14 +0100 (Mon, 27 Oct 2003)
          # Properties Last Updated: 2003-11-03 14:47:48 +0100 (Mon, 03 Nov 2003)
  
          d = {}
          for line in output.split('\n'):
              if not line.strip():
                  continue
              key, value = line.split(':', 1)
              key = key.lower().replace(' ', '')
              value = value.strip()
              d[key] = value
          try:
              self.url = d['url']
          except KeyError:
              raise  ValueError("Not a versioned resource")
              #raise ValueError, "Not a versioned resource %r" % path
          self.kind = d['nodekind'] == 'directory' and 'dir' or d['nodekind']
          try:
              self.rev = int(d['revision'])
          except KeyError:
              self.rev = None
  
          self.path = py.path.local(d['path'])
          self.size = self.path.size()
          if 'lastchangedrev' in d:
              self.created_rev = int(d['lastchangedrev'])
          if 'lastchangedauthor' in d:
              self.last_author = d['lastchangedauthor']
          if 'lastchangeddate' in d:
              self.mtime = parse_wcinfotime(d['lastchangeddate'])
              self.time = self.mtime * 1000000
  
      def __eq__(self, other):
          return self.__dict__ == other.__dict__
  
  def parse_wcinfotime(timestr):
      """ Returns seconds since epoch, UTC. """
      # example: 2003-10-27 20:43:14 +0100 (Mon, 27 Oct 2003)
      m = re.match(r'(\d+-\d+-\d+ \d+:\d+:\d+) ([+-]\d+) .*', timestr)
      if not m:
          raise ValueError("timestring %r does not match" % timestr)
      timestr, timezone = m.groups()
      # do not handle timezone specially, return value should be UTC
      parsedtime = time.strptime(timestr, "%Y-%m-%d %H:%M:%S")
      return calendar.timegm(parsedtime)
  
  def make_recursive_propdict(wcroot,
                              output,
                              rex = re.compile("Properties on '(.*)':")):
      """ Return a dictionary of path->PropListDict mappings. """
      lines = [x for x in output.split('\n') if x]
      pdict = {}
      while lines:
          line = lines.pop(0)
          m = rex.match(line)
          if not m:
              raise ValueError("could not parse propget-line: %r" % line)
          path = m.groups()[0]
          wcpath = wcroot.join(path, abs=1)
          propnames = []
          while lines and lines[0].startswith('  '):
              propname = lines.pop(0).strip()
              propnames.append(propname)
          assert propnames, "must have found properties!"
          pdict[wcpath] = PropListDict(wcpath, propnames)
      return pdict
  
  
  def importxml(cache=[]):
      if cache:
          return cache
      from xml.dom import minidom
      from xml.parsers.expat import ExpatError
      cache.extend([minidom, ExpatError])
      return cache
  
  class LogEntry:
      def __init__(self, logentry):
          self.rev = int(logentry.getAttribute('revision'))
          for lpart in filter(None, logentry.childNodes):
              if lpart.nodeType == lpart.ELEMENT_NODE:
                  if lpart.nodeName == 'author':
                      self.author = lpart.firstChild.nodeValue
                  elif lpart.nodeName == 'msg':
                      if lpart.firstChild:
                          self.msg = lpart.firstChild.nodeValue
                      else:
                          self.msg = ''
                  elif lpart.nodeName == 'date':
                      #2003-07-29T20:05:11.598637Z
                      timestr = lpart.firstChild.nodeValue
                      self.date = parse_apr_time(timestr)
                  elif lpart.nodeName == 'paths':
                      self.strpaths = []
                      for ppart in filter(None, lpart.childNodes):
                          if ppart.nodeType == ppart.ELEMENT_NODE:
                              self.strpaths.append(PathEntry(ppart))
      def __repr__(self):
          return '<Logentry rev=%d author=%s date=%s>' % (
              self.rev, self.author, self.date)
  
  
  