##// END OF EJS Templates
caches: use repo.lru based Dict cache. This LRUDict uses Timing Algo to not have to use locking...
caches: use repo.lru based Dict cache. This LRUDict uses Timing Algo to not have to use locking for the LRU implementation, this it's safer to use for dogpile. We used it before with beaker, so it's generally more stable.

File last commit:

r2617:61a47f51 default
r2945:ec5716e4 default
Show More
repository.py
341 lines | 11.7 KiB | text/x-python | PythonLexer
# -*- coding: utf-8 -*-
# Copyright (C) 2014-2018 RhodeCode GmbH
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License, version 3
# (only), as published by the Free Software Foundation.
#
# 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 Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# This program is dual-licensed. If you wish to learn more about the
# RhodeCode Enterprise Edition, including its added features, Support services,
# and proprietary license terms, please see https://rhodecode.com/licenses/
"""
SVN repository module
"""
import logging
import os
import urllib
from zope.cachedescriptors.property import Lazy as LazyProperty
from rhodecode.lib.compat import OrderedDict
from rhodecode.lib.datelib import date_astimestamp
from rhodecode.lib.utils import safe_str, safe_unicode
from rhodecode.lib.vcs import connection, path as vcspath
from rhodecode.lib.vcs.backends import base
from rhodecode.lib.vcs.backends.svn.commit import (
SubversionCommit, _date_from_svn_properties)
from rhodecode.lib.vcs.backends.svn.diff import SubversionDiff
from rhodecode.lib.vcs.backends.svn.inmemory import SubversionInMemoryCommit
from rhodecode.lib.vcs.conf import settings
from rhodecode.lib.vcs.exceptions import (
CommitDoesNotExistError, EmptyRepositoryError, RepositoryError,
VCSError, NodeDoesNotExistError)
log = logging.getLogger(__name__)
class SubversionRepository(base.BaseRepository):
"""
Subversion backend implementation
.. important::
It is very important to distinguish the commit index and the commit id
which is assigned by Subversion. The first one is always handled as an
`int` by this implementation. The commit id assigned by Subversion on
the other side will always be a `str`.
There is a specific trap since the first commit will have the index
``0`` but the svn id will be ``"1"``.
"""
# Note: Subversion does not really have a default branch name.
DEFAULT_BRANCH_NAME = None
contact = base.BaseRepository.DEFAULT_CONTACT
description = base.BaseRepository.DEFAULT_DESCRIPTION
def __init__(self, repo_path, config=None, create=False, src_url=None,
**kwargs):
self.path = safe_str(os.path.abspath(repo_path))
self.config = config if config else self.get_default_config()
self._remote = connection.Svn(
self.path, self.config)
self._init_repo(create, src_url)
def _init_repo(self, create, src_url):
if create and os.path.exists(self.path):
raise RepositoryError(
"Cannot create repository at %s, location already exist"
% self.path)
if create:
self._remote.create_repository(settings.SVN_COMPATIBLE_VERSION)
if src_url:
src_url = _sanitize_url(src_url)
self._remote.import_remote_repository(src_url)
else:
self._check_path()
@LazyProperty
def commit_ids(self):
head = self._remote.lookup(None)
return [str(r) for r in xrange(1, head + 1)]
@LazyProperty
def branches(self):
return self._tags_or_branches('vcs_svn_branch')
@LazyProperty
def branches_closed(self):
return {}
@LazyProperty
def bookmarks(self):
return {}
@LazyProperty
def branches_all(self):
# TODO: johbo: Implement proper branch support
all_branches = {}
all_branches.update(self.branches)
all_branches.update(self.branches_closed)
return all_branches
@LazyProperty
def tags(self):
return self._tags_or_branches('vcs_svn_tag')
def _tags_or_branches(self, config_section):
found_items = {}
if self.is_empty():
return {}
for pattern in self._patterns_from_section(config_section):
pattern = vcspath.sanitize(pattern)
tip = self.get_commit()
try:
if pattern.endswith('*'):
basedir = tip.get_node(vcspath.dirname(pattern))
directories = basedir.dirs
else:
directories = (tip.get_node(pattern), )
except NodeDoesNotExistError:
continue
found_items.update(
(safe_unicode(n.path),
self.commit_ids[-1])
for n in directories)
def get_name(item):
return item[0]
return OrderedDict(sorted(found_items.items(), key=get_name))
def _patterns_from_section(self, section):
return (pattern for key, pattern in self.config.items(section))
def get_common_ancestor(self, commit_id1, commit_id2, repo2):
if self != repo2:
raise ValueError(
"Subversion does not support getting common ancestor of"
" different repositories.")
if int(commit_id1) < int(commit_id2):
return commit_id1
return commit_id2
def verify(self):
verify = self._remote.verify()
self._remote.invalidate_vcs_cache()
return verify
def compare(self, commit_id1, commit_id2, repo2, merge, pre_load=None):
# TODO: johbo: Implement better comparison, this is a very naive
# version which does not allow to compare branches, tags or folders
# at all.
if repo2 != self:
raise ValueError(
"Subversion does not support comparison of of different "
"repositories.")
if commit_id1 == commit_id2:
return []
commit_idx1 = self._get_commit_idx(commit_id1)
commit_idx2 = self._get_commit_idx(commit_id2)
commits = [
self.get_commit(commit_idx=idx)
for idx in range(commit_idx1 + 1, commit_idx2 + 1)]
return commits
def _get_commit_idx(self, commit_id):
try:
svn_rev = int(commit_id)
except:
# TODO: johbo: this might be only one case, HEAD, check this
svn_rev = self._remote.lookup(commit_id)
commit_idx = svn_rev - 1
if commit_idx >= len(self.commit_ids):
raise CommitDoesNotExistError(
"Commit at index %s does not exist." % (commit_idx, ))
return commit_idx
@staticmethod
def check_url(url, config):
"""
Check if `url` is a valid source to import a Subversion repository.
"""
# convert to URL if it's a local directory
if os.path.isdir(url):
url = 'file://' + urllib.pathname2url(url)
return connection.Svn.check_url(url, config.serialize())
@staticmethod
def is_valid_repository(path):
try:
SubversionRepository(path)
return True
except VCSError:
pass
return False
def _check_path(self):
if not os.path.exists(self.path):
raise VCSError('Path "%s" does not exist!' % (self.path, ))
if not self._remote.is_path_valid_repository(self.path):
raise VCSError(
'Path "%s" does not contain a Subversion repository' %
(self.path, ))
@LazyProperty
def last_change(self):
"""
Returns last change made on this repository as
`datetime.datetime` object.
"""
# Subversion always has a first commit which has id "0" and contains
# what we are looking for.
last_id = len(self.commit_ids)
properties = self._remote.revision_properties(last_id)
return _date_from_svn_properties(properties)
@LazyProperty
def in_memory_commit(self):
return SubversionInMemoryCommit(self)
def get_hook_location(self):
"""
returns absolute path to location where hooks are stored
"""
return os.path.join(self.path, 'hooks')
def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
if self.is_empty():
raise EmptyRepositoryError("There are no commits yet")
if commit_id is not None:
self._validate_commit_id(commit_id)
elif commit_idx is not None:
self._validate_commit_idx(commit_idx)
try:
commit_id = self.commit_ids[commit_idx]
except IndexError:
raise CommitDoesNotExistError
commit_id = self._sanitize_commit_id(commit_id)
commit = SubversionCommit(repository=self, commit_id=commit_id)
return commit
def get_commits(
self, start_id=None, end_id=None, start_date=None, end_date=None,
branch_name=None, show_hidden=False, pre_load=None):
if self.is_empty():
raise EmptyRepositoryError("There are no commit_ids yet")
self._validate_branch_name(branch_name)
if start_id is not None:
self._validate_commit_id(start_id)
if end_id is not None:
self._validate_commit_id(end_id)
start_raw_id = self._sanitize_commit_id(start_id)
start_pos = self.commit_ids.index(start_raw_id) if start_id else None
end_raw_id = self._sanitize_commit_id(end_id)
end_pos = max(0, self.commit_ids.index(end_raw_id)) if end_id else None
if None not in [start_id, end_id] and start_pos > end_pos:
raise RepositoryError(
"Start commit '%s' cannot be after end commit '%s'" %
(start_id, end_id))
if end_pos is not None:
end_pos += 1
# Date based filtering
if start_date or end_date:
start_raw_id, end_raw_id = self._remote.lookup_interval(
date_astimestamp(start_date) if start_date else None,
date_astimestamp(end_date) if end_date else None)
start_pos = start_raw_id - 1
end_pos = end_raw_id
commit_ids = self.commit_ids
# TODO: johbo: Reconsider impact of DEFAULT_BRANCH_NAME here
if branch_name not in [None, self.DEFAULT_BRANCH_NAME]:
svn_rev = long(self.commit_ids[-1])
commit_ids = self._remote.node_history(
path=branch_name, revision=svn_rev, limit=None)
commit_ids = [str(i) for i in reversed(commit_ids)]
if start_pos or end_pos:
commit_ids = commit_ids[start_pos:end_pos]
return base.CollectionGenerator(self, commit_ids, pre_load=pre_load)
def _sanitize_commit_id(self, commit_id):
if commit_id and commit_id.isdigit():
if int(commit_id) <= len(self.commit_ids):
return commit_id
else:
raise CommitDoesNotExistError(
"Commit %s does not exist." % (commit_id, ))
if commit_id not in [
None, 'HEAD', 'tip', self.DEFAULT_BRANCH_NAME]:
raise CommitDoesNotExistError(
"Commit id %s not understood." % (commit_id, ))
svn_rev = self._remote.lookup('HEAD')
return str(svn_rev)
def get_diff(
self, commit1, commit2, path=None, ignore_whitespace=False,
context=3, path1=None):
self._validate_diff_commits(commit1, commit2)
svn_rev1 = long(commit1.raw_id)
svn_rev2 = long(commit2.raw_id)
diff = self._remote.diff(
svn_rev1, svn_rev2, path1=path1, path2=path,
ignore_whitespace=ignore_whitespace, context=context)
return SubversionDiff(diff)
def _sanitize_url(url):
if '://' not in url:
url = 'file://' + urllib.pathname2url(url)
return url