Server : Apache System : Linux iad1-shared-b8-43 6.6.49-grsec-jammy+ #10 SMP Thu Sep 12 23:23:08 UTC 2024 x86_64 User : dh_edsupp ( 6597262) PHP Version : 8.2.26 Disable Function : NONE Directory : /lib/python3/dist-packages/tracopt/versioncontrol/svn/ |
Upload File : |
# -*- coding: utf-8 -*- # # Copyright (C) 2005-2021 Edgewall Software # Copyright (C) 2005 Christopher Lenz <cmlenz@gmx.de> # Copyright (C) 2005-2007 Christian Boos <cboos@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 https://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 https://trac.edgewall.org/log/. # # Author: Christopher Lenz <cmlenz@gmx.de> # Christian Boos <cboos@edgewall.org> import posixpath 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.html import tag from trac.util.translation import _, tag_ from trac.web.chrome import chrome_resource_path 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 [%(svnbook)s 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. """, doc_args={'svnbook': 'http://svnbook.red-bean.com/en/1.7/svn.advanced.externals.html'}) 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 _is_abs_url(self, url): return url and '://' in url 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.warning("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 if not self._is_abs_url(url): externals.append((external, None, None, None, None)) continue prefix = [] base_url = url while base_url: if base_url in self._externals_map or base_url == '/': 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): url = chrome_resource_path(context.req, 'common/lock-locked.png') return tag.img(src=url, 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(',', ',\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 = [] eligible_infos = [] 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(range(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: node = repos.get_node(spath, max(eligible)) eligible_infos.append((spath, node, eligible, row)) continue 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(',', ',\u200b') rows.append((deleted, spath, [tag.td('/' + spath), tag.td(revs, colspan=revs_cols)])) # fetch eligible revisions for each path at a time changed_revs = {} changed_nodes = [(node, min(eligible)) for spath, node, eligible, row in eligible_infos] if changed_nodes: changed_revs = repos._get_changed_revs(changed_nodes) for spath, node, eligible, row in eligible_infos: if spath in changed_revs: eligible &= set(changed_revs[spath]) else: eligible.clear() row.append(_get_revs_link(_("eligible"), context, spath, to_ranges(eligible))) rows.append((False, spath, [tag.td(each) for each in row])) if not rows: return None rows.sort() if rows and rows[-1][0]: toggledeleted = tag.a(_("(toggle deleted branches)"), class_='trac-toggledeleted', href='#') else: toggledeleted = None return tag(toggledeleted, 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 = {} value = props[name] lines = value.splitlines() if name == 'svn:mergeinfo' \ else value.split() for line in lines: 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: ") sources = [] changed_revs = {} changed_nodes = [] for spath, (new_revs, new_revs_ni) in new_sources.items(): new_spath = spath not in old_sources if new_spath: old_revs = old_revs_ni = set() else: old_revs, old_revs_ni = old_sources.pop(spath) added = new_revs - old_revs removed = old_revs - new_revs # unless new revisions differ from old revisions if not added and not removed: continue added_ni = new_revs_ni - old_revs_ni removed_ni = old_revs_ni - new_revs_ni revs = sorted(added | removed | added_ni | removed_ni) try: node = repos.get_node(spath, revs[-1]) changed_nodes.append((node, revs[0])) except NoSuchNode: pass sources.append((spath, new_spath, added, removed, added_ni, removed_ni)) if changed_nodes: changed_revs = repos._get_changed_revs(changed_nodes) def revs_link(revs, context): if revs: revs = to_ranges(revs) return _get_revs_link(revs.replace(',', ',\u200b'), context, spath, revs) modified_sources = [] for spath, new_spath, added, removed, added_ni, removed_ni in sources: if spath in changed_revs: revs = set(changed_revs[spath]) added &= revs removed &= revs added_ni &= revs removed_ni &= revs if added or removed: if new_spath: status = _(" (added)") else: status = None 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.items(): 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)