"Fossies" - the Fresh Open Source Software Archive

Member "PURELIB/tracopt/versioncontrol/svn/svn_prop.py" (27 Aug 2019, 20203 Bytes) of package /windows/misc/Trac-1.4.win-amd64.exe:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Python source code syntax highlighting (style: standard) with prefixed line numbers. Alternatively you can here view or download the uninterpreted source code file. See also the last Fossies "Diffs" side-by-side code changes report for "svn_prop.py": 1.3.5_vs_1.3.6.

    1 # -*- coding: utf-8 -*-
    2 #
    3 # Copyright (C) 2005-2019 Edgewall Software
    4 # Copyright (C) 2005 Christopher Lenz <cmlenz@gmx.de>
    5 # Copyright (C) 2005-2007 Christian Boos <cboos@edgewall.org>
    6 # All rights reserved.
    7 #
    8 # This software is licensed as described in the file COPYING, which
    9 # you should have received as part of this distribution. The terms
   10 # are also available at https://trac.edgewall.org/wiki/TracLicense.
   11 #
   12 # This software consists of voluntary contributions made by many
   13 # individuals. For the exact contribution history, see the revision
   14 # history and logs, available at https://trac.edgewall.org/log/.
   15 #
   16 # Author: Christopher Lenz <cmlenz@gmx.de>
   17 #         Christian Boos <cboos@edgewall.org>
   18 
   19 import posixpath
   20 
   21 from trac.config import ConfigSection
   22 from trac.core import *
   23 from trac.versioncontrol.api import NoSuchNode, RepositoryManager
   24 from trac.versioncontrol.web_ui.browser import IPropertyRenderer
   25 from trac.versioncontrol.web_ui.changeset import IPropertyDiffRenderer
   26 from trac.util import Ranges, to_ranges
   27 from trac.util.html import tag
   28 from trac.util.translation import _, tag_
   29 from trac.web.chrome import chrome_resource_path
   30 from tracopt.versioncontrol.svn.svn_fs import _path_within_scope
   31 
   32 
   33 class SubversionPropertyRenderer(Component):
   34 
   35     implements(IPropertyRenderer)
   36 
   37     svn_externals_section = ConfigSection('svn:externals',
   38         """The TracBrowser for Subversion can interpret the `svn:externals`
   39         property of folders. By default, it only turns the URLs into links as
   40         Trac can't browse remote repositories.
   41 
   42         However, if you have another Trac instance (or an other repository
   43         browser like [http://www.viewvc.org/ ViewVC]) configured to browse the
   44         target repository, then you can instruct Trac which other repository
   45         browser to use for which external URL. This mapping is done in the
   46         `[svn:externals]` section of the TracIni.
   47 
   48         Example:
   49         {{{
   50         [svn:externals]
   51         1 = svn://server/repos1                       http://trac/proj1/browser/$path?rev=$rev
   52         2 = svn://server/repos2                       http://trac/proj2/browser/$path?rev=$rev
   53         3 = http://theirserver.org/svn/eng-soft       http://ourserver/viewvc/svn/$path/?pathrev=25914
   54         4 = svn://anotherserver.com/tools_repository  http://ourserver/tracs/tools/browser/$path?rev=$rev
   55         }}}
   56         With the above, the
   57         `svn://anotherserver.com/tools_repository/tags/1.1/tools` external will
   58         be mapped to `http://ourserver/tracs/tools/browser/tags/1.1/tools?rev=`
   59         (and `rev` will be set to the appropriate revision number if the
   60         external additionally specifies a revision, see the
   61         [%(svnbook)s SVN Book on externals] for more details).
   62 
   63         Note that the number used as a key in the above section is purely used
   64         as a place holder, as the URLs themselves can't be used as a key due to
   65         various limitations in the configuration file parser.
   66 
   67         Finally, the relative URLs introduced in
   68         [http://subversion.apache.org/docs/release-notes/1.5.html#externals Subversion 1.5]
   69         are not yet supported.
   70         """,
   71         doc_args={'svnbook': 'http://svnbook.red-bean.com/en/1.7/svn.advanced.externals.html'})
   72 
   73     def __init__(self):
   74         self._externals_map = {}
   75 
   76     # IPropertyRenderer methods
   77 
   78     def match_property(self, name, mode):
   79         if name in ('svn:externals', 'svn:needs-lock'):
   80             return 4
   81         return 2 if name in ('svn:mergeinfo', 'svnmerge-blocked',
   82                              'svnmerge-integrated') else 0
   83 
   84     def render_property(self, name, mode, context, props):
   85         if name == 'svn:externals':
   86             return self._render_externals(props[name])
   87         elif name == 'svn:needs-lock':
   88             return self._render_needslock(context)
   89         elif name == 'svn:mergeinfo' or name.startswith('svnmerge-'):
   90             return self._render_mergeinfo(name, mode, context, props)
   91 
   92     def _is_abs_url(self, url):
   93         return url and '://' in url
   94 
   95     def _render_externals(self, prop):
   96         if not self._externals_map:
   97             for dummykey, value in self.svn_externals_section.options():
   98                 value = value.split()
   99                 if len(value) != 2:
  100                     self.log.warning("svn:externals entry %s doesn't contain "
  101                                      "a space-separated key value pair, "
  102                                      "skipping.", dummykey)
  103                     continue
  104                 key, value = value
  105                 self._externals_map[key] = value.replace('%', '%%') \
  106                                            .replace('$path', '%(path)s') \
  107                                            .replace('$rev', '%(rev)s')
  108         externals = []
  109         for external in prop.splitlines():
  110             elements = external.split()
  111             if not elements:
  112                 continue
  113             localpath, rev, url = elements[0], '', elements[-1]
  114             if localpath.startswith('#'):
  115                 externals.append((external, None, None, None, None))
  116                 continue
  117             if len(elements) == 3:
  118                 rev = elements[1]
  119                 rev = rev.replace('-r', '')
  120             # retrieve a matching entry in the externals map
  121             if not self._is_abs_url(url):
  122                 externals.append((external, None, None, None, None))
  123                 continue
  124             prefix = []
  125             base_url = url
  126             while base_url:
  127                 if base_url in self._externals_map or base_url == u'/':
  128                     break
  129                 base_url, pref = posixpath.split(base_url)
  130                 prefix.append(pref)
  131             href = self._externals_map.get(base_url)
  132             revstr = ' at revision ' + rev if rev else ''
  133             if not href and (url.startswith('http://') or
  134                              url.startswith('https://')):
  135                 href = url.replace('%', '%%')
  136             if href:
  137                 remotepath = ''
  138                 if prefix:
  139                     remotepath = posixpath.join(*reversed(prefix))
  140                 externals.append((localpath, revstr, base_url, remotepath,
  141                                   href % {'path': remotepath, 'rev': rev}))
  142             else:
  143                 externals.append((localpath, revstr, url, None, None))
  144         externals_data = []
  145         for localpath, rev, url, remotepath, href in externals:
  146             label = localpath
  147             if url is None:
  148                 title = ''
  149             elif href:
  150                 if url:
  151                     url = ' in ' + url
  152                 label += rev + url
  153                 title = ''.join((remotepath, rev, url))
  154             else:
  155                 title = _('No svn:externals configured in trac.ini')
  156             externals_data.append((label, href, title))
  157         return tag.ul([tag.li(tag.a(label, href=href, title=title))
  158                        for label, href, title in externals_data])
  159 
  160     def _render_needslock(self, context):
  161         url = chrome_resource_path(context.req, 'common/lock-locked.png')
  162         return tag.img(src=url, alt=_("needs lock"), title=_("needs lock"))
  163 
  164     def _render_mergeinfo(self, name, mode, context, props):
  165         rows = []
  166         for row in props[name].splitlines():
  167             try:
  168                 (path, revs) = row.rsplit(':', 1)
  169                 rows.append([tag.td(path),
  170                              tag.td(revs.replace(',', u',\u200b'))])
  171             except ValueError:
  172                 rows.append(tag.td(row, colspan=2))
  173         return tag.table(tag.tbody([tag.tr(row) for row in rows]),
  174                          class_='props')
  175 
  176 
  177 class SubversionMergePropertyRenderer(Component):
  178     implements(IPropertyRenderer)
  179 
  180     # IPropertyRenderer methods
  181 
  182     def match_property(self, name, mode):
  183         return 4 if name in ('svn:mergeinfo', 'svnmerge-blocked',
  184                              'svnmerge-integrated') else 0
  185 
  186     def render_property(self, name, mode, context, props):
  187         """Parse svn:mergeinfo and svnmerge-* properties, converting branch
  188         names to links and providing links to the revision log for merged
  189         and eligible revisions.
  190         """
  191         has_eligible = name in ('svnmerge-integrated', 'svn:mergeinfo')
  192         revs_label = _('blocked') if name.endswith('blocked') else _('merged')
  193         revs_cols = 2 if has_eligible else None
  194         reponame = context.resource.parent.id
  195         target_path = context.resource.id
  196         repos = RepositoryManager(self.env).get_repository(reponame)
  197         target_rev = context.resource.version
  198         if has_eligible:
  199             node = repos.get_node(target_path, target_rev)
  200             branch_starts = {}
  201             for path, rev in node.get_copy_ancestry():
  202                 if path not in branch_starts:
  203                     branch_starts[path] = rev + 1
  204         rows = []
  205         eligible_infos = []
  206         if name.startswith('svnmerge-'):
  207             sources = props[name].split()
  208         else:
  209             sources = props[name].splitlines()
  210         for line in sources:
  211             path, revs = line.split(':', 1)
  212             spath = _path_within_scope(repos.scope, path)
  213             if spath is None:
  214                 continue
  215             revs = revs.strip()
  216             inheritable, non_inheritable = _partition_inheritable(revs)
  217             revs = ','.join(inheritable)
  218             deleted = False
  219             try:
  220                 node = repos.get_node(spath, target_rev)
  221                 resource = context.resource.parent.child('source', spath)
  222                 if 'LOG_VIEW' in context.perm(resource):
  223                     row = [_get_source_link(spath, context),
  224                            _get_revs_link(revs_label, context, spath, revs)]
  225                     if non_inheritable:
  226                         non_inheritable = ','.join(non_inheritable)
  227                         row.append(_get_revs_link(_('non-inheritable'), context,
  228                                                   spath, non_inheritable,
  229                                                   _('merged on the directory '
  230                                                     'itself but not below')))
  231                     if has_eligible:
  232                         first_rev = branch_starts.get(spath)
  233                         if not first_rev:
  234                             first_rev = node.get_branch_origin()
  235                         eligible = set(xrange(first_rev or 1, target_rev + 1))
  236                         eligible -= set(Ranges(revs))
  237                         blocked = _get_blocked_revs(props, name, spath)
  238                         if blocked:
  239                             eligible -= set(Ranges(blocked))
  240                         if eligible:
  241                             node = repos.get_node(spath, max(eligible))
  242                             eligible_infos.append((spath, node, eligible, row))
  243                             continue
  244                         eligible = to_ranges(eligible)
  245                         row.append(_get_revs_link(_('eligible'), context,
  246                                                   spath, eligible))
  247                     rows.append((False, spath, [tag.td(each) for each in row]))
  248                     continue
  249             except NoSuchNode:
  250                 deleted = True
  251             revs = revs.replace(',', u',\u200b')
  252             rows.append((deleted, spath,
  253                          [tag.td('/' + spath),
  254                           tag.td(revs, colspan=revs_cols)]))
  255 
  256         # fetch eligible revisions for each path at a time
  257         changed_revs = {}
  258         changed_nodes = [(node, min(eligible))
  259                          for spath, node, eligible, row in eligible_infos]
  260         if changed_nodes:
  261             changed_revs = repos._get_changed_revs(changed_nodes)
  262         for spath, node, eligible, row in eligible_infos:
  263             if spath in changed_revs:
  264                 eligible &= set(changed_revs[spath])
  265             else:
  266                 eligible.clear()
  267             row.append(_get_revs_link(_("eligible"), context, spath,
  268                                       to_ranges(eligible)))
  269             rows.append((False, spath, [tag.td(each) for each in row]))
  270 
  271         if not rows:
  272             return None
  273         rows.sort()
  274         if rows and rows[-1][0]:
  275             toggledeleted = tag.a(_("(toggle deleted branches)"),
  276                                   class_='trac-toggledeleted', href='#')
  277         else:
  278             toggledeleted = None
  279         return tag(toggledeleted,
  280                    tag.table(tag.tbody(
  281                        [tag.tr(row, class_='trac-deleted' if deleted else None)
  282                         for deleted, spath, row in rows]), class_='props'))
  283 
  284 
  285 def _partition_inheritable(revs):
  286     """Non-inheritable revision ranges are marked with a trailing '*'."""
  287     inheritable, non_inheritable = [], []
  288     for r in revs.split(','):
  289         if r and r[-1] == '*':
  290             non_inheritable.append(r[:-1])
  291         else:
  292             inheritable.append(r)
  293     return inheritable, non_inheritable
  294 
  295 def _get_blocked_revs(props, name, path):
  296     """Return the revisions blocked from merging for the given property
  297     name and path.
  298     """
  299     if name == 'svnmerge-integrated':
  300         prop = props.get('svnmerge-blocked', '')
  301     else:
  302         return ""
  303     for line in prop.splitlines():
  304         try:
  305             p, revs = line.split(':', 1)
  306             if p.strip('/') == path:
  307                 return revs
  308         except Exception:
  309             pass
  310     return ""
  311 
  312 def _get_source_link(spath, context):
  313     """Return a link to a merge source."""
  314     reponame = context.resource.parent.id
  315     return tag.a('/' + spath, title=_('View merge source'),
  316                  href=context.href.browser(reponame or None, spath,
  317                                            rev=context.resource.version))
  318 
  319 def _get_revs_link(label, context, spath, revs, title=None):
  320     """Return a link to the revision log when more than one revision is
  321     given, to the revision itself for a single revision, or a `<span>`
  322     with "no revision" for none.
  323     """
  324     reponame = context.resource.parent.id
  325     if not revs:
  326         return tag.span(label, title=_('No revisions'))
  327     elif ',' in revs or '-' in revs:
  328         revs_href = context.href.log(reponame or None, spath, revs=revs)
  329     else:
  330         revs_href = context.href.changeset(revs, reponame or None, spath)
  331     revs = revs.replace(',', ', ')
  332     if title:
  333         title = _("%(title)s: %(revs)s", title=title, revs=revs)
  334     else:
  335         title = revs
  336     return tag.a(label, title=title, href=revs_href)
  337 
  338 
  339 class SubversionMergePropertyDiffRenderer(Component):
  340     implements(IPropertyDiffRenderer)
  341 
  342     # IPropertyDiffRenderer methods
  343 
  344     def match_property_diff(self, name):
  345         return 4 if name in ('svn:mergeinfo', 'svnmerge-blocked',
  346                              'svnmerge-integrated') else 0
  347 
  348     def render_property_diff(self, name, old_context, old_props,
  349                              new_context, new_props, options):
  350         # Build 5 columns table showing modifications on merge sources
  351         # || source || added || removed || added (ni) || removed (ni) ||
  352         # || source || removed                                        ||
  353         rm = RepositoryManager(self.env)
  354         repos = rm.get_repository(old_context.resource.parent.id)
  355         def parse_sources(props):
  356             sources = {}
  357             value = props[name]
  358             lines = value.splitlines() if name == 'svn:mergeinfo' \
  359                                        else value.split()
  360             for line in lines:
  361                 path, revs = line.split(':', 1)
  362                 spath = _path_within_scope(repos.scope, path)
  363                 if spath is not None:
  364                     inheritable, non_inheritable = _partition_inheritable(revs)
  365                     sources[spath] = (set(Ranges(inheritable)),
  366                                       set(Ranges(non_inheritable)))
  367             return sources
  368         old_sources = parse_sources(old_props)
  369         new_sources = parse_sources(new_props)
  370         # Go through new sources, detect modified ones or added ones
  371         blocked = name.endswith('blocked')
  372         added_label = [_("merged: "), _("blocked: ")][blocked]
  373         removed_label = [_("reverse-merged: "), _("un-blocked: ")][blocked]
  374         added_ni_label = _("marked as non-inheritable: ")
  375         removed_ni_label = _("unmarked as non-inheritable: ")
  376 
  377         sources = []
  378         changed_revs = {}
  379         changed_nodes = []
  380         for spath, (new_revs, new_revs_ni) in new_sources.iteritems():
  381             new_spath = spath not in old_sources
  382             if new_spath:
  383                 old_revs = old_revs_ni = set()
  384             else:
  385                 old_revs, old_revs_ni = old_sources.pop(spath)
  386             added = new_revs - old_revs
  387             removed = old_revs - new_revs
  388             # unless new revisions differ from old revisions
  389             if not added and not removed:
  390                 continue
  391             added_ni = new_revs_ni - old_revs_ni
  392             removed_ni = old_revs_ni - new_revs_ni
  393             revs = sorted(added | removed | added_ni | removed_ni)
  394             try:
  395                 node = repos.get_node(spath, revs[-1])
  396                 changed_nodes.append((node, revs[0]))
  397             except NoSuchNode:
  398                 pass
  399             sources.append((spath, new_spath, added, removed, added_ni,
  400                             removed_ni))
  401         if changed_nodes:
  402             changed_revs = repos._get_changed_revs(changed_nodes)
  403 
  404         def revs_link(revs, context):
  405             if revs:
  406                 revs = to_ranges(revs)
  407                 return _get_revs_link(revs.replace(',', u',\u200b'),
  408                                       context, spath, revs)
  409         modified_sources = []
  410         for spath, new_spath, added, removed, added_ni, removed_ni in sources:
  411             if spath in changed_revs:
  412                 revs = set(changed_revs[spath])
  413                 added &= revs
  414                 removed &= revs
  415                 added_ni &= revs
  416                 removed_ni &= revs
  417             if added or removed:
  418                 if new_spath:
  419                     status = _(" (added)")
  420                 else:
  421                     status = None
  422                 modified_sources.append((
  423                     spath, [_get_source_link(spath, new_context), status],
  424                     added and tag(added_label, revs_link(added, new_context)),
  425                     removed and tag(removed_label,
  426                                     revs_link(removed, old_context)),
  427                     added_ni and tag(added_ni_label,
  428                                      revs_link(added_ni, new_context)),
  429                     removed_ni and tag(removed_ni_label,
  430                                        revs_link(removed_ni, old_context))
  431                     ))
  432         # Go through remaining old sources, those were deleted
  433         removed_sources = []
  434         for spath, old_revs in old_sources.iteritems():
  435             removed_sources.append((spath,
  436                                     _get_source_link(spath, old_context)))
  437         if modified_sources or removed_sources:
  438             modified_sources.sort()
  439             removed_sources.sort()
  440             changes = tag.table(tag.tbody(
  441                 [tag.tr(tag.td(c) for c in cols[1:])
  442                  for cols in modified_sources],
  443                 [tag.tr(tag.td(src), tag.td(_('removed'), colspan=4))
  444                  for spath, src in removed_sources]), class_='props')
  445         else:
  446             changes = tag.em(_(' (with no actual effect on merging)'))
  447         return tag.li(tag_('Property %(prop)s changed', prop=tag.strong(name)),
  448                       changes)