"Fossies" - the Fresh Open Source Software Archive

Member "PURELIB/trac/versioncontrol/web_ui/changeset.py" (27 Aug 2019, 56571 Bytes) of package /windows/misc/Trac-1.4.win32.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 "changeset.py": 1.3.5_vs_1.3.6.

    1 # -*- coding: utf-8 -*-
    2 #
    3 # Copyright (C) 2003-2019 Edgewall Software
    4 # Copyright (C) 2003-2005 Jonas Borgström <jonas@edgewall.com>
    5 # Copyright (C) 2004-2006 Christopher Lenz <cmlenz@gmx.de>
    6 # Copyright (C) 2005-2006 Christian Boos <cboos@edgewall.org>
    7 # All rights reserved.
    8 #
    9 # This software is licensed as described in the file COPYING, which
   10 # you should have received as part of this distribution. The terms
   11 # are also available at https://trac.edgewall.org/wiki/TracLicense.
   12 #
   13 # This software consists of voluntary contributions made by many
   14 # individuals. For the exact contribution history, see the revision
   15 # history and logs, available at https://trac.edgewall.org/log/.
   16 #
   17 # Author: Jonas Borgström <jonas@edgewall.com>
   18 #         Christopher Lenz <cmlenz@gmx.de>
   19 #         Christian Boos <cboos@edgewall.org>
   20 
   21 from functools import partial
   22 from itertools import groupby
   23 import os
   24 import posixpath
   25 import re
   26 
   27 from trac.config import BoolOption, IntOption, Option
   28 from trac.core import *
   29 from trac.mimeview.api import Mimeview
   30 from trac.perm import IPermissionRequestor
   31 from trac.resource import ResourceNotFound
   32 from trac.search import ISearchSource, search_to_sql, shorten_result
   33 from trac.timeline.api import ITimelineEventProvider
   34 from trac.util import as_bool, content_disposition, embedded_numbers, pathjoin
   35 from trac.util.datefmt import from_utimestamp, pretty_timedelta
   36 from trac.util.html import tag
   37 from trac.util.presentation import to_json
   38 from trac.util.text import CRLF, exception_to_unicode, shorten_line, \
   39                            to_unicode, unicode_urlencode
   40 from trac.util.translation import _, ngettext, tag_
   41 from trac.versioncontrol.api import Changeset, NoSuchChangeset, Node, \
   42                                     RepositoryManager
   43 from trac.versioncontrol.diff import diff_blocks, get_diff_options, \
   44                                      unified_diff
   45 from trac.versioncontrol.web_ui.browser import BrowserModule
   46 from trac.versioncontrol.web_ui.util import content_closing, render_zip
   47 from trac.web import IRequestHandler, RequestDone
   48 from trac.web.chrome import (Chrome, INavigationContributor, add_ctxtnav,
   49                              add_link, add_script, add_stylesheet,
   50                              prevnext_nav, web_context)
   51 from trac.wiki.api import IWikiSyntaxProvider, WikiParser
   52 from trac.wiki.formatter import format_to
   53 
   54 
   55 class IPropertyDiffRenderer(Interface):
   56     """Render node properties in TracBrowser and TracChangeset views."""
   57 
   58     def match_property_diff(name):
   59         """Indicate whether this renderer can treat the given property diffs
   60 
   61         Returns a quality number, ranging from 0 (unsupported) to 9
   62         (''perfect'' match).
   63         """
   64 
   65     def render_property_diff(name, old_context, old_props,
   66                              new_context, new_props, options):
   67         """Render the given diff of property to HTML.
   68 
   69         `name` is the property name as given to `match_property_diff()`,
   70         `old_context` corresponds to the old node being render
   71         (useful when the rendering depends on the node kind)
   72         and `old_props` is the corresponding collection of all properties.
   73         Same for `new_node` and `new_props`.
   74         `options` are the current diffs options.
   75 
   76         The rendered result can be one of the following:
   77          - `None`: the property change will be shown the normal way
   78            (''changed from `old` to `new`'')
   79          - an `unicode` value: the change will be shown as textual content
   80          - `Markup` or `Fragment`: the change will shown as block markup
   81         """
   82 
   83 
   84 class DefaultPropertyDiffRenderer(Component):
   85     """Default version control property difference renderer."""
   86 
   87     implements(IPropertyDiffRenderer)
   88 
   89     def match_property_diff(self, name):
   90         return 1
   91 
   92     def render_property_diff(self, name, old_context, old_props,
   93                              new_context, new_props, options):
   94         old, new = old_props[name], new_props[name]
   95         # Render as diff only if multiline (see #3002)
   96         if '\n' not in old and '\n' not in new:
   97             return None
   98         unidiff = '--- \n+++ \n' + \
   99                   '\n'.join(unified_diff(old.splitlines(), new.splitlines(),
  100                                          options.get('contextlines', 3)))
  101         return tag.li(tag_("Property %(name)s", name=tag.strong(name)),
  102                       Mimeview(self.env).render(old_context, 'text/x-diff',
  103                                                 unidiff))
  104 
  105 
  106 class ChangesetModule(Component):
  107     """Renderer providing flexible functionality for showing sets of
  108     differences.
  109 
  110     If the differences shown are coming from a specific changeset,
  111     then that changeset information can be shown too.
  112 
  113     In addition, it is possible to show only a subset of the changeset:
  114     Only the changes affecting a given path will be shown. This is called
  115     the ''restricted'' changeset.
  116 
  117     But the differences can also be computed in a more general way,
  118     between two arbitrary paths and/or between two arbitrary revisions.
  119     In that case, there's no changeset information displayed.
  120     """
  121 
  122     implements(INavigationContributor, IPermissionRequestor, IRequestHandler,
  123                ITimelineEventProvider, IWikiSyntaxProvider, ISearchSource)
  124 
  125     property_diff_renderers = ExtensionPoint(IPropertyDiffRenderer)
  126 
  127     realm = RepositoryManager.changeset_realm
  128 
  129     timeline_show_files = Option('timeline', 'changeset_show_files', '0',
  130         """Number of files to show (`-1` for unlimited, `0` to disable).
  131 
  132         This can also be `location`, for showing the common prefix for the
  133         changed files.
  134         """)
  135 
  136     timeline_long_messages = BoolOption('timeline', 'changeset_long_messages',
  137                                         'false',
  138         """Whether wiki-formatted changeset messages should be multiline or
  139         not.
  140 
  141         If this option is not specified or is false and `wiki_format_messages`
  142         is set to true, changeset messages will be single line only, losing
  143         some formatting (bullet points, etc).""")
  144 
  145     timeline_collapse = BoolOption('timeline', 'changeset_collapse_events',
  146                                    'false',
  147         """Whether consecutive changesets from the same author having
  148         exactly the same message should be presented as one event.
  149         That event will link to the range of changesets in the log view.
  150         """)
  151 
  152     max_diff_files = IntOption('changeset', 'max_diff_files', 0,
  153         """Maximum number of modified files for which the changeset view will
  154         attempt to show the diffs inlined.""")
  155 
  156     max_diff_bytes = IntOption('changeset', 'max_diff_bytes', 10000000,
  157         """Maximum total size in bytes of the modified files (their old size
  158         plus their new size) for which the changeset view will attempt to show
  159         the diffs inlined.""")
  160 
  161     wiki_format_messages = BoolOption('changeset', 'wiki_format_messages',
  162                                       'true',
  163         """Whether wiki formatting should be applied to changeset messages.
  164 
  165         If this option is disabled, changeset messages will be rendered as
  166         pre-formatted text.""")
  167 
  168     # INavigationContributor methods
  169 
  170     def get_active_navigation_item(self, req):
  171         return 'browser'
  172 
  173     def get_navigation_items(self, req):
  174         return []
  175 
  176     # IPermissionRequestor methods
  177 
  178     def get_permission_actions(self):
  179         return ['CHANGESET_VIEW']
  180 
  181     # IRequestHandler methods
  182 
  183     _request_re = re.compile(r"/changeset(?:/([^/]+)(/.*)?)?$")
  184 
  185     def match_request(self, req):
  186         match = re.match(self._request_re, req.path_info)
  187         if match:
  188             new, new_path = match.groups()
  189             if new:
  190                 req.args['new'] = new
  191             if new_path:
  192                 req.args['new_path'] = new_path
  193             return True
  194 
  195     def process_request(self, req):
  196         """The appropriate mode of operation is inferred from the request
  197         parameters:
  198 
  199          * If `new_path` and `old_path` are equal (or `old_path` is omitted)
  200            and `new` and `old` are equal (or `old` is omitted),
  201            then we're about to view a revision Changeset: `chgset` is True.
  202            Furthermore, if the path is not the root, the changeset is
  203            ''restricted'' to that path (only the changes affecting that path,
  204            its children or its ancestor directories will be shown).
  205          * In any other case, the set of changes corresponds to arbitrary
  206            differences between path@rev pairs. If `new_path` and `old_path`
  207            are equal, the ''restricted'' flag will also be set, meaning in this
  208            case that the differences between two revisions are restricted to
  209            those occurring on that path.
  210 
  211         In any case, either path@rev pairs must exist.
  212         """
  213         req.perm.require('CHANGESET_VIEW')
  214 
  215         # -- retrieve arguments
  216         new_path = req.args.get('new_path')
  217         new = req.args.get('new')
  218         old_path = req.args.get('old_path')
  219         old = req.args.get('old')
  220         reponame = req.args.get('reponame')
  221 
  222         # -- support for the revision log ''View changes'' form,
  223         #    where we need to give the path and revision at the same time
  224         if old and '@' in old:
  225             old, old_path = old.split('@', 1)
  226         if new and '@' in new:
  227             new, new_path = new.split('@', 1)
  228 
  229         rm = RepositoryManager(self.env)
  230         if reponame:
  231             repos = rm.get_repository(reponame)
  232         else:
  233             reponame, repos, new_path = rm.get_repository_by_path(new_path)
  234 
  235             if old_path:
  236                 old_reponame, old_repos, old_path = \
  237                     rm.get_repository_by_path(old_path)
  238                 if old_repos != repos:
  239                     raise TracError(_("Can't compare across different "
  240                                       "repositories: %(old)s vs. %(new)s",
  241                                       old=old_reponame, new=reponame))
  242 
  243         if not repos:
  244             if reponame or (new_path and new_path != '/'):
  245                 raise TracError(_("Repository '%(repo)s' not found",
  246                                   repo=reponame or new_path.strip('/')))
  247             else:
  248                 raise TracError(_("No repository specified and no default "
  249                                   "repository configured."))
  250 
  251         # -- normalize and check for special case
  252         try:
  253             new = repos.normalize_rev(new)
  254             old = repos.normalize_rev(old or new)
  255         except NoSuchChangeset as e:
  256             raise ResourceNotFound(e, _("Invalid Changeset Number"))
  257         new_path = repos.normalize_path(new_path)
  258         old_path = repos.normalize_path(old_path or new_path)
  259         full_new_path = '/' + pathjoin(repos.reponame, new_path)
  260         full_old_path = '/' + pathjoin(repos.reponame, old_path)
  261 
  262         if old_path == new_path and old == new:  # revert to Changeset
  263             old_path = old = None
  264 
  265         style, options, diff_data = get_diff_options(req)
  266         diff_opts = diff_data['options']
  267 
  268         # -- setup the `chgset` and `restricted` flags, see docstring above.
  269         chgset = not old and old_path is None
  270         if chgset:
  271             restricted = new_path not in ('', '/')  # (subset or not)
  272         else:
  273             restricted = old_path == new_path  # (same path or not)
  274 
  275         # -- redirect if changing the diff options or alias requested
  276         if 'update' in req.args or reponame != repos.reponame:
  277             contextall = diff_opts['contextall'] or None
  278             reponame = repos.reponame or None
  279             if chgset:
  280                 if restricted:
  281                     req.redirect(req.href.changeset(new, reponame, new_path,
  282                                                     contextall=contextall))
  283                 else:
  284                     req.redirect(req.href.changeset(new, reponame,
  285                                                     contextall=contextall))
  286             else:
  287                 req.redirect(req.href.changeset(new, reponame,
  288                                                 new_path, old=old,
  289                                                 old_path=full_old_path,
  290                                                 contextall=contextall))
  291 
  292         # -- preparing the data
  293         if chgset:
  294             prev = repos.get_node(new_path, new).get_previous()
  295             if prev:
  296                 prev_path, prev_rev = prev[:2]
  297             else:
  298                 prev_path, prev_rev = new_path, repos.previous_rev(new)
  299             data = {'old_path': prev_path, 'old_rev': prev_rev,
  300                     'new_path': new_path, 'new_rev': new}
  301         else:
  302             if not new:
  303                 new = repos.youngest_rev
  304             elif not old:
  305                 old = repos.youngest_rev
  306             if old_path is None:
  307                 old_path = new_path
  308             data = {'old_path': old_path, 'old_rev': old,
  309                     'new_path': new_path, 'new_rev': new}
  310         data.update({'repos': repos, 'reponame': repos.reponame or None,
  311                      'diff': diff_data,
  312                      'wiki_format_messages': self.wiki_format_messages})
  313 
  314         if chgset:
  315             chgset = repos.get_changeset(new)
  316             req.perm(chgset.resource).require('CHANGESET_VIEW')
  317 
  318             # TODO: find a cheaper way to reimplement r2636
  319             req.check_modified(chgset.date, [
  320                 style, ''.join(options), repos.name,
  321                 diff_opts['contextlines'], diff_opts['contextall'],
  322                 repos.rev_older_than(new, repos.youngest_rev),
  323                 chgset.message, req.is_xhr,
  324                 pretty_timedelta(chgset.date, None, 3600)])
  325 
  326         format = req.args.get('format')
  327 
  328         if format in ['diff', 'zip']:
  329             # choosing an appropriate filename
  330             rpath = new_path.replace('/', '_')
  331             if chgset:
  332                 if restricted:
  333                     filename = 'changeset_%s_%s' % (rpath, new)
  334                 else:
  335                     filename = 'changeset_%s' % new
  336             else:
  337                 if restricted:
  338                     filename = 'diff-%s-from-%s-to-%s' % (rpath, old, new)
  339                 else:
  340                     filename = 'diff-from-%s-%s-to-%s-%s' \
  341                                % (old_path.replace('/', '_'), old, rpath, new)
  342             if format == 'diff':
  343                 self._render_diff(req, filename, repos, data)
  344             elif format == 'zip':
  345                 render_zip(req, filename + '.zip', repos, None,
  346                            partial(self._zip_iter_nodes, req, repos, data))
  347 
  348         # -- HTML format
  349         self._render_html(req, repos, chgset, restricted, data)
  350 
  351         if chgset:
  352             diff_params = 'new=%s' % new
  353         else:
  354             diff_params = unicode_urlencode({
  355                 'new_path': full_new_path, 'new': new,
  356                 'old_path': full_old_path, 'old': old})
  357         add_link(req, 'alternate', '?format=diff&' + diff_params,
  358                  _('Unified Diff'), 'text/plain', 'diff')
  359         add_link(req, 'alternate', '?format=zip&' + diff_params,
  360                  _('Zip Archive'), 'application/zip', 'zip')
  361         add_script(req, 'common/js/diff.js')
  362         add_stylesheet(req, 'common/css/changeset.css')
  363         add_stylesheet(req, 'common/css/diff.css')
  364         add_stylesheet(req, 'common/css/code.css')
  365         if chgset:
  366             if restricted:
  367                 prevnext_nav(req, _('Previous Change'), _('Next Change'))
  368             else:
  369                 prevnext_nav(req, _('Previous Changeset'), _('Next Changeset'))
  370         else:
  371             rev_href = req.href.changeset(old, full_old_path,
  372                                           old=new, old_path=full_new_path)
  373             add_ctxtnav(req, _('Reverse Diff'), href=rev_href)
  374 
  375         return 'changeset.html', data
  376 
  377     # Internal methods
  378 
  379     def _render_html(self, req, repos, chgset, restricted, data):
  380         """HTML version"""
  381         data['restricted'] = restricted
  382         display_rev = repos.display_rev
  383         data['display_rev'] = display_rev
  384         browser = BrowserModule(self.env)
  385         reponame = repos.reponame or None
  386 
  387         if chgset:  # Changeset Mode (possibly restricted on a path)
  388             path, rev = data['new_path'], data['new_rev']
  389 
  390             # -- getting the change summary from the Changeset.get_changes
  391             def get_changes():
  392                 for npath, kind, change, opath, orev in chgset.get_changes():
  393                     old_node = new_node = None
  394                     if (restricted and
  395                         not (npath == path or                 # same path
  396                              npath.startswith(path + '/') or  # npath is below
  397                              path.startswith(npath + '/'))):  # npath is above
  398                         continue
  399                     if change != Changeset.ADD:
  400                         old_node = repos.get_node(opath, orev)
  401                     if change != Changeset.DELETE:
  402                         new_node = repos.get_node(npath, rev)
  403                     else:
  404                         # support showing paths deleted below a copy target
  405                         old_node.path = npath
  406                     yield old_node, new_node, kind, change
  407 
  408             def _changeset_title(rev):
  409                 rev = display_rev(rev)
  410                 if restricted:
  411                     return _('Changeset %(id)s for %(path)s', id=rev,
  412                              path=path)
  413                 else:
  414                     return _('Changeset %(id)s', id=rev)
  415 
  416             data['changeset'] = chgset
  417             title = _changeset_title(rev)
  418 
  419             # Support for revision properties (#2545)
  420             context = web_context(req, self.realm, chgset.rev,
  421                                   parent=repos.resource)
  422             data['context'] = context
  423             revprops = chgset.get_properties()
  424             data['properties'] = browser.render_properties('revprop', context,
  425                                                            revprops)
  426             oldest_rev = repos.oldest_rev
  427             if chgset.rev != oldest_rev:
  428                 if restricted:
  429                     prev = repos.get_node(path, rev).get_previous()
  430                     if prev:
  431                         prev_path, prev_rev = prev[:2]
  432                         if prev_rev:
  433                             prev_href = req.href.changeset(prev_rev, reponame,
  434                                                            prev_path)
  435                     else:
  436                         prev_path = prev_rev = None
  437                 else:
  438                     add_link(req, 'first',
  439                              req.href.changeset(oldest_rev, reponame),
  440                              _('Changeset %(id)s', id=display_rev(oldest_rev)))
  441                     prev_path = data['old_path']
  442                     prev_rev = repos.previous_rev(chgset.rev)
  443                     if prev_rev:
  444                         prev_href = req.href.changeset(prev_rev, reponame)
  445                 if prev_rev:
  446                     add_link(req, 'prev', prev_href,
  447                              _changeset_title(prev_rev))
  448             youngest_rev = repos.youngest_rev
  449             if str(chgset.rev) != str(youngest_rev):
  450                 if restricted:
  451                     next_rev = repos.next_rev(chgset.rev, path)
  452                     if next_rev:
  453                         if repos.has_node(path, next_rev):
  454                             next_href = req.href.changeset(next_rev, reponame,
  455                                                            path)
  456                         else:  # must be 'D'elete or 'R'ename, show full cset
  457                             next_href = req.href.changeset(next_rev, reponame)
  458                 else:
  459                     add_link(req, 'last',
  460                              req.href.changeset(youngest_rev, reponame),
  461                              _('Changeset %(id)s',
  462                                id=display_rev(youngest_rev)))
  463                     next_rev = repos.next_rev(chgset.rev)
  464                     if next_rev:
  465                         next_href = req.href.changeset(next_rev, reponame)
  466                 if next_rev:
  467                     add_link(req, 'next', next_href,
  468                              _changeset_title(next_rev))
  469         else:  # Diff Mode
  470             # -- getting the change summary from the Repository.get_changes
  471             def get_changes():
  472                 for d in repos.get_changes(
  473                     new_path=data['new_path'], new_rev=data['new_rev'],
  474                     old_path=data['old_path'], old_rev=data['old_rev']):
  475                     yield d
  476             title = self.title_for_diff(data)
  477             data['changeset'] = False
  478 
  479         data['title'] = title
  480 
  481         if 'BROWSER_VIEW' not in req.perm:
  482             return
  483 
  484         def node_info(node, annotated):
  485             href = req.href.browser(
  486                 reponame, node.created_path, rev=node.created_rev,
  487                 annotate='blame' if annotated else None)
  488             title = _("Show revision %(rev)s of this file in browser",
  489                       rev=display_rev(node.rev))
  490             return {'path': node.path, 'rev': node.rev,
  491                     'shortrev': repos.short_rev(node.rev),
  492                     'href': href, 'title': title}
  493         # Reminder: node.path may not exist at node.rev
  494         #           as long as node.rev==node.created_rev
  495         #           ... and data['old_rev'] may have nothing to do
  496         #           with _that_ node specific history...
  497 
  498         options = data['diff']['options']
  499 
  500         def _prop_changes(old_node, new_node):
  501             old_props = old_node.get_properties()
  502             new_props = new_node.get_properties()
  503             old_ctx = web_context(req, old_node.resource)
  504             new_ctx = web_context(req, new_node.resource)
  505             changed_properties = []
  506             if old_props != new_props:
  507                 for k, v in sorted(old_props.items()):
  508                     new = old = diff = None
  509                     if not k in new_props:
  510                         old = v  # won't be displayed, no need to render it
  511                     elif v != new_props[k]:
  512                         diff = self.render_property_diff(
  513                             k, old_ctx, old_props, new_ctx, new_props, options)
  514                         if not diff:
  515                             old = browser.render_property(k, 'changeset',
  516                                                           old_ctx, old_props)
  517                             new = browser.render_property(k, 'changeset',
  518                                                           new_ctx, new_props)
  519                     if new or old or diff:
  520                         changed_properties.append({'name': k, 'old': old,
  521                                                    'new': new, 'diff': diff})
  522                 for k, v in sorted(new_props.items()):
  523                     if not k in old_props:
  524                         new = browser.render_property(k, 'changeset',
  525                                                       new_ctx, new_props)
  526                         if new is not None:
  527                             changed_properties.append({'name': k, 'new': new,
  528                                                        'old': None})
  529             return changed_properties
  530 
  531         def _estimate_changes(old_node, new_node):
  532             old_size = old_node.get_content_length()
  533             new_size = new_node.get_content_length()
  534             return old_size + new_size
  535 
  536         def _content_changes(old_node, new_node):
  537             """Returns the list of differences.
  538 
  539             The list is empty when no differences between comparable files
  540             are detected, but the return value is None for non-comparable
  541             files.
  542             """
  543             mview = Mimeview(self.env)
  544             if mview.is_binary(old_node.content_type, old_node.path):
  545                 return None
  546             if mview.is_binary(new_node.content_type, new_node.path):
  547                 return None
  548             old_content = _read_content(old_node)
  549             if mview.is_binary(content=old_content):
  550                 return None
  551             new_content = _read_content(new_node)
  552             if mview.is_binary(content=new_content):
  553                 return None
  554 
  555             old_content = mview.to_unicode(old_content, old_node.content_type)
  556             new_content = mview.to_unicode(new_content, new_node.content_type)
  557 
  558             if old_content != new_content:
  559                 context = options.get('contextlines', 3)
  560                 if context < 0 or options.get('contextall'):
  561                     context = None
  562                 tabwidth = self.config.getint('mimeviewer', 'tab_width', 8)
  563                 ignore_blank_lines = options.get('ignoreblanklines')
  564                 ignore_case = options.get('ignorecase')
  565                 ignore_space = options.get('ignorewhitespace')
  566                 return diff_blocks(old_content.splitlines(),
  567                                    new_content.splitlines(),
  568                                    context, tabwidth,
  569                                    ignore_blank_lines=ignore_blank_lines,
  570                                    ignore_case=ignore_case,
  571                                    ignore_space_changes=ignore_space)
  572             else:
  573                 return []
  574 
  575         diff_changes = list(get_changes())
  576         # XHR is used for blame support: display the changeset view without
  577         # the navigation and with the changes concerning the annotated file
  578         diff_bytes = diff_files = 0
  579         annotated = None
  580         if req.is_xhr:
  581             show_diffs = None
  582             annotated = repos.normalize_path(req.args.get('annotate'))
  583         else:
  584             if self.max_diff_bytes or self.max_diff_files:
  585                 for old_node, new_node, kind, change in diff_changes:
  586                     if change in Changeset.DIFF_CHANGES and \
  587                             kind == Node.FILE and \
  588                             old_node.is_viewable(req.perm) and \
  589                             new_node.is_viewable(req.perm):
  590                         diff_files += 1
  591                         diff_bytes += _estimate_changes(old_node, new_node)
  592             show_diffs = (not self.max_diff_files or
  593                           0 < diff_files <= self.max_diff_files) and \
  594                          (not self.max_diff_bytes or
  595                           diff_bytes <= self.max_diff_bytes or
  596                           diff_files == 1)
  597 
  598         has_diffs = False
  599         filestats = self._prepare_filestats()
  600         changes = []
  601         files = []
  602         for old_node, new_node, kind, change in diff_changes:
  603             props = []
  604             diffs = []
  605             show_old = old_node and old_node.is_viewable(req.perm)
  606             show_new = new_node and new_node.is_viewable(req.perm)
  607             show_entry = change != Changeset.EDIT
  608             show_diff = show_diffs or (new_node and new_node.path == annotated)
  609 
  610             if change in Changeset.DIFF_CHANGES and show_old and show_new:
  611                 assert old_node and new_node
  612                 props = _prop_changes(old_node, new_node)
  613                 if props:
  614                     show_entry = True
  615                 if kind == Node.FILE and show_diff:
  616                     diffs = _content_changes(old_node, new_node)
  617                     if diffs != []:
  618                         if diffs:
  619                             has_diffs = True
  620                         # elif None (means: manually compare to (previous))
  621                         show_entry = True
  622             if (show_old or show_new) and (show_entry or not show_diff):
  623                 info = {'change': change,
  624                         'old': old_node and node_info(old_node, annotated),
  625                         'new': new_node and node_info(new_node, annotated),
  626                         'props': props,
  627                         'diffs': diffs}
  628                 files.append(new_node.path if new_node else
  629                              old_node.path if old_node else '')
  630                 filestats[change] += 1
  631                 if change in Changeset.DIFF_CHANGES:
  632                     if chgset:
  633                         href = req.href.changeset(new_node.rev, reponame,
  634                                                   new_node.path)
  635                         title = _('Show the changeset %(id)s restricted to '
  636                                   '%(path)s', id=display_rev(new_node.rev),
  637                                   path=new_node.path)
  638                     else:
  639                         href = req.href.changeset(
  640                             new_node.created_rev, reponame,
  641                             new_node.created_path,
  642                             old=old_node.created_rev,
  643                             old_path=pathjoin(repos.reponame,
  644                                               old_node.created_path))
  645                         title = _('Show the %(range)s differences restricted '
  646                                   'to %(path)s', range='[%s:%s]' % (
  647                                       display_rev(old_node.rev),
  648                                       display_rev(new_node.rev)),
  649                                   path=new_node.path)
  650                     info['href'] = href
  651                     info['title'] = old_node and title
  652                 if change in Changeset.DIFF_CHANGES and not show_diff:
  653                     info['hide_diff'] = True
  654             else:
  655                 info = None
  656             changes.append(info)  # the sequence should be immutable
  657 
  658         data.update({
  659             'has_diffs': has_diffs,
  660             'show_diffs': show_diffs,
  661             'diff_files': diff_files,
  662             'diff_bytes': diff_bytes,
  663             'max_diff_files': self.max_diff_files,
  664             'max_diff_bytes': self.max_diff_bytes,
  665             'changes': changes,
  666             'filestats': filestats,
  667             'annotated': annotated,
  668             'files': files,
  669             'location': self._get_parent_location(files),
  670             'longcol': 'Revision',
  671             'shortcol': 'r'
  672         })
  673 
  674         if req.is_xhr:  # render and return the content only
  675             stream = Chrome(self.env).generate_fragment(
  676                 req, 'changeset_content.html', data)
  677             req.send(stream)
  678 
  679         return data
  680 
  681     def _render_diff(self, req, filename, repos, data):
  682         """Raw Unified Diff version"""
  683 
  684         output = (line.encode('utf-8') if isinstance(line, unicode) else line
  685                   for line in self._iter_diff_lines(req, repos, data))
  686         if Chrome(self.env).use_chunked_encoding:
  687             length = None
  688         else:
  689             output = ''.join(output)
  690             length = len(output)
  691 
  692         req.send_response(200)
  693         req.send_header('Content-Type', 'text/x-patch;charset=utf-8')
  694         req.send_header('Content-Disposition',
  695                         content_disposition('attachment', filename + '.diff'))
  696         if length is not None:
  697             req.send_header('Content-Length', length)
  698         req.end_headers()
  699         req.write(output)
  700         raise RequestDone
  701 
  702     def _iter_diff_lines(self, req, repos, data):
  703         mimeview = Mimeview(self.env)
  704 
  705         for old_node, new_node, kind, change in repos.get_changes(
  706                 new_path=data['new_path'], new_rev=data['new_rev'],
  707                 old_path=data['old_path'], old_rev=data['old_rev']):
  708             # TODO: Property changes
  709 
  710             # Content changes
  711             if kind == Node.DIRECTORY:
  712                 continue
  713 
  714             new_content = old_content = ''
  715             new_node_info = old_node_info = ('', '')
  716 
  717             if old_node:
  718                 if not old_node.is_viewable(req.perm):
  719                     continue
  720                 if mimeview.is_binary(old_node.content_type, old_node.path):
  721                     continue
  722                 old_content = _read_content(old_node)
  723                 if mimeview.is_binary(content=old_content):
  724                     continue
  725                 old_node_info = (old_node.path, old_node.rev)
  726                 old_content = mimeview.to_unicode(old_content,
  727                                                   old_node.content_type)
  728             if new_node:
  729                 if not new_node.is_viewable(req.perm):
  730                     continue
  731                 if mimeview.is_binary(new_node.content_type, new_node.path):
  732                     continue
  733                 new_content = _read_content(new_node)
  734                 if mimeview.is_binary(content=new_content):
  735                     continue
  736                 new_node_info = (new_node.path, new_node.rev)
  737                 new_path = new_node.path
  738                 new_content = mimeview.to_unicode(new_content,
  739                                                   new_node.content_type)
  740             else:
  741                 old_node_path = repos.normalize_path(old_node.path)
  742                 diff_old_path = repos.normalize_path(data['old_path'])
  743                 new_path = pathjoin(data['new_path'],
  744                                     old_node_path[len(diff_old_path) + 1:])
  745 
  746             if old_content != new_content:
  747                 options = data['diff']['options']
  748                 context = options.get('contextlines', 3)
  749                 if context < 0 or options.get('contextall'):
  750                     context = 3  # FIXME: unified_diff bugs with context=None
  751                 ignore_blank_lines = options.get('ignoreblanklines')
  752                 ignore_case = options.get('ignorecase')
  753                 ignore_space = options.get('ignorewhitespace')
  754                 if not old_node_info[0]:
  755                     old_node_info = new_node_info  # support for 'A'dd changes
  756                 yield 'Index: ' + new_path + CRLF
  757                 yield '=' * 67 + CRLF
  758                 yield '--- %s\t(revision %s)' % old_node_info + CRLF
  759                 yield '+++ %s\t(revision %s)' % new_node_info + CRLF
  760                 for line in unified_diff(old_content.splitlines(),
  761                                          new_content.splitlines(), context,
  762                                          ignore_blank_lines=ignore_blank_lines,
  763                                          ignore_case=ignore_case,
  764                                          ignore_space_changes=ignore_space):
  765                     yield line + CRLF
  766 
  767     def _zip_iter_nodes(self, req, repos, data, root_node):
  768         """Node iterator yielding all the added and/or modified files."""
  769         for old_node, new_node, kind, change in repos.get_changes(
  770             new_path=data['new_path'], new_rev=data['new_rev'],
  771             old_path=data['old_path'], old_rev=data['old_rev']):
  772             if kind in (Node.FILE, Node.DIRECTORY) and \
  773                     change != Changeset.DELETE \
  774                     and new_node.is_viewable(req.perm):
  775                 yield new_node
  776 
  777     def title_for_diff(self, data):
  778         # TRANSLATOR: 'latest' (revision)
  779         latest = _('latest')
  780         if data['new_path'] == data['old_path']:
  781             # ''diff between 2 revisions'' mode
  782             return _('Diff [%(old_rev)s:%(new_rev)s] for %(path)s',
  783                      old_rev=data['old_rev'] or latest,
  784                      new_rev=data['new_rev'] or latest,
  785                      path=data['new_path'] or '/')
  786         else:
  787             # ''generalized diff'' mode
  788             return _('Diff from %(old_path)s@%(old_rev)s to %(new_path)s@'
  789                      '%(new_rev)s',
  790                      old_path=data['old_path'] or '/',
  791                      old_rev=data['old_rev'] or latest,
  792                      new_path=data['new_path'] or '/',
  793                      new_rev=data['new_rev'] or latest)
  794 
  795     def render_property_diff(self, name, old_node, old_props,
  796                              new_node, new_props, options):
  797         """Renders diffs of a node property to HTML."""
  798         if name in BrowserModule(self.env).hidden_properties:
  799             return
  800         candidates = []
  801         for renderer in self.property_diff_renderers:
  802             quality = renderer.match_property_diff(name)
  803             if quality > 0:
  804                 candidates.append((quality, renderer))
  805         candidates.sort(reverse=True)
  806         for (quality, renderer) in candidates:
  807             try:
  808                 return renderer.render_property_diff(name, old_node, old_props,
  809                                                      new_node, new_props,
  810                                                      options)
  811             except Exception as e:
  812                 self.log.warning('Diff rendering failed for property %s with '
  813                                  'renderer %s: %s', name,
  814                                  renderer.__class__.__name__,
  815                                  exception_to_unicode(e, traceback=True))
  816 
  817     def _get_location(self, files):
  818         """Return the deepest common path for the given files.
  819            If all the files are actually the same, return that location."""
  820         if len(files) == 1:
  821             return files[0]
  822         else:
  823             return '/'.join(os.path.commonprefix([f.split('/')
  824                                                   for f in files]))
  825 
  826     def _get_parent_location(self, files):
  827         """Only get a location when there are different files,
  828            otherwise return the empty string."""
  829         if files:
  830             files.sort()
  831             prev = files[0]
  832             for f in files[1:]:
  833                 if f != prev:
  834                     return self._get_location(files)
  835         return ''
  836 
  837     def _prepare_filestats(self):
  838         filestats = {}
  839         for chg in Changeset.ALL_CHANGES:
  840             filestats[chg] = 0
  841         return filestats
  842 
  843     # ITimelineEventProvider methods
  844 
  845     def get_timeline_filters(self, req):
  846         if 'CHANGESET_VIEW' in req.perm:
  847             # Non-'hidden' repositories will be listed as additional
  848             # repository filters, unless there is only a single repository.
  849             filters = []
  850             rm = RepositoryManager(self.env)
  851             repositories = rm.get_real_repositories()
  852             if len(repositories) > 1:
  853                 filters = [
  854                     ('repo-' + repos.reponame,
  855                      u"\xa0\xa0-\xa0" + (repos.reponame or _('(default)')))
  856                     for repos in repositories
  857                     if not as_bool(repos.params.get('hidden'))
  858                     and repos.is_viewable(req.perm)]
  859                 filters.sort()
  860                 add_script(req, 'common/js/timeline_multirepos.js')
  861                 changeset_label = _('Changesets in all repositories')
  862             else:
  863                 changeset_label = _('Repository changesets')
  864             filters.insert(0, ('changeset', changeset_label))
  865             return filters
  866         else:
  867             return []
  868 
  869     def get_timeline_events(self, req, start, stop, filters):
  870         all_repos = 'changeset' in filters
  871         repo_filters = {f for f in filters if f.startswith('repo-')}
  872         if all_repos or repo_filters:
  873             show_files = self.timeline_show_files
  874             show_location = show_files == 'location'
  875             if show_files in ('-1', 'unlimited'):
  876                 show_files = -1
  877             elif show_files.isdigit():
  878                 show_files = int(show_files)
  879             else:
  880                 show_files = 0  # disabled
  881 
  882             if self.timeline_collapse:
  883                 collapse_changesets = lambda c: (c.author, c.message)
  884             else:
  885                 collapse_changesets = lambda c: c.rev
  886 
  887             uids_seen = {}
  888             def generate_changesets(repos):
  889                 for _, changesets in groupby(repos.get_changesets(start, stop),
  890                                              key=collapse_changesets):
  891                     viewable_changesets = []
  892                     for cset in changesets:
  893                         if cset.is_viewable(req.perm):
  894                             repos_for_uid = [repos.reponame]
  895                             uid = repos.get_changeset_uid(cset.rev)
  896                             if uid:
  897                                 # uid can be seen in multiple repositories
  898                                 if uid in uids_seen:
  899                                     uids_seen[uid].append(repos.reponame)
  900                                     continue  # already viewable, just append
  901                                 uids_seen[uid] = repos_for_uid
  902                             viewable_changesets.append((cset, cset.resource,
  903                                                         repos_for_uid))
  904                     if viewable_changesets:
  905                         cset = viewable_changesets[-1][0]
  906                         yield ('changeset', cset.date, cset.author,
  907                                (viewable_changesets,
  908                                 show_location, show_files))
  909 
  910             rm = RepositoryManager(self.env)
  911             for repos in sorted(rm.get_real_repositories(),
  912                                 key=lambda repos: repos.reponame):
  913                 if all_repos or ('repo-' + repos.reponame) in repo_filters:
  914                     try:
  915                         for event in generate_changesets(repos):
  916                             yield event
  917                     except TracError as e:
  918                         self.log.error("Timeline event provider for repository"
  919                                        " '%s' failed: %r",
  920                                        repos.reponame, exception_to_unicode(e))
  921 
  922     def render_timeline_event(self, context, field, event):
  923         changesets, show_location, show_files = event[3]
  924         cset, cset_resource, repos_for_uid = changesets[0]
  925         older_cset = changesets[-1][0]
  926         message = cset.message or ''
  927         reponame = cset_resource.parent.id
  928         rev_b, rev_a = cset.rev, older_cset.rev
  929 
  930         if field == 'url':
  931             if rev_a == rev_b:
  932                 return context.href.changeset(rev_a, reponame or None)
  933             else:
  934                 return context.href.log(reponame or None, rev=rev_b,
  935                                         stop_rev=rev_a)
  936 
  937         elif field == 'description':
  938             if self.wiki_format_messages:
  939                 markup = ''
  940                 if self.timeline_long_messages:  # override default flavor
  941                     context = context.child()
  942                     context.set_hints(wiki_flavor='html',
  943                                       preserve_newlines=True)
  944             else:
  945                 markup = message
  946                 message = None
  947             if 'BROWSER_VIEW' in context.perm:
  948                 files = []
  949                 if show_location:
  950                     filestats = self._prepare_filestats()
  951                     for c, r, repos_for_c in changesets:
  952                         for chg in c.get_changes():
  953                             resource = c.resource.parent.child('source',
  954                                                                chg[0] or '/',
  955                                                                r.id)
  956                             if not 'FILE_VIEW' in context.perm(resource):
  957                                 continue
  958                             filestats[chg[2]] += 1
  959                             files.append(chg[0])
  960                     stats = [(tag.div(class_=kind),
  961                               tag.span(count, ' ',
  962                                        count > 1 and
  963                                        (kind == 'copy' and
  964                                         'copies' or kind + 's') or kind))
  965                              for kind in Changeset.ALL_CHANGES
  966                              for count in (filestats[kind],) if count]
  967                     markup = tag.ul(
  968                         tag.li(stats, ' in ',
  969                                tag.strong(self._get_location(files) or '/')),
  970                         markup, class_="changes")
  971                 elif show_files:
  972                     unique_files = set()
  973                     for c, r, repos_for_c in changesets:
  974                         for chg in c.get_changes():
  975                             resource = c.resource.parent.child('source',
  976                                                                chg[0] or '/',
  977                                                                r.id)
  978                             if not 'FILE_VIEW' in context.perm(resource):
  979                                 continue
  980                             if 0 < show_files < len(files):
  981                                 break
  982                             unique_files.add((chg[0], chg[2]))
  983                     files = [tag.li(tag.div(class_=mod), path or '/')
  984                              for path, mod in sorted(unique_files)]
  985                     if 0 < show_files < len(files):
  986                         files = files[:show_files] + [tag.li(u'\u2026')]
  987                     markup = tag(tag.ul(files, class_="changes"), markup)
  988             if message:
  989                 markup += format_to(self.env, None,
  990                                     context.child(cset_resource), message)
  991             return markup
  992 
  993         single = rev_a == rev_b
  994         if not repos_for_uid[0]:
  995             repos_for_uid[0] = _('(default)')
  996         if reponame or len(repos_for_uid) > 1:
  997             title = ngettext('Changeset in %(repo)s ',
  998                              'Changesets in %(repo)s ',
  999                              1 if single else 2, repo=', '.join(repos_for_uid))
 1000         else:
 1001             title = ngettext('Changeset ', 'Changesets ', 1 if single else 2)
 1002         drev_a = older_cset.repos.display_rev(rev_a)
 1003         if single:
 1004             title = tag(title, tag.em('[%s]' % drev_a))
 1005         else:
 1006             drev_b = cset.repos.display_rev(rev_b)
 1007             title = tag(title, tag.em('[%s-%s]' % (drev_a, drev_b)))
 1008         if field == 'title':
 1009             labels = []
 1010             for name, head in cset.get_branches():
 1011                 if not head and name in ('default', 'master'):
 1012                     continue
 1013                 class_ = 'branch'
 1014                 if head:
 1015                     class_ += ' head'
 1016                 labels.append(tag.span(name, class_=class_))
 1017             for name in cset.get_tags():
 1018                 labels.append(tag.span(name, class_='tag'))
 1019             for name in cset.get_bookmarks():
 1020                 labels.append(tag.span(name, class_='trac-bookmark'))
 1021             return title if not labels else tag(title, labels)
 1022         elif field == 'summary':
 1023             return tag_("%(title)s: %(message)s",
 1024                         title=title, message=shorten_line(message))
 1025 
 1026     # IWikiSyntaxProvider methods
 1027 
 1028     CHANGESET_ID = r"(?:[0-9]+|[a-fA-F0-9]{8,})"  # only "long enough" hex ids
 1029 
 1030     def get_wiki_syntax(self):
 1031         yield (
 1032             # [...] form: start with optional intertrac: [T... or [trac ...
 1033             r"!?\[(?P<it_changeset>%s\s*)" % WikiParser.INTERTRAC_SCHEME +
 1034             # hex digits + optional /path for the restricted changeset
 1035             # + optional query and fragment
 1036             r"%s(?:/[^\]]*)?(?:\?[^\]]*)?(?:#[^\]]*)?\]|" % self.CHANGESET_ID +
 1037             # r... form: allow r1 but not r1:2 (handled by the log syntax)
 1038             r"(?:\b|!)r[0-9]+\b(?!:[0-9])(?:/[a-zA-Z0-9_/+-]+)?",
 1039             lambda x, y, z:
 1040             self._format_changeset_link(x, 'changeset',
 1041                                         y[1:] if y[0] == 'r' else y[1:-1],
 1042                                         y, z))
 1043 
 1044     def get_link_resolvers(self):
 1045         yield ('changeset', self._format_changeset_link)
 1046         yield ('diff', self._format_diff_link)
 1047 
 1048     def _format_changeset_link(self, formatter, ns, chgset, label,
 1049                                fullmatch=None):
 1050         intertrac = formatter.shorthand_intertrac_helper(ns, chgset, label,
 1051                                                          fullmatch)
 1052         if intertrac:
 1053             return intertrac
 1054 
 1055         # identifying repository
 1056         rm = RepositoryManager(self.env)
 1057         chgset, params, fragment = formatter.split_link(chgset)
 1058         sep = chgset.find('/')
 1059         if sep > 0:
 1060             rev, path = chgset[:sep], chgset[sep:]
 1061         else:
 1062             rev, path = chgset, '/'
 1063         try:
 1064             reponame, repos, path = rm.get_repository_by_path(path)
 1065             if not reponame:
 1066                 reponame = rm.get_default_repository(formatter.context)
 1067                 if reponame is not None:
 1068                     repos = rm.get_repository(reponame)
 1069             if path == '/':
 1070                 path = None
 1071 
 1072             # rendering changeset link
 1073             if repos:
 1074                 changeset = repos.get_changeset(rev)
 1075                 if changeset.is_viewable(formatter.perm):
 1076                     href = formatter.href.changeset(rev,
 1077                                                     repos.reponame or None,
 1078                                                     path)
 1079                     return tag.a(label, class_="changeset",
 1080                                  title=shorten_line(changeset.message),
 1081                                  href=href + params + fragment)
 1082                 errmsg = _("No permission to view changeset %(rev)s "
 1083                            "on %(repos)s", rev=rev,
 1084                            repos=reponame or _('(default)'))
 1085             elif reponame:
 1086                 errmsg = _("Repository '%(repo)s' not found", repo=reponame)
 1087             else:
 1088                 errmsg = _("No default repository defined")
 1089         except TracError as e:
 1090             errmsg = to_unicode(e)
 1091         return tag.a(label, class_="missing changeset", title=errmsg)
 1092 
 1093     def _format_diff_link(self, formatter, ns, target, label):
 1094         params, query, fragment = formatter.split_link(target)
 1095         def pathrev(path):
 1096             if '@' in path:
 1097                 return path.split('@', 1)
 1098             else:
 1099                 return path, None
 1100         if '//' in params:
 1101             p1, p2 = params.split('//', 1)
 1102             old, new = pathrev(p1), pathrev(p2)
 1103             data = {'old_path': old[0], 'old_rev': old[1],
 1104                     'new_path': new[0], 'new_rev': new[1]}
 1105         else:
 1106             old_path, old_rev = pathrev(params)
 1107             new_rev = None
 1108             if old_rev and ':' in old_rev:
 1109                 old_rev, new_rev = old_rev.split(':', 1)
 1110             data = {'old_path': old_path, 'old_rev': old_rev,
 1111                     'new_path': old_path, 'new_rev': new_rev}
 1112         title = self.title_for_diff(data)
 1113         href = None
 1114         if any(data.values()):
 1115             if query:
 1116                 query = '&' + query[1:]
 1117             href = formatter.href.changeset(new_path=data['new_path'] or None,
 1118                                             new=data['new_rev'],
 1119                                             old_path=data['old_path'] or None,
 1120                                             old=data['old_rev']) + query
 1121         return tag.a(label, class_="changeset", title=title, href=href)
 1122 
 1123     # ISearchSource methods
 1124 
 1125     ### FIXME: move this specific implementation into cache.py
 1126 
 1127     def get_search_filters(self, req):
 1128         if 'CHANGESET_VIEW' in req.perm:
 1129             yield ('changeset', _('Changesets'))
 1130 
 1131     def get_search_results(self, req, terms, filters):
 1132         if not 'changeset' in filters:
 1133             return
 1134         rm = RepositoryManager(self.env)
 1135         repositories = {repos.params['id']: repos
 1136                         for repos in rm.get_real_repositories()}
 1137         uids_seen = set()
 1138         with self.env.db_query as db:
 1139             sql, args = search_to_sql(db, ['rev', 'message', 'author'], terms)
 1140             for id, rev, ts, author, log in db("""
 1141                     SELECT repos, rev, time, author, message
 1142                     FROM revision WHERE """ + sql, args):
 1143                 repos = repositories.get(id)
 1144                 if not repos:
 1145                     continue  # revisions for a no longer active repository
 1146                 try:
 1147                     rev = repos.normalize_rev(rev)
 1148                     drev = repos.display_rev(rev)
 1149                 except NoSuchChangeset:
 1150                     continue
 1151                 uid = repos.get_changeset_uid(rev)
 1152                 if uid in uids_seen:
 1153                     continue
 1154                 cset = repos.resource.child(self.realm, rev)
 1155                 if 'CHANGESET_VIEW' in req.perm(cset):
 1156                     uids_seen.add(uid)
 1157                     yield (req.href.changeset(rev, repos.reponame or None),
 1158                            '[%s]: %s' % (drev, shorten_line(log)),
 1159                            from_utimestamp(ts), author,
 1160                            shorten_result(log, terms))
 1161 
 1162 
 1163 class AnyDiffModule(Component):
 1164 
 1165     implements(IRequestHandler)
 1166 
 1167     # IRequestHandler methods
 1168 
 1169     def match_request(self, req):
 1170         return req.path_info == '/diff'
 1171 
 1172     def process_request(self, req):
 1173         rm = RepositoryManager(self.env)
 1174 
 1175         if req.is_xhr:
 1176             dirname, prefix = posixpath.split(req.args.get('term'))
 1177             prefix = prefix.lower()
 1178             reponame, repos, path = rm.get_repository_by_path(dirname)
 1179             # an entry is a (isdir, name, path) tuple
 1180             def kind_order(entry):
 1181                 return not entry[0], embedded_numbers(entry[1])
 1182 
 1183             entries = []
 1184             if repos:
 1185                 entries.extend((e.isdir, e.name,
 1186                                 '/' + pathjoin(repos.reponame, e.path))
 1187                                for e in repos.get_node(path).get_entries()
 1188                                if e.is_viewable(req.perm))
 1189             if not reponame:
 1190                 entries.extend((True, repos.reponame, '/' + repos.reponame)
 1191                                for repos in rm.get_real_repositories()
 1192                                if repos.is_viewable(req.perm))
 1193 
 1194             paths = [{'label': path + ('/' if isdir else ''), 'value': path,
 1195                       'isdir': isdir}
 1196                      for isdir, name, path in sorted(entries, key=kind_order)
 1197                                            if name.lower().startswith(prefix)]
 1198 
 1199             content = to_json(paths)
 1200             req.send(content, 'application/json', 200)
 1201 
 1202         # -- retrieve arguments
 1203         new_path = req.args.get('new_path')
 1204         new_rev = req.args.get('new_rev')
 1205         old_path = req.args.get('old_path')
 1206         old_rev = req.args.get('old_rev')
 1207 
 1208         # -- normalize and prepare rendering
 1209         new_reponame, new_repos, new_path = \
 1210             rm.get_repository_by_path(new_path)
 1211         old_reponame, old_repos, old_path = \
 1212             rm.get_repository_by_path(old_path)
 1213 
 1214         data = {}
 1215         if new_repos:
 1216             data.update(new_path='/' + pathjoin(new_repos.reponame, new_path),
 1217                         new_rev=new_repos.normalize_rev(new_rev))
 1218         else:
 1219             data.update(new_path=req.args.get('new_path'), new_rev=new_rev)
 1220         if old_repos:
 1221             data.update(old_path='/' + pathjoin(old_repos.reponame, old_path),
 1222                         old_rev=old_repos.normalize_rev(old_rev))
 1223         else:
 1224             data.update(old_path=req.args.get('old_path'), old_rev=old_rev)
 1225 
 1226         Chrome(self.env).add_jquery_ui(req)
 1227         add_stylesheet(req, 'common/css/diff.css')
 1228         return 'diff_form.html', data
 1229 
 1230 
 1231 def _read_content(node):
 1232     with content_closing(node.get_content()) as content:
 1233         return content.read()