Christian Boos wrote:
> Peter Dimov wrote:
>> The RequestHandler idea is great! The definition of RequestHandler
>> should be moved elsewhere though. You'd know better than me which module
>> is most suitable.
>>
>
> Well, ideally it should go in trac/web/api.py, next to the IRequestHandler.
> Hm, probably a more distinctive name would be useful then, like
> RequestProcessor.
I did that.
> We usually use posts to trigger some side-effect in the database, so
> after having processed the POST we usually send a redirect so that the
> client GETs the latest version of the resource.
OK, that makes perfect sense!
> Ok, so I'd like to restart the sandbox/vc-refactoring branch, and I'd
> like that Peter could commit there as well.
Who should I talk to regarding permission rights?
> The idea would be to get that last patch there, then move the
> RequestHandler to trac.web.api.RequestProcessor and do a similar
> refactoring for the Browser and Changeset modules.
Attached is a patch containing Christian's modifications of the
LogRequest refactoring + RequestHandler moved to
trac.web.api.RequestProcessor + refactored BrowserModule. Please have a
look and comment!
I'm not sure why the colorization doesn't work for me but it didn't work
even before the refactoring. Any idea?
Just to make sure I got it right, what is the semantic of
Node.last_modified? Is it the modification time of the entry [EMAIL PROTECTED]
If that is the case then we don't need to do the lookup
changes[node.rev].date when sorting the entries but we can use
node.last_modified instead... seems to work at least (see
_date_file_order()).
> This cleanup would be
> beneficial to 0.11 and will enable Peter to become familiar with the
> existing code; Peter who has already volunteered for providing us with
> the perfect versioncontrol API for 0.12 (as I understood it, right? ;-) ).
:-) volunteer I do and we'll definitely need to discuss here on the list
what "perfect" means ;-)
Regards,
Peter
--~--~---------~--~----~------------~-------~--~----~
You received this message because you are subscribed to the Google Groups "Trac
Development" group.
To post to this group, send email to [email protected]
To unsubscribe from this group, send email to [EMAIL PROTECTED]
For more options, visit this group at
http://groups.google.com/group/trac-dev?hl=en
-~----------~----~----~----~------~----~------~--~---
Index: trac/versioncontrol/templates/revisionlog.html
===================================================================
--- trac/versioncontrol/templates/revisionlog.html (revision 4941)
+++ trac/versioncontrol/templates/revisionlog.html (working copy)
@@ -30,7 +30,6 @@
<form id="prefs" action="" method="get">
<div>
- <input type="hidden" name="action" value="$mode" />
<div class="choice">
<fieldset>
<legend>Revision Log Mode:</legend>
Index: trac/versioncontrol/web_ui/browser.py
===================================================================
--- trac/versioncontrol/web_ui/browser.py (revision 4941)
+++ trac/versioncontrol/web_ui/browser.py (working copy)
@@ -33,7 +33,7 @@
from trac.util.datefmt import http_date
from trac.util.html import escape, html, Markup
from trac.util.text import shorten_line
-from trac.web import IRequestHandler, RequestDone
+from trac.web import IRequestHandler, RequestProcessor, RequestDone
from trac.web.chrome import add_link, add_script, add_stylesheet, \
INavigationContributor
from trac.wiki.api import IWikiSyntaxProvider
@@ -294,175 +294,8 @@
return True
def process_request(self, req):
- go_to_preselected = req.args.get('preselected')
- if go_to_preselected:
- req.redirect(go_to_preselected)
-
- path = req.args.get('path', '/')
- rev = req.args.get('rev', None)
- order = req.args.get('order', None)
- desc = req.args.get('desc', None)
+ return BrowserRequest(self, req).process()
- # Find node for the requested path/rev
- repos = self.env.get_repository(req.authname)
- if rev:
- rev = repos.normalize_rev(rev)
- # If `rev` is `None`, we'll try to reuse `None` consistently,
- # as a special shortcut to the latest revision.
- rev_or_latest = rev or repos.youngest_rev
- node = get_existing_node(req, repos, path, rev_or_latest)
-
- context = Context(self.env, req, 'source', path,
- version=node.created_rev) # resource=node
-
- path_links = get_path_links(req.href, path, rev, order, desc)
- if len(path_links) > 1:
- add_link(req, 'up', path_links[-2]['href'], 'Parent directory')
-
- data = {
- 'context': context,
- 'path': path, 'rev': node.rev, 'stickyrev': rev,
- 'created_path': node.created_path,
- 'created_rev': node.created_rev,
- 'properties': self.render_properties('browser', context,
- node.get_properties()),
- 'path_links': path_links,
- 'dir': node.isdir and self._render_dir(req, repos, node, rev),
- 'file': node.isfile and self._render_file(context, repos,
- node, rev),
- 'quickjump_entries': list(repos.get_quickjump_entries(rev)),
- 'wiki_format_messages':
- self.config['changeset'].getbool('wiki_format_messages')
- }
- 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
- entries = list(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)
- for c in changes.values():
- if c:
- timerange.insert(c.date)
- 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')
-
- return {'order': order, 'desc': desc and 1 or None,
- 'entries': entries, 'changes': changes,
- 'timerange': timerange, 'colorize_age': custom_colorizer}
-
- def _render_file(self, context, repos, node, rev=None):
- req = context.req
- req.perm.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))
- 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 [EMAIL PROTECTED] 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, 'blame')
- 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):
@@ -629,3 +462,229 @@
blame_col.append(anchor)
self.prev_chgset = chgset
row.append(blame_col)
+
+class BrowserRequest(RequestProcessor):
+ """Completely handles a reqest to the BrowserModule."""
+
+ def __init__(self, module, req):
+ """Parses the data from req into member variables for use later."""
+ RequestProcessor.__init__(self, module.env, req)
+
+ self.module = module
+ self.req = req
+ self.go_to_preselected = req.args.get('preselected')
+ if self.go_to_preselected:
+ req.redirect(go_to_preselected)
+
+ self.path = req.args.get('path', '/')
+ self.rev = req.args.get('rev', None)
+ self.order = req.args.get('order', 'name').lower()
+ self.desc = req.args.has_key('desc')
+
+ # Find node for the requested path/rev
+ self.repos = self.module.env.get_repository(req.authname)
+ if self.rev:
+ self.rev = self.repos.normalize_rev(self.rev)
+
+ def render_view(self):
+ node = self.get_node()
+ data = self.get_template_context(node)
+
+ add_stylesheet(self.req, 'common/css/browser.css')
+ return 'browser.html', data, None
+
+ def get_node(self):
+ # If `rev` is `None`, we'll try to reuse `None` consistently,
+ # as a special shortcut to the latest revision.
+ rev_or_latest = self.rev or self.repos.youngest_rev
+ node = get_existing_node(self.req, self.repos, self.path, \
+ rev_or_latest)
+ self.context = Context(self.module.env, self.req, 'source', self.path,
+ version=node.created_rev) # resource=node
+ return node
+
+ def get_template_context(self, node):
+ """Prepares the template data to be returned as a response."""
+
+ path_links = get_path_links(self.req.href, self.path, self.rev, \
+ self.order, self.desc)
+ if len(path_links) > 1:
+ add_link(self.req, 'up', path_links[-2]['href'], 'Parent
directory')
+
+ return {
+ 'context': self.context,
+ 'path': self.path, 'rev': node.rev, 'stickyrev': self.rev,
+ 'created_path': node.created_path,
+ 'created_rev': node.created_rev,
+ 'properties': self.module.render_properties('browser',
self.context,
+ node.get_properties()),
+ 'path_links': path_links,
+ 'dir': node.isdir and self._render_dir(node),
+ 'file': node.isfile and self._render_file(node),
+ 'quickjump_entries':
+ list(self.repos.get_quickjump_entries(self.rev)),
+ 'wiki_format_messages':
+ self.module.config['changeset'].getbool('wiki_format_messages')
+ }
+
+ def _render_dir(self, node):
+ self.req.perm.require('BROWSER_VIEW')
+
+ entries = list(node.get_entries())
+ changes = self._get_changes(entries)
+ entries = self._sort_entries(entries, changes)
+ timerange, custom_colorizer = self._get_colorization(changes)
+ self._add_downloadable_link(node.path)
+
+ return {'order': self.order, 'desc': self.desc and 1 or None,
+ 'entries': entries, 'changes': changes,
+ 'timerange': timerange, 'colorize_age': custom_colorizer}
+
+ def _date_file_order(self, changes):
+ """Nodes sorting helper."""
+ #return lambda a: changes[a.rev].date
+ return a.last_modified
+
+ def _size_file_order(self, a):
+ """Nodes sorting helper."""
+ return (a.content_length, embedded_numbers(a.name.lower()))
+
+ def _name_file_order(self, a):
+ """Nodes sorting helper."""
+ return embedded_numbers(a.name.lower())
+
+ def _sort_entries(self, entries, changes):
+ """Returns a correctly ordered list of node's entries."""
+
+ if self.order == 'date':
+ # \todo After the version control refactoring we will not need to
+ # pass 'changes' as argument because we would be able to access
the
+ # node modification date via node:
+ file_order = self._date_file_order#(changes)
+ elif self.order == 'size':
+ file_order = self._size_file_order
+ else:
+ file_order = self._name_file_order
+
+ dir_order = self.desc and 1 or -1
+ def browse_order(a):
+ return a.isdir and dir_order or 0, file_order(a)
+
+ return sorted(entries, key=browse_order, reverse=self.desc)
+
+ def _get_changes(self, entries):
+ return get_changes(self.repos, [i.rev for i in entries])
+
+ def _get_colorization(self, changes):
+ """Returns colorization data for the given changes."""
+
+ if self.rev:
+ newest = self.repos.get_changeset(self.rev).date
+ else:
+ newest = datetime.now(self.req.tz)
+
+ # Color scale for the age column
+ timerange = custom_colorizer = None
+ if self.module.color_scale:
+ timerange = TimeRange(newest)
+ for c in changes.values():
+ if c:
+ timerange.insert(c.date)
+ custom_colorizer = self.module.get_custom_colorizer()
+
+ return timerange, custom_colorizer
+
+ def _add_downloadable_link(self, path):
+ """Add alternative link for zip download."""
+
+ patterns = self.module.downloadable_paths
+ if path and patterns and \
+ filter(None, [fnmatchcase(path, p) for p in patterns]):
+ zip_href = self.req.href.changeset(
+ self.rev or self.repos.youngest_rev, path,
+ old=self.rev, old_path='/', format='zip')
+ add_link(self.req, 'alternate', zip_href, 'Zip Archive',
+ 'application/zip', 'zip')
+
+ def _render_file(self, node):
+ """Either sends the file contents in raw format or renders it in a
+ template."""
+
+ req = self.context.req
+ req.perm.require('FILE_VIEW')
+
+ mimeview, mime_type, content, chunk = self._get_mime_data (node)
+ format = req.args.get('format')
+ if format in ('raw', 'txt'):
+ self._send_file_raw (req, node, format, mime_type, content, chunk)
+ else:
+ del content # the remainder of that content is not needed
+ return self._render_file_content(req, node, mimeview, \
+ mime_type, chunk)
+
+ def _get_mime_data(self, node):
+ mimeview = Mimeview(self.module.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'
+ return mimeview, mime_type, content, chunk
+
+ def _send_file_raw(self, req, node, format, mime_type, content, chunk):
+ """Sends the file contents in raw format and raises RequestDone."""
+
+ 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))
+ req.end_headers()
+
+ while 1:
+ if not chunk:
+ raise RequestDone
+ req.write(chunk)
+ chunk = content.read(CHUNK_SIZE)
+
+ def _render_file_content(self, req, node, mimeview, mime_type, chunk):
+ """Returns the rendered file content."""
+
+ # The changeset corresponding to the last change on `node`
+ # is more interesting than the `rev` changeset.
+ changeset = self.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=self.rev,
format='txt')
+ add_link(req, 'alternate', plain_href, 'Plain Text',
+ 'text/plain')
+
+ # add ''Original Format'' alternate link (always)
+ raw_href = req.href.export(self.rev or self.repos.youngest_rev,
node.path)
+ add_link(req, 'alternate', raw_href, 'Original Format', mime_type)
+
+ self.module.log.debug("Rendering preview of node [EMAIL PROTECTED]
with mime-type %s"
+ % (node.name, str(self.rev), mime_type))
+
+ add_stylesheet(req, 'common/css/code.css')
+
+ annotations = ['lineno']
+ force_source = False
+ if 'annotate' in req.args:
+ force_source = True
+ annotations.insert(0, 'blame')
+ preview_data = mimeview.preview_data(self.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,
+ }
Index: trac/versioncontrol/web_ui/log.py
===================================================================
--- trac/versioncontrol/web_ui/log.py (revision 4941)
+++ trac/versioncontrol/web_ui/log.py (working copy)
@@ -30,11 +30,12 @@
from trac.versioncontrol import Changeset
from trac.versioncontrol.web_ui.changeset import ChangesetModule
from trac.versioncontrol.web_ui.util import *
-from trac.web import IRequestHandler
+from trac.web import IRequestHandler, RequestProcessor
from trac.web.chrome import add_link, add_stylesheet, INavigationContributor, \
Chrome
from trac.wiki import IWikiSyntaxProvider, WikiParser
+
class LogModule(Component):
implements(INavigationContributor, IPermissionRequestor, IRequestHandler,
@@ -69,175 +70,8 @@
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)
+ return LogRequest(self, req).process()
- 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 == []:
- # FIXME: we should send a 404 error here
- raise TracError("The file or directory '%s' doesn't exist "
- "at revision %s or at any previous revision."
- % (path, 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 %s, rev. %s)'
- % (next_path, 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 path, kind, chg, bpath, brev in changeset.get_changes():
- files.append(chg == Changeset.DELETE and bpath or path)
- actions.append(chg)
- cs['files'] = files
- cs['actions'] = actions
- extra_changes[rev] = cs
- data = {
- 'context': Context(self.env, 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':
- 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')
-
- return 'revisionlog.html', data, None
-
# IWikiSyntaxProvider methods
REV_RANGE = r"(?:%s|%s)" % (Ranges.RE_STR, ChangesetModule.CHANGESET_ID)
@@ -304,4 +138,226 @@
ranges = ''.join([str(rev)+sep for rev, sep in zip(revs, seps)])
revranges = Ranges(ranges)
return str(revranges) or None
-
+
+
+class LogRequest(RequestProcessor):
+ """Completely handles a request to the LogModule."""
+
+ def __init__(self, module, req):
+ """Parses the data from req into member variables for use later."""
+ RequestProcessor.__init__(self, module.env, req)
+
+ self.module = module
+ self.mode = req.args.get('mode', 'stop_on_copy')
+ self.path = req.args.get('path', '/')
+ self.rev = req.args.get('rev')
+ self.stop_rev = req.args.get('stop_rev')
+ revs = req.args.get('revs', self.rev)
+ self.format = req.args.get('format')
+ self.verbose = req.args.get('verbose')
+ self.limit = int(req.args.get('limit') or
+ self.module.default_log_limit)
+
+ self.repos = self.module.env.get_repository(req.authname)
+ self.normpath = self.repos.normalize_path(self.path)
+ self.revranges = None
+ self.rev = revs
+ if revs:
+ try:
+ self.revranges = Ranges(revs)
+ self.rev = self.revranges.b
+ except ValueError:
+ pass
+ self.rev = unicode(self.repos.normalize_rev(self.rev))
+
+ def render_view(self):
+ history = self.get_history_provider()
+ items = self.get_log_items(history)
+ data = self.get_template_context(items)
+
+ if len(items) == self.limit+1:
+ # limit+1 reached, there _might_ be some more
+ next_rev = items[-1]['rev']
+ next_path = items[-1]['path']
+ add_link(self.req, 'next',
+ self._make_log_href(next_path, rev=next_rev),
+ 'Revision Log (restarting at %s, rev. %s)' %
+ (next_path, next_rev))
+ # only show fully 'limit' results, use `change == None` as a marker
+ items[-1]['change'] = None
+
+ if self.format == 'changelog':
+ return 'revisionlog.txt', data, 'text/plain'
+ elif self.format == 'rss':
+ return 'revisionlog.rss', data, 'application/rss+xml'
+
+ add_stylesheet(self.req, 'common/css/diff.css')
+ add_stylesheet(self.req, 'common/css/browser.css')
+
+ rss_href = self._make_log_href(self.path, format='rss',
+ stop_rev=self.stop_rev)
+ add_link(self.req, 'alternate', rss_href, 'RSS Feed',
+ 'application/rss+xml', 'rss')
+ changelog_href = self._make_log_href(self.path, format='changelog',
+ stop_rev=self.stop_rev)
+ add_link(self.req, 'alternate', changelog_href, 'ChangeLog',
+ 'text/plain')
+
+ return 'revisionlog.html', data, None
+
+ def _get_path_history(self, limit):
+ """A generator yielding the changes history of a path, i.e. possibly
+ different files or directories that have been moved/copied to this
+ location in different revisions."""
+
+ for h in self.repos.get_path_history(self.path, self.rev, limit):
+ yield h
+
+ def _get_node_history(self, limit):
+ """Returns a gererator yielding the changes to a node."""
+
+ return get_existing_node(self.req, self.repos, \
+ self.path, self.rev).get_history(limit)
+
+ def _get_node_history_ranges(self, limit):
+ """A generator yielding the changes to a node, restricted to certain
+ revisions. The revisions outside the given ranges are filtered."""
+
+ prevpath = self.path
+ ranges = list(self.revranges.pairs)
+ ranges.reverse()
+ for (a,b) in ranges:
+ while b >= a:
+ rev = repos.normalize_rev(b) # \todo
+ node = get_existing_node(self.req, self.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)
+
+ def get_history_provider(self):
+ """Depending on the selected mode prepares and selects a generator
+ function, which will be used to extract the history in the next step.
+
+ * for ''stop on copy'' and ''follow copies'',
+ it's `Node.get_history()` unless explicit ranges have been
+ specified, in which case some of the entries are filtered.
+ * for ''show only add, delete'' we're using
+ `Repository.get_path_history()`"""
+
+ if self.mode == 'path_history':
+ return self._get_path_history
+ else:
+ if not self.revranges or self.revranges.a == self.revranges.b:
+ return self._get_node_history
+ else:
+ return self._get_node_history_ranges
+
+ def get_log_items(self, history):
+ """Retrieves history and extracts info for at most limit+1 results"""
+
+ info = []
+ depth = 1
+ fix_deleted_rev = False
+ previous_path = self.normpath
+ for old_path, old_rev, old_chg in history(self.limit+1):
+ if fix_deleted_rev:
+ fix_deleted_rev['existing_rev'] = old_rev
+ fix_deleted_rev = False
+ if self.stop_rev and self.repos.rev_older_than(old_rev,
+ self.stop_rev):
+ break
+ old_path = self.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 (self.mode == 'path_history' and old_chg == Changeset.EDIT):
+ info.append(item)
+ if old_path and old_path != previous_path and \
+ not (self.mode == 'path_history' and
+ old_path == self.normpath):
+ depth += 1
+ item['depth'] = depth
+ item['copyfrom_path'] = old_path
+ if self.mode == 'stop_on_copy':
+ break
+ if len(info) > self.limit: # we want limit+1 entries
+ break
+ previous_path = old_path
+ if info == []:
+ # FIXME: we should send a 404 error here
+ raise TracError("The file or directory '%s' doesn't exist "
+ "at revision %s or at any previous revision."
+ % (path, rev), 'Nonexistent path')
+ else:
+ return info
+
+ def get_template_context(self, items):
+ """Prepares the template data to be returned as a response."""
+
+ # \todo I actually wanted this get_path_links part to be in response()
+ # but path_links is needed for self.data (see below).
+ self.path_links = get_path_links(self.req.href, self.path, self.rev)
+ if self.path_links:
+ add_link(self.req, 'up', self.path_links[-1]['href'],
+ 'Parent directory')
+
+ revs = [i['rev'] for i in items]
+ changes = get_changes(self.repos, revs)
+ extra_changes = {}
+ email_map = {}
+
+ if self.format == 'rss':
+ # Get the email addresses of all known users
+ if Chrome(self.module.env).show_email_addresses:
+ for username,name,email in self.module.env.get_known_users():
+ if email:
+ email_map[username] = email
+ elif self.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 path, kind, chg, bpath, brev in changeset.get_changes():
+ files.append(chg == Changeset.DELETE and bpath or path)
+ actions.append(chg)
+ cs['files'] = files
+ cs['actions'] = actions
+ extra_changes[rev] = cs
+ return {
+ 'context': Context(self.module.env, self.req, 'source', self.path),
+ 'path': self.path, 'rev': self.rev, 'stop_rev': self.stop_rev,
+ 'mode': self.mode, 'verbose': self.verbose,
+ 'path_links': self.path_links, 'limit' : self.limit,
+ 'items': items, 'changes': changes,
+ 'email_map': email_map, 'extra_changes': extra_changes,
+ 'wiki_format_messages':
+ self.module.config['changeset'].getbool('wiki_format_messages')
+ }
+
+ def _make_log_href(self, path, **args):
+ link_rev = self.rev
+ if self.rev == str(self.repos.youngest_rev):
+ link_rev = None
+ params = {'rev': link_rev, 'mode': self.mode, 'limit': self.limit}
+ params.update(args)
+ if self.verbose:
+ params['verbose'] = self.verbose
+ return self.req.href.log(path, **params)
+
Index: trac/web/api.py
===================================================================
--- trac/web/api.py (revision 4941)
+++ trac/web/api.py (working copy)
@@ -517,6 +517,26 @@
"""
+class RequestProcessor(object):
+
+ default_action = 'view'
+
+ def __init__(self, env, req):
+ self.env = env
+ self.req = req
+
+ def process(self):
+ if self.req.method == 'POST':
+ prefix = 'do_'
+ else:
+ prefix = 'render_'
+ method = prefix + self.req.args.get('action', self.default_action)
+ if hasattr(self, method):
+ return getattr(self, method)()
+ raise TracError("The request handler '%s.%s' doesn't exist." %
+ (self.__class__.__name__, method))
+
+
class IRequestFilter(Interface):
"""Extension point interface for components that want to filter HTTP
requests, before and/or after they are processed by the main handler."""