Index: trac/versioncontrol/web_ui/log.py =================================================================== --- trac/versioncontrol/web_ui/log.py (revision 4937) +++ trac/versioncontrol/web_ui/log.py (working copy) @@ -69,175 +69,13 @@ 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) + request = LogRequest(self, req) + request.prepare_history() + request.extract_info() + request.extract_changes() + request.prepare_response() + return request.get_response() - 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 +142,218 @@ ranges = ''.join([str(rev)+sep for rev, sep in zip(revs, seps)]) revranges = Ranges(ranges) return str(revranges) or None - + + +class LogRequest: + """Completely handles a request to the LogModule.""" + + def __init__(self, module, req): + """Parses the data from req into member variables for use later.""" + + self.module = module + self.req = req + 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') + self.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 = self.revs + if self.revs: + try: + self.revranges = Ranges(self.revs) + self.rev = self.revranges.b + except ValueError: + pass + self.rev = unicode(self.repos.normalize_rev(self.rev)) + + 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 prepare_history(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': + self.rev = self.revranges.b + self.history = self._get_path_history + else: + if not self.revranges or self.revranges.a == self.revranges.b: + self.history = self._get_node_history + else: + self.history = self._get_node_history_ranges + + def extract_info(self): + """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 self.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 == 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: + self.info = info + + def extract_changes(self): + """Extract the required information for the revisions in the history.""" + + self.revs = [i['rev'] for i in self.info] + self.changes = get_changes(self.repos, self.revs) + + def prepare_response(self): + """Prepares the data to be returned as a response to this request.""" + + # \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') + + 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 self.revs: + changeset = self.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 + self.data = { + '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': self.info, 'changes': self.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) + + def get_response(self): + if len(self.info) == self.limit+1: # limit+1 reached, there _might_ be some more + next_rev = self.info[-1]['rev'] + next_path = self.info[-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 + self.info[-1]['change'] = None + + if self.format == 'changelog': + return 'revisionlog.txt', self.data, 'text/plain' + elif self.format == 'rss': + return 'revisionlog.rss', self.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', self.data, None