"Fossies" - the Fresh Open Source Software Archive

Member "viewvc-1.2.1/lib/vclib/svn/svn_repos.py" (26 Mar 2020, 33405 Bytes) of package /linux/misc/viewvc-1.2.1.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Python source code syntax highlighting (style: standard) with prefixed line numbers. Alternatively you can here view or download the uninterpreted source code file. For more information about "svn_repos.py" see the Fossies "Dox" file reference documentation and the latest Fossies "Diffs" side-by-side code changes report: 1.1.28_vs_1.2.1.

    1 # -*-python-*-
    2 #
    3 # Copyright (C) 1999-2020 The ViewCVS Group. All Rights Reserved.
    4 #
    5 # By using this file, you agree to the terms and conditions set forth in
    6 # the LICENSE.html file which can be found at the top level of the ViewVC
    7 # distribution or at http://viewvc.org/license-1.html.
    8 #
    9 # For more information, visit http://viewvc.org/
   10 #
   11 # -----------------------------------------------------------------------
   12 
   13 "Version Control lib driver for locally accessible Subversion repositories"
   14 
   15 import vclib
   16 import os
   17 import os.path
   18 import cStringIO
   19 import time
   20 import tempfile
   21 import popen
   22 import re
   23 import urllib
   24 from svn import fs, repos, core, client, delta
   25 
   26 
   27 ### Require Subversion 1.3.1 or better.
   28 if (core.SVN_VER_MAJOR, core.SVN_VER_MINOR, core.SVN_VER_PATCH) < (1, 3, 1):
   29   raise Exception, "Version requirement not met (needs 1.3.1 or better)"
   30 
   31 
   32 ### Pre-1.5 Subversion doesn't have SVN_ERR_CEASE_INVOCATION
   33 try:
   34   _SVN_ERR_CEASE_INVOCATION = core.SVN_ERR_CEASE_INVOCATION
   35 except:
   36   _SVN_ERR_CEASE_INVOCATION = core.SVN_ERR_CANCELLED
   37 
   38 ### Pre-1.5 SubversionException's might not have the .msg and .apr_err members
   39 def _fix_subversion_exception(e):
   40   if not hasattr(e, 'apr_err'):
   41     e.apr_err = e[1]
   42   if not hasattr(e, 'message'):
   43     e.message = e[0]
   44 
   45 ### Pre-1.4 Subversion doesn't have svn_path_canonicalize()
   46 def _canonicalize_path(path):
   47   try:
   48     return core.svn_path_canonicalize(path)
   49   except AttributeError:
   50     return path
   51 
   52 def _allow_all(root, path, pool):
   53   """Generic authz_read_func that permits access to all paths"""
   54   return 1
   55 
   56 
   57 def _path_parts(path):
   58   return filter(None, path.split('/'))
   59 
   60 
   61 def _cleanup_path(path):
   62   """Return a cleaned-up Subversion filesystem path"""
   63   return '/'.join(_path_parts(path))
   64   
   65 
   66 def _fs_path_join(base, relative):
   67   return _cleanup_path(base + '/' + relative)
   68 
   69 
   70 def _compare_paths(path1, path2):
   71   path1_len = len (path1);
   72   path2_len = len (path2);
   73   min_len = min(path1_len, path2_len)
   74   i = 0
   75 
   76   # Are the paths exactly the same?
   77   if path1 == path2:
   78     return 0
   79   
   80   # Skip past common prefix
   81   while (i < min_len) and (path1[i] == path2[i]):
   82     i = i + 1
   83 
   84   # Children of paths are greater than their parents, but less than
   85   # greater siblings of their parents
   86   char1 = '\0'
   87   char2 = '\0'
   88   if (i < path1_len):
   89     char1 = path1[i]
   90   if (i < path2_len):
   91     char2 = path2[i]
   92     
   93   if (char1 == '/') and (i == path2_len):
   94     return 1
   95   if (char2 == '/') and (i == path1_len):
   96     return -1
   97   if (i < path1_len) and (char1 == '/'):
   98     return -1
   99   if (i < path2_len) and (char2 == '/'):
  100     return 1
  101 
  102   # Common prefix was skipped above, next character is compared to
  103   # determine order
  104   return cmp(char1, char2)
  105 
  106 
  107 def _rev2optrev(rev):
  108   assert type(rev) in (int, long)
  109   rt = core.svn_opt_revision_t()
  110   rt.kind = core.svn_opt_revision_number
  111   rt.value.number = rev
  112   return rt
  113 
  114 
  115 def _rootpath2url(rootpath, path):
  116   rootpath = os.path.abspath(rootpath)
  117   drive, rootpath = os.path.splitdrive(rootpath)
  118   if os.sep != '/':
  119     rootpath = rootpath.replace(os.sep, '/')
  120   rootpath = urllib.quote(rootpath)
  121   path = urllib.quote(path)
  122   if drive:
  123     url = 'file:///' + drive + rootpath + '/' + path
  124   else:
  125     url = 'file://' + rootpath + '/' + path
  126   return _canonicalize_path(url)
  127 
  128 
  129 # Given a dictionary REVPROPS of revision properties, pull special
  130 # ones out of them and return a 4-tuple containing the log message,
  131 # the author, the date (converted from the date string property), and
  132 # a dictionary of any/all other revprops.
  133 def _split_revprops(revprops):
  134   if not revprops:
  135     return None, None, None, {}
  136   special_props = []
  137   for prop in core.SVN_PROP_REVISION_LOG, \
  138               core.SVN_PROP_REVISION_AUTHOR, \
  139               core.SVN_PROP_REVISION_DATE:
  140     if revprops.has_key(prop):
  141       special_props.append(revprops[prop])
  142       del(revprops[prop])
  143     else:
  144       special_props.append(None)
  145   msg, author, datestr = tuple(special_props)
  146   date = _datestr_to_date(datestr)
  147   return msg, author, date, revprops  
  148 
  149 
  150 def _datestr_to_date(datestr):
  151   try:
  152     return core.svn_time_from_cstring(datestr) / 1000000
  153   except:
  154     return None
  155 
  156   
  157 class Revision(vclib.Revision):
  158   "Hold state for each revision's log entry."
  159   def __init__(self, rev, date, author, msg, size, lockinfo,
  160                filename, copy_path, copy_rev):
  161     vclib.Revision.__init__(self, rev, str(rev), date, author, None,
  162                             msg, size, lockinfo)
  163     self.filename = filename
  164     self.copy_path = copy_path
  165     self.copy_rev = copy_rev
  166 
  167 
  168 class NodeHistory:
  169   """An iterable object that returns 2-tuples of (revision, path)
  170   locations along a node's change history, ordered from youngest to
  171   oldest."""
  172   
  173   def __init__(self, fs_ptr, show_all_logs, limit=0):
  174     self.histories = []
  175     self.fs_ptr = fs_ptr
  176     self.show_all_logs = show_all_logs
  177     self.oldest_rev = None
  178     self.limit = limit
  179     
  180   def add_history(self, path, revision, pool):
  181     # If filtering, only add the path and revision to the histories
  182     # list if they were actually changed in this revision (where
  183     # change means the path itself was changed, or one of its parents
  184     # was copied).  This is useful for omitting bubble-up directory
  185     # changes.
  186     if not self.oldest_rev:
  187       self.oldest_rev = revision
  188     else:
  189       assert(revision < self.oldest_rev)
  190       
  191     if not self.show_all_logs:
  192       rev_root = fs.revision_root(self.fs_ptr, revision)
  193       changed_paths = fs.paths_changed(rev_root)
  194       paths = changed_paths.keys()
  195       if path not in paths:
  196         # Look for a copied parent
  197         test_path = path
  198         found = 0
  199         while 1:
  200           off = test_path.rfind('/')
  201           if off < 0:
  202             break
  203           test_path = test_path[0:off]
  204           if test_path in paths:
  205             copyfrom_rev, copyfrom_path = fs.copied_from(rev_root, test_path)
  206             if copyfrom_rev >= 0 and copyfrom_path:
  207               found = 1
  208               break
  209         if not found:
  210           return
  211     self.histories.append([revision, _cleanup_path(path)])
  212     if self.limit and len(self.histories) == self.limit:
  213       raise core.SubversionException("", _SVN_ERR_CEASE_INVOCATION)
  214 
  215   def __getitem__(self, idx):
  216     return self.histories[idx]
  217 
  218 def _get_last_history_rev(fsroot, path):
  219   history = fs.node_history(fsroot, path)
  220   history = fs.history_prev(history, 0)
  221   history_path, history_rev = fs.history_location(history)
  222   return history_rev
  223   
  224 def temp_checkout(svnrepos, path, rev):
  225   """Check out file revision to temporary file"""
  226   fd, temp = tempfile.mkstemp()
  227   fp = os.fdopen(fd, 'wb')
  228   try:
  229     root = svnrepos._getroot(rev)
  230     stream = fs.file_contents(root, path)
  231     try:
  232       while 1:
  233         chunk = core.svn_stream_read(stream, core.SVN_STREAM_CHUNK_SIZE)
  234         if not chunk:
  235           break
  236         fp.write(chunk)
  237     finally:
  238       core.svn_stream_close(stream)
  239   finally:
  240     fp.close()
  241   return temp
  242 
  243 class FileContentsPipe:
  244   def __init__(self, root, path):
  245     self._stream = fs.file_contents(root, path)
  246     self._eof = 0
  247 
  248   def read(self, len=None):
  249     chunk = None
  250     if not self._eof:
  251       if len is None:
  252         buffer = cStringIO.StringIO()
  253         try:
  254           while 1:
  255             hunk = core.svn_stream_read(self._stream, 8192)
  256             if not hunk:
  257               break
  258             buffer.write(hunk)
  259           chunk = buffer.getvalue()
  260         finally:
  261           buffer.close()
  262 
  263       else:
  264         chunk = core.svn_stream_read(self._stream, len)   
  265     if not chunk:
  266       self._eof = 1
  267     return chunk
  268   
  269   def readline(self):
  270     chunk = None
  271     if not self._eof:
  272       chunk, self._eof = core.svn_stream_readline(self._stream, '\n')
  273       if not self._eof:
  274         chunk = chunk + '\n'
  275     if not chunk:
  276       self._eof = 1
  277     return chunk
  278 
  279   def readlines(self):
  280     lines = []
  281     while True:
  282       line = self.readline()
  283       if not line:
  284         break
  285       lines.append(line)
  286     return lines
  287 
  288   def close(self):
  289     return core.svn_stream_close(self._stream)
  290 
  291   def eof(self):
  292     return self._eof
  293 
  294 
  295 class BlameSource:
  296   def __init__(self, local_url, rev, first_rev, include_text, config_dir):
  297     self.idx = -1
  298     self.first_rev = first_rev
  299     self.blame_data = []
  300     self.include_text = include_text
  301 
  302     ctx = client.svn_client_create_context()
  303     core.svn_config_ensure(config_dir)
  304     ctx.config = core.svn_config_get_config(config_dir)
  305     ctx.auth_baton = core.svn_auth_open([])
  306     try:
  307       ### TODO: Is this use of FIRST_REV always what we want?  Should we
  308       ### pass 1 here instead and do filtering later?
  309       client.blame2(local_url, _rev2optrev(rev), _rev2optrev(first_rev),
  310                     _rev2optrev(rev), self._blame_cb, ctx)
  311     except core.SubversionException, e:
  312       _fix_subversion_exception(e)
  313       if e.apr_err == core.SVN_ERR_CLIENT_IS_BINARY_FILE:
  314         raise vclib.NonTextualFileContents
  315       raise
  316 
  317   def _blame_cb(self, line_no, rev, author, date, text, pool):
  318     prev_rev = None
  319     if rev > self.first_rev:
  320       prev_rev = rev - 1
  321     if not self.include_text:
  322       text = None
  323     self.blame_data.append(vclib.Annotation(text, line_no + 1, rev,
  324                                             prev_rev, author, None))
  325 
  326   def __getitem__(self, idx):
  327     if idx != self.idx + 1:
  328       raise BlameSequencingError()
  329     self.idx = idx
  330     return self.blame_data[idx]
  331 
  332 
  333 class BlameSequencingError(Exception):
  334   pass
  335 
  336 
  337 class SVNChangedPath(vclib.ChangedPath):
  338   """Wrapper around vclib.ChangedPath which handles path splitting."""
  339   
  340   def __init__(self, path, rev, pathtype, base_path, base_rev,
  341                action, copied, text_changed, props_changed):
  342     path_parts = _path_parts(path or '')
  343     base_path_parts = _path_parts(base_path or '')
  344     vclib.ChangedPath.__init__(self, path_parts, rev, pathtype,
  345                                base_path_parts, base_rev, action,
  346                                copied, text_changed, props_changed)
  347 
  348   
  349 class LocalSubversionRepository(vclib.Repository):
  350   def __init__(self, name, rootpath, authorizer, utilities, config_dir):
  351     if not (os.path.isdir(rootpath) \
  352             and os.path.isfile(os.path.join(rootpath, 'format'))):
  353       raise vclib.ReposNotFound(name)
  354 
  355     # Initialize some stuff.
  356     self.rootpath = rootpath
  357     self.name = name
  358     self.auth = authorizer
  359     self.diff_cmd = utilities.diff or 'diff'
  360     self.config_dir = config_dir or None
  361 
  362     # See if this repository is even viewable, authz-wise.
  363     if not vclib.check_root_access(self):
  364       raise vclib.ReposNotFound(name)
  365 
  366   def open(self):
  367     # Open the repository and init some other variables.
  368     self.repos = repos.svn_repos_open(self.rootpath)
  369     self.fs_ptr = repos.svn_repos_fs(self.repos)
  370     self.youngest = fs.youngest_rev(self.fs_ptr)
  371     self._fsroots = {}
  372     self._revinfo_cache = {}
  373 
  374     # See if a universal read access determination can be made.
  375     if self.auth and self.auth.check_universal_access(self.name) == 1:
  376       self.auth = None
  377 
  378   def rootname(self):
  379     return self.name
  380 
  381   def rootpath(self):
  382     return self.rootpath
  383 
  384   def roottype(self):
  385     return vclib.SVN
  386 
  387   def authorizer(self):
  388     return self.auth
  389   
  390   def itemtype(self, path_parts, rev):
  391     rev = self._getrev(rev)
  392     basepath = self._getpath(path_parts)
  393     pathtype = self._gettype(basepath, rev)
  394     if pathtype is None:
  395       raise vclib.ItemNotFound(path_parts)
  396     if not vclib.check_path_access(self, path_parts, pathtype, rev):
  397       raise vclib.ItemNotFound(path_parts)
  398     return pathtype
  399 
  400   def openfile(self, path_parts, rev, options):
  401     path = self._getpath(path_parts)
  402     if self.itemtype(path_parts, rev) != vclib.FILE:  # does auth-check
  403       raise vclib.Error("Path '%s' is not a file." % path)
  404     rev = self._getrev(rev)
  405     fsroot = self._getroot(rev)
  406     revision = str(_get_last_history_rev(fsroot, path))
  407     fp = FileContentsPipe(fsroot, path)
  408     return fp, revision
  409 
  410   def listdir(self, path_parts, rev, options):
  411     path = self._getpath(path_parts)
  412     if self.itemtype(path_parts, rev) != vclib.DIR:  # does auth-check
  413       raise vclib.Error("Path '%s' is not a directory." % path)
  414     rev = self._getrev(rev)
  415     fsroot = self._getroot(rev)
  416     dirents = fs.dir_entries(fsroot, path)
  417     entries = [ ]
  418     for entry in dirents.values():
  419       if entry.kind == core.svn_node_dir:
  420         kind = vclib.DIR
  421       elif entry.kind == core.svn_node_file:
  422         kind = vclib.FILE
  423       if vclib.check_path_access(self, path_parts + [entry.name], kind, rev):
  424         entries.append(vclib.DirEntry(entry.name, kind))
  425     return entries
  426 
  427   def dirlogs(self, path_parts, rev, entries, options):
  428     path = self._getpath(path_parts)
  429     if self.itemtype(path_parts, rev) != vclib.DIR:  # does auth-check
  430       raise vclib.Error("Path '%s' is not a directory." % path)
  431     fsroot = self._getroot(self._getrev(rev))
  432     rev = self._getrev(rev)
  433     for entry in entries:
  434       entry_path_parts = path_parts + [entry.name]
  435       if not vclib.check_path_access(self, entry_path_parts, entry.kind, rev):
  436         continue
  437       path = self._getpath(entry_path_parts)
  438       entry_rev = _get_last_history_rev(fsroot, path)
  439       date, author, msg, revprops, changes = self._revinfo(entry_rev)
  440       entry.rev = str(entry_rev)
  441       entry.date = date
  442       entry.author = author
  443       entry.log = msg
  444       if entry.kind == vclib.FILE:
  445         entry.size = fs.file_length(fsroot, path)
  446       lock = fs.get_lock(self.fs_ptr, path)
  447       entry.lockinfo = lock and lock.owner or None
  448 
  449   def itemlog(self, path_parts, rev, sortby, first, limit, options):
  450     """see vclib.Repository.itemlog docstring
  451 
  452     Option values recognized by this implementation
  453 
  454       svn_show_all_dir_logs
  455         boolean, default false. if set for a directory path, will include
  456         revisions where files underneath the directory have changed
  457 
  458       svn_cross_copies
  459         boolean, default false. if set for a path created by a copy, will
  460         include revisions from before the copy
  461 
  462       svn_latest_log
  463         boolean, default false. if set will return only newest single log
  464         entry
  465     """
  466     assert sortby == vclib.SORTBY_DEFAULT or sortby == vclib.SORTBY_REV   
  467 
  468     path = self._getpath(path_parts)
  469     path_type = self.itemtype(path_parts, rev)  # does auth-check
  470     rev = self._getrev(rev)
  471     revs = []
  472     lockinfo = None
  473 
  474     # See if this path is locked.
  475     try:
  476       lock = fs.get_lock(self.fs_ptr, path)
  477       if lock:
  478         lockinfo = lock.owner
  479     except NameError:
  480       pass
  481 
  482     # If our caller only wants the latest log, we'll invoke
  483     # _log_helper for just the one revision.  Otherwise, we go off
  484     # into history-fetching mode.  ### TODO: we could stand to have a
  485     # 'limit' parameter here as numeric cut-off for the depth of our
  486     # history search.
  487     if options.get('svn_latest_log', 0):
  488       revision = self._log_helper(path, rev, lockinfo)
  489       if revision:
  490         revision.prev = None
  491         revs.append(revision)
  492     else:
  493       history = self._get_history(path, rev, path_type, first + limit, options)
  494       if len(history) < first:
  495         history = []
  496       if limit:
  497         history = history[first:first+limit]
  498 
  499       for hist_rev, hist_path in history:
  500         revision = self._log_helper(hist_path, hist_rev, lockinfo)
  501         if revision:
  502           # If we have unreadable copyfrom data, obscure it.
  503           if revision.copy_path is not None:
  504             cp_parts = _path_parts(revision.copy_path)
  505             if not vclib.check_path_access(self, cp_parts, path_type,
  506                                            revision.copy_rev):
  507               revision.copy_path = revision.copy_rev = None
  508           revision.prev = None
  509           if len(revs):
  510             revs[-1].prev = revision
  511           revs.append(revision)
  512     return revs
  513 
  514   def itemprops(self, path_parts, rev):
  515     path = self._getpath(path_parts)
  516     path_type = self.itemtype(path_parts, rev)  # does auth-check
  517     rev = self._getrev(rev)
  518     fsroot = self._getroot(rev)
  519     return fs.node_proplist(fsroot, path)
  520   
  521   def annotate(self, path_parts, rev, include_text=False):
  522     path = self._getpath(path_parts)
  523     path_type = self.itemtype(path_parts, rev)  # does auth-check
  524     if path_type != vclib.FILE:
  525       raise vclib.Error("Path '%s' is not a file." % path)
  526     rev = self._getrev(rev)
  527     fsroot = self._getroot(rev)
  528     history = self._get_history(path, rev, path_type, 0,
  529                                 {'svn_cross_copies': 1})
  530     youngest_rev, youngest_path = history[0]
  531     oldest_rev, oldest_path = history[-1]
  532     source = BlameSource(_rootpath2url(self.rootpath, path), youngest_rev,
  533                          oldest_rev, include_text, self.config_dir)
  534     return source, youngest_rev
  535 
  536   def revinfo(self, rev):
  537     return self._revinfo(rev, 1) 
  538   
  539   def rawdiff(self, path_parts1, rev1, path_parts2, rev2, type, options={}):
  540     p1 = self._getpath(path_parts1)
  541     p2 = self._getpath(path_parts2)
  542     r1 = self._getrev(rev1)
  543     r2 = self._getrev(rev2)
  544     if not vclib.check_path_access(self, path_parts1, vclib.FILE, rev1):
  545       raise vclib.ItemNotFound(path_parts1)
  546     if not vclib.check_path_access(self, path_parts2, vclib.FILE, rev2):
  547       raise vclib.ItemNotFound(path_parts2)
  548     
  549     args = vclib._diff_args(type, options)
  550 
  551     def _date_from_rev(rev):
  552       date, author, msg, revprops, changes = self._revinfo(rev)
  553       return date
  554 
  555     try:
  556       temp1 = temp_checkout(self, p1, r1)
  557       temp2 = temp_checkout(self, p2, r2)
  558       info1 = p1, _date_from_rev(r1), r1
  559       info2 = p2, _date_from_rev(r2), r2
  560       return vclib._diff_fp(temp1, temp2, info1, info2, self.diff_cmd, args)
  561     except core.SubversionException, e:
  562       _fix_subversion_exception(e)
  563       if e.apr_err == core.SVN_ERR_FS_NOT_FOUND:
  564         raise vclib.InvalidRevision
  565       raise
  566 
  567   def isexecutable(self, path_parts, rev):
  568     props = self.itemprops(path_parts, rev) # does authz-check
  569     return props.has_key(core.SVN_PROP_EXECUTABLE)
  570 
  571   def filesize(self, path_parts, rev):
  572     path = self._getpath(path_parts)
  573     if self.itemtype(path_parts, rev) != vclib.FILE:  # does auth-check
  574       raise vclib.Error("Path '%s' is not a file." % path)
  575     fsroot = self._getroot(self._getrev(rev))
  576     return fs.file_length(fsroot, path)   
  577 
  578   ##--- helpers ---##
  579 
  580   def _revinfo(self, rev, include_changed_paths=0):
  581     """Internal-use, cache-friendly revision information harvester."""
  582 
  583     def _get_changed_paths(fsroot):
  584       """Return a 3-tuple: found_readable, found_unreadable, changed_paths."""
  585       editor = repos.ChangeCollector(self.fs_ptr, fsroot)
  586       e_ptr, e_baton = delta.make_editor(editor)
  587       repos.svn_repos_replay(fsroot, e_ptr, e_baton)
  588       changedpaths = {}
  589       changes = editor.get_changes()
  590     
  591       # Copy the Subversion changes into a new hash, checking
  592       # authorization and converting them into ChangedPath objects.
  593       found_readable = found_unreadable = 0
  594       for path in changes.keys():
  595         change = changes[path]
  596         if change.path:
  597           change.path = _cleanup_path(change.path)
  598         if change.base_path:
  599           change.base_path = _cleanup_path(change.base_path)
  600         is_copy = 0
  601         if not hasattr(change, 'action'): # new to subversion 1.4.0
  602           action = vclib.MODIFIED
  603           if not change.path:
  604             action = vclib.DELETED
  605           elif change.added:
  606             action = vclib.ADDED
  607             replace_check_path = path
  608             if change.base_path and change.base_rev:
  609               replace_check_path = change.base_path
  610             if changedpaths.has_key(replace_check_path) \
  611                and changedpaths[replace_check_path].action == vclib.DELETED:
  612               action = vclib.REPLACED
  613         else:
  614           if change.action == repos.CHANGE_ACTION_ADD:
  615             action = vclib.ADDED
  616           elif change.action == repos.CHANGE_ACTION_DELETE:
  617             action = vclib.DELETED
  618           elif change.action == repos.CHANGE_ACTION_REPLACE:
  619             action = vclib.REPLACED
  620           else:
  621             action = vclib.MODIFIED
  622         if (action == vclib.ADDED or action == vclib.REPLACED) \
  623            and change.base_path \
  624            and change.base_rev:
  625           is_copy = 1
  626         if change.item_kind == core.svn_node_dir:
  627           pathtype = vclib.DIR
  628         elif change.item_kind == core.svn_node_file:
  629           pathtype = vclib.FILE
  630         else:
  631           pathtype = None
  632   
  633         parts = _path_parts(path)
  634         if vclib.check_path_access(self, parts, pathtype, rev):
  635           if is_copy and change.base_path and (change.base_path != path):
  636             parts = _path_parts(change.base_path)
  637             if not vclib.check_path_access(self, parts, pathtype,
  638                                            change.base_rev):
  639               is_copy = 0
  640               change.base_path = None
  641               change.base_rev = None
  642               found_unreadable = 1
  643           changedpaths[path] = SVNChangedPath(path, rev, pathtype,
  644                                               change.base_path,
  645                                               change.base_rev, action,
  646                                               is_copy, change.text_changed,
  647                                               change.prop_changes)
  648           found_readable = 1
  649         else:
  650           found_unreadable = 1
  651       return found_readable, found_unreadable, changedpaths.values()
  652 
  653     def _get_change_copyinfo(fsroot, path, change):
  654       # If we know the copyfrom info, return it...
  655       if hasattr(change, 'copyfrom_known') and change.copyfrom_known:
  656         copyfrom_path = change.copyfrom_path
  657         copyfrom_rev = change.copyfrom_rev
  658       # ...otherwise, if this change could be a copy (that is, it
  659       # contains an add action), query the copyfrom info ...
  660       elif (change.change_kind == fs.path_change_add or
  661             change.change_kind == fs.path_change_replace):
  662         copyfrom_rev, copyfrom_path = fs.copied_from(fsroot, path)
  663       # ...else, there's no copyfrom info.
  664       else:
  665         copyfrom_rev = core.SVN_INVALID_REVNUM
  666         copyfrom_path = None
  667       return copyfrom_path, copyfrom_rev
  668       
  669     def _simple_auth_check(fsroot):
  670       """Return a 2-tuple: found_readable, found_unreadable."""
  671       found_unreadable = found_readable = 0
  672       if hasattr(fs, 'paths_changed2'):
  673         changes = fs.paths_changed2(fsroot)
  674       else:
  675         changes = fs.paths_changed(fsroot)
  676       paths = changes.keys()
  677       for path in paths:
  678         change = changes[path]
  679         pathtype = None
  680         if hasattr(change, 'node_kind'):
  681           if change.node_kind == core.svn_node_file:
  682             pathtype = vclib.FILE
  683           elif change.node_kind == core.svn_node_dir:
  684             pathtype = vclib.DIR
  685         parts = _path_parts(path)
  686         if pathtype is None:
  687           # Figure out the pathtype so we can query the authz subsystem.
  688           if change.change_kind == fs.path_change_delete:
  689             # Deletions are annoying, because they might be underneath
  690             # copies (make their previous location non-trivial).
  691             prev_parts = parts
  692             prev_rev = rev - 1
  693             parent_parts = parts[:-1]
  694             while parent_parts:
  695               parent_path = '/' + self._getpath(parent_parts)
  696               parent_change = changes.get(parent_path)
  697               if not (parent_change and \
  698                       (parent_change.change_kind == fs.path_change_add or
  699                        parent_change.change_kind == fs.path_change_replace)):
  700                 del(parent_parts[-1])
  701                 continue
  702               copyfrom_path, copyfrom_rev = \
  703                 _get_change_copyinfo(fsroot, parent_path, parent_change)
  704               if copyfrom_path:
  705                 prev_rev = copyfrom_rev
  706                 prev_parts = _path_parts(copyfrom_path) + \
  707                              parts[len(parent_parts):]
  708                 break
  709               del(parent_parts[-1])
  710             pathtype = self._gettype(self._getpath(prev_parts), prev_rev)
  711           else:
  712             pathtype = self._gettype(self._getpath(parts), rev)
  713         if vclib.check_path_access(self, parts, pathtype, rev):
  714           found_readable = 1
  715           copyfrom_path, copyfrom_rev = \
  716             _get_change_copyinfo(fsroot, path, change)
  717           if copyfrom_path and copyfrom_path != path:
  718             parts = _path_parts(copyfrom_path)
  719             if not vclib.check_path_access(self, parts, pathtype,
  720                                            copyfrom_rev):
  721               found_unreadable = 1
  722         else:
  723           found_unreadable = 1
  724         if found_readable and found_unreadable:
  725           break
  726       return found_readable, found_unreadable
  727       
  728     def _revinfo_helper(rev, include_changed_paths):
  729       # Get the revision property info.  (Would use
  730       # editor.get_root_props(), but something is broken there...)
  731       revprops = fs.revision_proplist(self.fs_ptr, rev)
  732       msg, author, date, revprops = _split_revprops(revprops)
  733   
  734       # Optimization: If our caller doesn't care about the changed
  735       # paths, and we don't need them to do authz determinations, let's
  736       # get outta here.
  737       if self.auth is None and not include_changed_paths:
  738         return date, author, msg, revprops, None
  739   
  740       # If we get here, then we either need the changed paths because we
  741       # were asked for them, or we need them to do authorization checks.
  742       #
  743       # If we only need them for authorization checks, though, we
  744       # won't bother generating fully populated ChangedPath items (the
  745       # cost is too great).
  746       fsroot = self._getroot(rev)
  747       if include_changed_paths:
  748         found_readable, found_unreadable, changedpaths = \
  749           _get_changed_paths(fsroot)
  750       else:
  751         changedpaths = None
  752         found_readable, found_unreadable = _simple_auth_check(fsroot)
  753         
  754       # Filter our metadata where necessary, and return the requested data.
  755       if found_unreadable:
  756         msg = None
  757         if not found_readable:
  758           author = None
  759           date = None
  760       return date, author, msg, revprops, changedpaths
  761 
  762     # Consult the revinfo cache first.  If we don't have cached info,
  763     # or our caller wants changed paths and we don't have those for
  764     # this revision, go do the real work.
  765     rev = self._getrev(rev)
  766     cached_info = self._revinfo_cache.get(rev)
  767     if not cached_info \
  768        or (include_changed_paths and cached_info[4] is None):
  769       cached_info = _revinfo_helper(rev, include_changed_paths)
  770       self._revinfo_cache[rev] = cached_info
  771     return tuple(cached_info)
  772   
  773   def _log_helper(self, path, rev, lockinfo):
  774     rev_root = fs.revision_root(self.fs_ptr, rev)
  775     copyfrom_rev, copyfrom_path = fs.copied_from(rev_root, path)
  776     date, author, msg, revprops, changes = self._revinfo(rev)
  777     if fs.is_file(rev_root, path):
  778       size = fs.file_length(rev_root, path)
  779     else:
  780       size = None
  781     return Revision(rev, date, author, msg, size, lockinfo, path,
  782                     copyfrom_path and _cleanup_path(copyfrom_path),
  783                     copyfrom_rev)
  784 
  785   def _get_history(self, path, rev, path_type, limit=0, options={}):
  786     if self.youngest == 0:
  787       return []
  788 
  789     rev_paths = []
  790     fsroot = self._getroot(rev)
  791     show_all_logs = options.get('svn_show_all_dir_logs', 0)
  792     if not show_all_logs:
  793       # See if the path is a file or directory.
  794       kind = fs.check_path(fsroot, path)
  795       if kind is core.svn_node_file:
  796         show_all_logs = 1
  797       
  798     # Instantiate a NodeHistory collector object, and use it to collect
  799     # history items for PATH@REV.
  800     history = NodeHistory(self.fs_ptr, show_all_logs, limit)
  801     try:
  802       repos.svn_repos_history(self.fs_ptr, path, history.add_history,
  803                               1, rev, options.get('svn_cross_copies', 0))
  804     except core.SubversionException, e:
  805       _fix_subversion_exception(e)
  806       if e.apr_err != _SVN_ERR_CEASE_INVOCATION:
  807         raise
  808 
  809     # Now, iterate over those history items, checking for changes of
  810     # location, pruning as necessitated by authz rules.
  811     for hist_rev, hist_path in history:
  812       path_parts = _path_parts(hist_path)
  813       if not vclib.check_path_access(self, path_parts, path_type, hist_rev):
  814         break
  815       rev_paths.append([hist_rev, hist_path])
  816     return rev_paths
  817   
  818   def _getpath(self, path_parts):
  819     return '/'.join(path_parts)
  820 
  821   def _getrev(self, rev):
  822     if rev is None or rev == 'HEAD':
  823       return self.youngest
  824     try:
  825       if type(rev) == type(''):
  826         while rev[0] == 'r':
  827           rev = rev[1:]
  828       rev = int(rev)
  829     except:
  830       raise vclib.InvalidRevision(rev)
  831     if (rev < 0) or (rev > self.youngest):
  832       raise vclib.InvalidRevision(rev)
  833     return rev
  834 
  835   def _getroot(self, rev):
  836     try:
  837       return self._fsroots[rev]
  838     except KeyError:
  839       r = self._fsroots[rev] = fs.revision_root(self.fs_ptr, rev)
  840       return r
  841 
  842   def _gettype(self, path, rev):
  843     # Similar to itemtype(), but without the authz check.  Returns
  844     # None for missing paths.
  845     try:
  846       kind = fs.check_path(self._getroot(rev), path)
  847     except:
  848       return None
  849     if kind == core.svn_node_dir:
  850       return vclib.DIR
  851     if kind == core.svn_node_file:
  852       return vclib.FILE
  853     return None
  854   
  855   ##--- custom ---##
  856 
  857   def get_youngest_revision(self):
  858     return self.youngest
  859 
  860   def get_location(self, path, rev, old_rev):
  861     try:
  862       results = repos.svn_repos_trace_node_locations(self.fs_ptr, path,
  863                                                      rev, [old_rev], _allow_all)
  864     except core.SubversionException, e:
  865       _fix_subversion_exception(e)
  866       if e.apr_err == core.SVN_ERR_FS_NOT_FOUND:
  867         raise vclib.ItemNotFound(path)
  868       raise
  869     try:
  870       old_path = results[old_rev]
  871     except KeyError:
  872       raise vclib.ItemNotFound(path)
  873   
  874     return _cleanup_path(old_path)
  875   
  876   def created_rev(self, full_name, rev):
  877     return fs.node_created_rev(self._getroot(rev), full_name)
  878   
  879   def last_rev(self, path, peg_revision, limit_revision=None):
  880     """Given PATH, known to exist in PEG_REVISION, find the youngest
  881     revision older than, or equal to, LIMIT_REVISION in which path
  882     exists.  Return that revision, and the path at which PATH exists in
  883     that revision."""
  884     
  885     # Here's the plan, man.  In the trivial case (where PEG_REVISION is
  886     # the same as LIMIT_REVISION), this is a no-brainer.  If
  887     # LIMIT_REVISION is older than PEG_REVISION, we can use Subversion's
  888     # history tracing code to find the right location.  If, however,
  889     # LIMIT_REVISION is younger than PEG_REVISION, we suffer from
  890     # Subversion's lack of forward history searching.  Our workaround,
  891     # ugly as it may be, involves a binary search through the revisions
  892     # between PEG_REVISION and LIMIT_REVISION to find our last live
  893     # revision.
  894     peg_revision = self._getrev(peg_revision)
  895     limit_revision = self._getrev(limit_revision)
  896     try:
  897       if peg_revision == limit_revision:
  898         return peg_revision, path
  899       elif peg_revision > limit_revision:
  900         fsroot = self._getroot(peg_revision)
  901         history = fs.node_history(fsroot, path)
  902         while history:
  903           path, peg_revision = fs.history_location(history)
  904           if peg_revision <= limit_revision:
  905             return max(peg_revision, limit_revision), _cleanup_path(path)
  906           history = fs.history_prev(history, 1)
  907         return peg_revision, _cleanup_path(path)
  908       else:
  909         orig_id = fs.node_id(self._getroot(peg_revision), path)
  910         while peg_revision != limit_revision:
  911           mid = (peg_revision + 1 + limit_revision) / 2
  912           try:
  913             mid_id = fs.node_id(self._getroot(mid), path)
  914           except core.SubversionException, e:
  915             _fix_subversion_exception(e)
  916             if e.apr_err == core.SVN_ERR_FS_NOT_FOUND:
  917               cmp = -1
  918             else:
  919               raise
  920           else:
  921             ### Not quite right.  Need a comparison function that only returns
  922             ### true when the two nodes are the same copy, not just related.
  923             cmp = fs.compare_ids(orig_id, mid_id)
  924   
  925           if cmp in (0, 1):
  926             peg_revision = mid
  927           else:
  928             limit_revision = mid - 1
  929   
  930         return peg_revision, path
  931     finally:
  932       pass
  933 
  934   def get_symlink_target(self, path_parts, rev):
  935     """Return the target of the symbolic link versioned at PATH_PARTS
  936     in REV, or None if that object is not a symlink."""
  937 
  938     path = self._getpath(path_parts)
  939     rev = self._getrev(rev)
  940     path_type = self.itemtype(path_parts, rev)  # does auth-check
  941     fsroot = self._getroot(rev)
  942 
  943     # Symlinks must be files with the svn:special property set on them
  944     # and with file contents which read "link SOME_PATH".
  945     if path_type != vclib.FILE:
  946       return None
  947     props = fs.node_proplist(fsroot, path)
  948     if not props.has_key(core.SVN_PROP_SPECIAL):
  949       return None
  950     pathspec = ''
  951     ### FIXME: We're being a touch sloppy here, only checking the first line
  952     ### of the file.
  953     stream = fs.file_contents(fsroot, path)
  954     try:
  955       pathspec, eof = core.svn_stream_readline(stream, '\n')
  956     finally:
  957       core.svn_stream_close(stream)
  958     if pathspec[:5] != 'link ':
  959       return None
  960     return pathspec[5:]
  961