39
|
1 # -*- coding: utf-8 -*-
|
|
2 #
|
|
3 # Copyright (C) 2005 Edgewall Software
|
|
4 # Copyright (C) 2005 Christopher Lenz <cmlenz@gmx.de>
|
|
5 # All rights reserved.
|
|
6 #
|
|
7 # This software is licensed as described in the file COPYING, which
|
|
8 # you should have received as part of this distribution. The terms
|
|
9 # are also available at http://trac.edgewall.com/license.html.
|
|
10 #
|
|
11 # This software consists of voluntary contributions made by many
|
|
12 # individuals. For the exact contribution history, see the revision
|
|
13 # history and logs, available at http://projects.edgewall.com/trac/.
|
|
14 #
|
|
15 # Author: Christopher Lenz <cmlenz@gmx.de>
|
|
16 # Ludvig Strigeus
|
|
17
|
|
18 from trac.core import *
|
|
19 from trac.mimeview.api import content_to_unicode, IHTMLPreviewRenderer, Mimeview
|
|
20 from trac.util.markup import escape, Markup
|
|
21 from trac.web.chrome import add_stylesheet
|
|
22
|
|
23 __all__ = ['PatchRenderer']
|
|
24
|
|
25
|
|
26 class PatchRenderer(Component):
|
|
27 """Structured display of patches in unified diff format, similar to the
|
|
28 layout provided by the changeset view.
|
|
29 """
|
|
30
|
|
31 implements(IHTMLPreviewRenderer)
|
|
32
|
|
33 diff_cs = """
|
|
34 <?cs include:'macros.cs' ?>
|
|
35 <div class="diff"><ul class="entries"><?cs
|
|
36 each:file = diff.files ?><li class="entry">
|
|
37 <h2><?cs var:file.filename ?></h2>
|
|
38 <table class="inline" summary="Differences" cellspacing="0">
|
|
39 <colgroup><col class="lineno" /><col class="lineno" /><col class="content" /></colgroup>
|
|
40 <thead><tr>
|
|
41 <th><?cs var:file.oldrev ?></th>
|
|
42 <th><?cs var:file.newrev ?></th>
|
|
43 <th> </th>
|
|
44 </tr></thead><?cs
|
|
45 each:change = file.diff ?><?cs
|
|
46 call:diff_display(change, diff.style) ?><?cs
|
|
47 if:name(change) < len(file.diff) - 1 ?>
|
|
48 <tbody class="skipped">
|
|
49 <tr><th>…</th><th>…</th><td> </td></tr>
|
|
50 </tbody><?cs
|
|
51 /if ?><?cs
|
|
52 /each ?>
|
|
53 </table>
|
|
54 </li><?cs /each ?>
|
|
55 </ul></div>
|
|
56 """ # diff_cs
|
|
57
|
|
58 # IHTMLPreviewRenderer methods
|
|
59
|
|
60 def get_quality_ratio(self, mimetype):
|
|
61 if mimetype == 'text/x-diff':
|
|
62 return 8
|
|
63 return 0
|
|
64
|
|
65 def render(self, req, mimetype, content, filename=None, rev=None):
|
|
66 from trac.web.clearsilver import HDFWrapper
|
|
67
|
|
68 content = content_to_unicode(self.env, content, mimetype)
|
|
69 d = self._diff_to_hdf(content.splitlines(),
|
|
70 Mimeview(self.env).tab_width)
|
|
71 if not d:
|
|
72 raise TracError, 'Invalid unified diff content'
|
|
73 hdf = HDFWrapper(loadpaths=[self.env.get_templates_dir(),
|
|
74 self.config.get('trac', 'templates_dir')])
|
|
75 hdf['diff.files'] = d
|
|
76
|
|
77 add_stylesheet(req, 'common/css/diff.css')
|
|
78 return hdf.render(hdf.parse(self.diff_cs))
|
|
79
|
|
80 # Internal methods
|
|
81
|
|
82 # FIXME: This function should probably share more code with the
|
|
83 # trac.versioncontrol.diff module
|
|
84 def _diff_to_hdf(self, difflines, tabwidth):
|
|
85 """
|
|
86 Translate a diff file into something suitable for inclusion in HDF.
|
|
87 The result is [(filename, revname_old, revname_new, changes)],
|
|
88 where changes has the same format as the result of
|
|
89 `trac.versioncontrol.diff.hdf_diff`.
|
|
90
|
|
91 If the diff cannot be parsed, this method returns None.
|
|
92 """
|
|
93 def _markup_intraline_change(fromlines, tolines):
|
|
94 from trac.versioncontrol.diff import _get_change_extent
|
|
95 for i in xrange(len(fromlines)):
|
|
96 fr, to = fromlines[i], tolines[i]
|
|
97 (start, end) = _get_change_extent(fr, to)
|
|
98 if start != 0 and end != 0:
|
|
99 fromlines[i] = fr[:start] + '\0' + fr[start:end+len(fr)] + \
|
|
100 '\1' + fr[end:]
|
|
101 tolines[i] = to[:start] + '\0' + to[start:end+len(to)] + \
|
|
102 '\1' + to[end:]
|
|
103
|
|
104 import re
|
|
105 space_re = re.compile(' ( +)|^ ')
|
|
106 def htmlify(match):
|
|
107 div, mod = divmod(len(match.group(0)), 2)
|
|
108 return div * ' ' + mod * ' '
|
|
109
|
|
110 output = []
|
|
111 filename, groups = None, None
|
|
112 lines = iter(difflines)
|
|
113 try:
|
|
114 line = lines.next()
|
|
115 while True:
|
|
116 if not line.startswith('--- '):
|
|
117 line = lines.next()
|
|
118 continue
|
|
119
|
|
120 # Base filename/version
|
|
121 words = line.split(None, 2)
|
|
122 filename, fromrev = words[1], 'old'
|
|
123 groups, blocks = None, None
|
|
124
|
|
125 # Changed filename/version
|
|
126 line = lines.next()
|
|
127 if not line.startswith('+++ '):
|
|
128 return None
|
|
129
|
|
130 words = line.split(None, 2)
|
|
131 if len(words[1]) < len(filename):
|
|
132 # Always use the shortest filename for display
|
|
133 filename = words[1]
|
|
134 groups = []
|
|
135 output.append({'filename' : filename, 'oldrev' : fromrev,
|
|
136 'newrev' : 'new', 'diff' : groups})
|
|
137
|
|
138 for line in lines:
|
|
139 # @@ -333,10 +329,8 @@
|
|
140 r = re.match(r'@@ -(\d+),(\d+) \+(\d+),(\d+) @@', line)
|
|
141 if not r:
|
|
142 break
|
|
143 blocks = []
|
|
144 groups.append(blocks)
|
|
145 fromline,fromend,toline,toend = map(int, r.groups())
|
|
146 last_type = None
|
|
147
|
|
148 fromend += fromline
|
|
149 toend += toline
|
|
150
|
|
151 while fromline < fromend or toline < toend:
|
|
152 line = lines.next()
|
|
153
|
|
154 # First character is the command
|
|
155 command, line = line[0], line[1:]
|
|
156 # Make a new block?
|
|
157 if (command == ' ') != last_type:
|
|
158 last_type = command == ' '
|
|
159 blocks.append({'type': last_type and 'unmod' or 'mod',
|
|
160 'base.offset': fromline - 1,
|
|
161 'base.lines': [],
|
|
162 'changed.offset': toline - 1,
|
|
163 'changed.lines': []})
|
|
164 if command == ' ':
|
|
165 blocks[-1]['changed.lines'].append(line)
|
|
166 blocks[-1]['base.lines'].append(line)
|
|
167 fromline += 1
|
|
168 toline += 1
|
|
169 elif command == '+':
|
|
170 blocks[-1]['changed.lines'].append(line)
|
|
171 toline += 1
|
|
172 elif command == '-':
|
|
173 blocks[-1]['base.lines'].append(line)
|
|
174 fromline += 1
|
|
175 else:
|
|
176 return None
|
|
177 line = lines.next()
|
|
178 except StopIteration:
|
|
179 pass
|
|
180
|
|
181 # Go through all groups/blocks and mark up intraline changes, and
|
|
182 # convert to html
|
|
183 for o in output:
|
|
184 for group in o['diff']:
|
|
185 for b in group:
|
|
186 f, t = b['base.lines'], b['changed.lines']
|
|
187 if b['type'] == 'mod':
|
|
188 if len(f) == 0:
|
|
189 b['type'] = 'add'
|
|
190 elif len(t) == 0:
|
|
191 b['type'] = 'rem'
|
|
192 elif len(f) == len(t):
|
|
193 _markup_intraline_change(f, t)
|
|
194 for i in xrange(len(f)):
|
|
195 line = f[i].expandtabs(tabwidth)
|
|
196 line = escape(line).replace('\0', '<del>') \
|
|
197 .replace('\1', '</del>')
|
|
198 f[i] = Markup(space_re.sub(htmlify, line))
|
|
199 for i in xrange(len(t)):
|
|
200 line = t[i].expandtabs(tabwidth)
|
|
201 line = escape(line).replace('\0', '<ins>') \
|
|
202 .replace('\1', '</ins>')
|
|
203 t[i] = Markup(space_re.sub(htmlify, line))
|
|
204 return output
|