diff examples/trac/trac/versioncontrol/web_ui/changeset.py @ 39:93b4dcbafd7b trunk

Copy Trac to main branch.
author cmlenz
date Mon, 03 Jul 2006 18:53:27 +0000
parents
children
line wrap: on
line diff
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/versioncontrol/web_ui/changeset.py
@@ -0,0 +1,808 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2003-2006 Edgewall Software
+# Copyright (C) 2003-2005 Jonas Borgström <jonas@edgewall.com>
+# Copyright (C) 2004-2006 Christopher Lenz <cmlenz@gmx.de>
+# Copyright (C) 2005-2006 Christian Boos <cboos@neuf.fr>
+# 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.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+#
+# Author: Jonas Borgström <jonas@edgewall.com>
+#         Christopher Lenz <cmlenz@gmx.de>
+#         Christian Boos <cboos@neuf.fr>
+
+import posixpath
+import re
+from StringIO import StringIO
+import time
+
+from trac import util
+from trac.config import BoolOption, IntOption
+from trac.core import *
+from trac.mimeview import Mimeview, is_binary
+from trac.perm import IPermissionRequestor
+from trac.Search import ISearchSource, search_to_sql, shorten_result
+from trac.Timeline import ITimelineEventProvider
+from trac.util.datefmt import format_datetime, pretty_timedelta
+from trac.util.markup import html, escape, unescape, Markup
+from trac.util.text import unicode_urlencode, shorten_line, CRLF
+from trac.versioncontrol import Changeset, Node
+from trac.versioncontrol.diff import get_diff_options, hdf_diff, unified_diff
+from trac.versioncontrol.svn_authz import SubversionAuthorizer
+from trac.versioncontrol.web_ui.util import render_node_property
+from trac.web import IRequestHandler
+from trac.web.chrome import INavigationContributor, add_link, add_stylesheet
+from trac.wiki import wiki_to_html, wiki_to_oneliner, IWikiSyntaxProvider, \
+                      Formatter
+
+
+class DiffArgs(dict):
+    def __getattr__(self, str):
+        return self[str]
+
+
+class ChangesetModule(Component):
+    """Provide flexible functionality for showing sets of differences.
+
+    If the differences shown are coming from a specific changeset,
+    then that changeset informations can be shown too.
+
+    In addition, it is possible to show only a subset of the changeset:
+    Only the changes affecting a given path will be shown.
+    This is called the ''restricted'' changeset.
+
+    But the differences can also be computed in a more general way,
+    between two arbitrary paths and/or between two arbitrary revisions.
+    In that case, there's no changeset information displayed.
+    """
+
+    implements(INavigationContributor, IPermissionRequestor, IRequestHandler,
+               ITimelineEventProvider, IWikiSyntaxProvider, ISearchSource)
+
+    timeline_show_files = IntOption('timeline', 'changeset_show_files', 0,
+        """Number of files to show (`-1` for unlimited, `0` to disable).""")
+
+    timeline_long_messages = BoolOption('timeline', 'changeset_long_messages',
+                                        'false',
+        """Whether wiki-formatted changeset messages should be multiline or not.
+
+        If this option is not specified or is false and `wiki_format_messages`
+        is set to true, changeset messages will be single line only, losing
+        some formatting (bullet points, etc).""")
+
+    max_diff_files = IntOption('changeset', 'max_diff_files', 0,
+        """Maximum number of modified files for which the changeset view will
+        attempt to show the diffs inlined (''since 0.10'')."""),
+
+    max_diff_bytes = IntOption('changeset', 'max_diff_bytes', 10000000,
+        """Maximum total size in bytes of the modified files (their old size
+        plus their new size) for which the changeset view will attempt to show
+        the diffs inlined (''since 0.10'').""")
+
+    wiki_format_messages = BoolOption('changeset', 'wiki_format_messages',
+                                      'true',
+        """Whether wiki formatting should be applied to changeset messages.
+        
+        If this option is disabled, changeset messages will be rendered as
+        pre-formatted text.""")
+
+    # INavigationContributor methods
+
+    def get_active_navigation_item(self, req):
+        return 'browser'
+
+    def get_navigation_items(self, req):
+        return []
+
+    # IPermissionRequestor methods
+
+    def get_permission_actions(self):
+        return ['CHANGESET_VIEW']
+
+    # IRequestHandler methods
+
+    _request_re = re.compile(r"/changeset(?:/([^/]+))?(/.*)?$")
+
+    def match_request(self, req):
+        match = re.match(self._request_re, req.path_info)
+        if match:
+            new, new_path = match.groups()
+            if new:
+                req.args['new'] = new
+            if new_path:
+                req.args['new_path'] = new_path
+            return True
+
+    def process_request(self, req):
+        """The appropriate mode of operation is inferred from the request
+        parameters:
+
+         * If `new_path` and `old_path` are equal (or `old_path` is omitted)
+           and `new` and `old` are equal (or `old` is omitted),
+           then we're about to view a revision Changeset: `chgset` is True.
+           Furthermore, if the path is not the root, the changeset is
+           ''restricted'' to that path (only the changes affecting that path,
+           its children or its ancestor directories will be shown).
+         * In any other case, the set of changes corresponds to arbitrary
+           differences between path@rev pairs. If `new_path` and `old_path`
+           are equal, the ''restricted'' flag will also be set, meaning in this
+           case that the differences between two revisions are restricted to
+           those occurring on that path.
+
+        In any case, either path@rev pairs must exist.
+        """
+        req.perm.assert_permission('CHANGESET_VIEW')
+
+        # -- retrieve arguments
+        new_path = req.args.get('new_path')
+        new = req.args.get('new')
+        old_path = req.args.get('old_path')
+        old = req.args.get('old')
+
+        if old and '@' in old:
+            old_path, old = unescape(old).split('@')
+        if new and '@' in new:
+            new_path, new = unescape(new).split('@')
+
+        # -- normalize and check for special case
+        repos = self.env.get_repository(req.authname)
+        new_path = repos.normalize_path(new_path)
+        new = repos.normalize_rev(new)
+        old_path = repos.normalize_path(old_path or new_path)
+        old = repos.normalize_rev(old or new)
+
+        authzperm = SubversionAuthorizer(self.env, req.authname)
+        authzperm.assert_permission_for_changeset(new)
+
+        if old_path == new_path and old == new: # revert to Changeset
+            old_path = old = None
+
+        diff_options = get_diff_options(req)
+
+        # -- setup the `chgset` and `restricted` flags, see docstring above.
+        chgset = not old and not old_path
+        if chgset:
+            restricted = new_path not in ('', '/') # (subset or not)
+        else:
+            restricted = old_path == new_path # (same path or not)
+
+        # -- redirect if changing the diff options
+        if req.args.has_key('update'):
+            if chgset:
+                if restricted:
+                    req.redirect(req.href.changeset(new, new_path))
+                else:
+                    req.redirect(req.href.changeset(new))
+            else:
+                req.redirect(req.href.changeset(new, new_path, old=old,
+                                                old_path=old_path))
+
+        # -- preparing the diff arguments
+        if chgset:
+            prev = repos.get_node(new_path, new).get_previous()
+            if prev:
+                prev_path, prev_rev = prev[:2]
+            else:
+                prev_path, prev_rev = new_path, repos.previous_rev(new)
+            diff_args = DiffArgs(old_path=prev_path, old_rev=prev_rev,
+                                 new_path=new_path, new_rev=new)
+        else:
+            if not new:
+                new = repos.youngest_rev
+            elif not old:
+                old = repos.youngest_rev
+            if not old_path:
+                old_path = new_path
+            diff_args = DiffArgs(old_path=old_path, old_rev=old,
+                                 new_path=new_path, new_rev=new)
+        if chgset:
+            chgset = repos.get_changeset(new)
+            message = chgset.message or '--'
+            if self.wiki_format_messages:
+                message = wiki_to_html(message, self.env, req,
+                                              escape_newlines=True)
+            else:
+                message = html.PRE(message)
+            req.check_modified(chgset.date, [
+                diff_options[0],
+                ''.join(diff_options[1]),
+                repos.name,
+                repos.rev_older_than(new, repos.youngest_rev),
+                message,
+                pretty_timedelta(chgset.date, None, 3600)])
+        else:
+            message = None # FIXME: what date should we choose for a diff?
+
+        req.hdf['changeset'] = diff_args
+
+        format = req.args.get('format')
+
+        if format in ['diff', 'zip']:
+            req.perm.assert_permission('FILE_VIEW')
+            # choosing an appropriate filename
+            rpath = new_path.replace('/','_')
+            if chgset:
+                if restricted:
+                    filename = 'changeset_%s_r%s' % (rpath, new)
+                else:
+                    filename = 'changeset_r%s' % new
+            else:
+                if restricted:
+                    filename = 'diff-%s-from-r%s-to-r%s' \
+                                  % (rpath, old, new)
+                elif old_path == '/': # special case for download (#238)
+                    filename = '%s-r%s' % (rpath, old)
+                else:
+                    filename = 'diff-from-%s-r%s-to-%s-r%s' \
+                               % (old_path.replace('/','_'), old, rpath, new)
+            if format == 'diff':
+                self._render_diff(req, filename, repos, diff_args,
+                                  diff_options)
+                return
+            elif format == 'zip':
+                self._render_zip(req, filename, repos, diff_args)
+                return
+
+        # -- HTML format
+        self._render_html(req, repos, chgset, restricted, message,
+                          diff_args, diff_options)
+        if chgset:
+            diff_params = 'new=%s' % new
+        else:
+            diff_params = unicode_urlencode({'new_path': new_path,
+                                             'new': new,
+                                             'old_path': old_path,
+                                             'old': old})
+        add_link(req, 'alternate', '?format=diff&'+diff_params, 'Unified Diff',
+                 'text/plain', 'diff')
+        add_link(req, 'alternate', '?format=zip&'+diff_params, 'Zip Archive',
+                 'application/zip', 'zip')
+        add_stylesheet(req, 'common/css/changeset.css')
+        add_stylesheet(req, 'common/css/diff.css')
+        add_stylesheet(req, 'common/css/code.css')
+        return 'changeset.cs', None
+
+    # Internal methods
+
+    def _render_html(self, req, repos, chgset, restricted, message,
+                     diff, diff_options):
+        """HTML version"""
+        req.hdf['changeset'] = {
+            'chgset': chgset and True,
+            'restricted': restricted,
+            'href': {
+                'new_rev': req.href.changeset(diff.new_rev),
+                'old_rev': req.href.changeset(diff.old_rev),
+                'new_path': req.href.browser(diff.new_path, rev=diff.new_rev),
+                'old_path': req.href.browser(diff.old_path, rev=diff.old_rev)
+            }
+        }
+
+        if chgset: # Changeset Mode (possibly restricted on a path)
+            path, rev = diff.new_path, diff.new_rev
+
+            # -- getting the change summary from the Changeset.get_changes
+            def get_changes():
+                for npath, kind, change, opath, orev in chgset.get_changes():
+                    old_node = new_node = None
+                    if (restricted and
+                        not (npath == path or                # same path
+                             npath.startswith(path + '/') or # npath is below
+                             path.startswith(npath + '/'))): # npath is above
+                        continue
+                    if change != Changeset.ADD:
+                        old_node = repos.get_node(opath, orev)
+                    if change != Changeset.DELETE:
+                        new_node = repos.get_node(npath, rev)
+                    yield old_node, new_node, kind, change
+
+            def _changeset_title(rev):
+                if restricted:
+                    return 'Changeset %s for %s' % (rev, path)
+                else:
+                    return 'Changeset %s' % rev
+
+            title = _changeset_title(rev)
+            properties = []
+            for name, value, wikiflag, htmlclass in chgset.get_properties():
+                if wikiflag:
+                    value = wiki_to_html(value or '', self.env, req)
+                properties.append({'name': name, 'value': value,
+                                   'htmlclass': htmlclass})
+
+            req.hdf['changeset'] = {
+                'revision': chgset.rev,
+                'time': format_datetime(chgset.date),
+                'age': pretty_timedelta(chgset.date, None, 3600),
+                'author': chgset.author or 'anonymous',
+                'message': message, 'properties': properties
+            }
+            oldest_rev = repos.oldest_rev
+            if chgset.rev != oldest_rev:
+                if restricted:
+                    prev = repos.get_node(path, rev).get_previous()
+                    if prev:
+                        prev_path, prev_rev = prev[:2]
+                        if prev_rev:
+                            prev_href = req.href.changeset(prev_rev, prev_path)
+                    else:
+                        prev_path = prev_rev = None
+                else:
+                    add_link(req, 'first', req.href.changeset(oldest_rev),
+                             'Changeset %s' % oldest_rev)
+                    prev_path = diff.old_path
+                    prev_rev = repos.previous_rev(chgset.rev)
+                    if prev_rev:
+                        prev_href = req.href.changeset(prev_rev)
+                if prev_rev:
+                    add_link(req, 'prev', prev_href, _changeset_title(prev_rev))
+            youngest_rev = repos.youngest_rev
+            if str(chgset.rev) != str(youngest_rev):
+                if restricted:
+                    next_rev = repos.next_rev(chgset.rev, path)
+                    if next_rev:
+                        next_href = req.href.changeset(next_rev, path)
+                else:
+                    add_link(req, 'last', req.href.changeset(youngest_rev),
+                             'Changeset %s' % youngest_rev)
+                    next_rev = repos.next_rev(chgset.rev)
+                    if next_rev:
+                        next_href = req.href.changeset(next_rev)
+                if next_rev:
+                    add_link(req, 'next', next_href, _changeset_title(next_rev))
+
+        else: # Diff Mode
+            # -- getting the change summary from the Repository.get_changes
+            def get_changes():
+                for d in repos.get_changes(**diff):
+                    yield d
+
+            reverse_href = req.href.changeset(diff.old_rev, diff.old_path,
+                                                   old=diff.new_rev,
+                                                   old_path=diff.new_path)
+            req.hdf['changeset.reverse_href'] = reverse_href
+            req.hdf['changeset.href.log'] = req.href.log(
+                diff.new_path, rev=diff.new_rev, stop_rev=diff.old_rev)
+            title = self.title_for_diff(diff)
+        req.hdf['title'] = title
+
+        if not req.perm.has_permission('BROWSER_VIEW'):
+            return
+
+        def _change_info(old_node, new_node, change):
+            info = {'change': change}
+            if old_node:
+                info['path.old'] = old_node.path
+                info['rev.old'] = old_node.rev
+                info['shortrev.old'] = repos.short_rev(old_node.rev)
+                old_href = req.href.browser(old_node.created_path,
+                                            rev=old_node.created_rev)
+                # Reminder: old_node.path may not exist at old_node.rev
+                #           as long as old_node.rev==old_node.created_rev
+                #           ... and diff.old_rev may have nothing to do
+                #           with _that_ node specific history...
+                info['browser_href.old'] = old_href
+            if new_node:
+                info['path.new'] = new_node.path
+                info['rev.new'] = new_node.rev # created rev.
+                info['shortrev.new'] = repos.short_rev(new_node.rev)
+                new_href = req.href.browser(new_node.created_path,
+                                            rev=new_node.created_rev)
+                # (same remark as above)
+                info['browser_href.new'] = new_href
+            return info
+
+        hidden_properties = self.config.getlist('browser', 'hide_properties')
+
+        def _prop_changes(old_node, new_node):
+            old_props = old_node.get_properties()
+            new_props = new_node.get_properties()
+            changed_props = {}
+            if old_props != new_props:
+                for k,v in old_props.items():
+                    if not k in new_props:
+                        changed_props[k] = {
+                            'old': render_node_property(self.env, k, v)}
+                    elif v != new_props[k]:
+                        changed_props[k] = {
+                            'old': render_node_property(self.env, k, v),
+                            'new': render_node_property(self.env, k,
+                                                        new_props[k])}
+                for k,v in new_props.items():
+                    if not k in old_props:
+                        changed_props[k] = {
+                            'new': render_node_property(self.env, k, v)}
+                for k in hidden_properties:
+                    if k in changed_props:
+                        del changed_props[k]
+            changed_properties = []
+            for name, props in changed_props.iteritems():
+                props.update({'name': name})
+                changed_properties.append(props)
+            return changed_properties
+
+        def _estimate_changes(old_node, new_node):
+            old_size = old_node.get_content_length()
+            new_size = new_node.get_content_length()
+            return old_size + new_size
+
+        def _content_changes(old_node, new_node):
+            """Returns the list of differences.
+
+            The list is empty when no differences between comparable files
+            are detected, but the return value is None for non-comparable files.
+            """
+            old_content = old_node.get_content().read()
+            if is_binary(old_content):
+                return None
+
+            new_content = new_node.get_content().read()
+            if is_binary(new_content):
+                return None
+
+            mview = Mimeview(self.env)
+            old_content = mview.to_unicode(old_content, old_node.content_type)
+            new_content = mview.to_unicode(new_content, new_node.content_type)
+
+            if old_content != new_content:
+                context = 3
+                options = diff_options[1]
+                for option in options:
+                    if option.startswith('-U'):
+                        context = int(option[2:])
+                        break
+                if context < 0:
+                    context = None
+                tabwidth = self.config['diff'].getint('tab_width',
+                                self.config['mimeviewer'].getint('tab_width'))
+                return hdf_diff(old_content.splitlines(),
+                                new_content.splitlines(),
+                                context, tabwidth,
+                                ignore_blank_lines='-B' in options,
+                                ignore_case='-i' in options,
+                                ignore_space_changes='-b' in options)
+            else:
+                return []
+
+        if req.perm.has_permission('FILE_VIEW'):
+            diff_bytes = diff_files = 0
+            if self.max_diff_bytes or self.max_diff_files:
+                for old_node, new_node, kind, change in get_changes():
+                    if change == Changeset.EDIT and kind == Node.FILE:
+                        diff_files += 1
+                        diff_bytes += _estimate_changes(old_node, new_node)
+            show_diffs = (not self.max_diff_files or \
+                          diff_files <= self.max_diff_files) and \
+                         (not self.max_diff_bytes or \
+                          diff_bytes <= self.max_diff_bytes or \
+                          diff_files == 1)
+        else:
+            show_diffs = False
+
+        idx = 0
+        for old_node, new_node, kind, change in get_changes():
+            show_entry = change != Changeset.EDIT
+            if change in (Changeset.EDIT, Changeset.COPY, Changeset.MOVE) and \
+                   req.perm.has_permission('FILE_VIEW'):
+                assert old_node and new_node
+                props = _prop_changes(old_node, new_node)
+                if props:
+                    req.hdf['changeset.changes.%d.props' % idx] = props
+                    show_entry = True
+                if kind == Node.FILE and show_diffs:
+                    diffs = _content_changes(old_node, new_node)
+                    if diffs != []:
+                        if diffs:
+                            req.hdf['changeset.changes.%d.diff' % idx] = diffs
+                        # elif None (means: manually compare to (previous))
+                        show_entry = True
+            if show_entry or not show_diffs:
+                info = _change_info(old_node, new_node, change)
+                if change == Changeset.EDIT and not show_diffs:
+                    if chgset:
+                        diff_href = req.href.changeset(new_node.rev,
+                                                       new_node.path)
+                    else:
+                        diff_href = req.href.changeset(
+                            new_node.created_rev, new_node.created_path,
+                            old=old_node.created_rev,
+                            old_path=old_node.created_path)
+                    info['diff_href'] = diff_href
+                req.hdf['changeset.changes.%d' % idx] = info
+            idx += 1 # the sequence should be immutable
+
+    def _render_diff(self, req, filename, repos, diff, diff_options):
+        """Raw Unified Diff version"""
+        req.send_response(200)
+        req.send_header('Content-Type', 'text/plain;charset=utf-8')
+        req.send_header('Content-Disposition', 'inline;'
+                        'filename=%s.diff' % filename)
+        req.end_headers()
+
+        mimeview = Mimeview(self.env)
+        for old_node, new_node, kind, change in repos.get_changes(**diff):
+            # TODO: Property changes
+
+            # Content changes
+            if kind == Node.DIRECTORY:
+                continue
+
+            new_content = old_content = ''
+            new_node_info = old_node_info = ('','')
+            mimeview = Mimeview(self.env)
+
+            if old_node:
+                old_content = old_node.get_content().read()
+                if is_binary(old_content):
+                    continue
+                old_node_info = (old_node.path, old_node.rev)
+                old_content = mimeview.to_unicode(old_content,
+                                                  old_node.content_type)
+            if new_node:
+                new_content = new_node.get_content().read()
+                if is_binary(new_content):
+                    continue
+                new_node_info = (new_node.path, new_node.rev)
+                new_path = new_node.path
+                new_content = mimeview.to_unicode(new_content,
+                                                  new_node.content_type)
+            else:
+                old_node_path = repos.normalize_path(old_node.path)
+                diff_old_path = repos.normalize_path(diff.old_path)
+                new_path = posixpath.join(diff.new_path,
+                                          old_node_path[len(diff_old_path)+1:])
+
+            if old_content != new_content:
+                context = 3
+                options = diff_options[1]
+                for option in options:
+                    if option.startswith('-U'):
+                        context = int(option[2:])
+                        break
+                if not old_node_info[0]:
+                    old_node_info = new_node_info # support for 'A'dd changes
+                req.write('Index: ' + new_path + CRLF)
+                req.write('=' * 67 + CRLF)
+                req.write('--- %s (revision %s)' % old_node_info + CRLF)
+                req.write('+++ %s (revision %s)' % new_node_info + CRLF)
+                for line in unified_diff(old_content.splitlines(),
+                                         new_content.splitlines(), context,
+                                         ignore_blank_lines='-B' in options,
+                                         ignore_case='-i' in options,
+                                         ignore_space_changes='-b' in options):
+                    req.write(line + CRLF)
+
+    def _render_zip(self, req, filename, repos, diff):
+        """ZIP archive with all the added and/or modified files."""
+        new_rev = diff.new_rev
+        req.send_response(200)
+        req.send_header('Content-Type', 'application/zip')
+        req.send_header('Content-Disposition', 'attachment;'
+                        'filename=%s.zip' % filename)
+
+        from zipfile import ZipFile, ZipInfo, ZIP_DEFLATED
+
+        buf = StringIO()
+        zipfile = ZipFile(buf, 'w', ZIP_DEFLATED)
+        for old_node, new_node, kind, change in repos.get_changes(**diff):
+            if kind == Node.FILE and change != Changeset.DELETE:
+                assert new_node
+                zipinfo = ZipInfo()
+                zipinfo.filename = new_node.path.encode('utf-8')
+                # Note: unicode filenames are not supported by zipfile.
+                # UTF-8 is not supported by all Zip tools either,
+                # but as some does, I think UTF-8 is the best option here.
+                zipinfo.date_time = time.gmtime(new_node.last_modified)[:6]
+                zipinfo.compress_type = ZIP_DEFLATED
+                zipfile.writestr(zipinfo, new_node.get_content().read())
+        zipfile.close()
+
+        buf.seek(0, 2) # be sure to be at the end
+        req.send_header("Content-Length", buf.tell())
+        req.end_headers()
+
+        req.write(buf.getvalue())
+
+    def title_for_diff(self, diff):
+        if diff.new_path == diff.old_path: # ''diff between 2 revisions'' mode
+            return 'Diff r%s:%s for %s' \
+                   % (diff.old_rev or 'latest', diff.new_rev or 'latest',
+                      diff.new_path or '/')
+        else:                              # ''arbitrary diff'' mode
+            return 'Diff from %s@%s to %s@%s' \
+                   % (diff.old_path or '/', diff.old_rev or 'latest',
+                      diff.new_path or '/', diff.new_rev or 'latest')
+
+    # ITimelineEventProvider methods
+
+    def get_timeline_filters(self, req):
+        if req.perm.has_permission('CHANGESET_VIEW'):
+            yield ('changeset', 'Repository checkins')
+
+    def get_timeline_events(self, req, start, stop, filters):
+        if 'changeset' in filters:
+            format = req.args.get('format')
+            wiki_format = self.wiki_format_messages
+            show_files = self.timeline_show_files
+            db = self.env.get_db_cnx()
+            repos = self.env.get_repository(req.authname)
+            for chgset in repos.get_changesets(start, stop):
+                message = chgset.message or '--'
+                if wiki_format:
+                    shortlog = wiki_to_oneliner(message, self.env, db,
+                                                shorten=True)
+                else:
+                    shortlog = shorten_line(message)
+
+                if format == 'rss':
+                    title = Markup('Changeset [%s]: %s', chgset.rev, shortlog)
+                    href = req.abs_href.changeset(chgset.rev)
+                    if wiki_format:
+                        message = wiki_to_html(message, self.env, req, db,
+                                               absurls=True)
+                    else:
+                        message = html.PRE(message)
+                else:
+                    title = Markup('Changeset <em>[%s]</em> by %s', chgset.rev,
+                                   chgset.author)
+                    href = req.href.changeset(chgset.rev)
+
+                    if wiki_format:
+                        if self.timeline_long_messages:
+                            message = wiki_to_html(message, self.env, req, db,
+                                                   absurls=True)
+                        else:
+                            message = wiki_to_oneliner(message, self.env, db,
+                                                       shorten=True)
+                    else:
+                        message = shortlog
+
+                if show_files and req.perm.has_permission('BROWSER_VIEW'):
+                    files = []
+                    for chg in chgset.get_changes():
+                        if show_files > 0 and len(files) >= show_files:
+                            files.append(html.LI(Markup('&hellip;')))
+                            break
+                        files.append(html.LI(html.DIV(class_=chg[2]),
+                                             chg[0] or '/'))
+                    message = html.UL(files, class_="changes") + message
+
+                yield 'changeset', href, title, chgset.date, chgset.author,\
+                      message
+
+    # IWikiSyntaxProvider methods
+
+    CHANGESET_ID = r"(?:\d+|[a-fA-F\d]{6,})" # only "long enough" hexa ids
+
+    def get_wiki_syntax(self):
+        yield (
+            # [...] form: start with optional intertrac: [T... or [trac ...
+            r"!?\[(?P<it_changeset>%s\s*)" % Formatter.INTERTRAC_SCHEME +
+            # hex digits + optional /path for the restricted changeset
+            r"%s(?:/[^\]]*)?\]|" % self.CHANGESET_ID +
+            # r... form: allow r1 but not r1:2 (handled by the log syntax)
+            r"(?:\b|!)r%s\b(?!:%s)" % ((self.CHANGESET_ID,)*2),
+            lambda x, y, z:
+            self._format_changeset_link(x, 'changeset',
+                                        y[0] == 'r' and y[1:] or y[1:-1],
+                                        y, z))
+
+    def get_link_resolvers(self):
+        yield ('changeset', self._format_changeset_link)
+        yield ('diff', self._format_diff_link)
+
+    def _format_changeset_link(self, formatter, ns, chgset, label,
+                               fullmatch=None):
+        intertrac = formatter.shorthand_intertrac_helper(ns, chgset, label,
+                                                         fullmatch)
+        if intertrac:
+            return intertrac
+        sep = chgset.find('/')
+        if sep > 0:
+            rev, path = chgset[:sep], chgset[sep:]
+        else:
+            rev, path = chgset, None
+        cursor = formatter.db.cursor()
+        cursor.execute('SELECT message FROM revision WHERE rev=%s', (rev,))
+        row = cursor.fetchone()
+        if row:
+            return html.A(label, class_="changeset",
+                          title=shorten_line(row[0]),
+                          href=formatter.href.changeset(rev, path))
+        else:
+            return html.A(label, class_="missing changeset",
+                          href=formatter.href.changeset(rev, path),
+                          rel="nofollow")
+
+    def _format_diff_link(self, formatter, ns, params, label):
+        def pathrev(path):
+            if '@' in path:
+                return path.split('@', 1)
+            else:
+                return (path, None)
+        if '//' in params:
+            p1, p2 = params.split('//', 1)
+            old, new = pathrev(p1), pathrev(p2)
+            diff = DiffArgs(old_path=old[0], old_rev=old[1],
+                            new_path=new[0], new_rev=new[1])
+        else:
+            old_path, old_rev = pathrev(params)
+            new_rev = None
+            if old_rev and ':' in old_rev:
+                old_rev, new_rev = old_rev.split(':', 1)
+            diff = DiffArgs(old_path=old_path, old_rev=old_rev,
+                            new_path=old_path, new_rev=new_rev)
+        title = self.title_for_diff(diff)
+        href = formatter.href.changeset(new_path=diff.new_path or None,
+                                        new=diff.new_rev,
+                                        old_path=diff.old_path or None,
+                                        old=diff.old_rev)
+        return html.A(label, class_="changeset", title=title, href=href)
+
+    # ISearchSource methods
+
+    def get_search_filters(self, req):
+        if req.perm.has_permission('CHANGESET_VIEW'):
+            yield ('changeset', 'Changesets')
+
+    def get_search_results(self, req, terms, filters):
+        if not 'changeset' in filters:
+            return
+        authzperm = SubversionAuthorizer(self.env, req.authname)
+        db = self.env.get_db_cnx()
+        sql, args = search_to_sql(db, ['message', 'author'], terms)
+        cursor = db.cursor()
+        cursor.execute("SELECT rev,time,author,message "
+                       "FROM revision WHERE " + sql, args)
+        for rev, date, author, log in cursor:
+            if not authzperm.has_permission_for_changeset(rev):
+                continue
+            yield (req.href.changeset(rev),
+                   '[%s]: %s' % (rev, shorten_line(log)),
+                   date, author, shorten_result(log, terms))
+
+
+class AnyDiffModule(Component):
+
+    implements(IRequestHandler)
+
+    # IRequestHandler methods
+
+    def match_request(self, req):
+        return re.match(r'/anydiff$', req.path_info)
+
+    def process_request(self, req):
+        # -- retrieve arguments
+        new_path = req.args.get('new_path')
+        new_rev = req.args.get('new_rev')
+        old_path = req.args.get('old_path')
+        old_rev = req.args.get('old_rev')
+
+        # -- normalize
+        repos = self.env.get_repository(req.authname)
+        new_path = repos.normalize_path(new_path)
+        new_rev = repos.normalize_rev(new_rev)
+        old_path = repos.normalize_path(old_path)
+        old_rev = repos.normalize_rev(old_rev)
+
+        authzperm = SubversionAuthorizer(self.env, req.authname)
+        authzperm.assert_permission_for_changeset(new_rev)
+        authzperm.assert_permission_for_changeset(old_rev)
+
+        # -- prepare rendering
+        req.hdf['anydiff'] = {
+            'new_path': new_path,
+            'new_rev': new_rev,
+            'old_path': old_path,
+            'old_rev': old_rev,
+            'changeset_href': req.href.changeset(),
+        }
+
+        return 'anydiff.cs', None
Copyright (C) 2012-2017 Edgewall Software