📄 browser.py
字号:
'path_links': path_links, 'dir': node.isdir and self._render_dir(req, repos, node, rev), 'file': node.isfile and self._render_file(req, context, repos, node, rev), 'quickjump_entries': xhr or list(repos.get_quickjump_entries(rev)), 'wiki_format_messages': self.config['changeset'].getbool('wiki_format_messages') } if xhr: # render and return the content only data['xhr'] = True return 'dir_entries.html', data, None # Links for contextual navigation add_ctxtnav(req, tag.a(_('Last Change'), href=req.href.changeset(node.rev, node.created_path))) if node.isfile: if data['file']['annotate']: add_ctxtnav(req, _('Normal'), title=_('View file without annotations'), href=req.href.browser(node.created_path, rev=node.rev)) else: add_ctxtnav(req, _('Annotate'), title=_('Annotate each line with the last ' 'changed revision ' '(this can be time consuming...)'), href=req.href.browser(node.created_path, rev=node.rev, annotate='blame')) add_ctxtnav(req, _('Revision Log'), href=req.href.log(path, rev=rev)) add_stylesheet(req, 'common/css/browser.css') return 'browser.html', data, None # Internal methods def _render_dir(self, req, repos, node, rev=None): req.perm.require('BROWSER_VIEW') # Entries metadata class entry(object): __slots__ = 'name rev kind isdir path content_length'.split() def __init__(self, node): for f in entry.__slots__: setattr(self, f, getattr(n, f)) entries = [entry(n) for n in node.get_entries()] changes = get_changes(repos, [i.rev for i in entries]) if rev: newest = repos.get_changeset(rev).date else: newest = datetime.now(req.tz) # Color scale for the age column timerange = custom_colorizer = None if self.color_scale: timerange = TimeRange(newest) max_s = req.args.get('range_max_secs') min_s = req.args.get('range_min_secs') parent_range = [timerange.from_seconds(long(s)) for s in [max_s, min_s] if s] this_range = [c.date for c in changes.values() if c] for dt in this_range + parent_range: timerange.insert(dt) custom_colorizer = self.get_custom_colorizer() # Ordering of entries order = req.args.get('order', 'name').lower() desc = req.args.has_key('desc') if order == 'date': def file_order(a): return changes[a.rev].date elif order == 'size': def file_order(a): return (a.content_length, embedded_numbers(a.name.lower())) else: def file_order(a): return embedded_numbers(a.name.lower()) dir_order = desc and 1 or -1 def browse_order(a): return a.isdir and dir_order or 0, file_order(a) entries = sorted(entries, key=browse_order, reverse=desc) # ''Zip Archive'' alternate link patterns = self.downloadable_paths if node.path and patterns and \ filter(None, [fnmatchcase(node.path, p) for p in patterns]): zip_href = req.href.changeset(rev or repos.youngest_rev, node.path, old=rev, old_path='/', format='zip') add_link(req, 'alternate', zip_href, _('Zip Archive'), 'application/zip', 'zip') add_script(req, 'common/js/expand_dir.js') add_script(req, 'common/js/keyboard_nav.js') return {'order': order, 'desc': desc and 1 or None, 'entries': entries, 'changes': changes, 'timerange': timerange, 'colorize_age': custom_colorizer, 'range_max_secs': (timerange and timerange.to_seconds(timerange.newest)), 'range_min_secs': (timerange and timerange.to_seconds(timerange.oldest)), } def _render_file(self, req, context, repos, node, rev=None): req.perm(context.resource).require('FILE_VIEW') mimeview = Mimeview(self.env) # MIME type detection content = node.get_content() chunk = content.read(CHUNK_SIZE) mime_type = node.content_type if not mime_type or mime_type == 'application/octet-stream': mime_type = mimeview.get_mimetype(node.name, chunk) or \ mime_type or 'text/plain' # Eventually send the file directly format = req.args.get('format') if format in ('raw', 'txt'): req.send_response(200) req.send_header('Content-Type', format == 'txt' and 'text/plain' or mime_type) req.send_header('Content-Length', node.content_length) req.send_header('Last-Modified', http_date(node.last_modified)) if not self.render_unsafe_content: # Force browser to download files instead of rendering # them, since they might contain malicious code enabling # XSS attacks req.send_header('Content-Disposition', 'attachment') req.end_headers() while 1: if not chunk: raise RequestDone req.write(chunk) chunk = content.read(CHUNK_SIZE) else: # The changeset corresponding to the last change on `node` # is more interesting than the `rev` changeset. changeset = repos.get_changeset(node.rev) # add ''Plain Text'' alternate link if needed if not is_binary(chunk) and mime_type != 'text/plain': plain_href = req.href.browser(node.path, rev=rev, format='txt') add_link(req, 'alternate', plain_href, _('Plain Text'), 'text/plain') # add ''Original Format'' alternate link (always) raw_href = req.href.export(rev or repos.youngest_rev, node.path) add_link(req, 'alternate', raw_href, _('Original Format'), mime_type) self.log.debug("Rendering preview of node %s@%s with mime-type %s" % (node.name, str(rev), mime_type)) del content # the remainder of that content is not needed add_stylesheet(req, 'common/css/code.css') annotations = ['lineno'] force_source = False if 'annotate' in req.args: force_source = True annotations.insert(0, req.args['annotate']) preview_data = mimeview.preview_data(context, node.get_content(), node.get_content_length(), mime_type, node.created_path, raw_href, annotations=annotations, force_source=force_source) return { 'changeset': changeset, 'size': node.content_length, 'preview': preview_data, 'annotate': force_source, } # public methods def render_properties(self, mode, context, props): """Prepare rendering of a collection of properties.""" return filter(None, [self.render_property(name, mode, context, props) for name in props]) def render_property(self, name, mode, context, props): """Renders a node property to HTML.""" candidates = [] for renderer in self.property_renderers: quality = renderer.match_property(name, mode) if quality > 0: candidates.append((quality, renderer)) if candidates: renderer = sorted(candidates, reverse=True)[0][1] rendered = renderer.render_property(name, mode, context, props) if rendered: if isinstance(rendered, RenderedProperty): value = rendered.content rendered = rendered else: value = rendered rendered = None prop = {'name': name, 'value': value, 'rendered': rendered} return prop # IWikiSyntaxProvider methods def get_wiki_syntax(self): return [] def get_link_resolvers(self): """TracBrowser link resolvers. - `source:` and `browser:` * simple paths (/dir/file) * paths at a given revision (/dir/file@234) * paths with line number marks (/dir/file@234:10,20-30) * paths with line number anchor (/dir/file@234#L100) Marks and anchor can be combined. The revision must be present when specifying line numbers. In the few cases where it would be redundant (e.g. for tags), the revision number itself can be omitted: /tags/v10/file@100-110#L99 """ return [('repos', self._format_browser_link), ('export', self._format_export_link), ('source', self._format_browser_link), ('browser', self._format_browser_link)] def _format_export_link(self, formatter, ns, export, label): export, query, fragment = formatter.split_link(export) if ':' in export: rev, path = export.split(':', 1) elif '@' in export: path, rev = export.split('@', 1) else: rev, path = self.env.get_repository().youngest_rev, export return tag.a(label, class_='source', href=formatter.href.export(rev, path) + fragment) def _format_browser_link(self, formatter, ns, path, label): path, query, fragment = formatter.split_link(path) rev = marks = None match = self.PATH_LINK_RE.match(path) if match: path, rev, marks = match.groups() return tag.a(label, class_='source', href=(formatter.href.browser(path, rev=rev, marks=marks) + query + fragment)) PATH_LINK_RE = re.compile(r"([^@#:]*)" # path r"[@:]([^#:]+)?" # rev r"(?::(\d+(?:-\d+)?(?:,\d+(?:-\d+)?)*))?" # marks ) # IHTMLPreviewAnnotator methods def get_annotation_type(self): return 'blame', _('Rev'), _('Revision in which the line changed') def get_annotation_data(self, context): """Cache the annotation data corresponding to each revision.""" return BlameAnnotator(self.env, context) def annotate_row(self, context, row, lineno, line, blame_annotator): blame_annotator.annotate(row, lineno)class BlameAnnotator(object): def __init__(self, env, context): self.env = env # `context`'s resource is ('source', path, version=rev) self.context = context self.resource = context.resource self.repos = env.get_repository() # maintain state self.prev_chgset = None self.chgset_data = {} add_script(context.req, 'common/js/blame.js') add_stylesheet(context.req, 'common/css/changeset.css') add_stylesheet(context.req, 'common/css/diff.css') self.reset() def reset(self): rev = self.resource.version node = self.repos.get_node(self.resource.id, rev) # FIXME: get_annotations() should be in the Resource API # -- get revision numbers for each line self.annotations = node.get_annotations() # -- from the annotations, retrieve changesets and # determine the span of dates covered, for the color code. # Note: changesets[i].rev can differ from annotations[i] # (long form vs. compact, short rev form for the latter). self.changesets = [] chgset = self.repos.get_changeset(rev) chgsets = {rev: chgset} self.timerange = TimeRange(chgset.date) for idx in range(len(self.annotations)): rev = self.annotations[idx] chgset = chgsets.get(rev) if not chgset: chgset = self.repos.get_changeset(rev) chgsets[rev] = chgset self.timerange.insert(chgset.date) # get list of changeset parallel to annotations self.changesets.append(chgset) # -- retrieve the original path of the source, for each rev # (support for copy/renames) self.paths = {} for path, rev, chg in node.get_history(): self.paths[rev] = path # -- get custom colorize function browser = BrowserModule(self.env) self.colorize_age = browser.get_custom_colorizer() def annotate(self, row, lineno): if lineno > len(self.annotations): row.append(tag.th()) return rev = self.annotations[lineno-1] chgset = self.changesets[lineno-1] path = self.paths.get(rev, None) # Note: path will be None if copy/rename is not supported # by get_history # -- compute anchor and style once per revision if rev not in self.chgset_data: chgset_href = self.context.href.changeset(rev, path) short_author = chgset.author.split(' ', 1)[0] title = shorten_line('%s: %s' % (short_author, chgset.message)) anchor = tag.a('[%s]' % self.repos.short_rev(rev), # shortname title=title, href=chgset_href) color = self.colorize_age(self.timerange.relative(chgset.date)) style = 'background-color: rgb(%d, %d, %d);' % color self.chgset_data[rev] = (anchor, style) else: anchor, style = self.chgset_data[rev] if self.prev_chgset != chgset: self.prev_style = style # optimize away the path if there's no copy/rename info if not path or path == self.resource.id: path = '' # -- produce blame column, eventually with an anchor style = self.prev_style if lineno < len(self.changesets) and self.changesets[lineno] == chgset: style += ' border-bottom: none;' blame_col = tag.th(style=style, class_='blame r%s' % rev) if self.prev_chgset != chgset: blame_col.append(anchor) self.prev_chgset = chgset row.append(blame_col)
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -