##// END OF EJS Templates
diff parser: prefer git headers over old unified diff headers...
Mads Kiilerich -
r3820:8df1bc51 beta
parent child Browse files
Show More
@@ -1,691 +1,688 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.lib.diffs
4 4 ~~~~~~~~~~~~~~~~~~~
5 5
6 6 Set of diffing helpers, previously part of vcs
7 7
8 8
9 9 :created_on: Dec 4, 2011
10 10 :author: marcink
11 11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
12 12 :original copyright: 2007-2008 by Armin Ronacher
13 13 :license: GPLv3, see COPYING for more details.
14 14 """
15 15 # This program is free software: you can redistribute it and/or modify
16 16 # it under the terms of the GNU General Public License as published by
17 17 # the Free Software Foundation, either version 3 of the License, or
18 18 # (at your option) any later version.
19 19 #
20 20 # This program is distributed in the hope that it will be useful,
21 21 # but WITHOUT ANY WARRANTY; without even the implied warranty of
22 22 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23 23 # GNU General Public License for more details.
24 24 #
25 25 # You should have received a copy of the GNU General Public License
26 26 # along with this program. If not, see <http://www.gnu.org/licenses/>.
27 27
28 28 import re
29 29 import difflib
30 30 import logging
31 31
32 32 from itertools import tee, imap
33 33
34 34 from pylons.i18n.translation import _
35 35
36 36 from rhodecode.lib.vcs.exceptions import VCSError
37 37 from rhodecode.lib.vcs.nodes import FileNode, SubModuleNode
38 38 from rhodecode.lib.vcs.backends.base import EmptyChangeset
39 39 from rhodecode.lib.helpers import escape
40 40 from rhodecode.lib.utils2 import safe_unicode, safe_str
41 41
42 42 log = logging.getLogger(__name__)
43 43
44 44
45 45 def wrap_to_table(str_):
46 46 return '''<table class="code-difftable">
47 47 <tr class="line no-comment">
48 48 <td class="lineno new"></td>
49 49 <td class="code no-comment"><pre>%s</pre></td>
50 50 </tr>
51 51 </table>''' % str_
52 52
53 53
54 54 def wrapped_diff(filenode_old, filenode_new, cut_off_limit=None,
55 55 ignore_whitespace=True, line_context=3,
56 56 enable_comments=False):
57 57 """
58 58 returns a wrapped diff into a table, checks for cut_off_limit and presents
59 59 proper message
60 60 """
61 61
62 62 if filenode_old is None:
63 63 filenode_old = FileNode(filenode_new.path, '', EmptyChangeset())
64 64
65 65 if filenode_old.is_binary or filenode_new.is_binary:
66 66 diff = wrap_to_table(_('Binary file'))
67 67 stats = (0, 0)
68 68 size = 0
69 69
70 70 elif cut_off_limit != -1 and (cut_off_limit is None or
71 71 (filenode_old.size < cut_off_limit and filenode_new.size < cut_off_limit)):
72 72
73 73 f_gitdiff = get_gitdiff(filenode_old, filenode_new,
74 74 ignore_whitespace=ignore_whitespace,
75 75 context=line_context)
76 76 diff_processor = DiffProcessor(f_gitdiff, format='gitdiff')
77 77
78 78 diff = diff_processor.as_html(enable_comments=enable_comments)
79 79 stats = diff_processor.stat()
80 80 size = len(diff or '')
81 81 else:
82 82 diff = wrap_to_table(_('Changeset was too big and was cut off, use '
83 83 'diff menu to display this diff'))
84 84 stats = (0, 0)
85 85 size = 0
86 86 if not diff:
87 87 submodules = filter(lambda o: isinstance(o, SubModuleNode),
88 88 [filenode_new, filenode_old])
89 89 if submodules:
90 90 diff = wrap_to_table(escape('Submodule %r' % submodules[0]))
91 91 else:
92 92 diff = wrap_to_table(_('No changes detected'))
93 93
94 94 cs1 = filenode_old.changeset.raw_id
95 95 cs2 = filenode_new.changeset.raw_id
96 96
97 97 return size, cs1, cs2, diff, stats
98 98
99 99
100 100 def get_gitdiff(filenode_old, filenode_new, ignore_whitespace=True, context=3):
101 101 """
102 102 Returns git style diff between given ``filenode_old`` and ``filenode_new``.
103 103
104 104 :param ignore_whitespace: ignore whitespaces in diff
105 105 """
106 106 # make sure we pass in default context
107 107 context = context or 3
108 108 submodules = filter(lambda o: isinstance(o, SubModuleNode),
109 109 [filenode_new, filenode_old])
110 110 if submodules:
111 111 return ''
112 112
113 113 for filenode in (filenode_old, filenode_new):
114 114 if not isinstance(filenode, FileNode):
115 115 raise VCSError("Given object should be FileNode object, not %s"
116 116 % filenode.__class__)
117 117
118 118 repo = filenode_new.changeset.repository
119 119 old_raw_id = getattr(filenode_old.changeset, 'raw_id', repo.EMPTY_CHANGESET)
120 120 new_raw_id = getattr(filenode_new.changeset, 'raw_id', repo.EMPTY_CHANGESET)
121 121
122 122 vcs_gitdiff = repo.get_diff(old_raw_id, new_raw_id, filenode_new.path,
123 123 ignore_whitespace, context)
124 124 return vcs_gitdiff
125 125
126 126 NEW_FILENODE = 1
127 127 DEL_FILENODE = 2
128 128 MOD_FILENODE = 3
129 129 RENAMED_FILENODE = 4
130 130 CHMOD_FILENODE = 5
131 131
132 132
133 133 class DiffLimitExceeded(Exception):
134 134 pass
135 135
136 136
137 137 class LimitedDiffContainer(object):
138 138
139 139 def __init__(self, diff_limit, cur_diff_size, diff):
140 140 self.diff = diff
141 141 self.diff_limit = diff_limit
142 142 self.cur_diff_size = cur_diff_size
143 143
144 144 def __iter__(self):
145 145 for l in self.diff:
146 146 yield l
147 147
148 148
149 149 class DiffProcessor(object):
150 150 """
151 151 Give it a unified or git diff and it returns a list of the files that were
152 152 mentioned in the diff together with a dict of meta information that
153 153 can be used to render it in a HTML template.
154 154 """
155 155 _chunk_re = re.compile(r'^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)')
156 156 _newline_marker = re.compile(r'^\\ No newline at end of file')
157 157 _git_header_re = re.compile(r"""
158 158 #^diff[ ]--git
159 159 [ ]a/(?P<a_path>.+?)[ ]b/(?P<b_path>.+?)\n
160 160 (?:^similarity[ ]index[ ](?P<similarity_index>\d+)%\n
161 161 ^rename[ ]from[ ](?P<rename_from>\S+)\n
162 162 ^rename[ ]to[ ](?P<rename_to>\S+)(?:\n|$))?
163 163 (?:^old[ ]mode[ ](?P<old_mode>\d+)\n
164 164 ^new[ ]mode[ ](?P<new_mode>\d+)(?:\n|$))?
165 165 (?:^new[ ]file[ ]mode[ ](?P<new_file_mode>.+)(?:\n|$))?
166 166 (?:^deleted[ ]file[ ]mode[ ](?P<deleted_file_mode>.+)(?:\n|$))?
167 167 (?:^index[ ](?P<a_blob_id>[0-9A-Fa-f]+)
168 168 \.\.(?P<b_blob_id>[0-9A-Fa-f]+)[ ]?(?P<b_mode>.+)?(?:\n|$))?
169 169 (?:^---[ ](a/(?P<a_file>.+)|/dev/null)(?:\n|$))?
170 170 (?:^\+\+\+[ ](b/(?P<b_file>.+)|/dev/null)(?:\n|$))?
171 171 """, re.VERBOSE | re.MULTILINE)
172 172 _hg_header_re = re.compile(r"""
173 173 #^diff[ ]--git
174 174 [ ]a/(?P<a_path>.+?)[ ]b/(?P<b_path>.+?)\n
175 175 (?:^old[ ]mode[ ](?P<old_mode>\d+)\n
176 176 ^new[ ]mode[ ](?P<new_mode>\d+)(?:\n|$))?
177 177 (?:^similarity[ ]index[ ](?P<similarity_index>\d+)%(?:\n|$))?
178 178 (?:^rename[ ]from[ ](?P<rename_from>\S+)\n
179 179 ^rename[ ]to[ ](?P<rename_to>\S+)(?:\n|$))?
180 180 (?:^new[ ]file[ ]mode[ ](?P<new_file_mode>.+)(?:\n|$))?
181 181 (?:^deleted[ ]file[ ]mode[ ](?P<deleted_file_mode>.+)(?:\n|$))?
182 182 (?:^index[ ](?P<a_blob_id>[0-9A-Fa-f]+)
183 183 \.\.(?P<b_blob_id>[0-9A-Fa-f]+)[ ]?(?P<b_mode>.+)?(?:\n|$))?
184 184 (?:^---[ ](a/(?P<a_file>.+)|/dev/null)(?:\n|$))?
185 185 (?:^\+\+\+[ ](b/(?P<b_file>.+)|/dev/null)(?:\n|$))?
186 186 """, re.VERBOSE | re.MULTILINE)
187 187
188 188 #used for inline highlighter word split
189 189 _token_re = re.compile(r'()(&gt;|&lt;|&amp;|\W+?)')
190 190
191 191 def __init__(self, diff, vcs='hg', format='gitdiff', diff_limit=None):
192 192 """
193 193 :param diff: a text in diff format
194 194 :param vcs: type of version controll hg or git
195 195 :param format: format of diff passed, `udiff` or `gitdiff`
196 196 :param diff_limit: define the size of diff that is considered "big"
197 197 based on that parameter cut off will be triggered, set to None
198 198 to show full diff
199 199 """
200 200 if not isinstance(diff, basestring):
201 201 raise Exception('Diff must be a basestring got %s instead' % type(diff))
202 202
203 203 self._diff = diff
204 204 self._format = format
205 205 self.adds = 0
206 206 self.removes = 0
207 207 # calculate diff size
208 208 self.diff_size = len(diff)
209 209 self.diff_limit = diff_limit
210 210 self.cur_diff_size = 0
211 211 self.parsed = False
212 212 self.parsed_diff = []
213 213 self.vcs = vcs
214 214
215 215 if format == 'gitdiff':
216 216 self.differ = self._highlight_line_difflib
217 217 self._parser = self._parse_gitdiff
218 218 else:
219 219 self.differ = self._highlight_line_udiff
220 220 self._parser = self._parse_udiff
221 221
222 222 def _copy_iterator(self):
223 223 """
224 224 make a fresh copy of generator, we should not iterate thru
225 225 an original as it's needed for repeating operations on
226 226 this instance of DiffProcessor
227 227 """
228 228 self.__udiff, iterator_copy = tee(self.__udiff)
229 229 return iterator_copy
230 230
231 231 def _escaper(self, string):
232 232 """
233 233 Escaper for diff escapes special chars and checks the diff limit
234 234
235 235 :param string:
236 236 :type string:
237 237 """
238 238
239 239 self.cur_diff_size += len(string)
240 240
241 241 # escaper get's iterated on each .next() call and it checks if each
242 242 # parsed line doesn't exceed the diff limit
243 243 if self.diff_limit is not None and self.cur_diff_size > self.diff_limit:
244 244 raise DiffLimitExceeded('Diff Limit Exceeded')
245 245
246 246 return safe_unicode(string).replace('&', '&amp;')\
247 247 .replace('<', '&lt;')\
248 248 .replace('>', '&gt;')
249 249
250 250 def _line_counter(self, l):
251 251 """
252 252 Checks each line and bumps total adds/removes for this diff
253 253
254 254 :param l:
255 255 """
256 256 if l.startswith('+') and not l.startswith('+++'):
257 257 self.adds += 1
258 258 elif l.startswith('-') and not l.startswith('---'):
259 259 self.removes += 1
260 260 return safe_unicode(l)
261 261
262 262 def _highlight_line_difflib(self, line, next_):
263 263 """
264 264 Highlight inline changes in both lines.
265 265 """
266 266
267 267 if line['action'] == 'del':
268 268 old, new = line, next_
269 269 else:
270 270 old, new = next_, line
271 271
272 272 oldwords = self._token_re.split(old['line'])
273 273 newwords = self._token_re.split(new['line'])
274 274 sequence = difflib.SequenceMatcher(None, oldwords, newwords)
275 275
276 276 oldfragments, newfragments = [], []
277 277 for tag, i1, i2, j1, j2 in sequence.get_opcodes():
278 278 oldfrag = ''.join(oldwords[i1:i2])
279 279 newfrag = ''.join(newwords[j1:j2])
280 280 if tag != 'equal':
281 281 if oldfrag:
282 282 oldfrag = '<del>%s</del>' % oldfrag
283 283 if newfrag:
284 284 newfrag = '<ins>%s</ins>' % newfrag
285 285 oldfragments.append(oldfrag)
286 286 newfragments.append(newfrag)
287 287
288 288 old['line'] = "".join(oldfragments)
289 289 new['line'] = "".join(newfragments)
290 290
291 291 def _highlight_line_udiff(self, line, next_):
292 292 """
293 293 Highlight inline changes in both lines.
294 294 """
295 295 start = 0
296 296 limit = min(len(line['line']), len(next_['line']))
297 297 while start < limit and line['line'][start] == next_['line'][start]:
298 298 start += 1
299 299 end = -1
300 300 limit -= start
301 301 while -end <= limit and line['line'][end] == next_['line'][end]:
302 302 end -= 1
303 303 end += 1
304 304 if start or end:
305 305 def do(l):
306 306 last = end + len(l['line'])
307 307 if l['action'] == 'add':
308 308 tag = 'ins'
309 309 else:
310 310 tag = 'del'
311 311 l['line'] = '%s<%s>%s</%s>%s' % (
312 312 l['line'][:start],
313 313 tag,
314 314 l['line'][start:last],
315 315 tag,
316 316 l['line'][last:]
317 317 )
318 318 do(line)
319 319 do(next_)
320 320
321 321 def _get_header(self, diff_chunk):
322 322 """
323 323 parses the diff header, and returns parts, and leftover diff
324 324 parts consists of 14 elements::
325 325
326 326 a_path, b_path, similarity_index, rename_from, rename_to,
327 327 old_mode, new_mode, new_file_mode, deleted_file_mode,
328 328 a_blob_id, b_blob_id, b_mode, a_file, b_file
329 329
330 330 :param diff_chunk:
331 331 :type diff_chunk:
332 332 """
333 333
334 334 if self.vcs == 'git':
335 335 match = self._git_header_re.match(diff_chunk)
336 336 diff = diff_chunk[match.end():]
337 337 return match.groupdict(), imap(self._escaper, diff.splitlines(1))
338 338 elif self.vcs == 'hg':
339 339 match = self._hg_header_re.match(diff_chunk)
340 340 diff = diff_chunk[match.end():]
341 341 return match.groupdict(), imap(self._escaper, diff.splitlines(1))
342 342 else:
343 343 raise Exception('VCS type %s is not supported' % self.vcs)
344 344
345 345 def _clean_line(self, line, command):
346 346 if command in ['+', '-', ' ']:
347 347 #only modify the line if it's actually a diff thing
348 348 line = line[1:]
349 349 return line
350 350
351 351 def _parse_gitdiff(self, inline_diff=True):
352 352 _files = []
353 353 diff_container = lambda arg: arg
354 354
355 355 ##split the diff in chunks of separate --git a/file b/file chunks
356 356 for raw_diff in ('\n' + self._diff).split('\ndiff --git')[1:]:
357 357 head, diff = self._get_header(raw_diff)
358 358
359 359 op = None
360 360 stats = None
361 361 msgs = []
362 362
363 if not head['a_file'] and head['b_file']:
364 op = 'A'
365 stats = ['b', NEW_FILENODE]
366 msgs.append('new file')
367 elif head['a_file'] and head['b_file']:
368 op = 'M'
369 stats = ['b', MOD_FILENODE]
370 elif head['a_file'] and not head['b_file']:
363 if head['deleted_file_mode']:
371 364 op = 'D'
372 365 stats = ['b', DEL_FILENODE]
373 366 msgs.append('deleted file')
367 elif head['new_file_mode']:
368 op = 'A'
369 stats = ['b', NEW_FILENODE]
370 msgs.append('new file %s' % head['new_file_mode'])
374 371 else:
375 if head['deleted_file_mode']:
376 op = 'D'
377 stats = ['b', DEL_FILENODE]
378 msgs.append('deleted file')
379 elif head['new_file_mode']:
380 op = 'A'
381 stats = ['b', NEW_FILENODE]
382 msgs.append('new file %s' % head['new_file_mode'])
383 else:
384 if head['new_mode'] and head['old_mode']:
385 op = 'M'
386 stats = ['b', CHMOD_FILENODE]
387 msgs.append('modified file chmod %s => %s'
388 % (head['old_mode'], head['new_mode']))
389 if (head['rename_from'] and head['rename_to']
390 and head['rename_from'] != head['rename_to']):
391 op = 'M'
392 stats = ['b', RENAMED_FILENODE] # might overwrite CHMOD_FILENODE
393 msgs.append('file renamed from %s to %s'
394 % (head['rename_from'], head['rename_to']))
395 if op is None:
396 op = 'M'
397 stats = ['b', MOD_FILENODE]
372 if head['new_mode'] and head['old_mode']:
373 op = 'M'
374 stats = ['b', CHMOD_FILENODE]
375 msgs.append('modified file chmod %s => %s'
376 % (head['old_mode'], head['new_mode']))
377 if (head['rename_from'] and head['rename_to']
378 and head['rename_from'] != head['rename_to']):
379 op = 'M'
380 stats = ['b', RENAMED_FILENODE] # might overwrite CHMOD_FILENODE
381 msgs.append('file renamed from %s to %s'
382 % (head['rename_from'], head['rename_to']))
383 if op is None: # fall back: detect missed old style add or remove
384 if not head['a_file'] and head['b_file']:
385 op = 'A'
386 stats = ['b', NEW_FILENODE]
387 msgs.append('new file')
388 elif head['a_file'] and not head['b_file']:
389 op = 'D'
390 stats = ['b', DEL_FILENODE]
391 msgs.append('deleted file')
392 if op is None:
393 op = 'M'
394 stats = ['b', MOD_FILENODE]
398 395
399 396 if head['a_file'] or head['b_file']: # a real diff
400 397 try:
401 398 chunks, stats = self._parse_lines(diff)
402 399 except DiffLimitExceeded:
403 400 diff_container = lambda _diff: LimitedDiffContainer(
404 401 self.diff_limit,
405 402 self.cur_diff_size,
406 403 _diff)
407 404 break
408 405 else: # GIT binary patch (or empty diff)
409 406 chunks = []
410 407 msgs.append('binary diff not shown') # or no diff because it was a rename or chmod or add/remove of empty file
411 408
412 409 if msgs:
413 410 chunks.insert(0, [{
414 411 'old_lineno': '',
415 412 'new_lineno': '',
416 413 'action': 'binary',
417 414 'line': msg,
418 415 } for msg in msgs])
419 416
420 417 _files.append({
421 418 'filename': head['b_path'],
422 419 'old_revision': head['a_blob_id'],
423 420 'new_revision': head['b_blob_id'],
424 421 'chunks': chunks,
425 422 'operation': op,
426 423 'stats': stats,
427 424 })
428 425
429 426 sorter = lambda info: {'A': 0, 'M': 1, 'D': 2}.get(info['operation'])
430 427
431 428 if not inline_diff:
432 429 return diff_container(sorted(_files, key=sorter))
433 430
434 431 # highlight inline changes
435 432 for diff_data in _files:
436 433 for chunk in diff_data['chunks']:
437 434 lineiter = iter(chunk)
438 435 try:
439 436 while 1:
440 437 line = lineiter.next()
441 438 if line['action'] not in ['unmod', 'context']:
442 439 nextline = lineiter.next()
443 440 if nextline['action'] in ['unmod', 'context'] or \
444 441 nextline['action'] == line['action']:
445 442 continue
446 443 self.differ(line, nextline)
447 444 except StopIteration:
448 445 pass
449 446
450 447 return diff_container(sorted(_files, key=sorter))
451 448
452 449 def _parse_udiff(self, inline_diff=True):
453 450 raise NotImplementedError()
454 451
455 452 def _parse_lines(self, diff):
456 453 """
457 454 Parse the diff an return data for the template.
458 455 """
459 456
460 457 lineiter = iter(diff)
461 458 stats = [0, 0]
462 459
463 460 try:
464 461 chunks = []
465 462 line = lineiter.next()
466 463
467 464 while line:
468 465 lines = []
469 466 chunks.append(lines)
470 467
471 468 match = self._chunk_re.match(line)
472 469
473 470 if not match:
474 471 break
475 472
476 473 gr = match.groups()
477 474 (old_line, old_end,
478 475 new_line, new_end) = [int(x or 1) for x in gr[:-1]]
479 476 old_line -= 1
480 477 new_line -= 1
481 478
482 479 context = len(gr) == 5
483 480 old_end += old_line
484 481 new_end += new_line
485 482
486 483 if context:
487 484 # skip context only if it's first line
488 485 if int(gr[0]) > 1:
489 486 lines.append({
490 487 'old_lineno': '...',
491 488 'new_lineno': '...',
492 489 'action': 'context',
493 490 'line': line,
494 491 })
495 492
496 493 line = lineiter.next()
497 494
498 495 while old_line < old_end or new_line < new_end:
499 496 command = ' '
500 497 if line:
501 498 command = line[0]
502 499
503 500 affects_old = affects_new = False
504 501
505 502 # ignore those if we don't expect them
506 503 if command in '#@':
507 504 continue
508 505 elif command == '+':
509 506 affects_new = True
510 507 action = 'add'
511 508 stats[0] += 1
512 509 elif command == '-':
513 510 affects_old = True
514 511 action = 'del'
515 512 stats[1] += 1
516 513 else:
517 514 affects_old = affects_new = True
518 515 action = 'unmod'
519 516
520 517 if not self._newline_marker.match(line):
521 518 old_line += affects_old
522 519 new_line += affects_new
523 520 lines.append({
524 521 'old_lineno': affects_old and old_line or '',
525 522 'new_lineno': affects_new and new_line or '',
526 523 'action': action,
527 524 'line': self._clean_line(line, command)
528 525 })
529 526
530 527 line = lineiter.next()
531 528
532 529 if self._newline_marker.match(line):
533 530 # we need to append to lines, since this is not
534 531 # counted in the line specs of diff
535 532 lines.append({
536 533 'old_lineno': '...',
537 534 'new_lineno': '...',
538 535 'action': 'context',
539 536 'line': self._clean_line(line, command)
540 537 })
541 538
542 539 except StopIteration:
543 540 pass
544 541 return chunks, stats
545 542
546 543 def _safe_id(self, idstring):
547 544 """Make a string safe for including in an id attribute.
548 545
549 546 The HTML spec says that id attributes 'must begin with
550 547 a letter ([A-Za-z]) and may be followed by any number
551 548 of letters, digits ([0-9]), hyphens ("-"), underscores
552 549 ("_"), colons (":"), and periods (".")'. These regexps
553 550 are slightly over-zealous, in that they remove colons
554 551 and periods unnecessarily.
555 552
556 553 Whitespace is transformed into underscores, and then
557 554 anything which is not a hyphen or a character that
558 555 matches \w (alphanumerics and underscore) is removed.
559 556
560 557 """
561 558 # Transform all whitespace to underscore
562 559 idstring = re.sub(r'\s', "_", '%s' % idstring)
563 560 # Remove everything that is not a hyphen or a member of \w
564 561 idstring = re.sub(r'(?!-)\W', "", idstring).lower()
565 562 return idstring
566 563
567 564 def prepare(self, inline_diff=True):
568 565 """
569 566 Prepare the passed udiff for HTML rendering. It'l return a list
570 567 of dicts with diff information
571 568 """
572 569 parsed = self._parser(inline_diff=inline_diff)
573 570 self.parsed = True
574 571 self.parsed_diff = parsed
575 572 return parsed
576 573
577 574 def as_raw(self, diff_lines=None):
578 575 """
579 576 Returns raw string diff
580 577 """
581 578 return self._diff
582 579 #return u''.join(imap(self._line_counter, self._diff.splitlines(1)))
583 580
584 581 def as_html(self, table_class='code-difftable', line_class='line',
585 582 old_lineno_class='lineno old', new_lineno_class='lineno new',
586 583 code_class='code', enable_comments=False, parsed_lines=None):
587 584 """
588 585 Return given diff as html table with customized css classes
589 586 """
590 587 def _link_to_if(condition, label, url):
591 588 """
592 589 Generates a link if condition is meet or just the label if not.
593 590 """
594 591
595 592 if condition:
596 593 return '''<a href="%(url)s">%(label)s</a>''' % {
597 594 'url': url,
598 595 'label': label
599 596 }
600 597 else:
601 598 return label
602 599 if not self.parsed:
603 600 self.prepare()
604 601
605 602 diff_lines = self.parsed_diff
606 603 if parsed_lines:
607 604 diff_lines = parsed_lines
608 605
609 606 _html_empty = True
610 607 _html = []
611 608 _html.append('''<table class="%(table_class)s">\n''' % {
612 609 'table_class': table_class
613 610 })
614 611
615 612 for diff in diff_lines:
616 613 for line in diff['chunks']:
617 614 _html_empty = False
618 615 for change in line:
619 616 _html.append('''<tr class="%(lc)s %(action)s">\n''' % {
620 617 'lc': line_class,
621 618 'action': change['action']
622 619 })
623 620 anchor_old_id = ''
624 621 anchor_new_id = ''
625 622 anchor_old = "%(filename)s_o%(oldline_no)s" % {
626 623 'filename': self._safe_id(diff['filename']),
627 624 'oldline_no': change['old_lineno']
628 625 }
629 626 anchor_new = "%(filename)s_n%(oldline_no)s" % {
630 627 'filename': self._safe_id(diff['filename']),
631 628 'oldline_no': change['new_lineno']
632 629 }
633 630 cond_old = (change['old_lineno'] != '...' and
634 631 change['old_lineno'])
635 632 cond_new = (change['new_lineno'] != '...' and
636 633 change['new_lineno'])
637 634 if cond_old:
638 635 anchor_old_id = 'id="%s"' % anchor_old
639 636 if cond_new:
640 637 anchor_new_id = 'id="%s"' % anchor_new
641 638 ###########################################################
642 639 # OLD LINE NUMBER
643 640 ###########################################################
644 641 _html.append('''\t<td %(a_id)s class="%(olc)s">''' % {
645 642 'a_id': anchor_old_id,
646 643 'olc': old_lineno_class
647 644 })
648 645
649 646 _html.append('''%(link)s''' % {
650 647 'link': _link_to_if(True, change['old_lineno'],
651 648 '#%s' % anchor_old)
652 649 })
653 650 _html.append('''</td>\n''')
654 651 ###########################################################
655 652 # NEW LINE NUMBER
656 653 ###########################################################
657 654
658 655 _html.append('''\t<td %(a_id)s class="%(nlc)s">''' % {
659 656 'a_id': anchor_new_id,
660 657 'nlc': new_lineno_class
661 658 })
662 659
663 660 _html.append('''%(link)s''' % {
664 661 'link': _link_to_if(True, change['new_lineno'],
665 662 '#%s' % anchor_new)
666 663 })
667 664 _html.append('''</td>\n''')
668 665 ###########################################################
669 666 # CODE
670 667 ###########################################################
671 668 comments = '' if enable_comments else 'no-comment'
672 669 _html.append('''\t<td class="%(cc)s %(inc)s">''' % {
673 670 'cc': code_class,
674 671 'inc': comments
675 672 })
676 673 _html.append('''\n\t\t<pre>%(code)s</pre>\n''' % {
677 674 'code': change['line']
678 675 })
679 676
680 677 _html.append('''\t</td>''')
681 678 _html.append('''\n</tr>\n''')
682 679 _html.append('''</table>''')
683 680 if _html_empty:
684 681 return None
685 682 return ''.join(_html)
686 683
687 684 def stat(self):
688 685 """
689 686 Returns tuple of added, and removed lines for this instance
690 687 """
691 688 return self.adds, self.removes
General Comments 0
You need to be logged in to leave comments. Login now