You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@bloodhound.apache.org by gj...@apache.org on 2012/10/16 17:55:11 UTC
svn commit: r1398858 [32/33] - in /incubator/bloodhound/vendor/trac/current:
./ contrib/ doc/ doc/api/ doc/utils/ sample-plugins/
sample-plugins/permissions/ sample-plugins/workflow/ trac/ trac/admin/
trac/admin/templates/ trac/admin/tests/ trac/db/ tr...
Added: incubator/bloodhound/vendor/trac/current/tracopt/versioncontrol/svn/svn_fs.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/vendor/trac/current/tracopt/versioncontrol/svn/svn_fs.py?rev=1398858&view=auto
==============================================================================
--- incubator/bloodhound/vendor/trac/current/tracopt/versioncontrol/svn/svn_fs.py (added)
+++ incubator/bloodhound/vendor/trac/current/tracopt/versioncontrol/svn/svn_fs.py Tue Oct 16 15:55:00 2012
@@ -0,0 +1,1095 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2005-2011 Edgewall Software
+# Copyright (C) 2005 Christopher Lenz <cm...@gmx.de>
+# Copyright (C) 2005-2007 Christian Boos <cb...@edgewall.org>
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.org/wiki/TracLicense.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://trac.edgewall.org/log/.
+#
+# Author: Christopher Lenz <cm...@gmx.de>
+# Christian Boos <cb...@edgewall.org>
+
+"""
+
+Note about Unicode
+------------------
+
+The Subversion bindings are not unicode-aware and they expect to
+receive UTF-8 encoded `string` parameters,
+
+On the other hand, all paths manipulated by Trac are `unicode`
+objects.
+
+Therefore:
+
+ * before being handed out to SVN, the Trac paths have to be encoded
+ to UTF-8, using `_to_svn()`
+
+ * before being handed out to Trac, a SVN path has to be decoded from
+ UTF-8, using `_from_svn()`
+
+Whenever a value has to be stored as utf8, we explicitly mark the
+variable name with "_utf8", in order to avoid any possible confusion.
+
+Warning:
+ `SubversionNode.get_content()` returns an object from which one can
+ read a stream of bytes. NO guarantees can be given about what that
+ stream of bytes represents. It might be some text, encoded in some
+ way or another. SVN properties *might* give some hints about the
+ content, but they actually only reflect the beliefs of whomever set
+ those properties...
+"""
+
+import os.path
+import weakref
+import posixpath
+
+from trac.config import ListOption
+from trac.core import *
+from trac.env import ISystemInfoProvider
+from trac.versioncontrol import Changeset, Node, Repository, \
+ IRepositoryConnector, \
+ NoSuchChangeset, NoSuchNode
+from trac.versioncontrol.cache import CachedRepository
+from trac.util import embedded_numbers
+from trac.util.text import exception_to_unicode, to_unicode
+from trac.util.translation import _
+from trac.util.datefmt import from_utimestamp
+
+
+application_pool = None
+
+
+def _import_svn():
+ global fs, repos, core, delta, _kindmap
+ from svn import fs, repos, core, delta
+ _kindmap = {core.svn_node_dir: Node.DIRECTORY,
+ core.svn_node_file: Node.FILE}
+ # Protect svn.core methods from GC
+ Pool.apr_pool_clear = staticmethod(core.apr_pool_clear)
+ Pool.apr_pool_destroy = staticmethod(core.apr_pool_destroy)
+
+def _to_svn(pool, *args):
+ """Expect a pool and a list of `unicode` path components.
+
+ Returns an UTF-8 encoded string suitable for the Subversion python
+ bindings (the returned path never starts with a leading "/")
+ """
+ return core.svn_path_canonicalize('/'.join(args).lstrip('/')
+ .encode('utf-8'),
+ pool)
+
+def _from_svn(path):
+ """Expect an UTF-8 encoded string and transform it to an `unicode` object
+
+ But Subversion repositories built from conversion utilities can have
+ non-UTF-8 byte strings, so we have to convert using `to_unicode`.
+ """
+ return path and to_unicode(path, 'utf-8')
+
+# The following 3 helpers deal with unicode paths
+
+def _normalize_path(path):
+ """Remove leading "/", except for the root."""
+ return path and path.strip('/') or '/'
+
+def _path_within_scope(scope, fullpath):
+ """Remove the leading scope from repository paths.
+
+ Return `None` if the path is not is scope.
+ """
+ if fullpath is not None:
+ fullpath = fullpath.lstrip('/')
+ if scope == '/':
+ return _normalize_path(fullpath)
+ scope = scope.strip('/')
+ if (fullpath + '/').startswith(scope + '/'):
+ return fullpath[len(scope) + 1:] or '/'
+
+def _is_path_within_scope(scope, fullpath):
+ """Check whether the given `fullpath` is within the given `scope`"""
+ if scope == '/':
+ return fullpath is not None
+ fullpath = fullpath.lstrip('/') if fullpath else ''
+ scope = scope.strip('/')
+ return (fullpath + '/').startswith(scope + '/')
+
+# svn_opt_revision_t helpers
+
+def _svn_rev(num):
+ value = core.svn_opt_revision_value_t()
+ value.number = num
+ revision = core.svn_opt_revision_t()
+ revision.kind = core.svn_opt_revision_number
+ revision.value = value
+ return revision
+
+def _svn_head():
+ revision = core.svn_opt_revision_t()
+ revision.kind = core.svn_opt_revision_head
+ return revision
+
+# apr_pool_t helpers
+
+def _mark_weakpool_invalid(weakpool):
+ if weakpool():
+ weakpool()._mark_invalid()
+
+
+class Pool(object):
+ """A Pythonic memory pool object"""
+
+ def __init__(self, parent_pool=None):
+ """Create a new memory pool"""
+
+ global application_pool
+ self._parent_pool = parent_pool or application_pool
+
+ # Create pool
+ if self._parent_pool:
+ self._pool = core.svn_pool_create(self._parent_pool())
+ else:
+ # If we are an application-level pool,
+ # then initialize APR and set this pool
+ # to be the application-level pool
+ core.apr_initialize()
+ application_pool = self
+
+ self._pool = core.svn_pool_create(None)
+ self._mark_valid()
+
+ def __call__(self):
+ return self._pool
+
+ def valid(self):
+ """Check whether this memory pool and its parents
+ are still valid"""
+ return hasattr(self,"_is_valid")
+
+ def assert_valid(self):
+ """Assert that this memory_pool is still valid."""
+ assert self.valid()
+
+ def clear(self):
+ """Clear embedded memory pool. Invalidate all subpools."""
+ self.apr_pool_clear(self._pool)
+ self._mark_valid()
+
+ def destroy(self):
+ """Destroy embedded memory pool. If you do not destroy
+ the memory pool manually, Python will destroy it
+ automatically."""
+
+ global application_pool
+
+ self.assert_valid()
+
+ # Destroy pool
+ self.apr_pool_destroy(self._pool)
+
+ # Clear application pool and terminate APR if necessary
+ if not self._parent_pool:
+ application_pool = None
+
+ self._mark_invalid()
+
+ def __del__(self):
+ """Automatically destroy memory pools, if necessary"""
+ if self.valid():
+ self.destroy()
+
+ def _mark_valid(self):
+ """Mark pool as valid"""
+ if self._parent_pool:
+ # Refer to self using a weakreference so that we don't
+ # create a reference cycle
+ weakself = weakref.ref(self)
+
+ # Set up callbacks to mark pool as invalid when parents
+ # are destroyed
+ self._weakref = weakref.ref(self._parent_pool._is_valid,
+ lambda x: \
+ _mark_weakpool_invalid(weakself))
+
+ # mark pool as valid
+ self._is_valid = lambda: 1
+
+ def _mark_invalid(self):
+ """Mark pool as invalid"""
+ if self.valid():
+ # Mark invalid
+ del self._is_valid
+
+ # Free up memory
+ del self._parent_pool
+ if hasattr(self, "_weakref"):
+ del self._weakref
+
+
+class SvnCachedRepository(CachedRepository):
+ """Subversion-specific cached repository, zero-pads revision numbers
+ in the cache tables.
+ """
+ has_linear_changesets = True
+
+ def db_rev(self, rev):
+ return '%010d' % rev
+
+ def rev_db(self, rev):
+ return int(rev or 0)
+
+
+class SubversionConnector(Component):
+
+ implements(ISystemInfoProvider, IRepositoryConnector)
+
+ branches = ListOption('svn', 'branches', 'trunk, branches/*', doc=
+ """Comma separated list of paths categorized as branches.
+ If a path ends with '*', then all the directory entries found below
+ that path will be included.
+ Example: `/trunk, /branches/*, /projectAlpha/trunk, /sandbox/*`
+ """)
+
+ tags = ListOption('svn', 'tags', 'tags/*', doc=
+ """Comma separated list of paths categorized as tags.
+
+ If a path ends with '*', then all the directory entries found below
+ that path will be included.
+ Example: `/tags/*, /projectAlpha/tags/A-1.0, /projectAlpha/tags/A-v1.1`
+ """)
+
+ error = None
+
+ def __init__(self):
+ self._version = None
+ try:
+ _import_svn()
+ self.log.debug('Subversion bindings imported')
+ except ImportError, e:
+ self.error = e
+ self.log.info('Failed to load Subversion bindings', exc_info=True)
+ else:
+ version = (core.SVN_VER_MAJOR, core.SVN_VER_MINOR,
+ core.SVN_VER_MICRO)
+ self._version = '%d.%d.%d' % version + core.SVN_VER_TAG
+ if version[0] < 1:
+ self.error = _("Subversion >= 1.0 required, found %(version)s",
+ version=self._version)
+ Pool()
+
+ # ISystemInfoProvider methods
+
+ def get_system_info(self):
+ if self._version is not None:
+ yield 'Subversion', self._version
+
+ # IRepositoryConnector methods
+
+ def get_supported_types(self):
+ prio = 1
+ if self.error:
+ prio = -1
+ yield ("direct-svnfs", prio * 4)
+ yield ("svnfs", prio * 4)
+ yield ("svn", prio * 2)
+
+ def get_repository(self, type, dir, params):
+ """Return a `SubversionRepository`.
+
+ The repository is wrapped in a `CachedRepository`, unless `type` is
+ 'direct-svnfs'.
+ """
+ params.update(tags=self.tags, branches=self.branches)
+ repos = SubversionRepository(dir, params, self.log)
+ if type != 'direct-svnfs':
+ repos = SvnCachedRepository(self.env, repos, self.log)
+ return repos
+
+
+class SubversionRepository(Repository):
+ """Repository implementation based on the svn.fs API."""
+
+ has_linear_changesets = True
+
+ def __init__(self, path, params, log):
+ self.log = log
+ self.pool = Pool()
+
+ # Remove any trailing slash or else subversion might abort
+ if isinstance(path, unicode):
+ path_utf8 = path.encode('utf-8')
+ else: # note that this should usually not happen (unicode arg expected)
+ path_utf8 = to_unicode(path).encode('utf-8')
+
+ path_utf8 = os.path.normpath(path_utf8).replace('\\', '/')
+ self.path = path_utf8.decode('utf-8')
+
+ root_path_utf8 = repos.svn_repos_find_root_path(path_utf8, self.pool())
+ if root_path_utf8 is None:
+ raise TracError(_("%(path)s does not appear to be a Subversion "
+ "repository.", path=to_unicode(path_utf8)))
+
+ try:
+ self.repos = repos.svn_repos_open(root_path_utf8, self.pool())
+ except core.SubversionException, e:
+ raise TracError(_("Couldn't open Subversion repository %(path)s: "
+ "%(svn_error)s", path=to_unicode(path_utf8),
+ svn_error=exception_to_unicode(e)))
+ self.fs_ptr = repos.svn_repos_fs(self.repos)
+
+ self.uuid = fs.get_uuid(self.fs_ptr, self.pool())
+ self.base = 'svn:%s:%s' % (self.uuid, _from_svn(root_path_utf8))
+ name = 'svn:%s:%s' % (self.uuid, self.path)
+
+ Repository.__init__(self, name, params, log)
+
+ # if root_path_utf8 is shorter than the path_utf8, the difference is
+ # this scope (which always starts with a '/')
+ if root_path_utf8 != path_utf8:
+ self.scope = path_utf8[len(root_path_utf8):].decode('utf-8')
+ if not self.scope[-1] == '/':
+ self.scope += '/'
+ else:
+ self.scope = '/'
+ assert self.scope[0] == '/'
+ # we keep root_path_utf8 for RA
+ ra_prefix = 'file:///' if os.name == 'nt' else 'file://'
+ self.ra_url_utf8 = ra_prefix + root_path_utf8
+ self.clear()
+
+ def clear(self, youngest_rev=None):
+ """Reset notion of `youngest` and `oldest`"""
+ self.youngest = None
+ if youngest_rev is not None:
+ self.youngest = self.normalize_rev(youngest_rev)
+ self.oldest = None
+
+ def __del__(self):
+ self.close()
+
+ def has_node(self, path, rev=None, pool=None):
+ """Check if `path` exists at `rev` (or latest if unspecified)"""
+ if not pool:
+ pool = self.pool
+ rev = self.normalize_rev(rev)
+ rev_root = fs.revision_root(self.fs_ptr, rev, pool())
+ node_type = fs.check_path(rev_root, _to_svn(pool(), self.scope, path),
+ pool())
+ return node_type in _kindmap
+
+ def normalize_path(self, path):
+ """Take any path specification and produce a path suitable for
+ the rest of the API
+ """
+ return _normalize_path(path)
+
+ def normalize_rev(self, rev):
+ """Take any revision specification and produce a revision suitable
+ for the rest of the API
+ """
+ if rev is None or isinstance(rev, basestring) and \
+ rev.lower() in ('', 'head', 'latest', 'youngest'):
+ return self.youngest_rev
+ else:
+ try:
+ rev = int(rev)
+ if rev <= self.youngest_rev:
+ return rev
+ except (ValueError, TypeError):
+ pass
+ raise NoSuchChangeset(rev)
+
+ def close(self):
+ """Dispose of low-level resources associated to this repository."""
+ if self.pool:
+ self.pool.destroy()
+ self.repos = self.fs_ptr = self.pool = None
+
+ def get_base(self):
+ """Retrieve the base path corresponding to the Subversion
+ repository itself.
+
+ This is the same as the `.path` property minus the
+ intra-repository scope, if one was specified.
+ """
+ return self.base
+
+ def _get_tags_or_branches(self, paths):
+ """Retrieve known branches or tags."""
+ for path in self.params.get(paths, []):
+ if path.endswith('*'):
+ folder = posixpath.dirname(path)
+ try:
+ entries = [n for n in self.get_node(folder).get_entries()]
+ for node in sorted(entries, key=lambda n:
+ embedded_numbers(n.path.lower())):
+ if node.kind == Node.DIRECTORY:
+ yield node
+ except Exception: # no right (TODO: use a specific Exception)
+ pass
+ else:
+ try:
+ yield self.get_node(path)
+ except Exception: # no right
+ pass
+
+ def get_quickjump_entries(self, rev):
+ """Retrieve known branches, as (name, id) pairs.
+
+ Purposedly ignores `rev` and always takes the last revision.
+ """
+ for n in self._get_tags_or_branches('branches'):
+ yield 'branches', n.path, n.path, None
+ for n in self._get_tags_or_branches('tags'):
+ yield 'tags', n.path, n.created_path, n.created_rev
+
+ def get_path_url(self, path, rev):
+ """Retrieve the "native" URL from which this repository is reachable
+ from Subversion clients.
+ """
+ url = self.params.get('url', '').rstrip('/')
+ if url:
+ if not path or path == '/':
+ return url
+ return url + '/' + path.lstrip('/')
+
+ def get_changeset(self, rev):
+ """Produce a `SubversionChangeset` from given revision
+ specification"""
+ rev = self.normalize_rev(rev)
+ return SubversionChangeset(self, rev, self.scope, self.pool)
+
+ def get_changeset_uid(self, rev):
+ """Build a value identifying the `rev` in this repository."""
+ return (self.uuid, rev)
+
+ def get_node(self, path, rev=None):
+ """Produce a `SubversionNode` from given path and optionally revision
+ specifications. No revision given means use the latest.
+ """
+ path = path or ''
+ if path and path[-1] == '/':
+ path = path[:-1]
+ rev = self.normalize_rev(rev) or self.youngest_rev
+ return SubversionNode(path, rev, self, self.pool)
+
+ def _get_node_revs(self, path, last=None, first=None):
+ """Return the revisions affecting `path` between `first` and `last`
+ revs. If `first` is not given, it goes down to the revision in which
+ the branch was created.
+ """
+ node = self.get_node(path, last)
+ revs = []
+ for (p, r, chg) in node.get_history():
+ if p != path or (first and r < first):
+ break
+ revs.append(r)
+ return revs
+
+ def _history(self, path, start, end, pool):
+ """`path` is a unicode path in the scope.
+
+ Generator yielding `(path, rev)` pairs, where `path` is an `unicode`
+ object. Must start with `(path, created rev)`.
+
+ (wraps ``fs.node_history``)
+ """
+ path_utf8 = _to_svn(pool(), self.scope, path)
+ if start < end:
+ start, end = end, start
+ if (start, end) == (1, 0): # only happens for empty repos
+ return
+ root = fs.revision_root(self.fs_ptr, start, pool())
+ # fs.node_history leaks when path doesn't exist (#6588)
+ if fs.check_path(root, path_utf8, pool()) == core.svn_node_none:
+ return
+ tmp1 = Pool(pool)
+ tmp2 = Pool(pool)
+ history_ptr = fs.node_history(root, path_utf8, tmp1())
+ cross_copies = 1
+ while history_ptr:
+ history_ptr = fs.history_prev(history_ptr, cross_copies, tmp2())
+ tmp1.clear()
+ tmp1, tmp2 = tmp2, tmp1
+ if history_ptr:
+ path_utf8, rev = fs.history_location(history_ptr, tmp2())
+ tmp2.clear()
+ if rev < end:
+ break
+ path = _from_svn(path_utf8)
+ yield path, rev
+ del tmp1
+ del tmp2
+
+ def _previous_rev(self, rev, path='', pool=None):
+ if rev > 1: # don't use oldest here, as it's too expensive
+ for _, prev in self._history(path, 1, rev-1, pool or self.pool):
+ return prev
+ return None
+
+
+ def get_oldest_rev(self):
+ """Gives an approximation of the oldest revision."""
+ if self.oldest is None:
+ self.oldest = 1
+ # trying to figure out the oldest rev for scoped repository
+ # is too expensive and uncovers a big memory leak (#5213)
+ # if self.scope != '/':
+ # self.oldest = self.next_rev(0, find_initial_rev=True)
+ return self.oldest
+
+ def get_youngest_rev(self):
+ """Retrieve the latest revision in the repository.
+
+ (wraps ``fs.youngest_rev``)
+ """
+ if not self.youngest:
+ self.youngest = fs.youngest_rev(self.fs_ptr, self.pool())
+ if self.scope != '/':
+ for path, rev in self._history('', 1, self.youngest, self.pool):
+ self.youngest = rev
+ break
+ return self.youngest
+
+ def previous_rev(self, rev, path=''):
+ """Return revision immediately preceeding `rev`, eventually below
+ given `path` or globally.
+ """
+ # FIXME optimize for non-scoped
+ rev = self.normalize_rev(rev)
+ return self._previous_rev(rev, path)
+
+ def next_rev(self, rev, path='', find_initial_rev=False):
+ """Return revision immediately following `rev`, eventually below
+ given `path` or globally.
+ """
+ rev = self.normalize_rev(rev)
+ next = rev + 1
+ youngest = self.youngest_rev
+ subpool = Pool(self.pool)
+ while next <= youngest:
+ subpool.clear()
+ for _, next in self._history(path, rev+1, next, subpool):
+ return next
+ else:
+ if not find_initial_rev and \
+ not self.has_node(path, next, subpool):
+ return next # a 'delete' event is also interesting...
+ next += 1
+ return None
+
+ def rev_older_than(self, rev1, rev2):
+ """Check relative order between two revision specifications."""
+ return self.normalize_rev(rev1) < self.normalize_rev(rev2)
+
+ def get_path_history(self, path, rev=None, limit=None):
+ """Retrieve creation and deletion events that happened on
+ given `path`.
+ """
+ path = self.normalize_path(path)
+ rev = self.normalize_rev(rev)
+ expect_deletion = False
+ subpool = Pool(self.pool)
+ numrevs = 0
+ while rev and (not limit or numrevs < limit):
+ subpool.clear()
+ if self.has_node(path, rev, subpool):
+ if expect_deletion:
+ # it was missing, now it's there again:
+ # rev+1 must be a delete
+ numrevs += 1
+ yield path, rev+1, Changeset.DELETE
+ newer = None # 'newer' is the previously seen history tuple
+ older = None # 'older' is the currently examined history tuple
+ for p, r in self._history(path, 1, rev, subpool):
+ older = (_path_within_scope(self.scope, p), r,
+ Changeset.ADD)
+ rev = self._previous_rev(r, pool=subpool)
+ if newer:
+ numrevs += 1
+ if older[0] == path:
+ # still on the path: 'newer' was an edit
+ yield newer[0], newer[1], Changeset.EDIT
+ else:
+ # the path changed: 'newer' was a copy
+ rev = self._previous_rev(newer[1], pool=subpool)
+ # restart before the copy op
+ yield newer[0], newer[1], Changeset.COPY
+ older = (older[0], older[1], 'unknown')
+ break
+ newer = older
+ if older:
+ # either a real ADD or the source of a COPY
+ numrevs += 1
+ yield older
+ else:
+ expect_deletion = True
+ rev = self._previous_rev(rev, pool=subpool)
+
+ def get_changes(self, old_path, old_rev, new_path, new_rev,
+ ignore_ancestry=0):
+ """Determine differences between two arbitrary pairs of paths
+ and revisions.
+
+ (wraps ``repos.svn_repos_dir_delta``)
+ """
+ def key(value):
+ return value[1].path if value[1] is not None else value[0].path
+ return iter(sorted(self._get_changes(old_path, old_rev, new_path,
+ new_rev, ignore_ancestry),
+ key=key))
+
+ def _get_changes(self, old_path, old_rev, new_path, new_rev,
+ ignore_ancestry):
+ old_node = new_node = None
+ old_rev = self.normalize_rev(old_rev)
+ new_rev = self.normalize_rev(new_rev)
+ if self.has_node(old_path, old_rev):
+ old_node = self.get_node(old_path, old_rev)
+ else:
+ raise NoSuchNode(old_path, old_rev, 'The Base for Diff is invalid')
+ if self.has_node(new_path, new_rev):
+ new_node = self.get_node(new_path, new_rev)
+ else:
+ raise NoSuchNode(new_path, new_rev,
+ 'The Target for Diff is invalid')
+ if new_node.kind != old_node.kind:
+ raise TracError(_('Diff mismatch: Base is a %(oldnode)s '
+ '(%(oldpath)s in revision %(oldrev)s) and '
+ 'Target is a %(newnode)s (%(newpath)s in '
+ 'revision %(newrev)s).', oldnode=old_node.kind,
+ oldpath=old_path, oldrev=old_rev,
+ newnode=new_node.kind, newpath=new_path,
+ newrev=new_rev))
+ subpool = Pool(self.pool)
+ if new_node.isdir:
+ editor = DiffChangeEditor()
+ e_ptr, e_baton = delta.make_editor(editor, subpool())
+ old_root = fs.revision_root(self.fs_ptr, old_rev, subpool())
+ new_root = fs.revision_root(self.fs_ptr, new_rev, subpool())
+ def authz_cb(root, path, pool):
+ return 1
+ text_deltas = 0 # as this is anyway re-done in Diff.py...
+ entry_props = 0 # "... typically used only for working copy updates"
+ repos.svn_repos_dir_delta(old_root,
+ _to_svn(subpool(), self.scope, old_path),
+ '', new_root,
+ _to_svn(subpool(), self.scope, new_path),
+ e_ptr, e_baton, authz_cb,
+ text_deltas,
+ 1, # directory
+ entry_props,
+ ignore_ancestry,
+ subpool())
+ for path, kind, change in editor.deltas:
+ path = _from_svn(path)
+ old_node = new_node = None
+ if change != Changeset.ADD:
+ old_node = self.get_node(posixpath.join(old_path, path),
+ old_rev)
+ if change != Changeset.DELETE:
+ new_node = self.get_node(posixpath.join(new_path, path),
+ new_rev)
+ else:
+ kind = _kindmap[fs.check_path(old_root,
+ _to_svn(subpool(),
+ self.scope,
+ old_node.path),
+ subpool())]
+ yield (old_node, new_node, kind, change)
+ else:
+ old_root = fs.revision_root(self.fs_ptr, old_rev, subpool())
+ new_root = fs.revision_root(self.fs_ptr, new_rev, subpool())
+ if fs.contents_changed(old_root,
+ _to_svn(subpool(), self.scope, old_path),
+ new_root,
+ _to_svn(subpool(), self.scope, new_path),
+ subpool()):
+ yield (old_node, new_node, Node.FILE, Changeset.EDIT)
+
+
+class SubversionNode(Node):
+
+ def __init__(self, path, rev, repos, pool=None, parent_root=None):
+ self.fs_ptr = repos.fs_ptr
+ self.scope = repos.scope
+ self.pool = Pool(pool)
+ pool = self.pool()
+ self._scoped_path_utf8 = _to_svn(pool, self.scope, path)
+
+ if parent_root:
+ self.root = parent_root
+ else:
+ self.root = fs.revision_root(self.fs_ptr, rev, pool)
+ node_type = fs.check_path(self.root, self._scoped_path_utf8, pool)
+ if not node_type in _kindmap:
+ raise NoSuchNode(path, rev)
+ cp_utf8 = fs.node_created_path(self.root, self._scoped_path_utf8, pool)
+ cp = _from_svn(cp_utf8)
+ cr = fs.node_created_rev(self.root, self._scoped_path_utf8, pool)
+ # Note: `cp` differs from `path` if the last change was a copy,
+ # In that case, `path` doesn't even exist at `cr`.
+ # The only guarantees are:
+ # * this node exists at (path,rev)
+ # * the node existed at (created_path,created_rev)
+ # Also, `cp` might well be out of the scope of the repository,
+ # in this case, we _don't_ use the ''create'' information.
+ if _is_path_within_scope(self.scope, cp):
+ self.created_rev = cr
+ self.created_path = _path_within_scope(self.scope, cp)
+ else:
+ self.created_rev, self.created_path = rev, path
+ # TODO: check node id
+ Node.__init__(self, repos, path, rev, _kindmap[node_type])
+
+ def get_content(self):
+ """Retrieve raw content as a "read()"able object."""
+ if self.isdir:
+ return None
+ s = core.Stream(fs.file_contents(self.root, self._scoped_path_utf8,
+ self.pool()))
+ # The stream object needs to reference the pool to make sure the pool
+ # is not destroyed before the former.
+ s._pool = self.pool
+ return s
+
+ def get_entries(self):
+ """Yield `SubversionNode` corresponding to entries in this directory.
+
+ (wraps ``fs.dir_entries``)
+ """
+ if self.isfile:
+ return
+ pool = Pool(self.pool)
+ entries = fs.dir_entries(self.root, self._scoped_path_utf8, pool())
+ for item in entries.keys():
+ path = posixpath.join(self.path, _from_svn(item))
+ yield SubversionNode(path, self.rev, self.repos, self.pool,
+ self.root)
+
+ def get_history(self, limit=None):
+ """Yield change events that happened on this path"""
+ newer = None # 'newer' is the previously seen history tuple
+ older = None # 'older' is the currently examined history tuple
+ pool = Pool(self.pool)
+ numrevs = 0
+ for path, rev in self.repos._history(self.path, 1, self.rev, pool):
+ path = _path_within_scope(self.scope, path)
+ if rev > 0 and path:
+ older = (path, rev, Changeset.ADD)
+ if newer:
+ if newer[0] == older[0]: # stay on same path
+ change = Changeset.EDIT
+ else:
+ change = Changeset.COPY
+ newer = (newer[0], newer[1], change)
+ numrevs += 1
+ yield newer
+ newer = older
+ if limit and numrevs >= limit:
+ break
+ if newer and (not limit or numrevs < limit):
+ yield newer
+
+ def get_annotations(self):
+ """Return a list the last changed revision for each line.
+ (wraps ``client.blame2``)
+ """
+ annotations = []
+ if self.isfile:
+ def blame_receiver(line_no, revision, author, date, line, pool):
+ annotations.append(revision)
+ try:
+ rev = _svn_rev(self.rev)
+ start = _svn_rev(0)
+ file_url_utf8 = posixpath.join(self.repos.ra_url_utf8,
+ self._scoped_path_utf8)
+ self.repos.log.info('opening ra_local session to %r',
+ file_url_utf8)
+ from svn import client
+ client.blame2(file_url_utf8, rev, start, rev, blame_receiver,
+ client.create_context(), self.pool())
+ except (core.SubversionException, AttributeError), e:
+ # svn thinks file is a binary or blame not supported
+ raise TracError(_('svn blame failed on %(path)s: %(error)s',
+ path=self.path, error=to_unicode(e)))
+ return annotations
+
+# def get_previous(self):
+# # FIXME: redo it with fs.node_history
+
+ def get_properties(self):
+ """Return `dict` of node properties at current revision.
+
+ (wraps ``fs.node_proplist``)
+ """
+ props = fs.node_proplist(self.root, self._scoped_path_utf8, self.pool())
+ for name, value in props.items():
+ # Note that property values can be arbitrary binary values
+ # so we can't assume they are UTF-8 strings...
+ props[_from_svn(name)] = to_unicode(value)
+ return props
+
+ def get_content_length(self):
+ """Retrieve byte size of a file.
+
+ Return `None` for a folder. (wraps ``fs.file_length``)
+ """
+ if self.isdir:
+ return None
+ return fs.file_length(self.root, self._scoped_path_utf8, self.pool())
+
+ def get_content_type(self):
+ """Retrieve mime-type property of a file.
+
+ Return `None` for a folder. (wraps ``fs.revision_prop``)
+ """
+ if self.isdir:
+ return None
+ return self._get_prop(core.SVN_PROP_MIME_TYPE)
+
+ def get_last_modified(self):
+ """Retrieve timestamp of last modification, in micro-seconds.
+
+ (wraps ``fs.revision_prop``)
+ """
+ _date = fs.revision_prop(self.fs_ptr, self.created_rev,
+ core.SVN_PROP_REVISION_DATE, self.pool())
+ if not _date:
+ return None
+ return from_utimestamp(core.svn_time_from_cstring(_date, self.pool()))
+
+ def _get_prop(self, name):
+ return fs.node_prop(self.root, self._scoped_path_utf8, name,
+ self.pool())
+
+ def get_branch_origin(self):
+ """Return the revision in which the node's path was created.
+
+ (wraps ``fs.revision_root_revision(fs.closest_copy)``)
+ """
+ root_and_path = fs.closest_copy(self.root, self._scoped_path_utf8)
+ if root_and_path:
+ return fs.revision_root_revision(root_and_path[0])
+
+ def get_copy_ancestry(self):
+ """Retrieve the list of `(path,rev)` copy ancestors of this node.
+ Most recent ancestor first. Each ancestor `(path, rev)` corresponds
+ to the path and revision of the source at the time the copy or move
+ operation was performed.
+ """
+ ancestors = []
+ previous = (self._scoped_path_utf8, self.rev, self.root)
+ while previous:
+ (previous_path, previous_rev, previous_root) = previous
+ previous = None
+ root_path = fs.closest_copy(previous_root, previous_path)
+ if root_path:
+ (root, path) = root_path
+ path = path.lstrip('/')
+ rev = fs.revision_root_revision(root)
+ relpath = None
+ if path != previous_path:
+ # `previous_path` is a subfolder of `path` and didn't
+ # change since `path` was copied
+ relpath = previous_path[len(path):].strip('/')
+ copied_from = fs.copied_from(root, path)
+ if copied_from:
+ (rev, path) = copied_from
+ path = path.lstrip('/')
+ root = fs.revision_root(self.fs_ptr, rev, self.pool())
+ if relpath:
+ path += '/' + relpath
+ ui_path = _path_within_scope(self.scope, _from_svn(path))
+ if ui_path:
+ ancestors.append((ui_path, rev))
+ previous = (path, rev, root)
+ return ancestors
+
+
+class SubversionChangeset(Changeset):
+
+ def __init__(self, repos, rev, scope, pool=None):
+ self.rev = rev
+ self.scope = scope
+ self.fs_ptr = repos.fs_ptr
+ self.pool = Pool(pool)
+ try:
+ message = self._get_prop(core.SVN_PROP_REVISION_LOG)
+ except core.SubversionException:
+ raise NoSuchChangeset(rev)
+ author = self._get_prop(core.SVN_PROP_REVISION_AUTHOR)
+ # we _hope_ it's UTF-8, but can't be 100% sure (#4321)
+ message = message and to_unicode(message, 'utf-8')
+ author = author and to_unicode(author, 'utf-8')
+ _date = self._get_prop(core.SVN_PROP_REVISION_DATE)
+ if _date:
+ ts = core.svn_time_from_cstring(_date, self.pool())
+ date = from_utimestamp(ts)
+ else:
+ date = None
+ Changeset.__init__(self, repos, rev, message, author, date)
+
+ def get_properties(self):
+ """Retrieve `dict` of Subversion properties for this revision
+ (revprops)
+ """
+ props = fs.revision_proplist(self.fs_ptr, self.rev, self.pool())
+ properties = {}
+ for k, v in props.iteritems():
+ if k not in (core.SVN_PROP_REVISION_LOG,
+ core.SVN_PROP_REVISION_AUTHOR,
+ core.SVN_PROP_REVISION_DATE):
+ properties[k] = to_unicode(v)
+ # Note: the above `to_unicode` has a small probability
+ # to mess-up binary properties, like icons.
+ return properties
+
+ def get_changes(self):
+ """Retrieve file changes for a given revision.
+
+ (wraps ``repos.svn_repos_replay``)
+ """
+ pool = Pool(self.pool)
+ tmp = Pool(pool)
+ root = fs.revision_root(self.fs_ptr, self.rev, pool())
+ editor = repos.RevisionChangeCollector(self.fs_ptr, self.rev, pool())
+ e_ptr, e_baton = delta.make_editor(editor, pool())
+ repos.svn_repos_replay(root, e_ptr, e_baton, pool())
+
+ idx = 0
+ copies, deletions = {}, {}
+ changes = []
+ revroots = {}
+ for path_utf8, change in editor.changes.items():
+ new_path = _from_svn(path_utf8)
+
+ # Filtering on `path`
+ if not _is_path_within_scope(self.scope, new_path):
+ continue
+
+ path_utf8 = change.path
+ base_path_utf8 = change.base_path
+ path = _from_svn(path_utf8)
+ base_path = _from_svn(base_path_utf8)
+ base_rev = change.base_rev
+ change_action = getattr(change, 'action', None)
+
+ # Ensure `base_path` is within the scope
+ if not _is_path_within_scope(self.scope, base_path):
+ base_path, base_rev = None, -1
+
+ # Determine the action
+ if not path and not new_path and self.scope == '/':
+ action = Changeset.EDIT # root property change
+ elif not path or (change_action is not None
+ and change_action == repos.CHANGE_ACTION_DELETE):
+ if new_path: # deletion
+ action = Changeset.DELETE
+ deletions[new_path.lstrip('/')] = idx
+ else: # deletion outside of scope, ignore
+ continue
+ elif change.added or not base_path: # add or copy
+ action = Changeset.ADD
+ if base_path and base_rev:
+ action = Changeset.COPY
+ copies[base_path.lstrip('/')] = idx
+ else:
+ action = Changeset.EDIT
+ # identify the most interesting base_path/base_rev
+ # in terms of last changed information (see r2562)
+ if revroots.has_key(base_rev):
+ b_root = revroots[base_rev]
+ else:
+ b_root = fs.revision_root(self.fs_ptr, base_rev, pool())
+ revroots[base_rev] = b_root
+ tmp.clear()
+ cbase_path_utf8 = fs.node_created_path(b_root, base_path_utf8,
+ tmp())
+ cbase_path = _from_svn(cbase_path_utf8)
+ cbase_rev = fs.node_created_rev(b_root, base_path_utf8, tmp())
+ # give up if the created path is outside the scope
+ if _is_path_within_scope(self.scope, cbase_path):
+ base_path, base_rev = cbase_path, cbase_rev
+
+ kind = _kindmap[change.item_kind]
+ path = _path_within_scope(self.scope, new_path or base_path)
+ base_path = _path_within_scope(self.scope, base_path)
+ changes.append([path, kind, action, base_path, base_rev])
+ idx += 1
+
+ moves = []
+ # a MOVE is a COPY whose `base_path` corresponds to a `new_path`
+ # which has been deleted
+ for k, v in copies.items():
+ if k in deletions:
+ changes[v][2] = Changeset.MOVE
+ moves.append(deletions[k])
+ offset = 0
+ moves.sort()
+ for i in moves:
+ del changes[i - offset]
+ offset += 1
+
+ changes.sort()
+ for change in changes:
+ yield tuple(change)
+
+ def _get_prop(self, name):
+ return fs.revision_prop(self.fs_ptr, self.rev, name, self.pool())
+
+
+#
+# Delta editor for diffs between arbitrary nodes
+#
+# Note 1: the 'copyfrom_path' and 'copyfrom_rev' information is not used
+# because 'repos.svn_repos_dir_delta' *doesn't* provide it.
+#
+# Note 2: the 'dir_baton' is the path of the parent directory
+#
+
+
+def DiffChangeEditor():
+
+ class DiffChangeEditor(delta.Editor):
+
+ def __init__(self):
+ self.deltas = []
+
+ # -- svn.delta.Editor callbacks
+
+ def open_root(self, base_revision, dir_pool):
+ return ('/', Changeset.EDIT)
+
+ def add_directory(self, path, dir_baton, copyfrom_path, copyfrom_rev,
+ dir_pool):
+ self.deltas.append((path, Node.DIRECTORY, Changeset.ADD))
+ return (path, Changeset.ADD)
+
+ def open_directory(self, path, dir_baton, base_revision, dir_pool):
+ return (path, dir_baton[1])
+
+ def change_dir_prop(self, dir_baton, name, value, pool):
+ path, change = dir_baton
+ if change != Changeset.ADD:
+ self.deltas.append((path, Node.DIRECTORY, change))
+
+ def delete_entry(self, path, revision, dir_baton, pool):
+ self.deltas.append((path, None, Changeset.DELETE))
+
+ def add_file(self, path, dir_baton, copyfrom_path, copyfrom_revision,
+ dir_pool):
+ self.deltas.append((path, Node.FILE, Changeset.ADD))
+
+ def open_file(self, path, dir_baton, dummy_rev, file_pool):
+ self.deltas.append((path, Node.FILE, Changeset.EDIT))
+
+ return DiffChangeEditor()
+
Propchange: incubator/bloodhound/vendor/trac/current/tracopt/versioncontrol/svn/svn_fs.py
------------------------------------------------------------------------------
svn:eol-style = native
Added: incubator/bloodhound/vendor/trac/current/tracopt/versioncontrol/svn/svn_prop.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/vendor/trac/current/tracopt/versioncontrol/svn/svn_prop.py?rev=1398858&view=auto
==============================================================================
--- incubator/bloodhound/vendor/trac/current/tracopt/versioncontrol/svn/svn_prop.py (added)
+++ incubator/bloodhound/vendor/trac/current/tracopt/versioncontrol/svn/svn_prop.py Tue Oct 16 15:55:00 2012
@@ -0,0 +1,402 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2005-2009 Edgewall Software
+# Copyright (C) 2005 Christopher Lenz <cm...@gmx.de>
+# Copyright (C) 2005-2007 Christian Boos <cb...@edgewall.org>
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.org/wiki/TracLicense.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://trac.edgewall.org/log/.
+#
+# Author: Christopher Lenz <cm...@gmx.de>
+# Christian Boos <cb...@edgewall.org>
+
+import posixpath
+
+from genshi.builder import tag
+
+from trac.config import ConfigSection
+from trac.core import *
+from trac.versioncontrol.api import NoSuchNode, RepositoryManager
+from trac.versioncontrol.web_ui.browser import IPropertyRenderer
+from trac.versioncontrol.web_ui.changeset import IPropertyDiffRenderer
+from trac.util import Ranges, to_ranges
+from trac.util.translation import _, tag_
+from tracopt.versioncontrol.svn.svn_fs import _path_within_scope
+
+
+class SubversionPropertyRenderer(Component):
+
+ implements(IPropertyRenderer)
+
+ svn_externals_section = ConfigSection('svn:externals',
+ """The TracBrowser for Subversion can interpret the `svn:externals`
+ property of folders. By default, it only turns the URLs into links as
+ Trac can't browse remote repositories.
+
+ However, if you have another Trac instance (or an other repository
+ browser like [http://www.viewvc.org/ ViewVC]) configured to browse the
+ target repository, then you can instruct Trac which other repository
+ browser to use for which external URL. This mapping is done in the
+ `[svn:externals]` section of the TracIni.
+
+ Example:
+ {{{
+ [svn:externals]
+ 1 = svn://server/repos1 http://trac/proj1/browser/$path?rev=$rev
+ 2 = svn://server/repos2 http://trac/proj2/browser/$path?rev=$rev
+ 3 = http://theirserver.org/svn/eng-soft http://ourserver/viewvc/svn/$path/?pathrev=25914
+ 4 = svn://anotherserver.com/tools_repository http://ourserver/tracs/tools/browser/$path?rev=$rev
+ }}}
+ With the above, the
+ `svn://anotherserver.com/tools_repository/tags/1.1/tools` external will
+ be mapped to `http://ourserver/tracs/tools/browser/tags/1.1/tools?rev=`
+ (and `rev` will be set to the appropriate revision number if the
+ external additionally specifies a revision, see the
+ [http://svnbook.red-bean.com/en/1.4/svn.advanced.externals.html SVN Book on externals]
+ for more details).
+
+ Note that the number used as a key in the above section is purely used
+ as a place holder, as the URLs themselves can't be used as a key due to
+ various limitations in the configuration file parser.
+
+ Finally, the relative URLs introduced in
+ [http://subversion.apache.org/docs/release-notes/1.5.html#externals Subversion 1.5]
+ are not yet supported.
+
+ (''since 0.11'')""")
+
+ def __init__(self):
+ self._externals_map = {}
+
+ # IPropertyRenderer methods
+
+ def match_property(self, name, mode):
+ if name in ('svn:externals', 'svn:needs-lock'):
+ return 4
+ return 2 if name in ('svn:mergeinfo', 'svnmerge-blocked',
+ 'svnmerge-integrated') else 0
+
+ def render_property(self, name, mode, context, props):
+ if name == 'svn:externals':
+ return self._render_externals(props[name])
+ elif name == 'svn:needs-lock':
+ return self._render_needslock(context)
+ elif name == 'svn:mergeinfo' or name.startswith('svnmerge-'):
+ return self._render_mergeinfo(name, mode, context, props)
+
+ def _render_externals(self, prop):
+ if not self._externals_map:
+ for dummykey, value in self.svn_externals_section.options():
+ value = value.split()
+ if len(value) != 2:
+ self.log.warn("svn:externals entry %s doesn't contain "
+ "a space-separated key value pair, skipping.",
+ dummykey)
+ continue
+ key, value = value
+ self._externals_map[key] = value.replace('%', '%%') \
+ .replace('$path', '%(path)s') \
+ .replace('$rev', '%(rev)s')
+ externals = []
+ for external in prop.splitlines():
+ elements = external.split()
+ if not elements:
+ continue
+ localpath, rev, url = elements[0], '', elements[-1]
+ if localpath.startswith('#'):
+ externals.append((external, None, None, None, None))
+ continue
+ if len(elements) == 3:
+ rev = elements[1]
+ rev = rev.replace('-r', '')
+ # retrieve a matching entry in the externals map
+ prefix = []
+ base_url = url
+ while base_url:
+ if base_url in self._externals_map or base_url == u'/':
+ break
+ base_url, pref = posixpath.split(base_url)
+ prefix.append(pref)
+ href = self._externals_map.get(base_url)
+ revstr = ' at revision ' + rev if rev else ''
+ if not href and (url.startswith('http://') or
+ url.startswith('https://')):
+ href = url.replace('%', '%%')
+ if href:
+ remotepath = ''
+ if prefix:
+ remotepath = posixpath.join(*reversed(prefix))
+ externals.append((localpath, revstr, base_url, remotepath,
+ href % {'path': remotepath, 'rev': rev}))
+ else:
+ externals.append((localpath, revstr, url, None, None))
+ externals_data = []
+ for localpath, rev, url, remotepath, href in externals:
+ label = localpath
+ if url is None:
+ title = ''
+ elif href:
+ if url:
+ url = ' in ' + url
+ label += rev + url
+ title = ''.join((remotepath, rev, url))
+ else:
+ title = _('No svn:externals configured in trac.ini')
+ externals_data.append((label, href, title))
+ return tag.ul([tag.li(tag.a(label, href=href, title=title))
+ for label, href, title in externals_data])
+
+ def _render_needslock(self, context):
+ return tag.img(src=context.href.chrome('common/lock-locked.png'),
+ alt="needs lock", title="needs lock")
+
+ def _render_mergeinfo(self, name, mode, context, props):
+ rows = []
+ for row in props[name].splitlines():
+ try:
+ (path, revs) = row.rsplit(':', 1)
+ rows.append([tag.td(path),
+ tag.td(revs.replace(',', u',\u200b'))])
+ except ValueError:
+ rows.append(tag.td(row, colspan=2))
+ return tag.table(tag.tbody([tag.tr(row) for row in rows]),
+ class_='props')
+
+
+class SubversionMergePropertyRenderer(Component):
+ implements(IPropertyRenderer)
+
+ # IPropertyRenderer methods
+
+ def match_property(self, name, mode):
+ return 4 if name in ('svn:mergeinfo', 'svnmerge-blocked',
+ 'svnmerge-integrated') else 0
+
+ def render_property(self, name, mode, context, props):
+ """Parse svn:mergeinfo and svnmerge-* properties, converting branch
+ names to links and providing links to the revision log for merged
+ and eligible revisions.
+ """
+ has_eligible = name in ('svnmerge-integrated', 'svn:mergeinfo')
+ revs_label = _('blocked') if name.endswith('blocked') else _('merged')
+ revs_cols = 2 if has_eligible else None
+ reponame = context.resource.parent.id
+ target_path = context.resource.id
+ repos = RepositoryManager(self.env).get_repository(reponame)
+ target_rev = context.resource.version
+ if has_eligible:
+ node = repos.get_node(target_path, target_rev)
+ branch_starts = {}
+ for path, rev in node.get_copy_ancestry():
+ if path not in branch_starts:
+ branch_starts[path] = rev + 1
+ rows = []
+ if name.startswith('svnmerge-'):
+ sources = props[name].split()
+ else:
+ sources = props[name].splitlines()
+ for line in sources:
+ path, revs = line.split(':', 1)
+ spath = _path_within_scope(repos.scope, path)
+ if spath is None:
+ continue
+ revs = revs.strip()
+ inheritable, non_inheritable = _partition_inheritable(revs)
+ revs = ','.join(inheritable)
+ deleted = False
+ try:
+ node = repos.get_node(spath, target_rev)
+ resource = context.resource.parent.child('source', spath)
+ if 'LOG_VIEW' in context.perm(resource):
+ row = [_get_source_link(spath, context),
+ _get_revs_link(revs_label, context, spath, revs)]
+ if non_inheritable:
+ non_inheritable = ','.join(non_inheritable)
+ row.append(_get_revs_link(_('non-inheritable'), context,
+ spath, non_inheritable,
+ _('merged on the directory '
+ 'itself but not below')))
+ if has_eligible:
+ first_rev = branch_starts.get(spath)
+ if not first_rev:
+ first_rev = node.get_branch_origin()
+ eligible = set(xrange(first_rev or 1, target_rev + 1))
+ eligible -= set(Ranges(revs))
+ blocked = _get_blocked_revs(props, name, spath)
+ if blocked:
+ eligible -= set(Ranges(blocked))
+ if eligible:
+ nrevs = repos._get_node_revs(spath, max(eligible),
+ min(eligible))
+ eligible &= set(nrevs)
+ eligible = to_ranges(eligible)
+ row.append(_get_revs_link(_('eligible'), context,
+ spath, eligible))
+ rows.append((False, spath, [tag.td(each) for each in row]))
+ continue
+ except NoSuchNode:
+ deleted = True
+ revs = revs.replace(',', u',\u200b')
+ rows.append((deleted, spath,
+ [tag.td('/' + spath),
+ tag.td(revs, colspan=revs_cols)]))
+ if not rows:
+ return None
+ rows.sort()
+ has_deleted = rows[-1][0] if rows else None
+ return tag(has_deleted and tag.a(_('(toggle deleted branches)'),
+ class_='trac-toggledeleted',
+ href='#'),
+ tag.table(tag.tbody(
+ [tag.tr(row, class_='trac-deleted' if deleted else None)
+ for deleted, spath, row in rows]), class_='props'))
+
+
+def _partition_inheritable(revs):
+ """Non-inheritable revision ranges are marked with a trailing '*'."""
+ inheritable, non_inheritable = [], []
+ for r in revs.split(','):
+ if r and r[-1] == '*':
+ non_inheritable.append(r[:-1])
+ else:
+ inheritable.append(r)
+ return inheritable, non_inheritable
+
+def _get_blocked_revs(props, name, path):
+ """Return the revisions blocked from merging for the given property
+ name and path.
+ """
+ if name == 'svnmerge-integrated':
+ prop = props.get('svnmerge-blocked', '')
+ else:
+ return ""
+ for line in prop.splitlines():
+ try:
+ p, revs = line.split(':', 1)
+ if p.strip('/') == path:
+ return revs
+ except Exception:
+ pass
+ return ""
+
+def _get_source_link(spath, context):
+ """Return a link to a merge source."""
+ reponame = context.resource.parent.id
+ return tag.a('/' + spath, title=_('View merge source'),
+ href=context.href.browser(reponame or None, spath,
+ rev=context.resource.version))
+
+def _get_revs_link(label, context, spath, revs, title=None):
+ """Return a link to the revision log when more than one revision is
+ given, to the revision itself for a single revision, or a `<span>`
+ with "no revision" for none.
+ """
+ reponame = context.resource.parent.id
+ if not revs:
+ return tag.span(label, title=_('No revisions'))
+ elif ',' in revs or '-' in revs:
+ revs_href = context.href.log(reponame or None, spath, revs=revs)
+ else:
+ revs_href = context.href.changeset(revs, reponame or None, spath)
+ revs = revs.replace(',', ', ')
+ if title:
+ title = _("%(title)s: %(revs)s", title=title, revs=revs)
+ else:
+ title = revs
+ return tag.a(label, title=title, href=revs_href)
+
+
+class SubversionMergePropertyDiffRenderer(Component):
+ implements(IPropertyDiffRenderer)
+
+ # IPropertyDiffRenderer methods
+
+ def match_property_diff(self, name):
+ return 4 if name in ('svn:mergeinfo', 'svnmerge-blocked',
+ 'svnmerge-integrated') else 0
+
+ def render_property_diff(self, name, old_context, old_props,
+ new_context, new_props, options):
+ # Build 5 columns table showing modifications on merge sources
+ # || source || added || removed || added (ni) || removed (ni) ||
+ # || source || removed ||
+ rm = RepositoryManager(self.env)
+ repos = rm.get_repository(old_context.resource.parent.id)
+ def parse_sources(props):
+ sources = {}
+ for line in props[name].splitlines():
+ path, revs = line.split(':', 1)
+ spath = _path_within_scope(repos.scope, path)
+ if spath is not None:
+ inheritable, non_inheritable = _partition_inheritable(revs)
+ sources[spath] = (set(Ranges(inheritable)),
+ set(Ranges(non_inheritable)))
+ return sources
+ old_sources = parse_sources(old_props)
+ new_sources = parse_sources(new_props)
+ # Go through new sources, detect modified ones or added ones
+ blocked = name.endswith('blocked')
+ added_label = [_("merged: "), _("blocked: ")][blocked]
+ removed_label = [_("reverse-merged: "), _("un-blocked: ")][blocked]
+ added_ni_label = _("marked as non-inheritable: ")
+ removed_ni_label = _("unmarked as non-inheritable: ")
+ def revs_link(revs, context):
+ if revs:
+ revs = to_ranges(revs)
+ return _get_revs_link(revs.replace(',', u',\u200b'),
+ context, spath, revs)
+ modified_sources = []
+ for spath, (new_revs, new_revs_ni) in new_sources.iteritems():
+ if spath in old_sources:
+ (old_revs, old_revs_ni), status = old_sources.pop(spath), None
+ else:
+ old_revs = old_revs_ni = set()
+ status = _(' (added)')
+ added = new_revs - old_revs
+ removed = old_revs - new_revs
+ added_ni = new_revs_ni - old_revs_ni
+ removed_ni = old_revs_ni - new_revs_ni
+ try:
+ all_revs = set(repos._get_node_revs(spath))
+ # TODO: also pass first_rev here, for getting smaller a set
+ # (this is an optmization fix, result is already correct)
+ added &= all_revs
+ removed &= all_revs
+ added_ni &= all_revs
+ removed_ni &= all_revs
+ except NoSuchNode:
+ pass
+ if added or removed:
+ modified_sources.append((
+ spath, [_get_source_link(spath, new_context), status],
+ added and tag(added_label, revs_link(added, new_context)),
+ removed and tag(removed_label,
+ revs_link(removed, old_context)),
+ added_ni and tag(added_ni_label,
+ revs_link(added_ni, new_context)),
+ removed_ni and tag(removed_ni_label,
+ revs_link(removed_ni, old_context))
+ ))
+ # Go through remaining old sources, those were deleted
+ removed_sources = []
+ for spath, old_revs in old_sources.iteritems():
+ removed_sources.append((spath,
+ _get_source_link(spath, old_context)))
+ if modified_sources or removed_sources:
+ modified_sources.sort()
+ removed_sources.sort()
+ changes = tag.table(tag.tbody(
+ [tag.tr(tag.td(c) for c in cols[1:])
+ for cols in modified_sources],
+ [tag.tr(tag.td(src), tag.td(_('removed'), colspan=4))
+ for spath, src in removed_sources]), class_='props')
+ else:
+ changes = tag.em(_(' (with no actual effect on merging)'))
+ return tag.li(tag_('Property %(prop)s changed', prop=tag.strong(name)),
+ changes)
Propchange: incubator/bloodhound/vendor/trac/current/tracopt/versioncontrol/svn/svn_prop.py
------------------------------------------------------------------------------
svn:eol-style = native
Added: incubator/bloodhound/vendor/trac/current/tracopt/versioncontrol/svn/tests/__init__.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/vendor/trac/current/tracopt/versioncontrol/svn/tests/__init__.py?rev=1398858&view=auto
==============================================================================
--- incubator/bloodhound/vendor/trac/current/tracopt/versioncontrol/svn/tests/__init__.py (added)
+++ incubator/bloodhound/vendor/trac/current/tracopt/versioncontrol/svn/tests/__init__.py Tue Oct 16 15:55:00 2012
@@ -0,0 +1,11 @@
+import unittest
+
+from tracopt.versioncontrol.svn.tests import svn_fs
+
+def suite():
+ suite = unittest.TestSuite()
+ suite.addTest(svn_fs.suite())
+ return suite
+
+if __name__ == '__main__':
+ unittest.main(defaultTest='suite')
Propchange: incubator/bloodhound/vendor/trac/current/tracopt/versioncontrol/svn/tests/__init__.py
------------------------------------------------------------------------------
svn:eol-style = native