##// END OF EJS Templates
diff parser: more correct detection and reporting of binary git diffs...
Mads Kiilerich -
r3818:0d22458b beta
parent child Browse files
Show More
@@ -1,684 +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 binary = False
358 binary_msg = 'unknown binary'
359 357 head, diff = self._get_header(raw_diff)
360 358
359 op = None
360 stats = None
361 msg = None
362
361 363 if not head['a_file'] and head['b_file']:
362 364 op = 'A'
363 365 elif head['a_file'] and head['b_file']:
364 366 op = 'M'
365 367 elif head['a_file'] and not head['b_file']:
366 368 op = 'D'
367 369 else:
368 #probably we're dealing with a binary file 1
369 binary = True
370 370 if head['deleted_file_mode']:
371 371 op = 'D'
372 372 stats = ['b', DEL_FILENODE]
373 binary_msg = 'deleted binary file'
373 msg = 'deleted file'
374 374 elif head['new_file_mode']:
375 375 op = 'A'
376 376 stats = ['b', NEW_FILENODE]
377 binary_msg = 'new binary file %s' % head['new_file_mode']
377 msg = 'new file %s' % head['new_file_mode']
378 378 else:
379 379 if head['new_mode'] and head['old_mode']:
380 380 stats = ['b', CHMOD_FILENODE]
381 381 op = 'M'
382 binary_msg = ('modified binary file chmod %s => %s'
382 msg = ('modified file chmod %s => %s'
383 383 % (head['old_mode'], head['new_mode']))
384 384 elif (head['rename_from'] and head['rename_to']
385 385 and head['rename_from'] != head['rename_to']):
386 386 stats = ['b', RENAMED_FILENODE]
387 387 op = 'M'
388 binary_msg = ('file renamed from %s to %s'
388 msg = ('file renamed from %s to %s'
389 389 % (head['rename_from'], head['rename_to']))
390 390 else:
391 391 stats = ['b', MOD_FILENODE]
392 392 op = 'M'
393 binary_msg = 'modified binary file'
393 msg = 'modified file'
394 394
395 if not binary:
395 if head['a_file'] or head['b_file']: # a real diff
396 396 try:
397 397 chunks, stats = self._parse_lines(diff)
398 398 except DiffLimitExceeded:
399 399 diff_container = lambda _diff: LimitedDiffContainer(
400 400 self.diff_limit,
401 401 self.cur_diff_size,
402 402 _diff)
403 403 break
404 else:
404 else: # GIT binary patch (or empty diff)
405 405 chunks = []
406 chunks.append([{
406 if not msg: # don't overwrite more important message
407 msg = 'binary diff not shown'
408
409 if msg:
410 chunks.insert(0, [{
407 411 'old_lineno': '',
408 412 'new_lineno': '',
409 413 'action': 'binary',
410 'line': binary_msg,
414 'line': msg,
411 415 }])
412 416
413 417 _files.append({
414 418 'filename': head['b_path'],
415 419 'old_revision': head['a_blob_id'],
416 420 'new_revision': head['b_blob_id'],
417 421 'chunks': chunks,
418 422 'operation': op,
419 423 'stats': stats,
420 424 })
421 425
422 426 sorter = lambda info: {'A': 0, 'M': 1, 'D': 2}.get(info['operation'])
423 427
424 428 if not inline_diff:
425 429 return diff_container(sorted(_files, key=sorter))
426 430
427 431 # highlight inline changes
428 432 for diff_data in _files:
429 433 for chunk in diff_data['chunks']:
430 434 lineiter = iter(chunk)
431 435 try:
432 436 while 1:
433 437 line = lineiter.next()
434 438 if line['action'] not in ['unmod', 'context']:
435 439 nextline = lineiter.next()
436 440 if nextline['action'] in ['unmod', 'context'] or \
437 441 nextline['action'] == line['action']:
438 442 continue
439 443 self.differ(line, nextline)
440 444 except StopIteration:
441 445 pass
442 446
443 447 return diff_container(sorted(_files, key=sorter))
444 448
445 449 def _parse_udiff(self, inline_diff=True):
446 450 raise NotImplementedError()
447 451
448 452 def _parse_lines(self, diff):
449 453 """
450 454 Parse the diff an return data for the template.
451 455 """
452 456
453 457 lineiter = iter(diff)
454 458 stats = [0, 0]
455 459
456 460 try:
457 461 chunks = []
458 462 line = lineiter.next()
459 463
460 464 while line:
461 465 lines = []
462 466 chunks.append(lines)
463 467
464 468 match = self._chunk_re.match(line)
465 469
466 470 if not match:
467 471 break
468 472
469 473 gr = match.groups()
470 474 (old_line, old_end,
471 475 new_line, new_end) = [int(x or 1) for x in gr[:-1]]
472 476 old_line -= 1
473 477 new_line -= 1
474 478
475 479 context = len(gr) == 5
476 480 old_end += old_line
477 481 new_end += new_line
478 482
479 483 if context:
480 484 # skip context only if it's first line
481 485 if int(gr[0]) > 1:
482 486 lines.append({
483 487 'old_lineno': '...',
484 488 'new_lineno': '...',
485 489 'action': 'context',
486 490 'line': line,
487 491 })
488 492
489 493 line = lineiter.next()
490 494
491 495 while old_line < old_end or new_line < new_end:
492 496 command = ' '
493 497 if line:
494 498 command = line[0]
495 499
496 500 affects_old = affects_new = False
497 501
498 502 # ignore those if we don't expect them
499 503 if command in '#@':
500 504 continue
501 505 elif command == '+':
502 506 affects_new = True
503 507 action = 'add'
504 508 stats[0] += 1
505 509 elif command == '-':
506 510 affects_old = True
507 511 action = 'del'
508 512 stats[1] += 1
509 513 else:
510 514 affects_old = affects_new = True
511 515 action = 'unmod'
512 516
513 517 if not self._newline_marker.match(line):
514 518 old_line += affects_old
515 519 new_line += affects_new
516 520 lines.append({
517 521 'old_lineno': affects_old and old_line or '',
518 522 'new_lineno': affects_new and new_line or '',
519 523 'action': action,
520 524 'line': self._clean_line(line, command)
521 525 })
522 526
523 527 line = lineiter.next()
524 528
525 529 if self._newline_marker.match(line):
526 530 # we need to append to lines, since this is not
527 531 # counted in the line specs of diff
528 532 lines.append({
529 533 'old_lineno': '...',
530 534 'new_lineno': '...',
531 535 'action': 'context',
532 536 'line': self._clean_line(line, command)
533 537 })
534 538
535 539 except StopIteration:
536 540 pass
537 541 return chunks, stats
538 542
539 543 def _safe_id(self, idstring):
540 544 """Make a string safe for including in an id attribute.
541 545
542 546 The HTML spec says that id attributes 'must begin with
543 547 a letter ([A-Za-z]) and may be followed by any number
544 548 of letters, digits ([0-9]), hyphens ("-"), underscores
545 549 ("_"), colons (":"), and periods (".")'. These regexps
546 550 are slightly over-zealous, in that they remove colons
547 551 and periods unnecessarily.
548 552
549 553 Whitespace is transformed into underscores, and then
550 554 anything which is not a hyphen or a character that
551 555 matches \w (alphanumerics and underscore) is removed.
552 556
553 557 """
554 558 # Transform all whitespace to underscore
555 559 idstring = re.sub(r'\s', "_", '%s' % idstring)
556 560 # Remove everything that is not a hyphen or a member of \w
557 561 idstring = re.sub(r'(?!-)\W', "", idstring).lower()
558 562 return idstring
559 563
560 564 def prepare(self, inline_diff=True):
561 565 """
562 566 Prepare the passed udiff for HTML rendering. It'l return a list
563 567 of dicts with diff information
564 568 """
565 569 parsed = self._parser(inline_diff=inline_diff)
566 570 self.parsed = True
567 571 self.parsed_diff = parsed
568 572 return parsed
569 573
570 574 def as_raw(self, diff_lines=None):
571 575 """
572 576 Returns raw string diff
573 577 """
574 578 return self._diff
575 579 #return u''.join(imap(self._line_counter, self._diff.splitlines(1)))
576 580
577 581 def as_html(self, table_class='code-difftable', line_class='line',
578 582 old_lineno_class='lineno old', new_lineno_class='lineno new',
579 583 code_class='code', enable_comments=False, parsed_lines=None):
580 584 """
581 585 Return given diff as html table with customized css classes
582 586 """
583 587 def _link_to_if(condition, label, url):
584 588 """
585 589 Generates a link if condition is meet or just the label if not.
586 590 """
587 591
588 592 if condition:
589 593 return '''<a href="%(url)s">%(label)s</a>''' % {
590 594 'url': url,
591 595 'label': label
592 596 }
593 597 else:
594 598 return label
595 599 if not self.parsed:
596 600 self.prepare()
597 601
598 602 diff_lines = self.parsed_diff
599 603 if parsed_lines:
600 604 diff_lines = parsed_lines
601 605
602 606 _html_empty = True
603 607 _html = []
604 608 _html.append('''<table class="%(table_class)s">\n''' % {
605 609 'table_class': table_class
606 610 })
607 611
608 612 for diff in diff_lines:
609 613 for line in diff['chunks']:
610 614 _html_empty = False
611 615 for change in line:
612 616 _html.append('''<tr class="%(lc)s %(action)s">\n''' % {
613 617 'lc': line_class,
614 618 'action': change['action']
615 619 })
616 620 anchor_old_id = ''
617 621 anchor_new_id = ''
618 622 anchor_old = "%(filename)s_o%(oldline_no)s" % {
619 623 'filename': self._safe_id(diff['filename']),
620 624 'oldline_no': change['old_lineno']
621 625 }
622 626 anchor_new = "%(filename)s_n%(oldline_no)s" % {
623 627 'filename': self._safe_id(diff['filename']),
624 628 'oldline_no': change['new_lineno']
625 629 }
626 630 cond_old = (change['old_lineno'] != '...' and
627 631 change['old_lineno'])
628 632 cond_new = (change['new_lineno'] != '...' and
629 633 change['new_lineno'])
630 634 if cond_old:
631 635 anchor_old_id = 'id="%s"' % anchor_old
632 636 if cond_new:
633 637 anchor_new_id = 'id="%s"' % anchor_new
634 638 ###########################################################
635 639 # OLD LINE NUMBER
636 640 ###########################################################
637 641 _html.append('''\t<td %(a_id)s class="%(olc)s">''' % {
638 642 'a_id': anchor_old_id,
639 643 'olc': old_lineno_class
640 644 })
641 645
642 646 _html.append('''%(link)s''' % {
643 647 'link': _link_to_if(True, change['old_lineno'],
644 648 '#%s' % anchor_old)
645 649 })
646 650 _html.append('''</td>\n''')
647 651 ###########################################################
648 652 # NEW LINE NUMBER
649 653 ###########################################################
650 654
651 655 _html.append('''\t<td %(a_id)s class="%(nlc)s">''' % {
652 656 'a_id': anchor_new_id,
653 657 'nlc': new_lineno_class
654 658 })
655 659
656 660 _html.append('''%(link)s''' % {
657 661 'link': _link_to_if(True, change['new_lineno'],
658 662 '#%s' % anchor_new)
659 663 })
660 664 _html.append('''</td>\n''')
661 665 ###########################################################
662 666 # CODE
663 667 ###########################################################
664 668 comments = '' if enable_comments else 'no-comment'
665 669 _html.append('''\t<td class="%(cc)s %(inc)s">''' % {
666 670 'cc': code_class,
667 671 'inc': comments
668 672 })
669 673 _html.append('''\n\t\t<pre>%(code)s</pre>\n''' % {
670 674 'code': change['line']
671 675 })
672 676
673 677 _html.append('''\t</td>''')
674 678 _html.append('''\n</tr>\n''')
675 679 _html.append('''</table>''')
676 680 if _html_empty:
677 681 return None
678 682 return ''.join(_html)
679 683
680 684 def stat(self):
681 685 """
682 686 Returns tuple of added, and removed lines for this instance
683 687 """
684 688 return self.adds, self.removes
General Comments 0
You need to be logged in to leave comments. Login now