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
Line 
1# -*- coding: utf-8 -*-
2#
3# Copyright (C) 2005-2006 Team5
4# Copyright (C) Cinc
5# All rights reserved.
6#
7# This software is licensed as described in the file COPYING.txt, which
8# you should have received as part of this distribution.
9#
10# Author: Team5
11#
12
13from __future__ import generators
14import re
15try:
16    from functools import cmp_to_key
17except ImportError:
18    pass
19from trac.core import *
20from trac.mimeview import *
21from trac.mimeview.api import IHTMLPreviewAnnotator
22from trac.resource import ResourceNotFound
23from trac.util import embedded_numbers
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
27from trac.util.translation import _
28from trac.versioncontrol.api import NoSuchChangeset, RepositoryManager
29from trac.versioncontrol.web_ui.util import *
30from trac.web import IRequestHandler, RequestDone
31from trac.web.chrome import add_link, Chrome, web_context
32from trac.wiki import format_to_html
33
34from .compat import is_py3, iteritems
35
36
37try:
38    cmp
39except NameError:
40    def cmp(a, b):
41        return (a > b) - (a < b)
42
43
44IMG_RE = re.compile(r"\.(gif|jpg|jpeg|png)(\?.*)?$", re.IGNORECASE)
45CHUNK_SIZE = 4096
46DIGITS = re.compile(r'[0-9]+')
47
48
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
65class PeerRepoBrowser(Component):
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
74    implements(IRequestHandler, IHTMLPreviewAnnotator)
75
76    # ITextAnnotator methods
77    def get_annotation_type(self):
78        return 'lineno', 'Line', 'Line numbers'
79
80    def get_annotation_data(self, context):
81        return None
82
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        ))
87
88    # IRequestHandler methods
89
90    def match_request(self, req):
91        import re
92        match = re.match(r'/(peerReviewBrowser|file|adminrepobrowser)(?:(/.*))?', req.path_info)
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
97                req.redirect(req.href.peerReviewBrowser(req.args.get('path'),
98                                                   rev=req.args.get('rev'),
99                                                   format=req.args.get('format')))
100            elif match.group(1) == 'adminrepobrowser':
101                # This one is for the browser on admin pages
102                req.args['is_admin_browser'] = True
103            return True
104
105    def process_request(self, req):
106
107        path = req.args.get('path', '/')
108        rev = req.args.get('rev')
109        cur_repo = req.args.get('repo', '')
110        is_admin_browser = req.args.get('is_admin_browser', False)  # Set when we come from the file admin page
111        context = web_context(req)
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'
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'
118
119        # display_rev = lambda rev: rev
120
121        data = {'browse_url': req.href(browse_url_base),
122                'is_admin_browser': is_admin_browser,
123                'iteritems': iteritems
124                }
125
126        repoman = RepositoryManager(self.env)
127
128        all_repos = repoman.get_all_repositories()
129        if not all_repos:
130            data['norepo'] = _("No source repository available.")
131            if hasattr(Chrome, 'jenv'):
132                return 'peerrepobrowser_jinja.html', data
133            else:
134                return 'repobrowser.html', data, None
135
136        # Repositories may be hidden
137        filtered_repos = {}
138        for rname, info in iteritems(all_repos):
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.")
148            if hasattr(Chrome, 'jenv'):
149                return 'peerrepobrowser_jinja.html', data
150            else:
151                return 'repobrowser.html', data, None
152
153        data['all_repos'] = filtered_repos
154
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
158            data['show_repo_idx'] = True
159            if hasattr(Chrome, 'jenv'):
160                return template_file, data
161            else:
162                return template_file, data, None
163
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
167            if hasattr(Chrome, 'jenv'):
168                return template_file, data
169            else:
170                return template_file, data, None
171
172        # Find node for the requested repo/path/rev
173        repo = repoman.get_repository(cur_repo)
174        if repo:
175            try:
176                node, display_rev, context = get_node_from_repo(req, repo, path, rev)
177            except NoSuchChangeset as e:
178                data['norepo'] = _(e.message)
179                if hasattr(Chrome, 'jenv'):
180                    return template_file, data
181                else:
182                    return template_file, data, None
183            except ResourceNotFound as e:  # NoSuchNode is converted to this exception by Trac
184                data['nonode'] = e.message
185                node = None
186            display_rev = repo.display_rev
187        else:
188            data['norepo'] = _("No source repository available.")
189            if hasattr(Chrome, 'jenv'):
190                return template_file, data
191            else:
192                return template_file, data, None
193
194        hidden_properties = [p.strip() for p
195                             in self.config.get('browser', 'hide_properties',
196                                                'svk:merge').split(',')]
197
198        path_links = self.get_path_links_CRB(req.href, browse_url_base, path, rev, cur_repo)
199        if len(path_links) > 1:
200            add_link(req, 'up', path_links[-2]['href'], 'Parent directory')
201
202        if node:
203            props = [{'name': escape(name), 'value': escape(value)}
204                     for name, value in node.get_properties().items()
205                     if name not in hidden_properties]
206        else:
207            props = []
208        data.update({
209            'path': path,
210            'rev': node.rev if node else rev,
211            'stickyrev': rev,
212            'context': context,
213            'repo': repo,
214            'reponame': repo.reponame,  # for included path_links.html
215            'revision': rev or repo.get_youngest_rev(),
216            'props': props,
217            'log_href': escape(req.href.log(path, rev=rev or None)),
218            'path_links': path_links,
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),
221            'display_rev': display_rev,
222            'wiki_format_messages': self.config['changeset'].getbool('wiki_format_messages'),
223        })
224        if hasattr(Chrome, 'jenv'):
225            return template_file, data
226        else:
227            return template_file, data, None
228
229    # Internal methods
230
231    def get_path_links_CRB(self, href, browse_url_base, fullpath, rev, repo):
232        path = '/'
233        links = [{'name': 'Repository Index',
234                  'href': href(browse_url_base, path, rev=rev)}]
235
236        for part in [p for p in fullpath.split('/') if p]:
237            path += part + '/'
238            links.append({
239                'name': part,
240                'href': href(browse_url_base, path, rev=rev, repo=repo)
241                })
242        return links
243
244    def _render_directory(self, req, repos, node, rev=None, repo=''):
245        req.perm.assert_permission('BROWSER_VIEW')
246
247        order = req.args.get('order', 'name').lower()
248        desc = 'desc' in req.args
249
250        info = []
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
268        browse_url = "peerReviewBrowser" if not req.args.get('is_admin_browser', False) else "adminrepobrowser"
269        for entry in node.get_entries():
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,
276                    'size': pretty_size(entry.content_length),
277                    'rev': entry.created_rev,
278                    'permission': 1,  # FIXME
279                    'log_href': escape(req.href.log(repo, entry.path, rev=rev)),
280                    'browser_href': req.href(browse_url, entry.path, rev=rev, repo=repo)
281                    })
282
283        changes = get_changes(repos, [i['rev'] for i in info])
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())
298        if is_py3:
299            info.sort(key=cmp_to_key(cmp_func))
300        else:
301            info.sort(cmp_func)
302
303        return {'order': order, 'desc': desc and 1 or None,
304                'items': info, 'changes': changes}
305
306    def _render_file(self, req, context, repos, node, rev=None, repo=''):
307        req.perm(context.resource).require('FILE_VIEW')
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
321        content = node.get_content()
322        chunk = content.read(CHUNK_SIZE)
323
324        format = req.args.get('format')
325        if format in ('raw', 'txt'):
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)
330            req.send_header('Last-Modified', http_date(node.last_modified))
331            req.end_headers()
332
333            while 1:
334                if not chunk:
335                    raise RequestDone
336                req.write(chunk)
337                chunk = content.read(CHUNK_SIZE)
338        else:
339            # Generate HTML preview
340            mimeview = Mimeview(self.env)
341
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
352            raw_href = req.href.peerReviewBrowser(node.path, rev=rev and node.rev, repo=repo,
353                                             format='raw')
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'])
359
360            add_link(req, 'alternate', raw_href, 'Original Format', mime_type)
361
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)
365            return {
366                'changeset': changeset,
367                'size': node.content_length,
368                'preview': preview_data['rendered'],
369                'max_file_size_reached': preview_data['max_file_size_reached'],
370                'max_file_size': preview_data['max_file_size'],
371                'annotate': False,
372                'rev': node.rev,
373                'changeset_href': escape(req.href.changeset(node.rev)),
374                'date': format_datetime(changeset.date),
375                'age': pretty_timedelta(changeset.date),
376                'author': changeset.author or 'anonymous',
377                'message': msg
378            }
379
380
381def get_node_from_repo(req, repos, path, rev):
382
383    context = web_context(req)
384
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)
391
392    context = context(repos.resource.child('source', path,
393                                           version=rev_or_latest))
394    display_rev = repos.display_rev
395
396    return node, display_rev, context
Note: See TracBrowser for help on using the repository browser.