##// END OF EJS Templates
fix for latest vcs
marcink -
r1883:69d3c445 beta
parent child Browse files
Show More
@@ -1,515 +1,515 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 rhodecode.lib.diffs
3 rhodecode.lib.diffs
4 ~~~~~~~~~~~~~~~~~~~
4 ~~~~~~~~~~~~~~~~~~~
5
5
6 Set of diffing helpers, previously part of vcs
6 Set of diffing helpers, previously part of vcs
7
7
8
8
9 :created_on: Dec 4, 2011
9 :created_on: Dec 4, 2011
10 :author: marcink
10 :author: marcink
11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
12 :original copyright: 2007-2008 by Armin Ronacher
12 :original copyright: 2007-2008 by Armin Ronacher
13 :license: GPLv3, see COPYING for more details.
13 :license: GPLv3, see COPYING for more details.
14 """
14 """
15 # This program is free software: you can redistribute it and/or modify
15 # This program is free software: you can redistribute it and/or modify
16 # it under the terms of the GNU General Public License as published by
16 # it under the terms of the GNU General Public License as published by
17 # the Free Software Foundation, either version 3 of the License, or
17 # the Free Software Foundation, either version 3 of the License, or
18 # (at your option) any later version.
18 # (at your option) any later version.
19 #
19 #
20 # This program is distributed in the hope that it will be useful,
20 # This program is distributed in the hope that it will be useful,
21 # but WITHOUT ANY WARRANTY; without even the implied warranty of
21 # but WITHOUT ANY WARRANTY; without even the implied warranty of
22 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23 # GNU General Public License for more details.
23 # GNU General Public License for more details.
24 #
24 #
25 # You should have received a copy of the GNU General Public License
25 # You should have received a copy of the GNU General Public License
26 # along with this program. If not, see <http://www.gnu.org/licenses/>.
26 # along with this program. If not, see <http://www.gnu.org/licenses/>.
27
27
28 import re
28 import re
29 import difflib
29 import difflib
30 import markupsafe
30 import markupsafe
31 from itertools import tee, imap
31 from itertools import tee, imap
32
32
33 from pylons.i18n.translation import _
33 from pylons.i18n.translation import _
34
34
35 from vcs.exceptions import VCSError
35 from vcs.exceptions import VCSError
36 from vcs.nodes import FileNode
36 from vcs.nodes import FileNode
37
37
38 from rhodecode.lib.utils import EmptyChangeset
38 from rhodecode.lib.utils import EmptyChangeset
39
39
40
40
41 def wrap_to_table(str_):
41 def wrap_to_table(str_):
42 return '''<table class="code-difftable">
42 return '''<table class="code-difftable">
43 <tr class="line no-comment">
43 <tr class="line no-comment">
44 <td class="lineno new"></td>
44 <td class="lineno new"></td>
45 <td class="code no-comment"><pre>%s</pre></td>
45 <td class="code no-comment"><pre>%s</pre></td>
46 </tr>
46 </tr>
47 </table>''' % str_
47 </table>''' % str_
48
48
49
49
50 def wrapped_diff(filenode_old, filenode_new, cut_off_limit=None,
50 def wrapped_diff(filenode_old, filenode_new, cut_off_limit=None,
51 ignore_whitespace=True, line_context=3,
51 ignore_whitespace=True, line_context=3,
52 enable_comments=False):
52 enable_comments=False):
53 """
53 """
54 returns a wrapped diff into a table, checks for cut_off_limit and presents
54 returns a wrapped diff into a table, checks for cut_off_limit and presents
55 proper message
55 proper message
56 """
56 """
57
57
58 if filenode_old is None:
58 if filenode_old is None:
59 filenode_old = FileNode(filenode_new.path, '', EmptyChangeset())
59 filenode_old = FileNode(filenode_new.path, '', EmptyChangeset())
60
60
61 if filenode_old.is_binary or filenode_new.is_binary:
61 if filenode_old.is_binary or filenode_new.is_binary:
62 diff = wrap_to_table(_('binary file'))
62 diff = wrap_to_table(_('binary file'))
63 stats = (0, 0)
63 stats = (0, 0)
64 size = 0
64 size = 0
65
65
66 elif cut_off_limit != -1 and (cut_off_limit is None or
66 elif cut_off_limit != -1 and (cut_off_limit is None or
67 (filenode_old.size < cut_off_limit and filenode_new.size < cut_off_limit)):
67 (filenode_old.size < cut_off_limit and filenode_new.size < cut_off_limit)):
68
68
69 f_gitdiff = get_gitdiff(filenode_old, filenode_new,
69 f_gitdiff = get_gitdiff(filenode_old, filenode_new,
70 ignore_whitespace=ignore_whitespace,
70 ignore_whitespace=ignore_whitespace,
71 context=line_context)
71 context=line_context)
72 diff_processor = DiffProcessor(f_gitdiff, format='gitdiff')
72 diff_processor = DiffProcessor(f_gitdiff, format='gitdiff')
73
73
74 diff = diff_processor.as_html(enable_comments=enable_comments)
74 diff = diff_processor.as_html(enable_comments=enable_comments)
75 stats = diff_processor.stat()
75 stats = diff_processor.stat()
76 size = len(diff or '')
76 size = len(diff or '')
77 else:
77 else:
78 diff = wrap_to_table(_('Changeset was to big and was cut off, use '
78 diff = wrap_to_table(_('Changeset was to big and was cut off, use '
79 'diff menu to display this diff'))
79 'diff menu to display this diff'))
80 stats = (0, 0)
80 stats = (0, 0)
81 size = 0
81 size = 0
82
82
83 if not diff:
83 if not diff:
84 diff = wrap_to_table(_('No changes detected'))
84 diff = wrap_to_table(_('No changes detected'))
85
85
86 cs1 = filenode_old.last_changeset.raw_id
86 cs1 = filenode_old.last_changeset.raw_id
87 cs2 = filenode_new.last_changeset.raw_id
87 cs2 = filenode_new.last_changeset.raw_id
88
88
89 return size, cs1, cs2, diff, stats
89 return size, cs1, cs2, diff, stats
90
90
91
91
92 def get_gitdiff(filenode_old, filenode_new, ignore_whitespace=True, context=3):
92 def get_gitdiff(filenode_old, filenode_new, ignore_whitespace=True, context=3):
93 """
93 """
94 Returns git style diff between given ``filenode_old`` and ``filenode_new``.
94 Returns git style diff between given ``filenode_old`` and ``filenode_new``.
95
95
96 :param ignore_whitespace: ignore whitespaces in diff
96 :param ignore_whitespace: ignore whitespaces in diff
97 """
97 """
98
98
99 for filenode in (filenode_old, filenode_new):
99 for filenode in (filenode_old, filenode_new):
100 if not isinstance(filenode, FileNode):
100 if not isinstance(filenode, FileNode):
101 raise VCSError("Given object should be FileNode object, not %s"
101 raise VCSError("Given object should be FileNode object, not %s"
102 % filenode.__class__)
102 % filenode.__class__)
103
103
104 old_raw_id = getattr(filenode_old.changeset, 'raw_id', '0' * 40)
104 old_raw_id = getattr(filenode_old.changeset, 'raw_id', '0' * 40)
105 new_raw_id = getattr(filenode_new.changeset, 'raw_id', '0' * 40)
105 new_raw_id = getattr(filenode_new.changeset, 'raw_id', '0' * 40)
106
106
107 repo = filenode_new.changeset.repository
107 repo = filenode_new.changeset.repository
108 vcs_gitdiff = repo._get_diff(old_raw_id, new_raw_id, filenode_new.path,
108 vcs_gitdiff = repo.get_diff(old_raw_id, new_raw_id, filenode_new.path,
109 ignore_whitespace, context)
109 ignore_whitespace, context)
110
110
111 return vcs_gitdiff
111 return vcs_gitdiff
112
112
113
113
114 class DiffProcessor(object):
114 class DiffProcessor(object):
115 """
115 """
116 Give it a unified diff and it returns a list of the files that were
116 Give it a unified diff and it returns a list of the files that were
117 mentioned in the diff together with a dict of meta information that
117 mentioned in the diff together with a dict of meta information that
118 can be used to render it in a HTML template.
118 can be used to render it in a HTML template.
119 """
119 """
120 _chunk_re = re.compile(r'@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)')
120 _chunk_re = re.compile(r'@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)')
121
121
122 def __init__(self, diff, differ='diff', format='udiff'):
122 def __init__(self, diff, differ='diff', format='udiff'):
123 """
123 """
124 :param diff: a text in diff format or generator
124 :param diff: a text in diff format or generator
125 :param format: format of diff passed, `udiff` or `gitdiff`
125 :param format: format of diff passed, `udiff` or `gitdiff`
126 """
126 """
127 if isinstance(diff, basestring):
127 if isinstance(diff, basestring):
128 diff = [diff]
128 diff = [diff]
129
129
130 self.__udiff = diff
130 self.__udiff = diff
131 self.__format = format
131 self.__format = format
132 self.adds = 0
132 self.adds = 0
133 self.removes = 0
133 self.removes = 0
134
134
135 if isinstance(self.__udiff, basestring):
135 if isinstance(self.__udiff, basestring):
136 self.lines = iter(self.__udiff.splitlines(1))
136 self.lines = iter(self.__udiff.splitlines(1))
137
137
138 elif self.__format == 'gitdiff':
138 elif self.__format == 'gitdiff':
139 udiff_copy = self.copy_iterator()
139 udiff_copy = self.copy_iterator()
140 self.lines = imap(self.escaper, self._parse_gitdiff(udiff_copy))
140 self.lines = imap(self.escaper, self._parse_gitdiff(udiff_copy))
141 else:
141 else:
142 udiff_copy = self.copy_iterator()
142 udiff_copy = self.copy_iterator()
143 self.lines = imap(self.escaper, udiff_copy)
143 self.lines = imap(self.escaper, udiff_copy)
144
144
145 # Select a differ.
145 # Select a differ.
146 if differ == 'difflib':
146 if differ == 'difflib':
147 self.differ = self._highlight_line_difflib
147 self.differ = self._highlight_line_difflib
148 else:
148 else:
149 self.differ = self._highlight_line_udiff
149 self.differ = self._highlight_line_udiff
150
150
151 def escaper(self, string):
151 def escaper(self, string):
152 return markupsafe.escape(string)
152 return markupsafe.escape(string)
153
153
154 def copy_iterator(self):
154 def copy_iterator(self):
155 """
155 """
156 make a fresh copy of generator, we should not iterate thru
156 make a fresh copy of generator, we should not iterate thru
157 an original as it's needed for repeating operations on
157 an original as it's needed for repeating operations on
158 this instance of DiffProcessor
158 this instance of DiffProcessor
159 """
159 """
160 self.__udiff, iterator_copy = tee(self.__udiff)
160 self.__udiff, iterator_copy = tee(self.__udiff)
161 return iterator_copy
161 return iterator_copy
162
162
163 def _extract_rev(self, line1, line2):
163 def _extract_rev(self, line1, line2):
164 """
164 """
165 Extract the filename and revision hint from a line.
165 Extract the filename and revision hint from a line.
166 """
166 """
167
167
168 try:
168 try:
169 if line1.startswith('--- ') and line2.startswith('+++ '):
169 if line1.startswith('--- ') and line2.startswith('+++ '):
170 l1 = line1[4:].split(None, 1)
170 l1 = line1[4:].split(None, 1)
171 old_filename = (l1[0].replace('a/', '', 1)
171 old_filename = (l1[0].replace('a/', '', 1)
172 if len(l1) >= 1 else None)
172 if len(l1) >= 1 else None)
173 old_rev = l1[1] if len(l1) == 2 else 'old'
173 old_rev = l1[1] if len(l1) == 2 else 'old'
174
174
175 l2 = line2[4:].split(None, 1)
175 l2 = line2[4:].split(None, 1)
176 new_filename = (l2[0].replace('b/', '', 1)
176 new_filename = (l2[0].replace('b/', '', 1)
177 if len(l1) >= 1 else None)
177 if len(l1) >= 1 else None)
178 new_rev = l2[1] if len(l2) == 2 else 'new'
178 new_rev = l2[1] if len(l2) == 2 else 'new'
179
179
180 filename = (old_filename
180 filename = (old_filename
181 if old_filename != '/dev/null' else new_filename)
181 if old_filename != '/dev/null' else new_filename)
182
182
183 return filename, new_rev, old_rev
183 return filename, new_rev, old_rev
184 except (ValueError, IndexError):
184 except (ValueError, IndexError):
185 pass
185 pass
186
186
187 return None, None, None
187 return None, None, None
188
188
189 def _parse_gitdiff(self, diffiterator):
189 def _parse_gitdiff(self, diffiterator):
190 def line_decoder(l):
190 def line_decoder(l):
191 if l.startswith('+') and not l.startswith('+++'):
191 if l.startswith('+') and not l.startswith('+++'):
192 self.adds += 1
192 self.adds += 1
193 elif l.startswith('-') and not l.startswith('---'):
193 elif l.startswith('-') and not l.startswith('---'):
194 self.removes += 1
194 self.removes += 1
195 return l.decode('utf8', 'replace')
195 return l.decode('utf8', 'replace')
196
196
197 output = list(diffiterator)
197 output = list(diffiterator)
198 size = len(output)
198 size = len(output)
199
199
200 if size == 2:
200 if size == 2:
201 l = []
201 l = []
202 l.extend([output[0]])
202 l.extend([output[0]])
203 l.extend(output[1].splitlines(1))
203 l.extend(output[1].splitlines(1))
204 return map(line_decoder, l)
204 return map(line_decoder, l)
205 elif size == 1:
205 elif size == 1:
206 return map(line_decoder, output[0].splitlines(1))
206 return map(line_decoder, output[0].splitlines(1))
207 elif size == 0:
207 elif size == 0:
208 return []
208 return []
209
209
210 raise Exception('wrong size of diff %s' % size)
210 raise Exception('wrong size of diff %s' % size)
211
211
212 def _highlight_line_difflib(self, line, next_):
212 def _highlight_line_difflib(self, line, next_):
213 """
213 """
214 Highlight inline changes in both lines.
214 Highlight inline changes in both lines.
215 """
215 """
216
216
217 if line['action'] == 'del':
217 if line['action'] == 'del':
218 old, new = line, next_
218 old, new = line, next_
219 else:
219 else:
220 old, new = next_, line
220 old, new = next_, line
221
221
222 oldwords = re.split(r'(\W)', old['line'])
222 oldwords = re.split(r'(\W)', old['line'])
223 newwords = re.split(r'(\W)', new['line'])
223 newwords = re.split(r'(\W)', new['line'])
224
224
225 sequence = difflib.SequenceMatcher(None, oldwords, newwords)
225 sequence = difflib.SequenceMatcher(None, oldwords, newwords)
226
226
227 oldfragments, newfragments = [], []
227 oldfragments, newfragments = [], []
228 for tag, i1, i2, j1, j2 in sequence.get_opcodes():
228 for tag, i1, i2, j1, j2 in sequence.get_opcodes():
229 oldfrag = ''.join(oldwords[i1:i2])
229 oldfrag = ''.join(oldwords[i1:i2])
230 newfrag = ''.join(newwords[j1:j2])
230 newfrag = ''.join(newwords[j1:j2])
231 if tag != 'equal':
231 if tag != 'equal':
232 if oldfrag:
232 if oldfrag:
233 oldfrag = '<del>%s</del>' % oldfrag
233 oldfrag = '<del>%s</del>' % oldfrag
234 if newfrag:
234 if newfrag:
235 newfrag = '<ins>%s</ins>' % newfrag
235 newfrag = '<ins>%s</ins>' % newfrag
236 oldfragments.append(oldfrag)
236 oldfragments.append(oldfrag)
237 newfragments.append(newfrag)
237 newfragments.append(newfrag)
238
238
239 old['line'] = "".join(oldfragments)
239 old['line'] = "".join(oldfragments)
240 new['line'] = "".join(newfragments)
240 new['line'] = "".join(newfragments)
241
241
242 def _highlight_line_udiff(self, line, next_):
242 def _highlight_line_udiff(self, line, next_):
243 """
243 """
244 Highlight inline changes in both lines.
244 Highlight inline changes in both lines.
245 """
245 """
246 start = 0
246 start = 0
247 limit = min(len(line['line']), len(next_['line']))
247 limit = min(len(line['line']), len(next_['line']))
248 while start < limit and line['line'][start] == next_['line'][start]:
248 while start < limit and line['line'][start] == next_['line'][start]:
249 start += 1
249 start += 1
250 end = -1
250 end = -1
251 limit -= start
251 limit -= start
252 while -end <= limit and line['line'][end] == next_['line'][end]:
252 while -end <= limit and line['line'][end] == next_['line'][end]:
253 end -= 1
253 end -= 1
254 end += 1
254 end += 1
255 if start or end:
255 if start or end:
256 def do(l):
256 def do(l):
257 last = end + len(l['line'])
257 last = end + len(l['line'])
258 if l['action'] == 'add':
258 if l['action'] == 'add':
259 tag = 'ins'
259 tag = 'ins'
260 else:
260 else:
261 tag = 'del'
261 tag = 'del'
262 l['line'] = '%s<%s>%s</%s>%s' % (
262 l['line'] = '%s<%s>%s</%s>%s' % (
263 l['line'][:start],
263 l['line'][:start],
264 tag,
264 tag,
265 l['line'][start:last],
265 l['line'][start:last],
266 tag,
266 tag,
267 l['line'][last:]
267 l['line'][last:]
268 )
268 )
269 do(line)
269 do(line)
270 do(next_)
270 do(next_)
271
271
272 def _parse_udiff(self):
272 def _parse_udiff(self):
273 """
273 """
274 Parse the diff an return data for the template.
274 Parse the diff an return data for the template.
275 """
275 """
276 lineiter = self.lines
276 lineiter = self.lines
277 files = []
277 files = []
278 try:
278 try:
279 line = lineiter.next()
279 line = lineiter.next()
280 # skip first context
280 # skip first context
281 skipfirst = True
281 skipfirst = True
282 while 1:
282 while 1:
283 # continue until we found the old file
283 # continue until we found the old file
284 if not line.startswith('--- '):
284 if not line.startswith('--- '):
285 line = lineiter.next()
285 line = lineiter.next()
286 continue
286 continue
287
287
288 chunks = []
288 chunks = []
289 filename, old_rev, new_rev = \
289 filename, old_rev, new_rev = \
290 self._extract_rev(line, lineiter.next())
290 self._extract_rev(line, lineiter.next())
291 files.append({
291 files.append({
292 'filename': filename,
292 'filename': filename,
293 'old_revision': old_rev,
293 'old_revision': old_rev,
294 'new_revision': new_rev,
294 'new_revision': new_rev,
295 'chunks': chunks
295 'chunks': chunks
296 })
296 })
297
297
298 line = lineiter.next()
298 line = lineiter.next()
299 while line:
299 while line:
300 match = self._chunk_re.match(line)
300 match = self._chunk_re.match(line)
301 if not match:
301 if not match:
302 break
302 break
303
303
304 lines = []
304 lines = []
305 chunks.append(lines)
305 chunks.append(lines)
306
306
307 old_line, old_end, new_line, new_end = \
307 old_line, old_end, new_line, new_end = \
308 [int(x or 1) for x in match.groups()[:-1]]
308 [int(x or 1) for x in match.groups()[:-1]]
309 old_line -= 1
309 old_line -= 1
310 new_line -= 1
310 new_line -= 1
311 context = len(match.groups()) == 5
311 context = len(match.groups()) == 5
312 old_end += old_line
312 old_end += old_line
313 new_end += new_line
313 new_end += new_line
314
314
315 if context:
315 if context:
316 if not skipfirst:
316 if not skipfirst:
317 lines.append({
317 lines.append({
318 'old_lineno': '...',
318 'old_lineno': '...',
319 'new_lineno': '...',
319 'new_lineno': '...',
320 'action': 'context',
320 'action': 'context',
321 'line': line,
321 'line': line,
322 })
322 })
323 else:
323 else:
324 skipfirst = False
324 skipfirst = False
325
325
326 line = lineiter.next()
326 line = lineiter.next()
327 while old_line < old_end or new_line < new_end:
327 while old_line < old_end or new_line < new_end:
328 if line:
328 if line:
329 command, line = line[0], line[1:]
329 command, line = line[0], line[1:]
330 else:
330 else:
331 command = ' '
331 command = ' '
332 affects_old = affects_new = False
332 affects_old = affects_new = False
333
333
334 # ignore those if we don't expect them
334 # ignore those if we don't expect them
335 if command in '#@':
335 if command in '#@':
336 continue
336 continue
337 elif command == '+':
337 elif command == '+':
338 affects_new = True
338 affects_new = True
339 action = 'add'
339 action = 'add'
340 elif command == '-':
340 elif command == '-':
341 affects_old = True
341 affects_old = True
342 action = 'del'
342 action = 'del'
343 else:
343 else:
344 affects_old = affects_new = True
344 affects_old = affects_new = True
345 action = 'unmod'
345 action = 'unmod'
346
346
347 old_line += affects_old
347 old_line += affects_old
348 new_line += affects_new
348 new_line += affects_new
349 lines.append({
349 lines.append({
350 'old_lineno': affects_old and old_line or '',
350 'old_lineno': affects_old and old_line or '',
351 'new_lineno': affects_new and new_line or '',
351 'new_lineno': affects_new and new_line or '',
352 'action': action,
352 'action': action,
353 'line': line
353 'line': line
354 })
354 })
355 line = lineiter.next()
355 line = lineiter.next()
356
356
357 except StopIteration:
357 except StopIteration:
358 pass
358 pass
359
359
360 # highlight inline changes
360 # highlight inline changes
361 for _ in files:
361 for _ in files:
362 for chunk in chunks:
362 for chunk in chunks:
363 lineiter = iter(chunk)
363 lineiter = iter(chunk)
364 #first = True
364 #first = True
365 try:
365 try:
366 while 1:
366 while 1:
367 line = lineiter.next()
367 line = lineiter.next()
368 if line['action'] != 'unmod':
368 if line['action'] != 'unmod':
369 nextline = lineiter.next()
369 nextline = lineiter.next()
370 if nextline['action'] == 'unmod' or \
370 if nextline['action'] == 'unmod' or \
371 nextline['action'] == line['action']:
371 nextline['action'] == line['action']:
372 continue
372 continue
373 self.differ(line, nextline)
373 self.differ(line, nextline)
374 except StopIteration:
374 except StopIteration:
375 pass
375 pass
376
376
377 return files
377 return files
378
378
379 def prepare(self):
379 def prepare(self):
380 """
380 """
381 Prepare the passed udiff for HTML rendering. It'l return a list
381 Prepare the passed udiff for HTML rendering. It'l return a list
382 of dicts
382 of dicts
383 """
383 """
384 return self._parse_udiff()
384 return self._parse_udiff()
385
385
386 def _safe_id(self, idstring):
386 def _safe_id(self, idstring):
387 """Make a string safe for including in an id attribute.
387 """Make a string safe for including in an id attribute.
388
388
389 The HTML spec says that id attributes 'must begin with
389 The HTML spec says that id attributes 'must begin with
390 a letter ([A-Za-z]) and may be followed by any number
390 a letter ([A-Za-z]) and may be followed by any number
391 of letters, digits ([0-9]), hyphens ("-"), underscores
391 of letters, digits ([0-9]), hyphens ("-"), underscores
392 ("_"), colons (":"), and periods (".")'. These regexps
392 ("_"), colons (":"), and periods (".")'. These regexps
393 are slightly over-zealous, in that they remove colons
393 are slightly over-zealous, in that they remove colons
394 and periods unnecessarily.
394 and periods unnecessarily.
395
395
396 Whitespace is transformed into underscores, and then
396 Whitespace is transformed into underscores, and then
397 anything which is not a hyphen or a character that
397 anything which is not a hyphen or a character that
398 matches \w (alphanumerics and underscore) is removed.
398 matches \w (alphanumerics and underscore) is removed.
399
399
400 """
400 """
401 # Transform all whitespace to underscore
401 # Transform all whitespace to underscore
402 idstring = re.sub(r'\s', "_", '%s' % idstring)
402 idstring = re.sub(r'\s', "_", '%s' % idstring)
403 # Remove everything that is not a hyphen or a member of \w
403 # Remove everything that is not a hyphen or a member of \w
404 idstring = re.sub(r'(?!-)\W', "", idstring).lower()
404 idstring = re.sub(r'(?!-)\W', "", idstring).lower()
405 return idstring
405 return idstring
406
406
407 def raw_diff(self):
407 def raw_diff(self):
408 """
408 """
409 Returns raw string as udiff
409 Returns raw string as udiff
410 """
410 """
411 udiff_copy = self.copy_iterator()
411 udiff_copy = self.copy_iterator()
412 if self.__format == 'gitdiff':
412 if self.__format == 'gitdiff':
413 udiff_copy = self._parse_gitdiff(udiff_copy)
413 udiff_copy = self._parse_gitdiff(udiff_copy)
414 return u''.join(udiff_copy)
414 return u''.join(udiff_copy)
415
415
416 def as_html(self, table_class='code-difftable', line_class='line',
416 def as_html(self, table_class='code-difftable', line_class='line',
417 new_lineno_class='lineno old', old_lineno_class='lineno new',
417 new_lineno_class='lineno old', old_lineno_class='lineno new',
418 code_class='code', enable_comments=False):
418 code_class='code', enable_comments=False):
419 """
419 """
420 Return udiff as html table with customized css classes
420 Return udiff as html table with customized css classes
421 """
421 """
422 def _link_to_if(condition, label, url):
422 def _link_to_if(condition, label, url):
423 """
423 """
424 Generates a link if condition is meet or just the label if not.
424 Generates a link if condition is meet or just the label if not.
425 """
425 """
426
426
427 if condition:
427 if condition:
428 return '''<a href="%(url)s">%(label)s</a>''' % {
428 return '''<a href="%(url)s">%(label)s</a>''' % {
429 'url': url,
429 'url': url,
430 'label': label
430 'label': label
431 }
431 }
432 else:
432 else:
433 return label
433 return label
434 diff_lines = self.prepare()
434 diff_lines = self.prepare()
435 _html_empty = True
435 _html_empty = True
436 _html = []
436 _html = []
437 _html.append('''<table class="%(table_class)s">\n''' % {
437 _html.append('''<table class="%(table_class)s">\n''' % {
438 'table_class': table_class
438 'table_class': table_class
439 })
439 })
440 for diff in diff_lines:
440 for diff in diff_lines:
441 for line in diff['chunks']:
441 for line in diff['chunks']:
442 _html_empty = False
442 _html_empty = False
443 for change in line:
443 for change in line:
444 _html.append('''<tr class="%(lc)s %(action)s">\n''' % {
444 _html.append('''<tr class="%(lc)s %(action)s">\n''' % {
445 'lc': line_class,
445 'lc': line_class,
446 'action': change['action']
446 'action': change['action']
447 })
447 })
448 anchor_old_id = ''
448 anchor_old_id = ''
449 anchor_new_id = ''
449 anchor_new_id = ''
450 anchor_old = "%(filename)s_o%(oldline_no)s" % {
450 anchor_old = "%(filename)s_o%(oldline_no)s" % {
451 'filename': self._safe_id(diff['filename']),
451 'filename': self._safe_id(diff['filename']),
452 'oldline_no': change['old_lineno']
452 'oldline_no': change['old_lineno']
453 }
453 }
454 anchor_new = "%(filename)s_n%(oldline_no)s" % {
454 anchor_new = "%(filename)s_n%(oldline_no)s" % {
455 'filename': self._safe_id(diff['filename']),
455 'filename': self._safe_id(diff['filename']),
456 'oldline_no': change['new_lineno']
456 'oldline_no': change['new_lineno']
457 }
457 }
458 cond_old = (change['old_lineno'] != '...' and
458 cond_old = (change['old_lineno'] != '...' and
459 change['old_lineno'])
459 change['old_lineno'])
460 cond_new = (change['new_lineno'] != '...' and
460 cond_new = (change['new_lineno'] != '...' and
461 change['new_lineno'])
461 change['new_lineno'])
462 if cond_old:
462 if cond_old:
463 anchor_old_id = 'id="%s"' % anchor_old
463 anchor_old_id = 'id="%s"' % anchor_old
464 if cond_new:
464 if cond_new:
465 anchor_new_id = 'id="%s"' % anchor_new
465 anchor_new_id = 'id="%s"' % anchor_new
466 ###########################################################
466 ###########################################################
467 # OLD LINE NUMBER
467 # OLD LINE NUMBER
468 ###########################################################
468 ###########################################################
469 _html.append('''\t<td %(a_id)s class="%(olc)s">''' % {
469 _html.append('''\t<td %(a_id)s class="%(olc)s">''' % {
470 'a_id': anchor_old_id,
470 'a_id': anchor_old_id,
471 'olc': old_lineno_class
471 'olc': old_lineno_class
472 })
472 })
473
473
474 _html.append('''%(link)s''' % {
474 _html.append('''%(link)s''' % {
475 'link': _link_to_if(True, change['old_lineno'],
475 'link': _link_to_if(True, change['old_lineno'],
476 '#%s' % anchor_old)
476 '#%s' % anchor_old)
477 })
477 })
478 _html.append('''</td>\n''')
478 _html.append('''</td>\n''')
479 ###########################################################
479 ###########################################################
480 # NEW LINE NUMBER
480 # NEW LINE NUMBER
481 ###########################################################
481 ###########################################################
482
482
483 _html.append('''\t<td %(a_id)s class="%(nlc)s">''' % {
483 _html.append('''\t<td %(a_id)s class="%(nlc)s">''' % {
484 'a_id': anchor_new_id,
484 'a_id': anchor_new_id,
485 'nlc': new_lineno_class
485 'nlc': new_lineno_class
486 })
486 })
487
487
488 _html.append('''%(link)s''' % {
488 _html.append('''%(link)s''' % {
489 'link': _link_to_if(True, change['new_lineno'],
489 'link': _link_to_if(True, change['new_lineno'],
490 '#%s' % anchor_new)
490 '#%s' % anchor_new)
491 })
491 })
492 _html.append('''</td>\n''')
492 _html.append('''</td>\n''')
493 ###########################################################
493 ###########################################################
494 # CODE
494 # CODE
495 ###########################################################
495 ###########################################################
496 comments = '' if enable_comments else 'no-comment'
496 comments = '' if enable_comments else 'no-comment'
497 _html.append('''\t<td class="%(cc)s %(inc)s">''' % {
497 _html.append('''\t<td class="%(cc)s %(inc)s">''' % {
498 'cc': code_class,
498 'cc': code_class,
499 'inc': comments
499 'inc': comments
500 })
500 })
501 _html.append('''\n\t\t<pre>%(code)s</pre>\n''' % {
501 _html.append('''\n\t\t<pre>%(code)s</pre>\n''' % {
502 'code': change['line']
502 'code': change['line']
503 })
503 })
504 _html.append('''\t</td>''')
504 _html.append('''\t</td>''')
505 _html.append('''\n</tr>\n''')
505 _html.append('''\n</tr>\n''')
506 _html.append('''</table>''')
506 _html.append('''</table>''')
507 if _html_empty:
507 if _html_empty:
508 return None
508 return None
509 return ''.join(_html)
509 return ''.join(_html)
510
510
511 def stat(self):
511 def stat(self):
512 """
512 """
513 Returns tuple of added, and removed lines for this instance
513 Returns tuple of added, and removed lines for this instance
514 """
514 """
515 return self.adds, self.removes
515 return self.adds, self.removes
General Comments 0
You need to be logged in to leave comments. Login now