📄 log.py
字号:
# -*- coding: utf-8 -*-## Copyright (C) 2003-2008 Edgewall Software# Copyright (C) 2003-2005 Jonas Borgstr枚m <jonas@edgewall.com># 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># Christian Boos <cboos@neuf.fr>import reimport urllibfrom genshi.core import Markupfrom genshi.builder import tagfrom trac.config import IntOptionfrom trac.core import *from trac.mimeview import Contextfrom trac.perm import IPermissionRequestorfrom trac.util import Rangesfrom trac.util.datefmt import http_datefrom trac.util.html import htmlfrom trac.util.text import wrapfrom trac.util.translation import _from trac.versioncontrol.api import Changeset, NoSuchChangesetfrom trac.versioncontrol.web_ui.changeset import ChangesetModulefrom trac.versioncontrol.web_ui.util import *from trac.web import IRequestHandlerfrom trac.web.chrome import add_ctxtnav, add_link, add_stylesheet, \ INavigationContributor, Chromefrom trac.wiki import IWikiSyntaxProvider, WikiParser class LogModule(Component): implements(INavigationContributor, IPermissionRequestor, IRequestHandler, IWikiSyntaxProvider) default_log_limit = IntOption('revisionlog', 'default_log_limit', 100, """Default value for the limit argument in the TracRevisionLog (''since 0.11'').""") # 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 ['LOG_VIEW'] # IRequestHandler methods def match_request(self, req): import re match = re.match(r'/log(?:(/.*)|$)', req.path_info) if match: req.args['path'] = match.group(1) or '/' return True def process_request(self, req): req.perm.assert_permission('LOG_VIEW') mode = req.args.get('mode', 'stop_on_copy') path = req.args.get('path', '/') rev = req.args.get('rev') stop_rev = req.args.get('stop_rev') revs = req.args.get('revs', rev) format = req.args.get('format') verbose = req.args.get('verbose') limit = int(req.args.get('limit') or self.default_log_limit) repos = self.env.get_repository(req.authname) normpath = repos.normalize_path(path) revranges = None rev = revs if revs: try: revranges = Ranges(revs) rev = revranges.b except ValueError: pass rev = unicode(repos.normalize_rev(rev)) path_links = get_path_links(req.href, path, rev) if path_links: add_link(req, 'up', path_links[-1]['href'], _('Parent directory')) # The `history()` method depends on the mode: # * for ''stop on copy'' and ''follow copies'', it's `Node.history()` # unless explicit ranges have been specified # * for ''show only add, delete'' we're using # `Repository.get_path_history()` if mode == 'path_history': rev = revranges.b def history(limit): for h in repos.get_path_history(path, rev, limit): yield h else: if not revranges or revranges.a == revranges.b: history = get_existing_node(req, repos, path, rev).get_history else: def history(limit): prevpath = path ranges = list(revranges.pairs) ranges.reverse() for (a,b) in ranges: while b >= a: rev = repos.normalize_rev(b) node = get_existing_node(req, repos, prevpath, rev) node_history = list(node.get_history(2)) p, rev, chg = node_history[0] if rev < a: yield (p, rev, None) # separator break yield node_history[0] prevpath = node_history[-1][0] # follow copy b = rev-1 if b < a and len(node_history) > 1: p, rev, chg = node_history[1] yield (p, rev, None) # -- retrieve history, asking for limit+1 results info = [] depth = 1 fix_deleted_rev = False previous_path = normpath for old_path, old_rev, old_chg in history(limit+1): if fix_deleted_rev: fix_deleted_rev['existing_rev'] = old_rev fix_deleted_rev = False if stop_rev and repos.rev_older_than(old_rev, stop_rev): break old_path = repos.normalize_path(old_path) item = { 'path': old_path, 'rev': old_rev, 'existing_rev': old_rev, 'change': old_chg, 'depth': depth, } if old_chg == Changeset.DELETE: fix_deleted_rev = item if not (mode == 'path_history' and old_chg == Changeset.EDIT): info.append(item) if old_path and old_path != previous_path \ and not (mode == 'path_history' and old_path == normpath): depth += 1 item['depth'] = depth item['copyfrom_path'] = old_path if mode == 'stop_on_copy': break if len(info) > limit: # we want limit+1 entries break previous_path = old_path if info == []: node = get_existing_node(req, repos, path, rev) if repos.rev_older_than(stop_rev, node.created_rev): # FIXME: we should send a 404 error here raise TracError(_("The file or directory '%(path)s' doesn't " "exist at revision %(rev)s or at any previous revision.", path=path, rev=rev), _('Nonexistent path')) def make_log_href(path, **args): link_rev = rev if rev == str(repos.youngest_rev): link_rev = None params = {'rev': link_rev, 'mode': mode, 'limit': limit} params.update(args) if verbose: params['verbose'] = verbose return req.href.log(path, **params) if len(info) == limit+1: # limit+1 reached, there _might_ be some more next_rev = info[-1]['rev'] next_path = info[-1]['path'] add_link(req, 'next', make_log_href(next_path, rev=next_rev), _('Revision Log (restarting at %(path)s, rev. %(rev)s)', path=next_path, rev=next_rev)) # only show fully 'limit' results, use `change == None` as a marker info[-1]['change'] = None revs = [i['rev'] for i in info] changes = get_changes(repos, revs) extra_changes = {} email_map = {} if format == 'rss': # Get the email addresses of all known users if Chrome(self.env).show_email_addresses: for username,name,email in self.env.get_known_users(): if email: email_map[username] = email elif format == 'changelog': for rev in revs: changeset = changes[rev] cs = {} cs['message'] = wrap(changeset.message, 70, initial_indent='\t', subsequent_indent='\t') files = [] actions = [] for cpath, kind, chg, bpath, brev in changeset.get_changes(): files.append(chg == Changeset.DELETE and bpath or cpath) actions.append(chg) cs['files'] = files cs['actions'] = actions extra_changes[rev] = cs data = { 'context': Context.from_request(req, 'source', path), 'path': path, 'rev': rev, 'stop_rev': stop_rev, 'mode': mode, 'verbose': verbose, 'path_links': path_links, 'limit' : limit, 'items': info, 'changes': changes, 'email_map': email_map, 'extra_changes': extra_changes, 'wiki_format_messages': self.config['changeset'].getbool('wiki_format_messages') } if req.args.get('format') == 'changelog': return 'revisionlog.txt', data, 'text/plain' elif req.args.get('format') == 'rss': data['context'] = Context.from_request(req, 'source', path, absurls=True) return 'revisionlog.rss', data, 'application/rss+xml' add_stylesheet(req, 'common/css/diff.css') add_stylesheet(req, 'common/css/browser.css') rss_href = make_log_href(path, format='rss', stop_rev=stop_rev) add_link(req, 'alternate', rss_href, _('RSS Feed'), 'application/rss+xml', 'rss') changelog_href = make_log_href(path, format='changelog', stop_rev=stop_rev) add_link(req, 'alternate', changelog_href, _('ChangeLog'), 'text/plain') add_ctxtnav(req, _('View Latest Revision'), href=req.href.browser(path)) if 'next' in req.chrome['links']: next = req.chrome['links']['next'][0] add_ctxtnav(req, tag.span(tag.a(_('Older Revisions'), href=next['href']), Markup(' →'))) return 'revisionlog.html', data, None # IWikiSyntaxProvider methods REV_RANGE = r"(?:%s|%s)" % (Ranges.RE_STR, ChangesetModule.CHANGESET_ID) # int rev ranges or any kind of rev def get_wiki_syntax(self): yield ( # [...] form, starts with optional intertrac: [T... or [trac ... r"!?\[(?P<it_log>%s\s*)" % WikiParser.INTERTRAC_SCHEME + # <from>:<to> + optional path restriction r"(?P<log_revs>%s)(?P<log_path>[/?][^\]]*)?\]" % self.REV_RANGE, lambda x, y, z: self._format_link(x, 'log1', y[1:-1], y, z)) yield ( # r<from>:<to> form (no intertrac and no path restriction) r"(?:\b|!)r%s\b" % Ranges.RE_STR, lambda x, y, z: self._format_link(x, 'log2', '@' + y[1:], y)) def get_link_resolvers(self): yield ('log', self._format_link) def _format_link(self, formatter, ns, match, label, fullmatch=None): if ns == 'log1': it_log = fullmatch.group('it_log') revs = fullmatch.group('log_revs') path = fullmatch.group('log_path') or '/' target = '%s%s@%s' % (it_log, path, revs) # prepending it_log is needed, as the helper expects it there intertrac = formatter.shorthand_intertrac_helper( 'log', target, label, fullmatch) if intertrac: return intertrac path, query, fragment = formatter.split_link(path) else: assert ns in ('log', 'log2') if ns == 'log': match, query, fragment = formatter.split_link(match) else: query = fragment = '' path = match revs = '' if self.LOG_LINK_RE.match(match): indexes = [sep in match and match.index(sep) for sep in ':@'] idx = min([i for i in indexes if i is not False]) path, revs = match[:idx], match[idx+1:] try: revs = self._normalize_ranges(formatter.req, revs) except NoSuchChangeset: revs = None if revs and query: query = '&' + query[1:] href = formatter.href.log(path or '/', revs=revs) + query + fragment return html.A(label, class_='source', href=href) LOG_LINK_RE = re.compile(r"([^@:]*)[@:]%s?" % REV_RANGE) def _normalize_ranges(self, req, revs): ranges = revs.replace(':', '-') try: # fast path; only numbers revranges = Ranges(ranges) except ValueError: # slow path, normalize each rev repos = self.env.get_repository(req.authname) splitted_ranges = re.split(r'([-,])', ranges) revs = [repos.normalize_rev(r) for r in splitted_ranges[::2]] seps = splitted_ranges[1::2]+[''] ranges = ''.join([str(rev)+sep for rev, sep in zip(revs, seps)]) revranges = Ranges(ranges) return str(revranges) or None
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -