eggs/py-1.4.0-py2.6.egg/py/_path/svnurl.py
changeset 69 c6bca38c1cbf
equal deleted inserted replaced
68:5ff1fc726848 69:c6bca38c1cbf
       
     1 """
       
     2 module defining a subversion path object based on the external
       
     3 command 'svn'. This modules aims to work with svn 1.3 and higher
       
     4 but might also interact well with earlier versions.
       
     5 """
       
     6 
       
     7 import os, sys, time, re
       
     8 import py
       
     9 from py import path, process
       
    10 from py._path import common
       
    11 from py._path import svnwc as svncommon
       
    12 from py._path.cacheutil import BuildcostAccessCache, AgingCache
       
    13 
       
    14 DEBUG=False
       
    15 
       
    16 class SvnCommandPath(svncommon.SvnPathBase):
       
    17     """ path implementation that offers access to (possibly remote) subversion
       
    18     repositories. """
       
    19 
       
    20     _lsrevcache = BuildcostAccessCache(maxentries=128)
       
    21     _lsnorevcache = AgingCache(maxentries=1000, maxseconds=60.0)
       
    22 
       
    23     def __new__(cls, path, rev=None, auth=None):
       
    24         self = object.__new__(cls)
       
    25         if isinstance(path, cls):
       
    26             rev = path.rev
       
    27             auth = path.auth
       
    28             path = path.strpath
       
    29         svncommon.checkbadchars(path)
       
    30         path = path.rstrip('/')
       
    31         self.strpath = path
       
    32         self.rev = rev
       
    33         self.auth = auth
       
    34         return self
       
    35 
       
    36     def __repr__(self):
       
    37         if self.rev == -1:
       
    38             return 'svnurl(%r)' % self.strpath
       
    39         else:
       
    40             return 'svnurl(%r, %r)' % (self.strpath, self.rev)
       
    41 
       
    42     def _svnwithrev(self, cmd, *args):
       
    43         """ execute an svn command, append our own url and revision """
       
    44         if self.rev is None:
       
    45             return self._svnwrite(cmd, *args)
       
    46         else:
       
    47             args = ['-r', self.rev] + list(args)
       
    48             return self._svnwrite(cmd, *args)
       
    49 
       
    50     def _svnwrite(self, cmd, *args):
       
    51         """ execute an svn command, append our own url """
       
    52         l = ['svn %s' % cmd]
       
    53         args = ['"%s"' % self._escape(item) for item in args]
       
    54         l.extend(args)
       
    55         l.append('"%s"' % self._encodedurl())
       
    56         # fixing the locale because we can't otherwise parse
       
    57         string = " ".join(l)
       
    58         if DEBUG:
       
    59             print("execing %s" % string)
       
    60         out = self._svncmdexecauth(string)
       
    61         return out
       
    62 
       
    63     def _svncmdexecauth(self, cmd):
       
    64         """ execute an svn command 'as is' """
       
    65         cmd = svncommon.fixlocale() + cmd
       
    66         if self.auth is not None:
       
    67             cmd += ' ' + self.auth.makecmdoptions()
       
    68         return self._cmdexec(cmd)
       
    69 
       
    70     def _cmdexec(self, cmd):
       
    71         try:
       
    72             out = process.cmdexec(cmd)
       
    73         except py.process.cmdexec.Error:
       
    74             e = sys.exc_info()[1]
       
    75             if (e.err.find('File Exists') != -1 or
       
    76                             e.err.find('File already exists') != -1):
       
    77                 raise py.error.EEXIST(self)
       
    78             raise
       
    79         return out
       
    80 
       
    81     def _svnpopenauth(self, cmd):
       
    82         """ execute an svn command, return a pipe for reading stdin """
       
    83         cmd = svncommon.fixlocale() + cmd
       
    84         if self.auth is not None:
       
    85             cmd += ' ' + self.auth.makecmdoptions()
       
    86         return self._popen(cmd)
       
    87 
       
    88     def _popen(self, cmd):
       
    89         return os.popen(cmd)
       
    90 
       
    91     def _encodedurl(self):
       
    92         return self._escape(self.strpath)
       
    93 
       
    94     def _norev_delentry(self, path):
       
    95         auth = self.auth and self.auth.makecmdoptions() or None
       
    96         self._lsnorevcache.delentry((str(path), auth))
       
    97 
       
    98     def open(self, mode='r'):
       
    99         """ return an opened file with the given mode. """
       
   100         if mode not in ("r", "rU",):
       
   101             raise ValueError("mode %r not supported" % (mode,))
       
   102         assert self.check(file=1) # svn cat returns an empty file otherwise
       
   103         if self.rev is None:
       
   104             return self._svnpopenauth('svn cat "%s"' % (
       
   105                                       self._escape(self.strpath), ))
       
   106         else:
       
   107             return self._svnpopenauth('svn cat -r %s "%s"' % (
       
   108                                       self.rev, self._escape(self.strpath)))
       
   109 
       
   110     def dirpath(self, *args, **kwargs):
       
   111         """ return the directory path of the current path joined
       
   112             with any given path arguments.
       
   113         """
       
   114         l = self.strpath.split(self.sep)
       
   115         if len(l) < 4:
       
   116             raise py.error.EINVAL(self, "base is not valid")
       
   117         elif len(l) == 4:
       
   118             return self.join(*args, **kwargs)
       
   119         else:
       
   120             return self.new(basename='').join(*args, **kwargs)
       
   121 
       
   122     # modifying methods (cache must be invalidated)
       
   123     def mkdir(self, *args, **kwargs):
       
   124         """ create & return the directory joined with args.
       
   125         pass a 'msg' keyword argument to set the commit message.
       
   126         """
       
   127         commit_msg = kwargs.get('msg', "mkdir by py lib invocation")
       
   128         createpath = self.join(*args)
       
   129         createpath._svnwrite('mkdir', '-m', commit_msg)
       
   130         self._norev_delentry(createpath.dirpath())
       
   131         return createpath
       
   132 
       
   133     def copy(self, target, msg='copied by py lib invocation'):
       
   134         """ copy path to target with checkin message msg."""
       
   135         if getattr(target, 'rev', None) is not None:
       
   136             raise py.error.EINVAL(target, "revisions are immutable")
       
   137         self._svncmdexecauth('svn copy -m "%s" "%s" "%s"' %(msg,
       
   138                              self._escape(self), self._escape(target)))
       
   139         self._norev_delentry(target.dirpath())
       
   140 
       
   141     def rename(self, target, msg="renamed by py lib invocation"):
       
   142         """ rename this path to target with checkin message msg. """
       
   143         if getattr(self, 'rev', None) is not None:
       
   144             raise py.error.EINVAL(self, "revisions are immutable")
       
   145         self._svncmdexecauth('svn move -m "%s" --force "%s" "%s"' %(
       
   146                              msg, self._escape(self), self._escape(target)))
       
   147         self._norev_delentry(self.dirpath())
       
   148         self._norev_delentry(self)
       
   149 
       
   150     def remove(self, rec=1, msg='removed by py lib invocation'):
       
   151         """ remove a file or directory (or a directory tree if rec=1) with
       
   152 checkin message msg."""
       
   153         if self.rev is not None:
       
   154             raise py.error.EINVAL(self, "revisions are immutable")
       
   155         self._svncmdexecauth('svn rm -m "%s" "%s"' %(msg, self._escape(self)))
       
   156         self._norev_delentry(self.dirpath())
       
   157 
       
   158     def export(self, topath):
       
   159         """ export to a local path
       
   160 
       
   161             topath should not exist prior to calling this, returns a
       
   162             py.path.local instance
       
   163         """
       
   164         topath = py.path.local(topath)
       
   165         args = ['"%s"' % (self._escape(self),),
       
   166                 '"%s"' % (self._escape(topath),)]
       
   167         if self.rev is not None:
       
   168             args = ['-r', str(self.rev)] + args
       
   169         self._svncmdexecauth('svn export %s' % (' '.join(args),))
       
   170         return topath
       
   171 
       
   172     def ensure(self, *args, **kwargs):
       
   173         """ ensure that an args-joined path exists (by default as
       
   174             a file). If you specify a keyword argument 'dir=True'
       
   175             then the path is forced to be a directory path.
       
   176         """
       
   177         if getattr(self, 'rev', None) is not None:
       
   178             raise py.error.EINVAL(self, "revisions are immutable")
       
   179         target = self.join(*args)
       
   180         dir = kwargs.get('dir', 0)
       
   181         for x in target.parts(reverse=True):
       
   182             if x.check():
       
   183                 break
       
   184         else:
       
   185             raise py.error.ENOENT(target, "has not any valid base!")
       
   186         if x == target:
       
   187             if not x.check(dir=dir):
       
   188                 raise dir and py.error.ENOTDIR(x) or py.error.EISDIR(x)
       
   189             return x
       
   190         tocreate = target.relto(x)
       
   191         basename = tocreate.split(self.sep, 1)[0]
       
   192         tempdir = py.path.local.mkdtemp()
       
   193         try:
       
   194             tempdir.ensure(tocreate, dir=dir)
       
   195             cmd = 'svn import -m "%s" "%s" "%s"' % (
       
   196                     "ensure %s" % self._escape(tocreate),
       
   197                     self._escape(tempdir.join(basename)),
       
   198                     x.join(basename)._encodedurl())
       
   199             self._svncmdexecauth(cmd)
       
   200             self._norev_delentry(x)
       
   201         finally:
       
   202             tempdir.remove()
       
   203         return target
       
   204 
       
   205     # end of modifying methods
       
   206     def _propget(self, name):
       
   207         res = self._svnwithrev('propget', name)
       
   208         return res[:-1] # strip trailing newline
       
   209 
       
   210     def _proplist(self):
       
   211         res = self._svnwithrev('proplist')
       
   212         lines = res.split('\n')
       
   213         lines = [x.strip() for x in lines[1:]]
       
   214         return svncommon.PropListDict(self, lines)
       
   215 
       
   216     def info(self):
       
   217         """ return an Info structure with svn-provided information. """
       
   218         parent = self.dirpath()
       
   219         nameinfo_seq = parent._listdir_nameinfo()
       
   220         bn = self.basename
       
   221         for name, info in nameinfo_seq:
       
   222             if name == bn:
       
   223                 return info
       
   224         raise py.error.ENOENT(self)
       
   225 
       
   226 
       
   227     def _listdir_nameinfo(self):
       
   228         """ return sequence of name-info directory entries of self """
       
   229         def builder():
       
   230             try:
       
   231                 res = self._svnwithrev('ls', '-v')
       
   232             except process.cmdexec.Error:
       
   233                 e = sys.exc_info()[1]
       
   234                 if e.err.find('non-existent in that revision') != -1:
       
   235                     raise py.error.ENOENT(self, e.err)
       
   236                 elif e.err.find('File not found') != -1:
       
   237                     raise py.error.ENOENT(self, e.err)
       
   238                 elif e.err.find('not part of a repository')!=-1:
       
   239                     raise py.error.ENOENT(self, e.err)
       
   240                 elif e.err.find('Unable to open')!=-1:
       
   241                     raise py.error.ENOENT(self, e.err)
       
   242                 elif e.err.lower().find('method not allowed')!=-1:
       
   243                     raise py.error.EACCES(self, e.err)
       
   244                 raise py.error.Error(e.err)
       
   245             lines = res.split('\n')
       
   246             nameinfo_seq = []
       
   247             for lsline in lines:
       
   248                 if lsline:
       
   249                     info = InfoSvnCommand(lsline)
       
   250                     if info._name != '.':  # svn 1.5 produces '.' dirs,
       
   251                         nameinfo_seq.append((info._name, info))
       
   252             nameinfo_seq.sort()
       
   253             return nameinfo_seq
       
   254         auth = self.auth and self.auth.makecmdoptions() or None
       
   255         if self.rev is not None:
       
   256             return self._lsrevcache.getorbuild((self.strpath, self.rev, auth),
       
   257                                                builder)
       
   258         else:
       
   259             return self._lsnorevcache.getorbuild((self.strpath, auth),
       
   260                                                  builder)
       
   261 
       
   262     def listdir(self, fil=None, sort=None):
       
   263         """ list directory contents, possibly filter by the given fil func
       
   264             and possibly sorted.
       
   265         """
       
   266         if isinstance(fil, str):
       
   267             fil = common.FNMatcher(fil)
       
   268         nameinfo_seq = self._listdir_nameinfo()
       
   269         if len(nameinfo_seq) == 1:
       
   270             name, info = nameinfo_seq[0]
       
   271             if name == self.basename and info.kind == 'file':
       
   272                 #if not self.check(dir=1):
       
   273                 raise py.error.ENOTDIR(self)
       
   274         paths = [self.join(name) for (name, info) in nameinfo_seq]
       
   275         if fil:
       
   276             paths = [x for x in paths if fil(x)]
       
   277         self._sortlist(paths, sort)
       
   278         return paths
       
   279 
       
   280 
       
   281     def log(self, rev_start=None, rev_end=1, verbose=False):
       
   282         """ return a list of LogEntry instances for this path.
       
   283 rev_start is the starting revision (defaulting to the first one).
       
   284 rev_end is the last revision (defaulting to HEAD).
       
   285 if verbose is True, then the LogEntry instances also know which files changed.
       
   286 """
       
   287         assert self.check() #make it simpler for the pipe
       
   288         rev_start = rev_start is None and "HEAD" or rev_start
       
   289         rev_end = rev_end is None and "HEAD" or rev_end
       
   290 
       
   291         if rev_start == "HEAD" and rev_end == 1:
       
   292             rev_opt = ""
       
   293         else:
       
   294             rev_opt = "-r %s:%s" % (rev_start, rev_end)
       
   295         verbose_opt = verbose and "-v" or ""
       
   296         xmlpipe =  self._svnpopenauth('svn log --xml %s %s "%s"' %
       
   297                                       (rev_opt, verbose_opt, self.strpath))
       
   298         from xml.dom import minidom
       
   299         tree = minidom.parse(xmlpipe)
       
   300         result = []
       
   301         for logentry in filter(None, tree.firstChild.childNodes):
       
   302             if logentry.nodeType == logentry.ELEMENT_NODE:
       
   303                 result.append(svncommon.LogEntry(logentry))
       
   304         return result
       
   305 
       
   306 #01234567890123456789012345678901234567890123467
       
   307 #   2256      hpk        165 Nov 24 17:55 __init__.py
       
   308 # XXX spotted by Guido, SVN 1.3.0 has different aligning, breaks the code!!!
       
   309 #   1312 johnny           1627 May 05 14:32 test_decorators.py
       
   310 #
       
   311 class InfoSvnCommand:
       
   312     # the '0?' part in the middle is an indication of whether the resource is
       
   313     # locked, see 'svn help ls'
       
   314     lspattern = re.compile(
       
   315         r'^ *(?P<rev>\d+) +(?P<author>.+?) +(0? *(?P<size>\d+))? '
       
   316             '*(?P<date>\w+ +\d{2} +[\d:]+) +(?P<file>.*)$')
       
   317     def __init__(self, line):
       
   318         # this is a typical line from 'svn ls http://...'
       
   319         #_    1127      jum        0 Jul 13 15:28 branch/
       
   320         match = self.lspattern.match(line)
       
   321         data = match.groupdict()
       
   322         self._name = data['file']
       
   323         if self._name[-1] == '/':
       
   324             self._name = self._name[:-1]
       
   325             self.kind = 'dir'
       
   326         else:
       
   327             self.kind = 'file'
       
   328         #self.has_props = l.pop(0) == 'P'
       
   329         self.created_rev = int(data['rev'])
       
   330         self.last_author = data['author']
       
   331         self.size = data['size'] and int(data['size']) or 0
       
   332         self.mtime = parse_time_with_missing_year(data['date'])
       
   333         self.time = self.mtime * 1000000
       
   334 
       
   335     def __eq__(self, other):
       
   336         return self.__dict__ == other.__dict__
       
   337 
       
   338 
       
   339 #____________________________________________________
       
   340 #
       
   341 # helper functions
       
   342 #____________________________________________________
       
   343 def parse_time_with_missing_year(timestr):
       
   344     """ analyze the time part from a single line of "svn ls -v"
       
   345     the svn output doesn't show the year makes the 'timestr'
       
   346     ambigous.
       
   347     """
       
   348     import calendar
       
   349     t_now = time.gmtime()
       
   350 
       
   351     tparts = timestr.split()
       
   352     month = time.strptime(tparts.pop(0), '%b')[1]
       
   353     day = time.strptime(tparts.pop(0), '%d')[2]
       
   354     last = tparts.pop(0) # year or hour:minute
       
   355     try:
       
   356         if ":" in last:
       
   357             raise ValueError()
       
   358         year = time.strptime(last, '%Y')[0]
       
   359         hour = minute = 0
       
   360     except ValueError:
       
   361         hour, minute = time.strptime(last, '%H:%M')[3:5]
       
   362         year = t_now[0]
       
   363 
       
   364         t_result = (year, month, day, hour, minute, 0,0,0,0)
       
   365         if t_result > t_now:
       
   366             year -= 1
       
   367     t_result = (year, month, day, hour, minute, 0,0,0,0)
       
   368     return calendar.timegm(t_result)
       
   369 
       
   370 class PathEntry:
       
   371     def __init__(self, ppart):
       
   372         self.strpath = ppart.firstChild.nodeValue.encode('UTF-8')
       
   373         self.action = ppart.getAttribute('action').encode('UTF-8')
       
   374         if self.action == 'A':
       
   375             self.copyfrom_path = ppart.getAttribute('copyfrom-path').encode('UTF-8')
       
   376             if self.copyfrom_path:
       
   377                 self.copyfrom_rev = int(ppart.getAttribute('copyfrom-rev'))
       
   378