# -*- coding: utf-8 -*- # # Copyright (C) 2005-2008 Edgewall Software # Copyright (C) 2005 Christopher Lenz # Copyright (C) 2006 Christian Boos # 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: Christopher Lenz # Ludvig Strigeus import os.path from trac.core import * from trac.mimeview.api import content_to_unicode, IHTMLPreviewRenderer, \ Mimeview from trac.util.html import escape, Markup from trac.util.text import expandtabs from trac.util.translation import _ from trac.web.chrome import Chrome, add_script, add_stylesheet __all__ = ['PatchRenderer'] class PatchRenderer(Component): """Structured display of patches in unified diff format. This uses the same layout as in the wiki diff view or the changeset view. """ implements(IHTMLPreviewRenderer) # IHTMLPreviewRenderer methods def get_quality_ratio(self, mimetype): if mimetype == 'text/x-diff': return 8 return 0 def render(self, context, mimetype, content, filename=None, rev=None): req = context.req from trac.web.chrome import Chrome content = content_to_unicode(self.env, content, mimetype) changes = self._diff_to_hdf(content.splitlines(), Mimeview(self.env).tab_width) if not changes: raise TracError(_('Invalid unified diff content')) data = {'diff': {'style': 'inline'}, 'no_id': True, 'changes': changes, 'longcol': 'File', 'shortcol': ''} add_script(req, 'common/js/diff.js') add_stylesheet(req, 'common/css/diff.css') return Chrome(self.env).render_template(req, 'diff_div.html', data, fragment=True) # Internal methods # FIXME: This function should probably share more code with the # trac.versioncontrol.diff module def _diff_to_hdf(self, difflines, tabwidth): """ Translate a diff file into something suitable for inclusion in HDF. The result is [(filename, revname_old, revname_new, changes)], where changes has the same format as the result of `trac.versioncontrol.diff.hdf_diff`. If the diff cannot be parsed, this method returns None. """ def _markup_intraline_change(fromlines, tolines): from trac.versioncontrol.diff import _get_change_extent for i in xrange(len(fromlines)): fr, to = fromlines[i], tolines[i] (start, end) = _get_change_extent(fr, to) if start != 0 or end != 0: last = end+len(fr) fromlines[i] = fr[:start] + '\0' + fr[start:last] + \ '\1' + fr[last:] last = end+len(to) tolines[i] = to[:start] + '\0' + to[start:last] + \ '\1' + to[last:] import re space_re = re.compile(' ( +)|^ ') def htmlify(match): div, mod = divmod(len(match.group(0)), 2) return div * '  ' + mod * ' ' comments = [] changes = [] lines = iter(difflines) try: line = lines.next() while True: if not line.startswith('--- '): if not line.startswith('Index: ') and line != '='*67: comments.append(line) line = lines.next() continue oldpath = oldrev = newpath = newrev = '' # Base filename/version oldinfo = line.split(None, 2) if len(oldinfo) > 1: oldpath = oldinfo[1] if len(oldinfo) > 2: oldrev = oldinfo[2] # Changed filename/version line = lines.next() if not line.startswith('+++ '): self.log.debug('expected +++ after ---, got '+line) return None newinfo = line.split(None, 2) if len(newinfo) > 1: newpath = newinfo[1] if len(newinfo) > 2: newrev = newinfo[2] shortrev = ('old', 'new') if oldpath or newpath: sep = re.compile(r'([/.~\\])') commonprefix = ''.join(os.path.commonprefix( [sep.split(newpath), sep.split(oldpath)])) commonsuffix = ''.join(os.path.commonprefix( [sep.split(newpath)[::-1], sep.split(oldpath)[::-1]])[::-1]) if len(commonprefix) > len(commonsuffix): common = commonprefix elif commonsuffix: common = commonsuffix.lstrip('/') a = oldpath[:-len(commonsuffix)] b = newpath[:-len(commonsuffix)] if len(a) < 4 and len(b) < 4: shortrev = (a, b) else: common = '(a) %s vs. (b) %s' % (oldpath, newpath) shortrev = ('a', 'b') else: common = '' groups = [] changes.append({'change': 'edit', 'props': [], 'comments': '\n'.join(comments), 'diffs': groups, 'old': {'path': common, 'rev': ' '.join(oldinfo[1:]), 'shortrev': shortrev[0]}, 'new': {'path': common, 'rev': ' '.join(newinfo[1:]), 'shortrev': shortrev[1]}}) comments = [] line = lines.next() while line: # "@@ -333,10 +329,8 @@" or "@@ -1 +1 @@" r = re.match(r'@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@', line) if not r: break blocks = [] groups.append(blocks) fromline, fromend, toline, toend = [int(x or 1) for x in r.groups()] last_type = last_change = extra = None fromend += fromline toend += toline line = lines.next() while fromline < fromend or toline < toend or extra: # First character is the command command = ' ' if line: command, line = line[0], line[1:] # Make a new block? if (command == ' ') != last_type: last_type = command == ' ' kind = last_type and 'unmod' or 'mod' block = {'type': kind, 'base': {'offset': fromline - 1, 'lines': []}, 'changed': {'offset': toline - 1, 'lines': []}} blocks.append(block) else: block = blocks[-1] if command == ' ': sides = ['base', 'changed'] elif command == '+': last_side = 'changed' sides = [last_side] elif command == '-': last_side = 'base' sides = [last_side] elif command == '\\' and last_side: meta = block[last_side].setdefault('meta', {}) meta[len(block[last_side]['lines'])] = True sides = [last_side] else: self.log.debug('expected +, - or \\, got '+command) return None for side in sides: if side == 'base': fromline += 1 else: toline += 1 block[side]['lines'].append(line) line = lines.next() extra = line and line[0] == '\\' except StopIteration: pass # Go through all groups/blocks and mark up intraline changes, and # convert to html for o in changes: for group in o['diffs']: for b in group: base, changed = b['base'], b['changed'] f, t = base['lines'], changed['lines'] if b['type'] == 'mod': if len(f) == 0: b['type'] = 'add' elif len(t) == 0: b['type'] = 'rem' elif len(f) == len(t): _markup_intraline_change(f, t) for i in xrange(len(f)): line = expandtabs(f[i], tabwidth, '\0\1') line = escape(line, quotes=False) line = ''.join([space_re.sub(htmlify, seg) for seg in line.split('\0')]) line = line.replace('\1', '') f[i] = Markup(line) if 'meta' in base and i in base['meta']: f[i] = Markup('%s') % f[i] for i in xrange(len(t)): line = expandtabs(t[i], tabwidth, '\0\1') line = escape(line, quotes=False) line = ''.join([space_re.sub(htmlify, seg) for seg in line.split('\0')]) line = line.replace('\1', '') t[i] = Markup(line) if 'meta' in changed and i in changed['meta']: t[i] = Markup('%s') % t[i] return changes