##// END OF EJS Templates
svn: increase possibility to specify compatability to 1.9....
svn: increase possibility to specify compatability to 1.9. - now possible to spec pre-1.9-compat.

File last commit:

r218:698b4f9a default
r218:698b4f9a default
Show More
svn.py
656 lines | 22.5 KiB | text/x-python | PythonLexer
initial commit
r0 # RhodeCode VCSServer provides access to different vcs backends via network.
license: updated copyright year to 2017
r149 # Copyright (C) 2014-2017 RodeCode GmbH
initial commit
r0 #
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
from __future__ import absolute_import
from urllib2 import URLError
import logging
import posixpath as vcspath
import StringIO
import subprocess
import urllib
vcsserver: add debug information to some of the errors that might happen.
r217 import traceback
initial commit
r0
import svn.client
import svn.core
import svn.delta
import svn.diff
import svn.fs
import svn.repos
from vcsserver import svn_diff
backends: implemented functions for fetching backend versions via remote calls....
r101 from vcsserver import exceptions
exception-handling: better handling of remote exception and logging....
r171 from vcsserver.base import RepoFactory, raise_from_original
initial commit
r0
log = logging.getLogger(__name__)
# Set of svn compatible version flags.
# Compare with subversion/svnadmin/svnadmin.c
svn_compatible_versions = set([
'pre-1.4-compatible',
'pre-1.5-compatible',
'pre-1.6-compatible',
'pre-1.8-compatible',
svn: increase possibility to specify compatability to 1.9....
r218 'pre-1.9-compatible',
initial commit
r0 ])
svn: increase possibility to specify compatability to 1.9....
r218 svn_compatible_versions_map = {
'pre-1.4-compatible': '1.3',
'pre-1.5-compatible': '1.4',
'pre-1.6-compatible': '1.5',
'pre-1.8-compatible': '1.7',
'pre-1.9-compatible': '1.8',
}
initial commit
r0
backends: implemented functions for fetching backend versions via remote calls....
r101 def reraise_safe_exceptions(func):
"""Decorator for converting svn exceptions to something neutral."""
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except Exception as e:
if not hasattr(e, '_vcs_kind'):
log.exception("Unhandled exception in hg remote call")
raise_from_original(exceptions.UnhandledException)
raise
return wrapper
initial commit
r0 class SubversionFactory(RepoFactory):
def _create_repo(self, wire, create, compatible_version):
path = svn.core.svn_path_canonicalize(wire['path'])
if create:
svn: increase possibility to specify compatability to 1.9....
r218 fs_config = {'compatible-version': '1.9'}
initial commit
r0 if compatible_version:
if compatible_version not in svn_compatible_versions:
raise Exception('Unknown SVN compatible version "{}"'
.format(compatible_version))
svn: increase possibility to specify compatability to 1.9....
r218 fs_config['compatible-version'] = \
svn_compatible_versions_map[compatible_version]
log.debug('Create SVN repo with config "%s"', fs_config)
initial commit
r0 repo = svn.repos.create(path, "", "", None, fs_config)
else:
repo = svn.repos.open(path)
return repo
def repo(self, wire, create=False, compatible_version=None):
def create_new_repo():
return self._create_repo(wire, create, compatible_version)
return self._repo(wire, create_new_repo)
NODE_TYPE_MAPPING = {
svn.core.svn_node_file: 'file',
svn.core.svn_node_dir: 'dir',
}
class SvnRemote(object):
def __init__(self, factory, hg_factory=None):
self._factory = factory
# TODO: Remove once we do not use internal Mercurial objects anymore
# for subversion
self._hg_factory = hg_factory
backends: implemented functions for fetching backend versions via remote calls....
r101 @reraise_safe_exceptions
def discover_svn_version(self):
try:
import svn.core
svn_ver = svn.core.SVN_VERSION
except ImportError:
svn_ver = None
return svn_ver
initial commit
r0 def check_url(self, url, config_items):
# this can throw exception if not installed, but we detect this
from hgsubversion import svnrepo
baseui = self._hg_factory._create_config(config_items)
# uuid function get's only valid UUID from proper repo, else
# throws exception
try:
svnrepo.svnremoterepo(baseui, url).svn.uuid
vcsserver: add debug information to some of the errors that might happen.
r217 except Exception:
tb = traceback.format_exc()
log.debug("Invalid Subversion url: `%s`, tb: %s", url, tb)
initial commit
r0 raise URLError(
'"%s" is not a valid Subversion source url.' % (url, ))
return True
def is_path_valid_repository(self, wire, path):
try:
svn.repos.open(path)
except svn.core.SubversionException:
vcsserver: add debug information to some of the errors that might happen.
r217 tb = traceback.format_exc()
log.debug("Invalid Subversion path `%s`, tb: %s", path, tb)
initial commit
r0 return False
return True
def lookup(self, wire, revision):
if revision not in [-1, None, 'HEAD']:
raise NotImplementedError
repo = self._factory.repo(wire)
fs_ptr = svn.repos.fs(repo)
head = svn.fs.youngest_rev(fs_ptr)
return head
def lookup_interval(self, wire, start_ts, end_ts):
repo = self._factory.repo(wire)
fsobj = svn.repos.fs(repo)
start_rev = None
end_rev = None
if start_ts:
start_ts_svn = apr_time_t(start_ts)
start_rev = svn.repos.dated_revision(repo, start_ts_svn) + 1
else:
start_rev = 1
if end_ts:
end_ts_svn = apr_time_t(end_ts)
end_rev = svn.repos.dated_revision(repo, end_ts_svn)
else:
end_rev = svn.fs.youngest_rev(fsobj)
return start_rev, end_rev
def revision_properties(self, wire, revision):
repo = self._factory.repo(wire)
fs_ptr = svn.repos.fs(repo)
return svn.fs.revision_proplist(fs_ptr, revision)
def revision_changes(self, wire, revision):
repo = self._factory.repo(wire)
fsobj = svn.repos.fs(repo)
rev_root = svn.fs.revision_root(fsobj, revision)
editor = svn.repos.ChangeCollector(fsobj, rev_root)
editor_ptr, editor_baton = svn.delta.make_editor(editor)
base_dir = ""
send_deltas = False
svn.repos.replay2(
rev_root, base_dir, svn.core.SVN_INVALID_REVNUM, send_deltas,
editor_ptr, editor_baton, None)
added = []
changed = []
removed = []
# TODO: CHANGE_ACTION_REPLACE: Figure out where it belongs
for path, change in editor.changes.iteritems():
# TODO: Decide what to do with directory nodes. Subversion can add
# empty directories.
svn-backend: add handling of rename file as changes....
r104
initial commit
r0 if change.item_kind == svn.core.svn_node_dir:
continue
svn-backend: add handling of rename file as changes....
r104 if change.action in [svn.repos.CHANGE_ACTION_ADD]:
initial commit
r0 added.append(path)
svn-backend: add handling of rename file as changes....
r104 elif change.action in [svn.repos.CHANGE_ACTION_MODIFY,
svn.repos.CHANGE_ACTION_REPLACE]:
initial commit
r0 changed.append(path)
svn-backend: add handling of rename file as changes....
r104 elif change.action in [svn.repos.CHANGE_ACTION_DELETE]:
initial commit
r0 removed.append(path)
else:
raise NotImplementedError(
"Action %s not supported on path %s" % (
change.action, path))
changes = {
'added': added,
'changed': changed,
'removed': removed,
}
return changes
def node_history(self, wire, path, revision, limit):
cross_copies = False
repo = self._factory.repo(wire)
fsobj = svn.repos.fs(repo)
rev_root = svn.fs.revision_root(fsobj, revision)
history_revisions = []
history = svn.fs.node_history(rev_root, path)
history = svn.fs.history_prev(history, cross_copies)
while history:
__, node_revision = svn.fs.history_location(history)
history_revisions.append(node_revision)
if limit and len(history_revisions) >= limit:
break
history = svn.fs.history_prev(history, cross_copies)
return history_revisions
def node_properties(self, wire, path, revision):
repo = self._factory.repo(wire)
fsobj = svn.repos.fs(repo)
rev_root = svn.fs.revision_root(fsobj, revision)
return svn.fs.node_proplist(rev_root, path)
def file_annotate(self, wire, path, revision):
abs_path = 'file://' + urllib.pathname2url(
vcspath.join(wire['path'], path))
file_uri = svn.core.svn_path_canonicalize(abs_path)
start_rev = svn_opt_revision_value_t(0)
peg_rev = svn_opt_revision_value_t(revision)
end_rev = peg_rev
annotations = []
def receiver(line_no, revision, author, date, line, pool):
annotations.append((line_no, revision, line))
# TODO: Cannot use blame5, missing typemap function in the swig code
try:
svn.client.blame2(
file_uri, peg_rev, start_rev, end_rev,
receiver, svn.client.create_context())
except svn.core.SubversionException as exc:
log.exception("Error during blame operation.")
raise Exception(
"Blame not supported or file does not exist at path %s. "
"Error %s." % (path, exc))
return annotations
def get_node_type(self, wire, path, rev=None):
repo = self._factory.repo(wire)
fs_ptr = svn.repos.fs(repo)
if rev is None:
rev = svn.fs.youngest_rev(fs_ptr)
root = svn.fs.revision_root(fs_ptr, rev)
node = svn.fs.check_path(root, path)
return NODE_TYPE_MAPPING.get(node, None)
def get_nodes(self, wire, path, revision=None):
repo = self._factory.repo(wire)
fsobj = svn.repos.fs(repo)
if revision is None:
revision = svn.fs.youngest_rev(fsobj)
root = svn.fs.revision_root(fsobj, revision)
entries = svn.fs.dir_entries(root, path)
result = []
for entry_path, entry_info in entries.iteritems():
result.append(
(entry_path, NODE_TYPE_MAPPING.get(entry_info.kind, None)))
return result
def get_file_content(self, wire, path, rev=None):
repo = self._factory.repo(wire)
fsobj = svn.repos.fs(repo)
if rev is None:
rev = svn.fs.youngest_revision(fsobj)
root = svn.fs.revision_root(fsobj, rev)
content = svn.core.Stream(svn.fs.file_contents(root, path))
return content.read()
def get_file_size(self, wire, path, revision=None):
repo = self._factory.repo(wire)
fsobj = svn.repos.fs(repo)
if revision is None:
revision = svn.fs.youngest_revision(fsobj)
root = svn.fs.revision_root(fsobj, revision)
size = svn.fs.file_length(root, path)
return size
def create_repository(self, wire, compatible_version=None):
log.info('Creating Subversion repository in path "%s"', wire['path'])
self._factory.repo(wire, create=True,
compatible_version=compatible_version)
def import_remote_repository(self, wire, src_url):
repo_path = wire['path']
if not self.is_path_valid_repository(wire, repo_path):
raise Exception(
"Path %s is not a valid Subversion repository." % repo_path)
# TODO: johbo: URL checks ?
rdump = subprocess.Popen(
['svnrdump', 'dump', '--non-interactive', src_url],
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
load = subprocess.Popen(
['svnadmin', 'load', repo_path], stdin=rdump.stdout)
# TODO: johbo: This can be a very long operation, might be better
# to track some kind of status and provide an api to check if the
# import is done.
rdump.wait()
load.wait()
if rdump.returncode != 0:
errors = rdump.stderr.read()
log.error('svnrdump dump failed: statuscode %s: message: %s',
rdump.returncode, errors)
reason = 'UNKNOWN'
if 'svnrdump: E230001:' in errors:
reason = 'INVALID_CERTIFICATE'
raise Exception(
'Failed to dump the remote repository from %s.' % src_url,
reason)
if load.returncode != 0:
raise Exception(
'Failed to load the dump of remote repository from %s.' %
(src_url, ))
def commit(self, wire, message, author, timestamp, updated, removed):
assert isinstance(message, str)
assert isinstance(author, str)
repo = self._factory.repo(wire)
fsobj = svn.repos.fs(repo)
rev = svn.fs.youngest_rev(fsobj)
txn = svn.repos.fs_begin_txn_for_commit(repo, rev, author, message)
txn_root = svn.fs.txn_root(txn)
for node in updated:
TxnNodeProcessor(node, txn_root).update()
for node in removed:
TxnNodeProcessor(node, txn_root).remove()
commit_id = svn.repos.fs_commit_txn(repo, txn)
if timestamp:
apr_time = apr_time_t(timestamp)
ts_formatted = svn.core.svn_time_to_cstring(apr_time)
svn.fs.change_rev_prop(fsobj, commit_id, 'svn:date', ts_formatted)
log.debug('Committed revision "%s" to "%s".', commit_id, wire['path'])
return commit_id
def diff(self, wire, rev1, rev2, path1=None, path2=None,
ignore_whitespace=False, context=3):
svn-diffs: use consisten behaviour on generting diffs between hg/git backends.
r147
initial commit
r0 wire.update(cache=False)
repo = self._factory.repo(wire)
diff_creator = SvnDiffer(
repo, rev1, path1, rev2, path2, ignore_whitespace, context)
svn-diffs: use consisten behaviour on generting diffs between hg/git backends.
r147 try:
return diff_creator.generate_diff()
except svn.core.SubversionException as e:
log.exception(
"Error during diff operation operation. "
"Path might not exist %s, %s" % (path1, path2))
return ""
initial commit
r0
largefiles: added handling of detection and fetching of largefiles....
r182 @reraise_safe_exceptions
def is_large_file(self, wire, path):
return False
initial commit
r0
class SvnDiffer(object):
"""
Utility to create diffs based on difflib and the Subversion api
"""
binary_content = False
def __init__(
self, repo, src_rev, src_path, tgt_rev, tgt_path,
ignore_whitespace, context):
self.repo = repo
self.ignore_whitespace = ignore_whitespace
self.context = context
fsobj = svn.repos.fs(repo)
self.tgt_rev = tgt_rev
self.tgt_path = tgt_path or ''
self.tgt_root = svn.fs.revision_root(fsobj, tgt_rev)
self.tgt_kind = svn.fs.check_path(self.tgt_root, self.tgt_path)
self.src_rev = src_rev
self.src_path = src_path or self.tgt_path
self.src_root = svn.fs.revision_root(fsobj, src_rev)
self.src_kind = svn.fs.check_path(self.src_root, self.src_path)
self._validate()
def _validate(self):
if (self.tgt_kind != svn.core.svn_node_none and
self.src_kind != svn.core.svn_node_none and
self.src_kind != self.tgt_kind):
# TODO: johbo: proper error handling
raise Exception(
"Source and target are not compatible for diff generation. "
"Source type: %s, target type: %s" %
(self.src_kind, self.tgt_kind))
def generate_diff(self):
buf = StringIO.StringIO()
if self.tgt_kind == svn.core.svn_node_dir:
self._generate_dir_diff(buf)
else:
self._generate_file_diff(buf)
return buf.getvalue()
def _generate_dir_diff(self, buf):
editor = DiffChangeEditor()
editor_ptr, editor_baton = svn.delta.make_editor(editor)
svn.repos.dir_delta2(
self.src_root,
self.src_path,
'', # src_entry
self.tgt_root,
self.tgt_path,
editor_ptr, editor_baton,
authorization_callback_allow_all,
False, # text_deltas
svn.core.svn_depth_infinity, # depth
False, # entry_props
False, # ignore_ancestry
)
for path, __, change in sorted(editor.changes):
self._generate_node_diff(
buf, change, path, self.tgt_path, path, self.src_path)
def _generate_file_diff(self, buf):
svn-diffs: use consisten behaviour on generting diffs between hg/git backends.
r147 change = None
if self.src_kind == svn.core.svn_node_none:
change = "add"
elif self.tgt_kind == svn.core.svn_node_none:
change = "delete"
tgt_base, tgt_path = vcspath.split(self.tgt_path)
src_base, src_path = vcspath.split(self.src_path)
self._generate_node_diff(
buf, change, tgt_path, tgt_base, src_path, src_base)
initial commit
r0
def _generate_node_diff(
self, buf, change, tgt_path, tgt_base, src_path, src_base):
svn-diffs: use consisten behaviour on generting diffs between hg/git backends.
r147
if self.src_rev == self.tgt_rev and tgt_base == src_base:
# makes consistent behaviour with git/hg to return empty diff if
# we compare same revisions
return
initial commit
r0 tgt_full_path = vcspath.join(tgt_base, tgt_path)
src_full_path = vcspath.join(src_base, src_path)
self.binary_content = False
mime_type = self._get_mime_type(tgt_full_path)
svn-diffs: use consisten behaviour on generting diffs between hg/git backends.
r147
initial commit
r0 if mime_type and not mime_type.startswith('text'):
self.binary_content = True
buf.write("=" * 67 + '\n')
buf.write("Cannot display: file marked as a binary type.\n")
buf.write("svn:mime-type = %s\n" % mime_type)
buf.write("Index: %s\n" % (tgt_path, ))
buf.write("=" * 67 + '\n')
buf.write("diff --git a/%(tgt_path)s b/%(tgt_path)s\n" % {
'tgt_path': tgt_path})
if change == 'add':
# TODO: johbo: SVN is missing a zero here compared to git
buf.write("new file mode 10644\n")
svn-diffs: use consisten behaviour on generting diffs between hg/git backends.
r147
#TODO(marcink): intro to binary detection of svn patches
# if self.binary_content:
# buf.write('GIT binary patch\n')
initial commit
r0 buf.write("--- /dev/null\t(revision 0)\n")
src_lines = []
else:
if change == 'delete':
buf.write("deleted file mode 10644\n")
svn-diffs: use consisten behaviour on generting diffs between hg/git backends.
r147
#TODO(marcink): intro to binary detection of svn patches
# if self.binary_content:
# buf.write('GIT binary patch\n')
initial commit
r0 buf.write("--- a/%s\t(revision %s)\n" % (
src_path, self.src_rev))
src_lines = self._svn_readlines(self.src_root, src_full_path)
if change == 'delete':
buf.write("+++ /dev/null\t(revision %s)\n" % (self.tgt_rev, ))
tgt_lines = []
else:
buf.write("+++ b/%s\t(revision %s)\n" % (
tgt_path, self.tgt_rev))
tgt_lines = self._svn_readlines(self.tgt_root, tgt_full_path)
if not self.binary_content:
udiff = svn_diff.unified_diff(
src_lines, tgt_lines, context=self.context,
ignore_blank_lines=self.ignore_whitespace,
ignore_case=False,
ignore_space_changes=self.ignore_whitespace)
buf.writelines(udiff)
def _get_mime_type(self, path):
try:
mime_type = svn.fs.node_prop(
self.tgt_root, path, svn.core.SVN_PROP_MIME_TYPE)
except svn.core.SubversionException:
mime_type = svn.fs.node_prop(
self.src_root, path, svn.core.SVN_PROP_MIME_TYPE)
return mime_type
def _svn_readlines(self, fs_root, node_path):
if self.binary_content:
return []
node_kind = svn.fs.check_path(fs_root, node_path)
if node_kind not in (
svn.core.svn_node_file, svn.core.svn_node_symlink):
return []
content = svn.core.Stream(
svn.fs.file_contents(fs_root, node_path)).read()
return content.splitlines(True)
class DiffChangeEditor(svn.delta.Editor):
"""
Records changes between two given revisions
"""
def __init__(self):
self.changes = []
def delete_entry(self, path, revision, parent_baton, pool=None):
self.changes.append((path, None, 'delete'))
def add_file(
self, path, parent_baton, copyfrom_path, copyfrom_revision,
file_pool=None):
self.changes.append((path, 'file', 'add'))
def open_file(self, path, parent_baton, base_revision, file_pool=None):
self.changes.append((path, 'file', 'change'))
def authorization_callback_allow_all(root, path, pool):
return True
class TxnNodeProcessor(object):
"""
Utility to process the change of one node within a transaction root.
It encapsulates the knowledge of how to add, update or remove
a node for a given transaction root. The purpose is to support the method
`SvnRemote.commit`.
"""
def __init__(self, node, txn_root):
assert isinstance(node['path'], str)
self.node = node
self.txn_root = txn_root
def update(self):
self._ensure_parent_dirs()
self._add_file_if_node_does_not_exist()
self._update_file_content()
self._update_file_properties()
def remove(self):
svn.fs.delete(self.txn_root, self.node['path'])
# TODO: Clean up directory if empty
def _ensure_parent_dirs(self):
curdir = vcspath.dirname(self.node['path'])
dirs_to_create = []
while not self._svn_path_exists(curdir):
dirs_to_create.append(curdir)
curdir = vcspath.dirname(curdir)
for curdir in reversed(dirs_to_create):
log.debug('Creating missing directory "%s"', curdir)
svn.fs.make_dir(self.txn_root, curdir)
def _svn_path_exists(self, path):
path_status = svn.fs.check_path(self.txn_root, path)
return path_status != svn.core.svn_node_none
def _add_file_if_node_does_not_exist(self):
kind = svn.fs.check_path(self.txn_root, self.node['path'])
if kind == svn.core.svn_node_none:
svn.fs.make_file(self.txn_root, self.node['path'])
def _update_file_content(self):
assert isinstance(self.node['content'], str)
handler, baton = svn.fs.apply_textdelta(
self.txn_root, self.node['path'], None, None)
svn.delta.svn_txdelta_send_string(self.node['content'], handler, baton)
def _update_file_properties(self):
properties = self.node.get('properties', {})
for key, value in properties.iteritems():
svn.fs.change_node_prop(
self.txn_root, self.node['path'], key, value)
def apr_time_t(timestamp):
"""
Convert a Python timestamp into APR timestamp type apr_time_t
"""
return timestamp * 1E6
def svn_opt_revision_value_t(num):
"""
Put `num` into a `svn_opt_revision_value_t` structure.
"""
value = svn.core.svn_opt_revision_value_t()
value.number = num
revision = svn.core.svn_opt_revision_t()
revision.kind = svn.core.svn_opt_revision_number
revision.value = value
return revision