📄 changeset.py
字号:
# -*- coding: utf-8 -*-## Copyright (C) 2003-2008 Edgewall Software# Copyright (C) 2003-2005 Jonas Borgstr枚m <jonas@edgewall.com># Copyright (C) 2004-2006 Christopher Lenz <cmlenz@gmx.de># Copyright (C) 2005-2006 Christian Boos <cboos@neuf.fr># All rights reserved.## This software is licensed as described in the file COPYING, which# you should have received as part of this distribution. The terms# are also available at http://trac.edgewall.org/wiki/TracLicense.## This software consists of voluntary contributions made by many# individuals. For the exact contribution history, see the revision# history and logs, available at http://trac.edgewall.org/log/.## Author: Jonas Borgstr枚m <jonas@edgewall.com># Christopher Lenz <cmlenz@gmx.de># Christian Boos <cboos@neuf.fr>from datetime import datetimeimport osimport posixpathimport refrom StringIO import StringIOimport timefrom genshi.builder import tagfrom trac.config import Option, BoolOption, IntOptionfrom trac.core import *from trac.mimeview import Mimeview, is_binary, Contextfrom trac.perm import IPermissionRequestorfrom trac.resource import Resource, ResourceNotFoundfrom trac.search import ISearchSource, search_to_sql, shorten_resultfrom trac.timeline.api import ITimelineEventProviderfrom trac.util import embedded_numbers, content_dispositionfrom trac.util.compat import any, sorted, groupbyfrom trac.util.datefmt import pretty_timedelta, utcfrom trac.util.text import unicode_urlencode, shorten_line, CRLFfrom trac.util.translation import _from trac.versioncontrol import Changeset, Node, NoSuchChangesetfrom trac.versioncontrol.diff import get_diff_options, diff_blocks, unified_difffrom trac.versioncontrol.web_ui.browser import BrowserModule, \ DefaultPropertyRendererfrom trac.web import IRequestHandler, RequestDonefrom trac.web.chrome import add_ctxtnav, add_link, add_script, add_stylesheet, \ prevnext_nav, INavigationContributor, Chromefrom trac.wiki import IWikiSyntaxProvider, WikiParserfrom trac.wiki.formatter import format_to_htmlclass IPropertyDiffRenderer(Interface): """Render node properties in TracBrowser and TracChangeset views.""" def match_property_diff(name): """Indicate whether this renderer can treat the given property diffs Returns a quality number, ranging from 0 (unsupported) to 9 (''perfect'' match). """ def render_property_diff(name, old_context, old_props, new_context, new_props, options): """Render the given diff of property to HTML. `name` is the property name as given to `match_property_diff()`, `old_context` corresponds to the old node being render (useful when the rendering depends on the node kind) and `old_props` is the corresponding collection of all properties. Same for `new_node` and `new_props`. `options` are the current diffs options. The rendered result can be one of the following: - `None`: the property change will be shown the normal way (''changed from `old` to `new`'') - an `unicode` value: the change will be shown as textual content - `Markup` or other Genshi content: the change will shown as block markup """class DefaultPropertyDiffRenderer(Component): """Implement default behavior for rendering property differences.""" implements(IPropertyDiffRenderer) def match_property_diff(self, name): # Support everything but hidden properties. hidden_properties = DefaultPropertyRenderer(self.env).hidden_properties return name not in hidden_properties and 1 or 0 def render_property_diff(self, name, old_context, old_props, new_context, new_props, options): old, new = old_props[name], new_props[name] # Render as diff only if multiline (see #3002) if '\n' not in old and '\n' not in new: return None unidiff = '--- \n+++ \n' + \ '\n'.join(unified_diff(old.splitlines(), new.splitlines(), options.get('contextlines', 3))) return tag.li('Property ', tag.strong(name), Mimeview(self.env).render(old_context, 'text/x-diff', unidiff))class ChangesetModule(Component): """Provide flexible functionality for showing sets of differences. If the differences shown are coming from a specific changeset, then that changeset informations can be shown too. In addition, it is possible to show only a subset of the changeset: Only the changes affecting a given path will be shown. This is called the ''restricted'' changeset. But the differences can also be computed in a more general way, between two arbitrary paths and/or between two arbitrary revisions. In that case, there's no changeset information displayed. """ implements(INavigationContributor, IPermissionRequestor, IRequestHandler, ITimelineEventProvider, IWikiSyntaxProvider, ISearchSource) property_diff_renderers = ExtensionPoint(IPropertyDiffRenderer) timeline_show_files = Option('timeline', 'changeset_show_files', '0', """Number of files to show (`-1` for unlimited, `0` to disable). This can also be `location`, for showing the common prefix for the changed files. (since 0.11). """) timeline_long_messages = BoolOption('timeline', 'changeset_long_messages', 'false', """Whether wiki-formatted changeset messages should be multiline or not. If this option is not specified or is false and `wiki_format_messages` is set to true, changeset messages will be single line only, losing some formatting (bullet points, etc).""") timeline_collapse = BoolOption('timeline', 'changeset_collapse_events', 'false', """Whether consecutive changesets from the same author having exactly the same message should be presented as one event. That event will link to the range of changesets in the log view. (''since 0.11'')""") max_diff_files = IntOption('changeset', 'max_diff_files', 0, """Maximum number of modified files for which the changeset view will attempt to show the diffs inlined (''since 0.10'').""") max_diff_bytes = IntOption('changeset', 'max_diff_bytes', 10000000, """Maximum total size in bytes of the modified files (their old size plus their new size) for which the changeset view will attempt to show the diffs inlined (''since 0.10'').""") wiki_format_messages = BoolOption('changeset', 'wiki_format_messages', 'true', """Whether wiki formatting should be applied to changeset messages. If this option is disabled, changeset messages will be rendered as pre-formatted text.""") # INavigationContributor methods def get_active_navigation_item(self, req): return 'browser' def get_navigation_items(self, req): return [] # IPermissionRequestor methods def get_permission_actions(self): return ['CHANGESET_VIEW'] # IRequestHandler methods _request_re = re.compile(r"/changeset(?:/([^/]+))?(/.*)?$") def match_request(self, req): match = re.match(self._request_re, req.path_info) if match: new, new_path = match.groups() if new: req.args['new'] = new if new_path: req.args['new_path'] = new_path return True def process_request(self, req): """The appropriate mode of operation is inferred from the request parameters: * If `new_path` and `old_path` are equal (or `old_path` is omitted) and `new` and `old` are equal (or `old` is omitted), then we're about to view a revision Changeset: `chgset` is True. Furthermore, if the path is not the root, the changeset is ''restricted'' to that path (only the changes affecting that path, its children or its ancestor directories will be shown). * In any other case, the set of changes corresponds to arbitrary differences between path@rev pairs. If `new_path` and `old_path` are equal, the ''restricted'' flag will also be set, meaning in this case that the differences between two revisions are restricted to those occurring on that path. In any case, either path@rev pairs must exist. """ req.perm.require('CHANGESET_VIEW') repos = self.env.get_repository(req.authname) # -- retrieve arguments new_path = req.args.get('new_path') new = req.args.get('new') old_path = req.args.get('old_path') old = req.args.get('old') xhr = req.get_header('X-Requested-With') == 'XMLHttpRequest' # -- support for the revision log ''View changes'' form, # where we need to give the path and revision at the same time if old and '@' in old: old, old_path = old.split('@', 1) if new and '@' in new: new, new_path = new.split('@', 1) # -- normalize and check for special case try: new_path = repos.normalize_path(new_path) new = repos.normalize_rev(new) repos.authz.assert_permission_for_changeset(new) old_path = repos.normalize_path(old_path or new_path) old = repos.normalize_rev(old or new) except NoSuchChangeset, e: raise ResourceNotFound(e.message, _('Invalid Changeset Number')) if old_path == new_path and old == new: # revert to Changeset old_path = old = None style, options, diff_data = get_diff_options(req) # -- setup the `chgset` and `restricted` flags, see docstring above. chgset = not old and not old_path if chgset: restricted = new_path not in ('', '/') # (subset or not) else: restricted = old_path == new_path # (same path or not) # -- redirect if changing the diff options if req.args.has_key('update'): if chgset: if restricted: req.redirect(req.href.changeset(new, new_path)) else: req.redirect(req.href.changeset(new)) else: req.redirect(req.href.changeset(new, new_path, old=old, old_path=old_path)) # -- preparing the data if chgset: prev = repos.get_node(new_path, new).get_previous() if prev: prev_path, prev_rev = prev[:2] else: prev_path, prev_rev = new_path, repos.previous_rev(new) data = {'old_path': prev_path, 'old_rev': prev_rev, 'new_path': new_path, 'new_rev': new} else: if not new: new = repos.youngest_rev elif not old: old = repos.youngest_rev if not old_path: old_path = new_path data = {'old_path': old_path, 'old_rev': old, 'new_path': new_path, 'new_rev': new} data['diff'] = diff_data data['wiki_format_messages'] = self.wiki_format_messages if chgset: req.perm('changeset', new).require('CHANGESET_VIEW') chgset = repos.get_changeset(new) # TODO: find a cheaper way to reimplement r2636 req.check_modified(chgset.date, [ style, ''.join(options), repos.name, repos.rev_older_than(new, repos.youngest_rev), chgset.message, xhr, pretty_timedelta(chgset.date, None, 3600)]) format = req.args.get('format') if format in ['diff', 'zip']: req.perm.require('FILE_VIEW') # choosing an appropriate filename rpath = new_path.replace('/','_') if chgset: if restricted: filename = 'changeset_%s_r%s' % (rpath, new) else: filename = 'changeset_r%s' % new else: if restricted: filename = 'diff-%s-from-r%s-to-r%s' \ % (rpath, old, new) elif old_path == '/': # special case for download (#238) filename = '%s-r%s' % (rpath, old) else: filename = 'diff-from-%s-r%s-to-%s-r%s' \ % (old_path.replace('/','_'), old, rpath, new) if format == 'diff': self._render_diff(req, filename, repos, data) elif format == 'zip': self._render_zip(req, filename, repos, data) # -- HTML format self._render_html(req, repos, chgset, restricted, xhr, data) if chgset: diff_params = 'new=%s' % new else: diff_params = unicode_urlencode({'new_path': new_path, 'new': new, 'old_path': old_path, 'old': old}) add_link(req, 'alternate', '?format=diff&'+diff_params, _('Unified Diff'), 'text/plain', 'diff') add_link(req, 'alternate', '?format=zip&'+diff_params, _('Zip Archive'), 'application/zip', 'zip') add_script(req, 'common/js/diff.js') add_stylesheet(req, 'common/css/changeset.css') add_stylesheet(req, 'common/css/diff.css') add_stylesheet(req, 'common/css/code.css') if chgset: if restricted: prevnext_nav(req, _('Change')) else: prevnext_nav(req, _('Changeset')) else:
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -