source: peerreviewplugin/trunk/codereview/repobrowser.py

Last change on this file was 18276, checked in by Cinc-th, 2 years ago

PeerReviewPlugin: bugfixing and improvements for file admin page. Added spinner when loading repo, use display_rev() for rendering revision information, ...

File size: 15.2 KB
RevLine 
[18251]1# -*- coding: utf-8 -*-
[13497]2#
[15212]3# Copyright (C) 2005-2006 Team5
[18050]4# Copyright (C) Cinc
[13497]5# All rights reserved.
6#
[15212]7# This software is licensed as described in the file COPYING.txt, which
[13497]8# you should have received as part of this distribution.
9#
[717]10# Author: Team5
11#
12
13from __future__ import generators
14import re
[18251]15try:
16    from functools import cmp_to_key
17except ImportError:
18    pass
[717]19from trac.core import *
20from trac.mimeview import *
21from trac.mimeview.api import IHTMLPreviewAnnotator
[15194]22from trac.resource import ResourceNotFound
[13497]23from trac.util import embedded_numbers
[18251]24from trac.util.datefmt import format_datetime, http_date, pretty_timedelta
25from trac.util.html import escape, html as tag
26from trac.util.text import pretty_size
[16616]27from trac.util.translation import _
[18050]28from trac.versioncontrol.api import NoSuchChangeset, RepositoryManager
[13497]29from trac.versioncontrol.web_ui.util import *
[717]30from trac.web import IRequestHandler, RequestDone
[18242]31from trac.web.chrome import add_link, Chrome, web_context
[18050]32from trac.wiki import format_to_html
[717]33
[18251]34from .compat import is_py3, iteritems
35
36
[18249]37try:
38    cmp
39except NameError:
40    def cmp(a, b):
41        return (a > b) - (a < b)
42
[18251]43
[717]44IMG_RE = re.compile(r"\.(gif|jpg|jpeg|png)(\?.*)?$", re.IGNORECASE)
45CHUNK_SIZE = 4096
[13497]46DIGITS = re.compile(r'[0-9]+')
[717]47
[13497]48
[717]49def _natural_order(x, y):
50    """Comparison function for natural order sorting based on
51    http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/214202."""
52    nx = ny = 0
53    while True:
54        a = DIGITS.search(x, nx)
55        b = DIGITS.search(y, ny)
56        if None in (a, b):
57            return cmp(x[nx:], y[ny:])
58        r = (cmp(x[nx:a.start()], y[ny:b.start()]) or
59             cmp(int(x[a.start():a.end()]), int(y[b.start():b.end()])))
60        if r:
61            return r
62        nx, ny = a.end(), b.end()
63
64
[17450]65class PeerRepoBrowser(Component):
[17265]66    """Provide a repository browser for file selection for code reviews.
67
68    [[BR]]
69    Component used for browsing the repository for files.
70
71    '''Note:''' do not disable otherwise no files may be selected for a review.
72    """
73
[15164]74    implements(IRequestHandler, IHTMLPreviewAnnotator)
[717]75
76    # ITextAnnotator methods
77    def get_annotation_type(self):
[13497]78        return 'lineno', 'Line', 'Line numbers'
[3449]79
[3451]80    def get_annotation_data(self, context):
81        return None
82
[3449]83    def annotate_row(self, context, row, lineno, line, data):
84        row.append(tag.th(id='L%s' % lineno)(
85            tag.a(lineno, href='javascript:setLineNum(%s)' % lineno)
86        ))
[13497]87
[717]88    # IRequestHandler methods
89
90    def match_request(self, req):
91        import re
[15585]92        match = re.match(r'/(peerReviewBrowser|file|adminrepobrowser)(?:(/.*))?', req.path_info)
[717]93        if match:
94            req.args['path'] = match.group(2) or '/'
95            if match.group(1) == 'file':
96                # FIXME: This should be a permanent redirect
[17448]97                req.redirect(req.href.peerReviewBrowser(req.args.get('path'),
[717]98                                                   rev=req.args.get('rev'),
99                                                   format=req.args.get('format')))
[15585]100            elif match.group(1) == 'adminrepobrowser':
101                # This one is for the browser on admin pages
102                req.args['is_admin_browser'] = True
[717]103            return True
104
105    def process_request(self, req):
[3542]106
[717]107        path = req.args.get('path', '/')
108        rev = req.args.get('rev')
[15581]109        cur_repo = req.args.get('repo', '')
[15588]110        is_admin_browser = req.args.get('is_admin_browser', False)  # Set when we come from the file admin page
[18050]111        context = web_context(req)
[15588]112        # Depending on from where we are coming we have to preprocess in match_request() thus use different paths
113        browse_url_base = 'adminrepobrowser' if is_admin_browser else 'peerReviewBrowser'
[18242]114        if hasattr(Chrome, 'jenv'):
115            template_file = 'peeradmin_repobrowser_jinja.html' if is_admin_browser else 'peerrepobrowser_jinja.html'
116        else:
117            template_file = 'admin_repobrowser.html' if is_admin_browser else 'repobrowser.html'
[717]118
[15281]119        # display_rev = lambda rev: rev
120
[17448]121        data = {'browse_url': req.href(browse_url_base),
[18252]122                'is_admin_browser': is_admin_browser,
123                'iteritems': iteritems
[15588]124                }
125
[15581]126        repoman = RepositoryManager(self.env)
[15588]127
128        all_repos = repoman.get_all_repositories()
129        if not all_repos:
[15581]130            data['norepo'] = _("No source repository available.")
[18242]131            if hasattr(Chrome, 'jenv'):
132                return 'peerrepobrowser_jinja.html', data
133            else:
134                return 'repobrowser.html', data, None
[15581]135
[15588]136        # Repositories may be hidden
137        filtered_repos = {}
[18251]138        for rname, info in iteritems(all_repos):
[15588]139            try:
140                if not info['hidden'] == u'1':
141                    filtered_repos[rname] = info
142            except KeyError:
143                # This is the default repo
144                filtered_repos[rname] = info
145
146        if not filtered_repos:
147            data['norepo'] = _("No source repository available.")
[18242]148            if hasattr(Chrome, 'jenv'):
149                return 'peerrepobrowser_jinja.html', data
150            else:
151                return 'repobrowser.html', data, None
[15588]152
153        data['all_repos'] = filtered_repos
154
[15589]155        # if not req.args.get('repo', None): won't work here because of default repo name ''
156        if req.args.get('repo', None) == None:
157            # We open the page for the first time
[15581]158            data['show_repo_idx'] = True
[18242]159            if hasattr(Chrome, 'jenv'):
160                return template_file, data
161            else:
162                return template_file, data, None
[15581]163
[15589]164        if cur_repo not in data['all_repos']:
165            data['repo_gone'] = cur_repo if cur_repo else '(default)'
166            data['show_repo_idx'] = True
[18242]167            if hasattr(Chrome, 'jenv'):
168                return template_file, data
169            else:
170                return template_file, data, None
[15589]171
[15581]172        # Find node for the requested repo/path/rev
173        repo = repoman.get_repository(cur_repo)
174        if repo:
[15331]175            try:
[15581]176                node, display_rev, context = get_node_from_repo(req, repo, path, rev)
[17269]177            except NoSuchChangeset as e:
[15331]178                data['norepo'] = _(e.message)
[18242]179                if hasattr(Chrome, 'jenv'):
180                    return template_file, data
181                else:
182                    return template_file, data, None
[17269]183            except ResourceNotFound as e:  # NoSuchNode is converted to this exception by Trac
[15585]184                data['nonode'] = e.message
185                node = None
[18276]186            display_rev = repo.display_rev
[15212]187        else:
188            data['norepo'] = _("No source repository available.")
[18242]189            if hasattr(Chrome, 'jenv'):
190                return template_file, data
191            else:
192                return template_file, data, None
[15158]193
[717]194        hidden_properties = [p.strip() for p
195                             in self.config.get('browser', 'hide_properties',
196                                                'svk:merge').split(',')]
[3451]197
[17448]198        path_links = self.get_path_links_CRB(req.href, browse_url_base, path, rev, cur_repo)
[717]199        if len(path_links) > 1:
200            add_link(req, 'up', path_links[-2]['href'], 'Parent directory')
201
[15585]202        if node:
[18251]203            props = [{'name': escape(name), 'value': escape(value)}
[15585]204                     for name, value in node.get_properties().items()
205                     if name not in hidden_properties]
206        else:
207            props = []
[15581]208        data.update({
209            'path': path,
[15585]210            'rev': node.rev if node else rev,
[15581]211            'stickyrev': rev,
[15194]212            'context': context,
[15581]213            'repo': repo,
214            'reponame': repo.reponame,  # for included path_links.html
215            'revision': rev or repo.get_youngest_rev(),
[15585]216            'props': props,
[18251]217            'log_href': escape(req.href.log(path, rev=rev or None)),
[3542]218            'path_links': path_links,
[15585]219            'dir': node and node.isdir and self._render_directory(req, repo, node, rev, cur_repo),
220            'file': node and node.isfile and self._render_file(req, context, repo, node, rev, cur_repo),
[15158]221            'display_rev': display_rev,
[15581]222            'wiki_format_messages': self.config['changeset'].getbool('wiki_format_messages'),
223        })
[18242]224        if hasattr(Chrome, 'jenv'):
225            return template_file, data
226        else:
227            return template_file, data, None
[717]228
229    # Internal methods
230
[15585]231    def get_path_links_CRB(self, href, browse_url_base, fullpath, rev, repo):
[717]232        path = '/'
[15581]233        links = [{'name': 'Repository Index',
[15585]234                  'href': href(browse_url_base, path, rev=rev)}]
[3542]235
236        for part in [p for p in fullpath.split('/') if p]:
237            path += part + '/'
[717]238            links.append({
[3542]239                'name': part,
[15585]240                'href': href(browse_url_base, path, rev=rev, repo=repo)
[3542]241                })
[717]242        return links
243
[15581]244    def _render_directory(self, req, repos, node, rev=None, repo=''):
[717]245        req.perm.assert_permission('BROWSER_VIEW')
246
247        order = req.args.get('order', 'name').lower()
[18252]248        desc = 'desc' in req.args
[717]249
250        info = []
[3542]251
252        if order == 'date':
253            def file_order(a):
254                return changes[a.rev].date
255        elif order == 'size':
256            def file_order(a):
257                return (a.content_length,
258                        embedded_numbers(a.name.lower()))
259        else:
260            def file_order(a):
261                return embedded_numbers(a.name.lower())
262
263        dir_order = desc and 1 or -1
264
265        def browse_order(a):
266            return a.isdir and dir_order or 0, file_order(a)
267
[15585]268        browse_url = "peerReviewBrowser" if not req.args.get('is_admin_browser', False) else "adminrepobrowser"
[717]269        for entry in node.get_entries():
[15194]270            if entry.can_view(req.perm):
271                info.append({
272                    'name': entry.name,
273                    'fullpath': entry.path,
274                    'is_dir': int(entry.isdir),
275                    'content_length': entry.content_length,
[18251]276                    'size': pretty_size(entry.content_length),
[15194]277                    'rev': entry.created_rev,
278                    'permission': 1,  # FIXME
[18251]279                    'log_href': escape(req.href.log(repo, entry.path, rev=rev)),
[17448]280                    'browser_href': req.href(browse_url, entry.path, rev=rev, repo=repo)
[15194]281                    })
282
[3443]283        changes = get_changes(repos, [i['rev'] for i in info])
[717]284
285        def cmp_func(a, b):
286            dir_cmp = (a['is_dir'] and -1 or 0) + (b['is_dir'] and 1 or 0)
287            if dir_cmp:
288                return dir_cmp
289            neg = desc and -1 or 1
290            if order == 'date':
291                return neg * cmp(changes[b['rev']]['date_seconds'],
292                                 changes[a['rev']]['date_seconds'])
293            elif order == 'size':
294                return neg * cmp(a['content_length'], b['content_length'])
295            else:
296                return neg * _natural_order(a['name'].lower(),
297                                            b['name'].lower())
[18251]298        if is_py3:
299            info.sort(key=cmp_to_key(cmp_func))
300        else:
301            info.sort(cmp_func)
[717]302
[3542]303        return {'order': order, 'desc': desc and 1 or None,
[13497]304                'items': info, 'changes': changes}
[717]305
[15581]306    def _render_file(self, req, context, repos, node, rev=None, repo=''):
[3449]307        req.perm(context.resource).require('FILE_VIEW')
[717]308
309        mime_type = node.content_type
310        if not mime_type or mime_type == 'application/octet-stream':
311            mime_type = get_mimetype(node.name) or mime_type or 'text/plain'
312
313        # We don't have to guess if the charset is specified in the
314        # svn:mime-type property
315        ctpos = mime_type.find('charset=')
316        if ctpos >= 0:
317            charset = mime_type[ctpos + 8:]
318        else:
319            charset = None
320
[3449]321        content = node.get_content()
322        chunk = content.read(CHUNK_SIZE)
323
[717]324        format = req.args.get('format')
[3449]325        if format in ('raw', 'txt'):
[717]326            req.send_response(200)
327            req.send_header('Content-Type',
328                            format == 'txt' and 'text/plain' or mime_type)
329            req.send_header('Content-Length', node.content_length)
[18251]330            req.send_header('Last-Modified', http_date(node.last_modified))
[717]331            req.end_headers()
332
333            while 1:
334                if not chunk:
335                    raise RequestDone
336                req.write(chunk)
[3449]337                chunk = content.read(CHUNK_SIZE)
[717]338        else:
339            # Generate HTML preview
340            mimeview = Mimeview(self.env)
341
[3449]342            # The changeset corresponding to the last change on `node`
343            # is more interesting than the `rev` changeset.
344            changeset = repos.get_changeset(node.rev)
345
346            # add ''Plain Text'' alternate link if needed
347            if not is_binary(chunk) and mime_type != 'text/plain':
348                plain_href = req.href.browser(node.path, rev=rev, format='txt')
349                add_link(req, 'alternate', plain_href, 'Plain Text',
350                         'text/plain')
351
[17448]352            raw_href = req.href.peerReviewBrowser(node.path, rev=rev and node.rev, repo=repo,
[717]353                                             format='raw')
[3449]354            preview_data = mimeview.preview_data(context, node.get_content(),
355                                                    node.get_content_length(),
356                                                    mime_type, node.created_path,
357                                                    raw_href,
358                                                    annotations=['lineno'])
[2270]359
[717]360            add_link(req, 'alternate', raw_href, 'Original Format', mime_type)
361
[18050]362            # TODO: we have a context as a method parameter. Check if this is duplicate code.
363            context = web_context(req)
364            msg = format_to_html(self.env, context=context, wikidom=changeset.message or '', escape_newlines=True)
[3449]365            return {
366                'changeset': changeset,
367                'size': node.content_length,
[3542]368                'preview': preview_data['rendered'],
[15215]369                'max_file_size_reached': preview_data['max_file_size_reached'],
370                'max_file_size': preview_data['max_file_size'],
[3449]371                'annotate': False,
[3542]372                'rev': node.rev,
[18251]373                'changeset_href': escape(req.href.changeset(node.rev)),
374                'date': format_datetime(changeset.date),
375                'age': pretty_timedelta(changeset.date),
[3542]376                'author': changeset.author or 'anonymous',
[18050]377                'message': msg
[13497]378            }
[15281]379
380
381def get_node_from_repo(req, repos, path, rev):
382
[18050]383    context = web_context(req)
[15281]384
[15331]385    if rev:
386        rev = repos.normalize_rev(rev)
387    # If `rev` is `None`, we'll try to reuse `None` consistently,
388    # as a special shortcut to the latest revision.
389    rev_or_latest = rev or repos.youngest_rev
390    node = get_existing_node(req, repos, path, rev_or_latest)
[15281]391
392    context = context(repos.resource.child('source', path,
393                                           version=rev_or_latest))
394    display_rev = repos.display_rev
395
[16616]396    return node, display_rev, context
Note: See TracBrowser for help on using the repository browser.