source: peerreviewplugin/tags/0.12/3.1/codereview/repobrowser.py

Last change on this file was 17269, checked in by Cinc-th, 5 years ago

PeerReviewPlugin: change exception handling from except <ExceptionName>, e: to except <ExceptionName> as e:

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