813 lines
29 KiB
Python
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:]
|
|
|