"Fossies" - the Fresh Open Source Software Archive

Member "viewvc-1.2.1/lib/vclib/svn/svn_ra.py" (26 Mar 2020, 28906 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_ra.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 remotely accessible Subversion repositories."
   14 
   15 import vclib
   16 import sys
   17 import os
   18 import re
   19 import tempfile
   20 import time
   21 import urllib
   22 from svn_repos import Revision, SVNChangedPath, _datestr_to_date, \
   23                       _compare_paths, _path_parts, _cleanup_path, \
   24                       _rev2optrev, _fix_subversion_exception, \
   25                       _split_revprops, _canonicalize_path
   26 from svn import core, delta, client, wc, ra
   27 
   28 
   29 ### Require Subversion 1.3.1 or better. (for svn_ra_get_locations support)
   30 if (core.SVN_VER_MAJOR, core.SVN_VER_MINOR, core.SVN_VER_PATCH) < (1, 3, 1):
   31   raise Exception, "Version requirement not met (needs 1.3.1 or better)"
   32 
   33 
   34 ### BEGIN COMPATABILITY CODE ###
   35 
   36 try:
   37   SVN_INVALID_REVNUM = core.SVN_INVALID_REVNUM
   38 except AttributeError: # The 1.4.x bindings are missing core.SVN_INVALID_REVNUM
   39   SVN_INVALID_REVNUM = -1
   40 
   41 def list_directory(url, peg_rev, rev, flag, ctx):
   42   try:
   43     dirents, locks = client.svn_client_ls3(url, peg_rev, rev, flag, ctx)
   44   except TypeError: # 1.4.x bindings are goofed
   45     dirents = client.svn_client_ls3(None, url, peg_rev, rev, flag, ctx)
   46     locks = {}
   47   return dirents, locks  
   48 
   49 def get_directory_props(ra_session, path, rev):
   50   try:
   51     dirents, fetched_rev, props = ra.svn_ra_get_dir(ra_session, path, rev)
   52   except ValueError: # older bindings are goofed
   53     props = ra.svn_ra_get_dir(ra_session, path, rev)
   54   return props
   55 
   56 def client_log(url, start_rev, end_rev, log_limit, include_changes,
   57                cross_copies, cb_func, ctx):
   58   include_changes = include_changes and 1 or 0
   59   cross_copies = cross_copies and 1 or 0
   60   try:
   61     client.svn_client_log4([url], start_rev, start_rev, end_rev,
   62                            log_limit, include_changes, not cross_copies,
   63                            0, None, cb_func, ctx)
   64   except AttributeError:
   65     # Wrap old svn_log_message_receiver_t interface with a
   66     # svn_log_entry_t one.
   67     def cb_convert(paths, revision, author, date, message, pool):
   68       class svn_log_entry_t:
   69         pass
   70       log_entry = svn_log_entry_t()
   71       log_entry.changed_paths = paths
   72       log_entry.revision = revision
   73       log_entry.revprops = { core.SVN_PROP_REVISION_LOG : message,
   74                              core.SVN_PROP_REVISION_AUTHOR : author,
   75                              core.SVN_PROP_REVISION_DATE : date,
   76                              }
   77       cb_func(log_entry, pool)
   78     client.svn_client_log2([url], start_rev, end_rev, log_limit,
   79                            include_changes, not cross_copies, cb_convert, ctx)
   80 
   81 
   82 def setup_client_ctx(config_dir):
   83   # Ensure that the configuration directory exists.
   84   core.svn_config_ensure(config_dir)
   85 
   86   # Fetch the configuration (and 'config' bit thereof).
   87   cfg = core.svn_config_get_config(config_dir)
   88   config = cfg.get(core.SVN_CONFIG_CATEGORY_CONFIG)
   89 
   90   # Here's the compat-sensitive part: try to use
   91   # svn_cmdline_create_auth_baton(), and fall back to making our own
   92   # if that fails.
   93   try:
   94     auth_baton = core.svn_cmdline_create_auth_baton(1, None, None, config_dir,
   95                                                     1, 1, config, None)
   96   except AttributeError:
   97     auth_baton = core.svn_auth_open([
   98       client.svn_client_get_simple_provider(),
   99       client.svn_client_get_username_provider(),
  100       client.svn_client_get_ssl_server_trust_file_provider(),
  101       client.svn_client_get_ssl_client_cert_file_provider(),
  102       client.svn_client_get_ssl_client_cert_pw_file_provider(),
  103       ])
  104     if config_dir is not None:
  105       core.svn_auth_set_parameter(auth_baton,
  106                                   core.SVN_AUTH_PARAM_CONFIG_DIR,
  107                                   config_dir)
  108 
  109   # Create, setup, and return the client context baton.
  110   ctx = client.svn_client_create_context()
  111   ctx.config = cfg
  112   ctx.auth_baton = auth_baton
  113   return ctx
  114 
  115 ### END COMPATABILITY CODE ###
  116 
  117 
  118 class LogCollector:
  119   
  120   def __init__(self, path, show_all_logs, lockinfo, access_check_func):
  121     # This class uses leading slashes for paths internally
  122     if not path:
  123       self.path = '/'
  124     else:
  125       self.path = path[0] == '/' and path or '/' + path
  126     self.logs = []
  127     self.show_all_logs = show_all_logs
  128     self.lockinfo = lockinfo
  129     self.access_check_func = access_check_func
  130     self.done = False
  131     
  132   def add_log(self, log_entry, pool):
  133     if self.done:
  134       return
  135     paths = log_entry.changed_paths
  136     revision = log_entry.revision
  137     msg, author, date, revprops = _split_revprops(log_entry.revprops)
  138     
  139     # Changed paths have leading slashes
  140     changed_paths = paths.keys()
  141     changed_paths.sort(lambda a, b: _compare_paths(a, b))
  142     this_path = None
  143     if self.path in changed_paths:
  144       this_path = self.path
  145       change = paths[self.path]
  146       if change.copyfrom_path:
  147         this_path = change.copyfrom_path
  148     for changed_path in changed_paths:
  149       if changed_path != self.path:
  150         # If a parent of our path was copied, our "next previous"
  151         # (huh?) path will exist elsewhere (under the copy source).
  152         if (self.path.rfind(changed_path) == 0) and \
  153                self.path[len(changed_path)] == '/':
  154           change = paths[changed_path]
  155           if change.copyfrom_path:
  156             this_path = change.copyfrom_path + self.path[len(changed_path):]
  157     if self.show_all_logs or this_path:
  158       if self.access_check_func is None \
  159          or self.access_check_func(self.path[1:], revision):
  160         entry = Revision(revision, date, author, msg, None, self.lockinfo,
  161                          self.path[1:], None, None)
  162         self.logs.append(entry)
  163       else:
  164         self.done = True
  165     if this_path:
  166       self.path = this_path
  167     
  168 def cat_to_tempfile(svnrepos, path, rev):
  169   """Check out file revision to temporary file"""
  170   fd, temp = tempfile.mkstemp()
  171   fp = os.fdopen(fd, 'wb')
  172   url = svnrepos._geturl(path)
  173   client.svn_client_cat(fp, url, _rev2optrev(rev),
  174                         svnrepos.ctx)
  175   fp.close()
  176   return temp
  177 
  178 class SelfCleanFP:
  179   def __init__(self, path):
  180     self._fp = open(path, 'r')
  181     self._path = path
  182     self._eof = 0
  183     
  184   def read(self, len=None):
  185     if len:
  186       chunk = self._fp.read(len)
  187     else:
  188       chunk = self._fp.read()
  189     if chunk == '':
  190       self._eof = 1
  191     return chunk
  192   
  193   def readline(self):
  194     chunk = self._fp.readline()
  195     if chunk == '':
  196       self._eof = 1
  197     return chunk
  198 
  199   def readlines(self):
  200     lines = self._fp.readlines()
  201     self._eof = 1
  202     return lines
  203     
  204   def close(self):
  205     self._fp.close()
  206     os.remove(self._path)
  207 
  208   def __del__(self):
  209     self.close()
  210     
  211   def eof(self):
  212     return self._eof
  213 
  214 
  215 class RemoteSubversionRepository(vclib.Repository):
  216   def __init__(self, name, rootpath, authorizer, utilities, config_dir):
  217     self.name = name
  218     self.rootpath = rootpath
  219     self.auth = authorizer
  220     self.diff_cmd = utilities.diff or 'diff'
  221     self.config_dir = config_dir or None
  222 
  223     # See if this repository is even viewable, authz-wise.
  224     if not vclib.check_root_access(self):
  225       raise vclib.ReposNotFound(name)
  226 
  227   def open(self):
  228     # Setup the client context baton, complete with non-prompting authstuffs.
  229     self.ctx = setup_client_ctx(self.config_dir)
  230     
  231     ra_callbacks = ra.svn_ra_callbacks_t()
  232     ra_callbacks.auth_baton = self.ctx.auth_baton
  233     self.ra_session = ra.svn_ra_open(self.rootpath, ra_callbacks, None,
  234                                      self.ctx.config)
  235     self.youngest = ra.svn_ra_get_latest_revnum(self.ra_session)
  236     self._dirent_cache = { }
  237     self._revinfo_cache = { }
  238 
  239     # See if a universal read access determination can be made.
  240     if self.auth and self.auth.check_universal_access(self.name) == 1:
  241       self.auth = None
  242     
  243   def rootname(self):
  244     return self.name
  245 
  246   def rootpath(self):
  247     return self.rootpath
  248 
  249   def roottype(self):
  250     return vclib.SVN
  251 
  252   def authorizer(self):
  253     return self.auth
  254   
  255   def itemtype(self, path_parts, rev):
  256     pathtype = None
  257     if not len(path_parts):
  258       pathtype = vclib.DIR
  259     else:
  260       path = self._getpath(path_parts)
  261       rev = self._getrev(rev)
  262       try:
  263         kind = ra.svn_ra_check_path(self.ra_session, path, rev)
  264         if kind == core.svn_node_file:
  265           pathtype = vclib.FILE
  266         elif kind == core.svn_node_dir:
  267           pathtype = vclib.DIR
  268       except:
  269         pass
  270     if pathtype is None:
  271       raise vclib.ItemNotFound(path_parts)
  272     if not vclib.check_path_access(self, path_parts, pathtype, rev):
  273       raise vclib.ItemNotFound(path_parts)
  274     return pathtype
  275 
  276   def openfile(self, path_parts, rev, options):
  277     path = self._getpath(path_parts)
  278     if self.itemtype(path_parts, rev) != vclib.FILE:  # does auth-check
  279       raise vclib.Error("Path '%s' is not a file." % path)
  280     rev = self._getrev(rev)
  281     url = self._geturl(path)
  282     ### rev here should be the last history revision of the URL
  283     fp = SelfCleanFP(cat_to_tempfile(self, path, rev))
  284     lh_rev, c_rev = self._get_last_history_rev(path_parts, rev)
  285     return fp, lh_rev
  286 
  287   def listdir(self, path_parts, rev, options):
  288     path = self._getpath(path_parts)
  289     if self.itemtype(path_parts, rev) != vclib.DIR:  # does auth-check
  290       raise vclib.Error("Path '%s' is not a directory." % path)
  291     rev = self._getrev(rev)
  292     entries = []
  293     dirents, locks = self._get_dirents(path, rev)
  294     for name in dirents.keys():
  295       entry = dirents[name]
  296       if entry.kind == core.svn_node_dir:
  297         kind = vclib.DIR
  298       elif entry.kind == core.svn_node_file:
  299         kind = vclib.FILE
  300       else:
  301         kind = None
  302       entries.append(vclib.DirEntry(name, kind))
  303     return entries
  304 
  305   def dirlogs(self, path_parts, rev, entries, options):
  306     path = self._getpath(path_parts)
  307     if self.itemtype(path_parts, rev) != vclib.DIR:  # does auth-check
  308       raise vclib.Error("Path '%s' is not a directory." % path)
  309     rev = self._getrev(rev)
  310     dirents, locks = self._get_dirents(path, rev)
  311     for entry in entries:
  312       entry_path_parts = path_parts + [entry.name]
  313       dirent = dirents.get(entry.name, None)
  314       # dirents is authz-sanitized, so ensure the entry is found therein.
  315       if dirent is None:
  316         continue
  317       # Get authz-sanitized revision metadata.
  318       entry.date, entry.author, entry.log, revprops, changes = \
  319                   self._revinfo(dirent.created_rev)
  320       entry.rev = str(dirent.created_rev)
  321       entry.size = dirent.size
  322       entry.lockinfo = None
  323       if locks.has_key(entry.name):
  324         entry.lockinfo = locks[entry.name].owner
  325 
  326   def itemlog(self, path_parts, rev, sortby, first, limit, options):
  327     assert sortby == vclib.SORTBY_DEFAULT or sortby == vclib.SORTBY_REV   
  328     path_type = self.itemtype(path_parts, rev) # does auth-check
  329     path = self._getpath(path_parts)
  330     rev = self._getrev(rev)
  331     url = self._geturl(path)
  332 
  333     # If this is a file, fetch the lock status and size (as of REV)
  334     # for this item.
  335     lockinfo = size_in_rev = None
  336     if path_type == vclib.FILE:
  337       basename = path_parts[-1]
  338       list_url = self._geturl(self._getpath(path_parts[:-1]))
  339       dirents, locks = list_directory(list_url, _rev2optrev(rev),
  340                                       _rev2optrev(rev), 0, self.ctx)
  341       if locks.has_key(basename):
  342         lockinfo = locks[basename].owner
  343       if dirents.has_key(basename):
  344         size_in_rev = dirents[basename].size
  345     
  346     # Special handling for the 'svn_latest_log' scenario.
  347     ### FIXME: Don't like this hack.  We should just introduce
  348     ### something more direct in the vclib API.
  349     if options.get('svn_latest_log', 0):
  350       dir_lh_rev, dir_c_rev = self._get_last_history_rev(path_parts, rev)
  351       date, author, log, revprops, changes = self._revinfo(dir_lh_rev)
  352       return [vclib.Revision(dir_lh_rev, str(dir_lh_rev), date, author,
  353                              None, log, size_in_rev, lockinfo)]
  354 
  355     def _access_checker(check_path, check_rev):
  356       return vclib.check_path_access(self, _path_parts(check_path),
  357                                      path_type, check_rev)
  358       
  359     # It's okay if we're told to not show all logs on a file -- all
  360     # the revisions should match correctly anyway.
  361     lc = LogCollector(path, options.get('svn_show_all_dir_logs', 0),
  362                       lockinfo, _access_checker)
  363 
  364     cross_copies = options.get('svn_cross_copies', 0)
  365     log_limit = 0
  366     if limit:
  367       log_limit = first + limit
  368     client_log(url, _rev2optrev(rev), _rev2optrev(1), log_limit, 1,
  369                cross_copies, lc.add_log, self.ctx)
  370     revs = lc.logs
  371     revs.sort()
  372     prev = None
  373     for rev in revs:
  374       # Swap out revision info with stuff from the cache (which is
  375       # authz-sanitized).
  376       rev.date, rev.author, rev.log, revprops, changes \
  377                 = self._revinfo(rev.number)
  378       rev.prev = prev
  379       prev = rev
  380     revs.reverse()
  381 
  382     if len(revs) < first:
  383       return []
  384     if limit:
  385       return revs[first:first+limit]
  386     return revs
  387 
  388   def itemprops(self, path_parts, rev):
  389     path = self._getpath(path_parts)
  390     path_type = self.itemtype(path_parts, rev) # does auth-check
  391     rev = self._getrev(rev)
  392     url = self._geturl(path)
  393     pairs = client.svn_client_proplist2(url, _rev2optrev(rev),
  394                                         _rev2optrev(rev), 0, self.ctx)
  395     return pairs and pairs[0][1] or {}
  396   
  397   def annotate(self, path_parts, rev, include_text=False):
  398     path = self._getpath(path_parts)
  399     if self.itemtype(path_parts, rev) != vclib.FILE:  # does auth-check
  400       raise vclib.Error("Path '%s' is not a file." % path)
  401     rev = self._getrev(rev)
  402     url = self._geturl(path)
  403 
  404     # Examine logs for the file to determine the oldest revision we are
  405     # permitted to see.
  406     log_options = {
  407       'svn_cross_copies' : 1,
  408       'svn_show_all_dir_logs' : 1,
  409       }
  410     revs = self.itemlog(path_parts, rev, vclib.SORTBY_REV, 0, 0, log_options)
  411     oldest_rev = revs[-1].number
  412 
  413     # Now calculate the annotation data.  Note that we'll not
  414     # inherently trust the provided author and date, because authz
  415     # rules might necessitate that we strip that information out.
  416     blame_data = []
  417 
  418     def _blame_cb(line_no, revision, author, date,
  419                   line, pool, blame_data=blame_data):
  420       prev_rev = None
  421       if revision > 1:
  422         prev_rev = revision - 1
  423 
  424       # If we have an invalid revision, clear the date and author
  425       # values.  Otherwise, if we have authz filtering to do, use the
  426       # revinfo cache to do so.
  427       if revision < 0:
  428         date = author = None
  429       elif self.auth:
  430         date, author, msg, revprops, changes = self._revinfo(revision)
  431 
  432       # Strip text if the caller doesn't want it.
  433       if not include_text:
  434         line = None
  435       blame_data.append(vclib.Annotation(line, line_no + 1, revision, prev_rev,
  436                                          author, date))
  437       
  438     client.blame2(url, _rev2optrev(rev), _rev2optrev(oldest_rev),
  439                   _rev2optrev(rev), _blame_cb, self.ctx)
  440     return blame_data, rev
  441 
  442   def revinfo(self, rev):
  443     return self._revinfo(rev, 1)
  444     
  445   def rawdiff(self, path_parts1, rev1, path_parts2, rev2, type, options={}):
  446     p1 = self._getpath(path_parts1)
  447     p2 = self._getpath(path_parts2)
  448     r1 = self._getrev(rev1)
  449     r2 = self._getrev(rev2)
  450     if not vclib.check_path_access(self, path_parts1, vclib.FILE, rev1):
  451       raise vclib.ItemNotFound(path_parts1)
  452     if not vclib.check_path_access(self, path_parts2, vclib.FILE, rev2):
  453       raise vclib.ItemNotFound(path_parts2)
  454 
  455     args = vclib._diff_args(type, options)
  456 
  457     def _date_from_rev(rev):
  458       date, author, msg, revprops, changes = self._revinfo(rev)
  459       return date
  460     
  461     try:
  462       temp1 = cat_to_tempfile(self, p1, r1)
  463       temp2 = cat_to_tempfile(self, p2, r2)
  464       info1 = p1, _date_from_rev(r1), r1
  465       info2 = p2, _date_from_rev(r2), r2
  466       return vclib._diff_fp(temp1, temp2, info1, info2, self.diff_cmd, args)
  467     except core.SubversionException, e:
  468       _fix_subversion_exception(e)
  469       if e.apr_err == vclib.svn.core.SVN_ERR_FS_NOT_FOUND:
  470         raise vclib.InvalidRevision
  471       raise
  472 
  473   def isexecutable(self, path_parts, rev):
  474     props = self.itemprops(path_parts, rev) # does authz-check
  475     return props.has_key(core.SVN_PROP_EXECUTABLE)
  476   
  477   def filesize(self, path_parts, rev):
  478     path = self._getpath(path_parts)
  479     if self.itemtype(path_parts, rev) != vclib.FILE:  # does auth-check
  480       raise vclib.Error("Path '%s' is not a file." % path)
  481     rev = self._getrev(rev)
  482     dirents, locks = self._get_dirents(self._getpath(path_parts[:-1]), rev)
  483     dirent = dirents.get(path_parts[-1], None)
  484     return dirent.size
  485     
  486   def _getpath(self, path_parts):
  487     return '/'.join(path_parts)
  488 
  489   def _getrev(self, rev):
  490     if rev is None or rev == 'HEAD':
  491       return self.youngest
  492     try:
  493       if type(rev) == type(''):
  494         while rev[0] == 'r':
  495           rev = rev[1:]
  496       rev = int(rev)
  497     except:
  498       raise vclib.InvalidRevision(rev)
  499     if (rev < 0) or (rev > self.youngest):
  500       raise vclib.InvalidRevision(rev)
  501     return rev
  502 
  503   def _geturl(self, path=None):
  504     if not path:
  505       return self.rootpath
  506     path = self.rootpath + '/' + urllib.quote(path)
  507     return _canonicalize_path(path)
  508 
  509   def _get_dirents(self, path, rev):
  510     """Return a 2-type of dirents and locks, possibly reading/writing
  511     from a local cache of that information.  This functions performs
  512     authz checks, stripping out unreadable dirents."""
  513 
  514     dir_url = self._geturl(path)
  515     path_parts = _path_parts(path)    
  516     if path:
  517       key = str(rev) + '/' + path
  518     else:
  519       key = str(rev)
  520 
  521     # Ensure that the cache gets filled...
  522     dirents_locks = self._dirent_cache.get(key)
  523     if not dirents_locks:
  524       tmp_dirents, locks = list_directory(dir_url, _rev2optrev(rev),
  525                                           _rev2optrev(rev), 0, self.ctx)
  526       dirents = {}
  527       for name, dirent in tmp_dirents.items():
  528         dirent_parts = path_parts + [name]
  529         kind = dirent.kind 
  530         if (kind == core.svn_node_dir or kind == core.svn_node_file) \
  531            and vclib.check_path_access(self, dirent_parts,
  532                                        kind == core.svn_node_dir \
  533                                          and vclib.DIR or vclib.FILE, rev):
  534           lh_rev, c_rev = self._get_last_history_rev(dirent_parts, rev)
  535           dirent.created_rev = lh_rev
  536           dirents[name] = dirent
  537       dirents_locks = [dirents, locks]
  538       self._dirent_cache[key] = dirents_locks
  539 
  540     # ...then return the goodies from the cache.
  541     return dirents_locks[0], dirents_locks[1]
  542 
  543   def _get_last_history_rev(self, path_parts, rev):
  544     """Return the a 2-tuple which contains:
  545          - the last interesting revision equal to or older than REV in
  546            the history of PATH_PARTS.
  547          - the created_rev of of PATH_PARTS as of REV."""
  548     
  549     path = self._getpath(path_parts)
  550     url = self._geturl(self._getpath(path_parts))
  551     optrev = _rev2optrev(rev)
  552 
  553     # Get the last-changed-rev.
  554     revisions = []
  555     def _info_cb(path, info, pool, retval=revisions):
  556       revisions.append(info.last_changed_rev)
  557     client.svn_client_info(url, optrev, optrev, _info_cb, 0, self.ctx)
  558     last_changed_rev = revisions[0]
  559 
  560     # Now, this object might not have been directly edited since the
  561     # last-changed-rev, but it might have been the child of a copy.
  562     # To determine this, we'll run a potentially no-op log between
  563     # LAST_CHANGED_REV and REV.
  564     lc = LogCollector(path, 1, None, None)
  565     client_log(url, optrev, _rev2optrev(last_changed_rev), 1, 1, 0,
  566                lc.add_log, self.ctx)
  567     revs = lc.logs
  568     if revs:
  569       revs.sort()
  570       return revs[0].number, last_changed_rev
  571     else:
  572       return last_changed_rev, last_changed_rev
  573     
  574   def _revinfo_fetch(self, rev, include_changed_paths=0):
  575     need_changes = include_changed_paths or self.auth
  576     revs = []
  577     
  578     def _log_cb(log_entry, pool, retval=revs):
  579       # If Subversion happens to call us more than once, we choose not
  580       # to care.
  581       if retval:
  582         return
  583       
  584       revision = log_entry.revision
  585       msg, author, date, revprops = _split_revprops(log_entry.revprops)
  586       action_map = { 'D' : vclib.DELETED,
  587                      'A' : vclib.ADDED,
  588                      'R' : vclib.REPLACED,
  589                      'M' : vclib.MODIFIED,
  590                      }
  591 
  592       # Easy out: if we won't use the changed-path info, just return a
  593       # changes-less tuple.
  594       if not need_changes:
  595         return revs.append([date, author, msg, revprops, None])
  596 
  597       # Subversion 1.5 and earlier didn't offer the 'changed_paths2'
  598       # hash, and in Subversion 1.6, it's offered but broken.
  599       try: 
  600         changed_paths = log_entry.changed_paths2
  601         paths = (changed_paths or {}).keys()
  602       except:
  603         changed_paths = log_entry.changed_paths
  604         paths = (changed_paths or {}).keys()
  605       paths.sort(lambda a, b: _compare_paths(a, b))
  606 
  607       # If we get this far, our caller needs changed-paths, or we need
  608       # them for authz-related sanitization.
  609       changes = []
  610       found_readable = found_unreadable = 0
  611       for path in paths:
  612         change = changed_paths[path]
  613 
  614         # svn_log_changed_path_t (which we might get instead of the
  615         # svn_log_changed_path2_t we'd prefer) doesn't have the
  616         # 'node_kind' member.        
  617         pathtype = None
  618         if hasattr(change, 'node_kind'):
  619           if change.node_kind == core.svn_node_dir:
  620             pathtype = vclib.DIR
  621           elif change.node_kind == core.svn_node_file:
  622             pathtype = vclib.FILE
  623             
  624         # svn_log_changed_path2_t only has the 'text_modified' and
  625         # 'props_modified' bits in Subversion 1.7 and beyond.  And
  626         # svn_log_changed_path_t is without.
  627         text_modified = props_modified = 0
  628         if hasattr(change, 'text_modified'):
  629           if change.text_modified == core.svn_tristate_true:
  630             text_modified = 1
  631         if hasattr(change, 'props_modified'):
  632           if change.props_modified == core.svn_tristate_true:
  633             props_modified = 1
  634             
  635         # Wrong, diddily wrong wrong wrong.  Can you say,
  636         # "Manufacturing data left and right because it hurts to
  637         # figure out the right stuff?"
  638         action = action_map.get(change.action, vclib.MODIFIED)
  639         if change.copyfrom_path and change.copyfrom_rev:
  640           is_copy = 1
  641           base_path = change.copyfrom_path
  642           base_rev = change.copyfrom_rev
  643         elif action == vclib.ADDED or action == vclib.REPLACED:
  644           is_copy = 0
  645           base_path = base_rev = None
  646         else:
  647           is_copy = 0
  648           base_path = path
  649           base_rev = revision - 1
  650 
  651         # Check authz rules (sadly, we have to lie about the path type)
  652         parts = _path_parts(path)
  653         if vclib.check_path_access(self, parts, vclib.FILE, revision):
  654           if is_copy and base_path and (base_path != path):
  655             parts = _path_parts(base_path)
  656             if not vclib.check_path_access(self, parts, vclib.FILE, base_rev):
  657               is_copy = 0
  658               base_path = None
  659               base_rev = None
  660               found_unreadable = 1
  661           changes.append(SVNChangedPath(path, revision, pathtype, base_path,
  662                                         base_rev, action, is_copy,
  663                                         text_modified, props_modified))
  664           found_readable = 1
  665         else:
  666           found_unreadable = 1
  667 
  668         # If our caller doesn't want changed-path stuff, and we have
  669         # the info we need to make an authz determination already,
  670         # quit this loop and get on with it.
  671         if (not include_changed_paths) and found_unreadable and found_readable:
  672           break
  673 
  674       # Filter unreadable information.
  675       if found_unreadable:
  676         msg = None
  677         if not found_readable:
  678           author = None
  679           date = None
  680 
  681       # Drop unrequested changes.
  682       if not include_changed_paths:
  683         changes = None
  684 
  685       # Add this revision information to the "return" array.
  686       retval.append([date, author, msg, revprops, changes])
  687 
  688     optrev = _rev2optrev(rev)
  689     client_log(self.rootpath, optrev, optrev, 1, need_changes, 0,
  690                _log_cb, self.ctx)
  691     return tuple(revs[0])
  692 
  693   def _revinfo(self, rev, include_changed_paths=0):
  694     """Internal-use, cache-friendly revision information harvester."""
  695 
  696     # Consult the revinfo cache first.  If we don't have cached info,
  697     # or our caller wants changed paths and we don't have those for
  698     # this revision, go do the real work.
  699     rev = self._getrev(rev)
  700     cached_info = self._revinfo_cache.get(rev)
  701     if not cached_info \
  702        or (include_changed_paths and cached_info[4] is None):
  703       cached_info = self._revinfo_fetch(rev, include_changed_paths)
  704       self._revinfo_cache[rev] = cached_info
  705     return cached_info
  706 
  707   ##--- custom --##
  708 
  709   def get_youngest_revision(self):
  710     return self.youngest
  711   
  712   def get_location(self, path, rev, old_rev):
  713     try:
  714       results = ra.get_locations(self.ra_session, path, rev, [old_rev])
  715     except core.SubversionException, e:
  716       _fix_subversion_exception(e)
  717       if e.apr_err == core.SVN_ERR_FS_NOT_FOUND:
  718         raise vclib.ItemNotFound(path)
  719       raise
  720     try:
  721       old_path = results[old_rev]
  722     except KeyError:
  723       raise vclib.ItemNotFound(path)
  724     old_path = _cleanup_path(old_path)
  725     old_path_parts = _path_parts(old_path)
  726     # Check access (lying about path types)
  727     if not vclib.check_path_access(self, old_path_parts, vclib.FILE, old_rev):
  728       raise vclib.ItemNotFound(path)
  729     return old_path
  730   
  731   def created_rev(self, path, rev):
  732     lh_rev, c_rev = self._get_last_history_rev(_path_parts(path), rev)
  733     return lh_rev
  734 
  735   def last_rev(self, path, peg_revision, limit_revision=None):
  736     """Given PATH, known to exist in PEG_REVISION, find the youngest
  737     revision older than, or equal to, LIMIT_REVISION in which path
  738     exists.  Return that revision, and the path at which PATH exists in
  739     that revision."""
  740     
  741     # Here's the plan, man.  In the trivial case (where PEG_REVISION is
  742     # the same as LIMIT_REVISION), this is a no-brainer.  If
  743     # LIMIT_REVISION is older than PEG_REVISION, we can use Subversion's
  744     # history tracing code to find the right location.  If, however,
  745     # LIMIT_REVISION is younger than PEG_REVISION, we suffer from
  746     # Subversion's lack of forward history searching.  Our workaround,
  747     # ugly as it may be, involves a binary search through the revisions
  748     # between PEG_REVISION and LIMIT_REVISION to find our last live
  749     # revision.
  750     peg_revision = self._getrev(peg_revision)
  751     limit_revision = self._getrev(limit_revision)
  752     if peg_revision == limit_revision:
  753       return peg_revision, path
  754     elif peg_revision > limit_revision:
  755       path = self.get_location(path, peg_revision, limit_revision)
  756       return limit_revision, path
  757     else:
  758       direction = 1
  759       while peg_revision != limit_revision:
  760         mid = (peg_revision + 1 + limit_revision) / 2
  761         try:
  762           path = self.get_location(path, peg_revision, mid)
  763         except vclib.ItemNotFound:
  764           limit_revision = mid - 1
  765         else:
  766           peg_revision = mid
  767       return peg_revision, path
  768 
  769   def get_symlink_target(self, path_parts, rev):
  770     """Return the target of the symbolic link versioned at PATH_PARTS
  771     in REV, or None if that object is not a symlink."""
  772 
  773     path = self._getpath(path_parts)
  774     path_type = self.itemtype(path_parts, rev) # does auth-check
  775     rev = self._getrev(rev)
  776     url = self._geturl(path)
  777 
  778     # Symlinks must be files with the svn:special property set on them
  779     # and with file contents which read "link SOME_PATH".
  780     if path_type != vclib.FILE:
  781       return None
  782     pairs = client.svn_client_proplist2(url, _rev2optrev(rev),
  783                                         _rev2optrev(rev), 0, self.ctx)
  784     props = pairs and pairs[0][1] or {}
  785     if not props.has_key(core.SVN_PROP_SPECIAL):
  786       return None
  787     pathspec = ''
  788     ### FIXME: We're being a touch sloppy here, first by grabbing the
  789     ### whole file and then by checking only the first line
  790     ### of it.
  791     fp = SelfCleanFP(cat_to_tempfile(self, path, rev))
  792     pathspec = fp.readline()
  793     fp.close()
  794     if pathspec[:5] != 'link ':
  795       return None
  796     return pathspec[5:]
  797