viewvc-4intranet/lib/vclib/svn/svn_ra.py

813 lines
29 KiB
Python

# -*-python-*-
#
# Copyright (C) 1999-2013 The ViewCVS Group. All Rights Reserved.
#
# By using this file, you agree to the terms and conditions set forth in
# the LICENSE.html file which can be found at the top level of the ViewVC
# distribution or at http://viewvc.org/license-1.html.
#
# For more information, visit http://viewvc.org/
#
# -----------------------------------------------------------------------
"Version Control lib driver for remotely accessible Subversion repositories."
import vclib
import sys
import os
import re
import tempfile
import time
import urllib
from svn_repos import Revision, SVNChangedPath, _datestr_to_date, \
_compare_paths, _path_parts, _cleanup_path, \
_rev2optrev, _fix_subversion_exception, \
_split_revprops, _canonicalize_path
from svn import core, delta, client, wc, ra
### Require Subversion 1.3.1 or better. (for svn_ra_get_locations support)
if (core.SVN_VER_MAJOR, core.SVN_VER_MINOR, core.SVN_VER_PATCH) < (1, 3, 1):
raise Exception, "Version requirement not met (needs 1.3.1 or better)"
### BEGIN COMPATABILITY CODE ###
try:
SVN_INVALID_REVNUM = core.SVN_INVALID_REVNUM
except AttributeError: # The 1.4.x bindings are missing core.SVN_INVALID_REVNUM
SVN_INVALID_REVNUM = -1
def list_directory(url, peg_rev, rev, flag, ctx):
try:
dirents, locks = client.svn_client_ls3(url, peg_rev, rev, flag, ctx)
except TypeError: # 1.4.x bindings are goofed
dirents = client.svn_client_ls3(None, url, peg_rev, rev, flag, ctx)
locks = {}
return dirents, locks
def get_directory_props(ra_session, path, rev):
try:
dirents, fetched_rev, props = ra.svn_ra_get_dir(ra_session, path, rev)
except ValueError: # older bindings are goofed
props = ra.svn_ra_get_dir(ra_session, path, rev)
return props
def client_log(url, start_rev, end_rev, log_limit, include_changes,
cross_copies, cb_func, ctx):
include_changes = include_changes and 1 or 0
cross_copies = cross_copies and 1 or 0
try:
client.svn_client_log4([url], start_rev, start_rev, end_rev,
log_limit, include_changes, not cross_copies,
0, None, cb_func, ctx)
except AttributeError:
# Wrap old svn_log_message_receiver_t interface with a
# svn_log_entry_t one.
def cb_convert(paths, revision, author, date, message, pool):
class svn_log_entry_t:
pass
log_entry = svn_log_entry_t()
log_entry.changed_paths = paths
log_entry.revision = revision
log_entry.revprops = { core.SVN_PROP_REVISION_LOG : message,
core.SVN_PROP_REVISION_AUTHOR : author,
core.SVN_PROP_REVISION_DATE : date,
}
cb_func(log_entry, pool)
client.svn_client_log2([url], start_rev, end_rev, log_limit,
include_changes, not cross_copies, cb_convert, ctx)
def setup_client_ctx(config_dir):
# Ensure that the configuration directory exists.
core.svn_config_ensure(config_dir)
# Fetch the configuration (and 'config' bit thereof).
cfg = core.svn_config_get_config(config_dir)
config = cfg.get(core.SVN_CONFIG_CATEGORY_CONFIG)
# Here's the compat-sensitive part: try to use
# svn_cmdline_create_auth_baton(), and fall back to making our own
# if that fails.
try:
auth_baton = core.svn_cmdline_create_auth_baton(1, None, None, config_dir,
1, 1, config, None)
except AttributeError:
auth_baton = core.svn_auth_open([
client.svn_client_get_simple_provider(),
client.svn_client_get_username_provider(),
client.svn_client_get_ssl_server_trust_file_provider(),
client.svn_client_get_ssl_client_cert_file_provider(),
client.svn_client_get_ssl_client_cert_pw_file_provider(),
])
if config_dir is not None:
core.svn_auth_set_parameter(auth_baton,
core.SVN_AUTH_PARAM_CONFIG_DIR,
config_dir)
# Create, setup, and return the client context baton.
ctx = client.svn_client_create_context()
ctx.config = cfg
ctx.auth_baton = auth_baton
return ctx
### END COMPATABILITY CODE ###
class LogCollector:
def __init__(self, path, show_all_logs, lockinfo, access_check_func):
# This class uses leading slashes for paths internally
if not path:
self.path = '/'
else:
self.path = path[0] == '/' and path or '/' + path
self.logs = []
self.show_all_logs = show_all_logs
self.lockinfo = lockinfo
self.access_check_func = access_check_func
self.done = False
def add_log(self, log_entry, pool):
if self.done:
return
paths = log_entry.changed_paths
revision = log_entry.revision
msg, author, date, revprops = _split_revprops(log_entry.revprops)
# Changed paths have leading slashes
changed_paths = paths.keys()
changed_paths.sort(lambda a, b: _compare_paths(a, b))
this_path = None
if self.path in changed_paths:
this_path = self.path
change = paths[self.path]
if change.copyfrom_path:
this_path = change.copyfrom_path
for changed_path in changed_paths:
if changed_path != self.path:
# If a parent of our path was copied, our "next previous"
# (huh?) path will exist elsewhere (under the copy source).
if (self.path.rfind(changed_path) == 0) and \
self.path[len(changed_path)] == '/':
change = paths[changed_path]
if change.copyfrom_path:
this_path = change.copyfrom_path + self.path[len(changed_path):]
if self.show_all_logs or this_path:
if self.access_check_func is None \
or self.access_check_func(self.path[1:], revision):
entry = Revision(revision, date, author, msg, None, self.lockinfo,
self.path[1:], None, None)
self.logs.append(entry)
else:
self.done = True
if this_path:
self.path = this_path
def cat_to_tempfile(svnrepos, path, rev):
"""Check out file revision to temporary file"""
temp = tempfile.mktemp()
stream = core.svn_stream_from_aprfile(temp)
url = svnrepos._geturl(path)
client.svn_client_cat(core.Stream(stream), url, _rev2optrev(rev),
svnrepos.ctx)
core.svn_stream_close(stream)
return temp
class SelfCleanFP:
def __init__(self, path):
self._fp = open(path, 'r')
self._path = path
self._eof = 0
def read(self, len=None):
if len:
chunk = self._fp.read(len)
else:
chunk = self._fp.read()
if chunk == '':
self._eof = 1
return chunk
def readline(self):
chunk = self._fp.readline()
if chunk == '':
self._eof = 1
return chunk
def readlines(self):
lines = self._fp.readlines()
self._eof = 1
return lines
def close(self):
self._fp.close()
os.remove(self._path)
def __del__(self):
self.close()
def eof(self):
return self._eof
class RemoteSubversionRepository(vclib.Repository):
def __init__(self, name, rootpath, authorizer, utilities, config_dir):
self.name = name
self.rootpath = rootpath
self.auth = authorizer
self.diff_cmd = utilities.diff or 'diff'
self.config_dir = config_dir or None
# See if this repository is even viewable, authz-wise.
if not vclib.check_root_access(self):
raise vclib.ReposNotFound(name)
def open(self):
# Setup the client context baton, complete with non-prompting authstuffs.
self.ctx = setup_client_ctx(self.config_dir)
ra_callbacks = ra.svn_ra_callbacks_t()
ra_callbacks.auth_baton = self.ctx.auth_baton
self.ra_session = ra.svn_ra_open(self.rootpath, ra_callbacks, None,
self.ctx.config)
self.youngest = ra.svn_ra_get_latest_revnum(self.ra_session)
self._dirent_cache = { }
self._revinfo_cache = { }
# See if a universal read access determination can be made.
if self.auth and self.auth.check_universal_access(self.name) == 1:
self.auth = None
def rootname(self):
return self.name
def rootpath(self):
return self.rootpath
def roottype(self):
return vclib.SVN
def authorizer(self):
return self.auth
def itemtype(self, path_parts, rev):
pathtype = None
if not len(path_parts):
pathtype = vclib.DIR
else:
path = self._getpath(path_parts)
rev = self._getrev(rev)
try:
kind = ra.svn_ra_check_path(self.ra_session, path, rev)
if kind == core.svn_node_file:
pathtype = vclib.FILE
elif kind == core.svn_node_dir:
pathtype = vclib.DIR
except:
pass
if pathtype is None:
raise vclib.ItemNotFound(path_parts)
if not vclib.check_path_access(self, path_parts, pathtype, rev):
raise vclib.ItemNotFound(path_parts)
return pathtype
def openfile(self, path_parts, rev, options):
path = self._getpath(path_parts)
if self.itemtype(path_parts, rev) != vclib.FILE: # does auth-check
raise vclib.Error("Path '%s' is not a file." % path)
rev = self._getrev(rev)
url = self._geturl(path)
### rev here should be the last history revision of the URL
fp = SelfCleanFP(cat_to_tempfile(self, path, rev))
lh_rev, c_rev = self._get_last_history_rev(path_parts, rev)
return fp, lh_rev
def listdir(self, path_parts, rev, options):
path = self._getpath(path_parts)
if self.itemtype(path_parts, rev) != vclib.DIR: # does auth-check
raise vclib.Error("Path '%s' is not a directory." % path)
rev = self._getrev(rev)
entries = []
dirents, locks = self._get_dirents(path, rev)
for name in dirents.keys():
entry = dirents[name]
if entry.kind == core.svn_node_dir:
kind = vclib.DIR
elif entry.kind == core.svn_node_file:
kind = vclib.FILE
else:
kind = None
entries.append(vclib.DirEntry(name, kind))
return entries
def dirlogs(self, path_parts, rev, entries, options):
path = self._getpath(path_parts)
if self.itemtype(path_parts, rev) != vclib.DIR: # does auth-check
raise vclib.Error("Path '%s' is not a directory." % path)
rev = self._getrev(rev)
dirents, locks = self._get_dirents(path, rev)
for entry in entries:
entry_path_parts = path_parts + [entry.name]
dirent = dirents.get(entry.name, None)
# dirents is authz-sanitized, so ensure the entry is found therein.
if dirent is None:
continue
# Get authz-sanitized revision metadata.
entry.date, entry.author, entry.log, revprops, changes = \
self._revinfo(dirent.created_rev)
entry.rev = str(dirent.created_rev)
entry.size = dirent.size
entry.lockinfo = None
if locks.has_key(entry.name):
entry.lockinfo = locks[entry.name].owner
def itemlog(self, path_parts, rev, sortby, first, limit, options):
assert sortby == vclib.SORTBY_DEFAULT or sortby == vclib.SORTBY_REV
path_type = self.itemtype(path_parts, rev) # does auth-check
path = self._getpath(path_parts)
rev = self._getrev(rev)
url = self._geturl(path)
# If this is a file, fetch the lock status and size (as of REV)
# for this item.
lockinfo = size_in_rev = None
if path_type == vclib.FILE:
basename = path_parts[-1]
list_url = self._geturl(self._getpath(path_parts[:-1]))
dirents, locks = list_directory(list_url, _rev2optrev(rev),
_rev2optrev(rev), 0, self.ctx)
if locks.has_key(basename):
lockinfo = locks[basename].owner
if dirents.has_key(basename):
size_in_rev = dirents[basename].size
# Special handling for the 'svn_latest_log' scenario.
### FIXME: Don't like this hack. We should just introduce
### something more direct in the vclib API.
if options.get('svn_latest_log', 0):
dir_lh_rev, dir_c_rev = self._get_last_history_rev(path_parts, rev)
date, author, log, revprops, changes = self._revinfo(dir_lh_rev)
return [vclib.Revision(dir_lh_rev, str(dir_lh_rev), date, author,
None, log, size_in_rev, lockinfo)]
def _access_checker(check_path, check_rev):
return vclib.check_path_access(self, _path_parts(check_path),
path_type, check_rev)
# It's okay if we're told to not show all logs on a file -- all
# the revisions should match correctly anyway.
lc = LogCollector(path, options.get('svn_show_all_dir_logs', 0),
lockinfo, _access_checker)
cross_copies = options.get('svn_cross_copies', 0)
log_limit = 0
if limit:
log_limit = first + limit
client_log(url, _rev2optrev(rev), _rev2optrev(1), log_limit, 1,
cross_copies, lc.add_log, self.ctx)
revs = lc.logs
revs.sort()
prev = None
for rev in revs:
# Swap out revision info with stuff from the cache (which is
# authz-sanitized).
rev.date, rev.author, rev.log, revprops, changes \
= self._revinfo(rev.number)
rev.prev = prev
prev = rev
revs.reverse()
if len(revs) < first:
return []
if limit:
return revs[first:first+limit]
return revs
def itemprops(self, path_parts, rev):
path = self._getpath(path_parts)
path_type = self.itemtype(path_parts, rev) # does auth-check
rev = self._getrev(rev)
url = self._geturl(path)
pairs = client.svn_client_proplist2(url, _rev2optrev(rev),
_rev2optrev(rev), 0, self.ctx)
return pairs and pairs[0][1] or {}
def annotate(self, path_parts, rev, include_text=False):
path = self._getpath(path_parts)
if self.itemtype(path_parts, rev) != vclib.FILE: # does auth-check
raise vclib.Error("Path '%s' is not a file." % path)
rev = self._getrev(rev)
url = self._geturl(path)
# Examine logs for the file to determine the oldest revision we are
# permitted to see.
log_options = {
'svn_cross_copies' : 1,
'svn_show_all_dir_logs' : 1,
}
revs = self.itemlog(path_parts, rev, vclib.SORTBY_REV, 0, 0, log_options)
oldest_rev = revs[-1].number
# Now calculate the annotation data. Note that we'll not
# inherently trust the provided author and date, because authz
# rules might necessitate that we strip that information out.
blame_data = []
def _blame_cb(line_no, revision, author, date,
line, pool, blame_data=blame_data):
prev_rev = None
if revision > 1:
prev_rev = revision - 1
# If we have an invalid revision, clear the date and author
# values. Otherwise, if we have authz filtering to do, use the
# revinfo cache to do so.
if revision < 0:
date = author = None
elif self.auth:
date, author, msg, revprops, changes = self._revinfo(revision)
# Strip text if the caller doesn't want it.
if not include_text:
line = None
blame_data.append(vclib.Annotation(line, line_no + 1, revision, prev_rev,
author, date))
client.blame2(url, _rev2optrev(rev), _rev2optrev(oldest_rev),
_rev2optrev(rev), _blame_cb, self.ctx)
return blame_data, rev
def revinfo(self, rev):
return self._revinfo(rev, 1)
def rawdiff(self, path_parts1, rev1, path_parts2, rev2, type, options={}):
if path_parts1 is not None:
p1 = self._getpath(path_parts1)
r1 = self._getrev(rev1)
if not vclib.check_path_access(self, path_parts1, vclib.FILE, rev1):
raise vclib.ItemNotFound(path_parts1)
else:
p1 = None
if path_parts2 is not None:
if not p1:
raise vclib.ItemNotFound(parh_parts2)
p2 = self._getpath(path_parts2)
r2 = self._getrev(rev2)
if not vclib.check_path_access(self, path_parts2, vclib.FILE, rev2):
raise vclib.ItemNotFound(path_parts2)
else:
p2 = None
args = vclib._diff_args(type, options)
def _date_from_rev(rev):
date, author, msg, revprops, changes = self._revinfo(rev)
return date
try:
info1 = p1, _date_from_rev(r1), r1
info2 = p2, _date_from_rev(r2), r2
if p1:
temp1 = cat_to_tempfile(self, p1, r1)
else:
temp1 = '/dev/null'
if p2:
temp2 = cat_to_tempfile(self, p2, r2)
else:
temp2 = '/dev/null'
return vclib._diff_fp(temp1, temp2, info1, info2, self.diff_cmd, args)
except core.SubversionException, e:
_fix_subversion_exception(e)
if e.apr_err == core.SVN_ERR_FS_NOT_FOUND:
raise vclib.InvalidRevision
raise
def isexecutable(self, path_parts, rev):
props = self.itemprops(path_parts, rev) # does authz-check
return props.has_key(core.SVN_PROP_EXECUTABLE)
def filesize(self, path_parts, rev):
path = self._getpath(path_parts)
if self.itemtype(path_parts, rev) != vclib.FILE: # does auth-check
raise vclib.Error("Path '%s' is not a file." % path)
rev = self._getrev(rev)
dirents, locks = self._get_dirents(self._getpath(path_parts[:-1]), rev)
dirent = dirents.get(path_parts[-1], None)
return dirent.size
def _getpath(self, path_parts):
return '/'.join(path_parts)
def _getrev(self, rev):
if rev is None or rev == 'HEAD':
return self.youngest
try:
if type(rev) == type(''):
while rev[0] == 'r':
rev = rev[1:]
rev = int(rev)
except:
raise vclib.InvalidRevision(rev)
if (rev < 0) or (rev > self.youngest):
raise vclib.InvalidRevision(rev)
return rev
def _geturl(self, path=None):
if not path:
return self.rootpath
path = self.rootpath + '/' + urllib.quote(path)
return _canonicalize_path(path)
def _get_dirents(self, path, rev):
"""Return a 2-type of dirents and locks, possibly reading/writing
from a local cache of that information. This functions performs
authz checks, stripping out unreadable dirents."""
dir_url = self._geturl(path)
path_parts = _path_parts(path)
if path:
key = str(rev) + '/' + path
else:
key = str(rev)
# Ensure that the cache gets filled...
dirents_locks = self._dirent_cache.get(key)
if not dirents_locks:
tmp_dirents, locks = list_directory(dir_url, _rev2optrev(rev),
_rev2optrev(rev), 0, self.ctx)
dirents = {}
for name, dirent in tmp_dirents.items():
dirent_parts = path_parts + [name]
kind = dirent.kind
if (kind == core.svn_node_dir or kind == core.svn_node_file) \
and vclib.check_path_access(self, dirent_parts,
kind == core.svn_node_dir \
and vclib.DIR or vclib.FILE, rev):
lh_rev, c_rev = self._get_last_history_rev(dirent_parts, rev)
dirent.created_rev = lh_rev
dirents[name] = dirent
dirents_locks = [dirents, locks]
self._dirent_cache[key] = dirents_locks
# ...then return the goodies from the cache.
return dirents_locks[0], dirents_locks[1]
def _get_last_history_rev(self, path_parts, rev):
"""Return the a 2-tuple which contains:
- the last interesting revision equal to or older than REV in
the history of PATH_PARTS.
- the created_rev of of PATH_PARTS as of REV."""
path = self._getpath(path_parts)
url = self._geturl(self._getpath(path_parts))
optrev = _rev2optrev(rev)
# Get the last-changed-rev.
revisions = []
def _info_cb(path, info, pool, retval=revisions):
revisions.append(info.last_changed_rev)
client.svn_client_info(url, optrev, optrev, _info_cb, 0, self.ctx)
last_changed_rev = revisions[0]
# Now, this object might not have been directly edited since the
# last-changed-rev, but it might have been the child of a copy.
# To determine this, we'll run a potentially no-op log between
# LAST_CHANGED_REV and REV.
lc = LogCollector(path, 1, None, None)
client_log(url, optrev, _rev2optrev(last_changed_rev), 1, 1, 0,
lc.add_log, self.ctx)
revs = lc.logs
if revs:
revs.sort()
return revs[0].number, last_changed_rev
else:
return last_changed_rev, last_changed_rev
def _revinfo_fetch(self, rev, include_changed_paths=0):
need_changes = include_changed_paths or self.auth
revs = []
def _log_cb(log_entry, pool, retval=revs):
# If Subversion happens to call us more than once, we choose not
# to care.
if retval:
return
revision = log_entry.revision
msg, author, date, revprops = _split_revprops(log_entry.revprops)
action_map = { 'D' : vclib.DELETED,
'A' : vclib.ADDED,
'R' : vclib.REPLACED,
'M' : vclib.MODIFIED,
}
# Easy out: if we won't use the changed-path info, just return a
# changes-less tuple.
if not need_changes:
return revs.append([date, author, msg, revprops, None])
# Subversion 1.5 and earlier didn't offer the 'changed_paths2'
# hash, and in Subversion 1.6, it's offered but broken.
try:
changed_paths = log_entry.changed_paths2
paths = (changed_paths or {}).keys()
except:
changed_paths = log_entry.changed_paths
paths = (changed_paths or {}).keys()
paths.sort(lambda a, b: _compare_paths(a, b))
# If we get this far, our caller needs changed-paths, or we need
# them for authz-related sanitization.
changes = []
found_readable = found_unreadable = 0
for path in paths:
change = changed_paths[path]
# svn_log_changed_path_t (which we might get instead of the
# svn_log_changed_path2_t we'd prefer) doesn't have the
# 'node_kind' member.
pathtype = None
if hasattr(change, 'node_kind'):
if change.node_kind == core.svn_node_dir:
pathtype = vclib.DIR
elif change.node_kind == core.svn_node_file:
pathtype = vclib.FILE
# svn_log_changed_path2_t only has the 'text_modified' and
# 'props_modified' bits in Subversion 1.7 and beyond. And
# svn_log_changed_path_t is without.
text_modified = props_modified = 0
if hasattr(change, 'text_modified'):
if change.text_modified == core.svn_tristate_true:
text_modified = 1
if hasattr(change, 'props_modified'):
if change.props_modified == core.svn_tristate_true:
props_modified = 1
# Wrong, diddily wrong wrong wrong. Can you say,
# "Manufacturing data left and right because it hurts to
# figure out the right stuff?"
action = action_map.get(change.action, vclib.MODIFIED)
if change.copyfrom_path and change.copyfrom_rev:
is_copy = 1
base_path = change.copyfrom_path
base_rev = change.copyfrom_rev
elif action == vclib.ADDED or action == vclib.REPLACED:
is_copy = 0
base_path = base_rev = None
else:
is_copy = 0
base_path = path
base_rev = revision - 1
# Check authz rules (sadly, we have to lie about the path type)
parts = _path_parts(path)
if vclib.check_path_access(self, parts, vclib.FILE, revision):
if is_copy and base_path and (base_path != path):
parts = _path_parts(base_path)
if not vclib.check_path_access(self, parts, vclib.FILE, base_rev):
is_copy = 0
base_path = None
base_rev = None
found_unreadable = 1
changes.append(SVNChangedPath(path, revision, pathtype, base_path,
base_rev, action, is_copy,
text_modified, props_modified))
found_readable = 1
else:
found_unreadable = 1
# If our caller doesn't want changed-path stuff, and we have
# the info we need to make an authz determination already,
# quit this loop and get on with it.
if (not include_changed_paths) and found_unreadable and found_readable:
break
# Filter unreadable information.
if found_unreadable:
msg = None
if not found_readable:
author = None
date = None
# Drop unrequested changes.
if not include_changed_paths:
changes = None
# Add this revision information to the "return" array.
retval.append([date, author, msg, revprops, changes])
optrev = _rev2optrev(rev)
client_log(self.rootpath, optrev, optrev, 1, need_changes, 0,
_log_cb, self.ctx)
return tuple(revs[0])
def _revinfo(self, rev, include_changed_paths=0):
"""Internal-use, cache-friendly revision information harvester."""
# Consult the revinfo cache first. If we don't have cached info,
# or our caller wants changed paths and we don't have those for
# this revision, go do the real work.
rev = self._getrev(rev)
cached_info = self._revinfo_cache.get(rev)
if not cached_info \
or (include_changed_paths and cached_info[4] is None):
cached_info = self._revinfo_fetch(rev, include_changed_paths)
self._revinfo_cache[rev] = cached_info
return cached_info
##--- custom --##
def get_youngest_revision(self):
return self.youngest
def get_location(self, path, rev, old_rev):
try:
results = ra.get_locations(self.ra_session, path, rev, [old_rev])
except core.SubversionException, e:
_fix_subversion_exception(e)
if e.apr_err == core.SVN_ERR_FS_NOT_FOUND:
raise vclib.ItemNotFound(path)
raise
try:
old_path = results[old_rev]
except KeyError:
raise vclib.ItemNotFound(path)
old_path = _cleanup_path(old_path)
old_path_parts = _path_parts(old_path)
# Check access (lying about path types)
if not vclib.check_path_access(self, old_path_parts, vclib.FILE, old_rev):
raise vclib.ItemNotFound(path)
return old_path
def created_rev(self, path, rev):
lh_rev, c_rev = self._get_last_history_rev(_path_parts(path), rev)
return lh_rev
def last_rev(self, path, peg_revision, limit_revision=None):
"""Given PATH, known to exist in PEG_REVISION, find the youngest
revision older than, or equal to, LIMIT_REVISION in which path
exists. Return that revision, and the path at which PATH exists in
that revision."""
# Here's the plan, man. In the trivial case (where PEG_REVISION is
# the same as LIMIT_REVISION), this is a no-brainer. If
# LIMIT_REVISION is older than PEG_REVISION, we can use Subversion's
# history tracing code to find the right location. If, however,
# LIMIT_REVISION is younger than PEG_REVISION, we suffer from
# Subversion's lack of forward history searching. Our workaround,
# ugly as it may be, involves a binary search through the revisions
# between PEG_REVISION and LIMIT_REVISION to find our last live
# revision.
peg_revision = self._getrev(peg_revision)
limit_revision = self._getrev(limit_revision)
if peg_revision == limit_revision:
return peg_revision, path
elif peg_revision > limit_revision:
path = self.get_location(path, peg_revision, limit_revision)
return limit_revision, path
else:
direction = 1
while peg_revision != limit_revision:
mid = (peg_revision + 1 + limit_revision) / 2
try:
path = self.get_location(path, peg_revision, mid)
except vclib.ItemNotFound:
limit_revision = mid - 1
else:
peg_revision = mid
return peg_revision, path
def get_symlink_target(self, path_parts, rev):
"""Return the target of the symbolic link versioned at PATH_PARTS
in REV, or None if that object is not a symlink."""
path = self._getpath(path_parts)
path_type = self.itemtype(path_parts, rev) # does auth-check
rev = self._getrev(rev)
url = self._geturl(path)
# Symlinks must be files with the svn:special property set on them
# and with file contents which read "link SOME_PATH".
if path_type != vclib.FILE:
return None
pairs = client.svn_client_proplist2(url, _rev2optrev(rev),
_rev2optrev(rev), 0, self.ctx)
props = pairs and pairs[0][1] or {}
if not props.has_key(core.SVN_PROP_SPECIAL):
return None
pathspec = ''
### FIXME: We're being a touch sloppy here, first by grabbing the
### whole file and then by checking only the first line
### of it.
fp = SelfCleanFP(cat_to_tempfile(self, path, rev))
pathspec = fp.readline()
fp.close()
if pathspec[:5] != 'link ':
return None
return pathspec[5:]