##// END OF EJS Templates
diffs: add translation string
ergo -
r384:7507e2a3 default
parent child Browse files
Show More
@@ -1,872 +1,874 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 """
23 23 Set of diffing helpers, previously part of vcs
24 24 """
25 25
26 26 import collections
27 27 import re
28 28 import difflib
29 29 import logging
30 30
31 31 from itertools import tee, imap
32 32
33 33 from pylons.i18n.translation import _
34 34
35 35 from rhodecode.lib.vcs.exceptions import VCSError
36 36 from rhodecode.lib.vcs.nodes import FileNode, SubModuleNode
37 37 from rhodecode.lib.vcs.backends.base import EmptyCommit
38 38 from rhodecode.lib.helpers import escape
39 39 from rhodecode.lib.utils2 import safe_unicode
40 40
41 41 log = logging.getLogger(__name__)
42 42
43 43
44 44 class OPS(object):
45 45 ADD = 'A'
46 46 MOD = 'M'
47 47 DEL = 'D'
48 48
49 49 def wrap_to_table(str_):
50 50 return '''<table class="code-difftable">
51 51 <tr class="line no-comment">
52 <td class="add-comment-line tooltip" title="Click to comment"><span class="add-comment-content"></span></td>
52 <td class="add-comment-line tooltip" title="%s"><span class="add-comment-content"></span></td>
53 53 <td class="lineno new"></td>
54 54 <td class="code no-comment"><pre>%s</pre></td>
55 55 </tr>
56 </table>''' % str_
56 </table>''' % (_('Click to comment'), str_)
57 57
58 58
59 59 def wrapped_diff(filenode_old, filenode_new, diff_limit=None, file_limit=None,
60 60 show_full_diff=False, ignore_whitespace=True, line_context=3,
61 61 enable_comments=False):
62 62 """
63 63 returns a wrapped diff into a table, checks for cut_off_limit for file and
64 64 whole diff and presents proper message
65 65 """
66 66
67 67 if filenode_old is None:
68 68 filenode_old = FileNode(filenode_new.path, '', EmptyCommit())
69 69
70 70 if filenode_old.is_binary or filenode_new.is_binary:
71 71 diff = wrap_to_table(_('Binary file'))
72 72 stats = None
73 73 size = 0
74 74 data = None
75 75
76 76 elif diff_limit != -1 and (diff_limit is None or
77 77 (filenode_old.size < diff_limit and filenode_new.size < diff_limit)):
78 78
79 79 f_gitdiff = get_gitdiff(filenode_old, filenode_new,
80 80 ignore_whitespace=ignore_whitespace,
81 81 context=line_context)
82 82 diff_processor = DiffProcessor(f_gitdiff, format='gitdiff', diff_limit=diff_limit,
83 83 file_limit=file_limit, show_full_diff=show_full_diff)
84 84 _parsed = diff_processor.prepare()
85 85
86 86 diff = diff_processor.as_html(enable_comments=enable_comments)
87 87 stats = _parsed[0]['stats'] if _parsed else None
88 88 size = len(diff or '')
89 89 data = _parsed[0] if _parsed else None
90 90 else:
91 91 diff = wrap_to_table(_('Changeset was too big and was cut off, use '
92 92 'diff menu to display this diff'))
93 93 stats = None
94 94 size = 0
95 95 data = None
96 96 if not diff:
97 97 submodules = filter(lambda o: isinstance(o, SubModuleNode),
98 98 [filenode_new, filenode_old])
99 99 if submodules:
100 100 diff = wrap_to_table(escape('Submodule %r' % submodules[0]))
101 101 else:
102 102 diff = wrap_to_table(_('No changes detected'))
103 103
104 104 cs1 = filenode_old.commit.raw_id
105 105 cs2 = filenode_new.commit.raw_id
106 106
107 107 return size, cs1, cs2, diff, stats, data
108 108
109 109
110 110 def get_gitdiff(filenode_old, filenode_new, ignore_whitespace=True, context=3):
111 111 """
112 112 Returns git style diff between given ``filenode_old`` and ``filenode_new``.
113 113
114 114 :param ignore_whitespace: ignore whitespaces in diff
115 115 """
116 116 # make sure we pass in default context
117 117 context = context or 3
118 118 submodules = filter(lambda o: isinstance(o, SubModuleNode),
119 119 [filenode_new, filenode_old])
120 120 if submodules:
121 121 return ''
122 122
123 123 for filenode in (filenode_old, filenode_new):
124 124 if not isinstance(filenode, FileNode):
125 125 raise VCSError(
126 126 "Given object should be FileNode object, not %s"
127 127 % filenode.__class__)
128 128
129 129 repo = filenode_new.commit.repository
130 130 old_commit = filenode_old.commit or repo.EMPTY_COMMIT
131 131 new_commit = filenode_new.commit
132 132
133 133 vcs_gitdiff = repo.get_diff(
134 134 old_commit, new_commit, filenode_new.path,
135 135 ignore_whitespace, context, path1=filenode_old.path)
136 136 return vcs_gitdiff
137 137
138 138 NEW_FILENODE = 1
139 139 DEL_FILENODE = 2
140 140 MOD_FILENODE = 3
141 141 RENAMED_FILENODE = 4
142 142 COPIED_FILENODE = 5
143 143 CHMOD_FILENODE = 6
144 144 BIN_FILENODE = 7
145 145
146 146
147 147 class LimitedDiffContainer(object):
148 148
149 149 def __init__(self, diff_limit, cur_diff_size, diff):
150 150 self.diff = diff
151 151 self.diff_limit = diff_limit
152 152 self.cur_diff_size = cur_diff_size
153 153
154 154 def __getitem__(self, key):
155 155 return self.diff.__getitem__(key)
156 156
157 157 def __iter__(self):
158 158 for l in self.diff:
159 159 yield l
160 160
161 161
162 162 class Action(object):
163 163 """
164 164 Contains constants for the action value of the lines in a parsed diff.
165 165 """
166 166
167 167 ADD = 'add'
168 168 DELETE = 'del'
169 169 UNMODIFIED = 'unmod'
170 170
171 171 CONTEXT = 'context'
172 172
173 173
174 174 class DiffProcessor(object):
175 175 """
176 176 Give it a unified or git diff and it returns a list of the files that were
177 177 mentioned in the diff together with a dict of meta information that
178 178 can be used to render it in a HTML template.
179 179
180 180 .. note:: Unicode handling
181 181
182 182 The original diffs are a byte sequence and can contain filenames
183 183 in mixed encodings. This class generally returns `unicode` objects
184 184 since the result is intended for presentation to the user.
185 185
186 186 """
187 187 _chunk_re = re.compile(r'^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)')
188 188 _newline_marker = re.compile(r'^\\ No newline at end of file')
189 189
190 190 # used for inline highlighter word split
191 191 _token_re = re.compile(r'()(&gt;|&lt;|&amp;|\W+?)')
192 192
193 193 def __init__(self, diff, format='gitdiff', diff_limit=None, file_limit=None, show_full_diff=True):
194 194 """
195 195 :param diff: A `Diff` object representing a diff from a vcs backend
196 196 :param format: format of diff passed, `udiff` or `gitdiff`
197 197 :param diff_limit: define the size of diff that is considered "big"
198 198 based on that parameter cut off will be triggered, set to None
199 199 to show full diff
200 200 """
201 201 self._diff = diff
202 202 self._format = format
203 203 self.adds = 0
204 204 self.removes = 0
205 205 # calculate diff size
206 206 self.diff_limit = diff_limit
207 207 self.file_limit = file_limit
208 208 self.show_full_diff = show_full_diff
209 209 self.cur_diff_size = 0
210 210 self.parsed = False
211 211 self.parsed_diff = []
212 212
213 213 if format == 'gitdiff':
214 214 self.differ = self._highlight_line_difflib
215 215 self._parser = self._parse_gitdiff
216 216 else:
217 217 self.differ = self._highlight_line_udiff
218 218 self._parser = self._parse_udiff
219 219
220 220 def _copy_iterator(self):
221 221 """
222 222 make a fresh copy of generator, we should not iterate thru
223 223 an original as it's needed for repeating operations on
224 224 this instance of DiffProcessor
225 225 """
226 226 self.__udiff, iterator_copy = tee(self.__udiff)
227 227 return iterator_copy
228 228
229 229 def _escaper(self, string):
230 230 """
231 231 Escaper for diff escapes special chars and checks the diff limit
232 232
233 233 :param string:
234 234 """
235 235
236 236 self.cur_diff_size += len(string)
237 237
238 238 if not self.show_full_diff and (self.cur_diff_size > self.diff_limit):
239 239 raise DiffLimitExceeded('Diff Limit Exceeded')
240 240
241 241 return safe_unicode(string)\
242 242 .replace('&', '&amp;')\
243 243 .replace('<', '&lt;')\
244 244 .replace('>', '&gt;')
245 245
246 246 def _line_counter(self, l):
247 247 """
248 248 Checks each line and bumps total adds/removes for this diff
249 249
250 250 :param l:
251 251 """
252 252 if l.startswith('+') and not l.startswith('+++'):
253 253 self.adds += 1
254 254 elif l.startswith('-') and not l.startswith('---'):
255 255 self.removes += 1
256 256 return safe_unicode(l)
257 257
258 258 def _highlight_line_difflib(self, line, next_):
259 259 """
260 260 Highlight inline changes in both lines.
261 261 """
262 262
263 263 if line['action'] == Action.DELETE:
264 264 old, new = line, next_
265 265 else:
266 266 old, new = next_, line
267 267
268 268 oldwords = self._token_re.split(old['line'])
269 269 newwords = self._token_re.split(new['line'])
270 270 sequence = difflib.SequenceMatcher(None, oldwords, newwords)
271 271
272 272 oldfragments, newfragments = [], []
273 273 for tag, i1, i2, j1, j2 in sequence.get_opcodes():
274 274 oldfrag = ''.join(oldwords[i1:i2])
275 275 newfrag = ''.join(newwords[j1:j2])
276 276 if tag != 'equal':
277 277 if oldfrag:
278 278 oldfrag = '<del>%s</del>' % oldfrag
279 279 if newfrag:
280 280 newfrag = '<ins>%s</ins>' % newfrag
281 281 oldfragments.append(oldfrag)
282 282 newfragments.append(newfrag)
283 283
284 284 old['line'] = "".join(oldfragments)
285 285 new['line'] = "".join(newfragments)
286 286
287 287 def _highlight_line_udiff(self, line, next_):
288 288 """
289 289 Highlight inline changes in both lines.
290 290 """
291 291 start = 0
292 292 limit = min(len(line['line']), len(next_['line']))
293 293 while start < limit and line['line'][start] == next_['line'][start]:
294 294 start += 1
295 295 end = -1
296 296 limit -= start
297 297 while -end <= limit and line['line'][end] == next_['line'][end]:
298 298 end -= 1
299 299 end += 1
300 300 if start or end:
301 301 def do(l):
302 302 last = end + len(l['line'])
303 303 if l['action'] == Action.ADD:
304 304 tag = 'ins'
305 305 else:
306 306 tag = 'del'
307 307 l['line'] = '%s<%s>%s</%s>%s' % (
308 308 l['line'][:start],
309 309 tag,
310 310 l['line'][start:last],
311 311 tag,
312 312 l['line'][last:]
313 313 )
314 314 do(line)
315 315 do(next_)
316 316
317 317 def _clean_line(self, line, command):
318 318 if command in ['+', '-', ' ']:
319 319 # only modify the line if it's actually a diff thing
320 320 line = line[1:]
321 321 return line
322 322
323 323 def _parse_gitdiff(self, inline_diff=True):
324 324 _files = []
325 325 diff_container = lambda arg: arg
326 326
327 327 for chunk in self._diff.chunks():
328 328 head = chunk.header
329 329
330 330 diff = imap(self._escaper, chunk.diff.splitlines(1))
331 331 raw_diff = chunk.raw
332 332 limited_diff = False
333 333 exceeds_limit = False
334 334
335 335 op = None
336 336 stats = {
337 337 'added': 0,
338 338 'deleted': 0,
339 339 'binary': False,
340 340 'ops': {},
341 341 }
342 342
343 343 if head['deleted_file_mode']:
344 344 op = OPS.DEL
345 345 stats['binary'] = True
346 346 stats['ops'][DEL_FILENODE] = 'deleted file'
347 347
348 348 elif head['new_file_mode']:
349 349 op = OPS.ADD
350 350 stats['binary'] = True
351 351 stats['ops'][NEW_FILENODE] = 'new file %s' % head['new_file_mode']
352 352 else: # modify operation, can be copy, rename or chmod
353 353
354 354 # CHMOD
355 355 if head['new_mode'] and head['old_mode']:
356 356 op = OPS.MOD
357 357 stats['binary'] = True
358 358 stats['ops'][CHMOD_FILENODE] = (
359 359 'modified file chmod %s => %s' % (
360 360 head['old_mode'], head['new_mode']))
361 361 # RENAME
362 362 if head['rename_from'] != head['rename_to']:
363 363 op = OPS.MOD
364 364 stats['binary'] = True
365 365 stats['ops'][RENAMED_FILENODE] = (
366 366 'file renamed from %s to %s' % (
367 367 head['rename_from'], head['rename_to']))
368 368 # COPY
369 369 if head.get('copy_from') and head.get('copy_to'):
370 370 op = OPS.MOD
371 371 stats['binary'] = True
372 372 stats['ops'][COPIED_FILENODE] = (
373 373 'file copied from %s to %s' % (
374 374 head['copy_from'], head['copy_to']))
375 375
376 376 # If our new parsed headers didn't match anything fallback to
377 377 # old style detection
378 378 if op is None:
379 379 if not head['a_file'] and head['b_file']:
380 380 op = OPS.ADD
381 381 stats['binary'] = True
382 382 stats['ops'][NEW_FILENODE] = 'new file'
383 383
384 384 elif head['a_file'] and not head['b_file']:
385 385 op = OPS.DEL
386 386 stats['binary'] = True
387 387 stats['ops'][DEL_FILENODE] = 'deleted file'
388 388
389 389 # it's not ADD not DELETE
390 390 if op is None:
391 391 op = OPS.MOD
392 392 stats['binary'] = True
393 393 stats['ops'][MOD_FILENODE] = 'modified file'
394 394
395 395 # a real non-binary diff
396 396 if head['a_file'] or head['b_file']:
397 397 try:
398 398 raw_diff, chunks, _stats = self._parse_lines(diff)
399 399 stats['binary'] = False
400 400 stats['added'] = _stats[0]
401 401 stats['deleted'] = _stats[1]
402 402 # explicit mark that it's a modified file
403 403 if op == OPS.MOD:
404 404 stats['ops'][MOD_FILENODE] = 'modified file'
405 405 exceeds_limit = len(raw_diff) > self.file_limit
406 406
407 407 # changed from _escaper function so we validate size of
408 408 # each file instead of the whole diff
409 409 # diff will hide big files but still show small ones
410 410 # from my tests, big files are fairly safe to be parsed
411 411 # but the browser is the bottleneck
412 412 if not self.show_full_diff and exceeds_limit:
413 413 raise DiffLimitExceeded('File Limit Exceeded')
414 414
415 415 except DiffLimitExceeded:
416 416 diff_container = lambda _diff: \
417 417 LimitedDiffContainer(
418 418 self.diff_limit, self.cur_diff_size, _diff)
419 419
420 420 exceeds_limit = len(raw_diff) > self.file_limit
421 421 limited_diff = True
422 422 chunks = []
423 423
424 424 else: # GIT format binary patch, or possibly empty diff
425 425 if head['bin_patch']:
426 426 # we have operation already extracted, but we mark simply
427 427 # it's a diff we wont show for binary files
428 428 stats['ops'][BIN_FILENODE] = 'binary diff hidden'
429 429 chunks = []
430 430
431 431 if chunks and not self.show_full_diff and op == OPS.DEL:
432 432 # if not full diff mode show deleted file contents
433 433 # TODO: anderson: if the view is not too big, there is no way
434 434 # to see the content of the file
435 435 chunks = []
436 436
437 437 chunks.insert(0, [{
438 438 'old_lineno': '',
439 439 'new_lineno': '',
440 440 'action': Action.CONTEXT,
441 441 'line': msg,
442 442 } for _op, msg in stats['ops'].iteritems()
443 443 if _op not in [MOD_FILENODE]])
444 444
445 445 _files.append({
446 446 'filename': safe_unicode(head['b_path']),
447 447 'old_revision': head['a_blob_id'],
448 448 'new_revision': head['b_blob_id'],
449 449 'chunks': chunks,
450 450 'raw_diff': safe_unicode(raw_diff),
451 451 'operation': op,
452 452 'stats': stats,
453 453 'exceeds_limit': exceeds_limit,
454 454 'is_limited_diff': limited_diff,
455 455 })
456 456
457 457 sorter = lambda info: {OPS.ADD: 0, OPS.MOD: 1,
458 458 OPS.DEL: 2}.get(info['operation'])
459 459
460 460 if not inline_diff:
461 461 return diff_container(sorted(_files, key=sorter))
462 462
463 463 # highlight inline changes
464 464 for diff_data in _files:
465 465 for chunk in diff_data['chunks']:
466 466 lineiter = iter(chunk)
467 467 try:
468 468 while 1:
469 469 line = lineiter.next()
470 470 if line['action'] not in (
471 471 Action.UNMODIFIED, Action.CONTEXT):
472 472 nextline = lineiter.next()
473 473 if nextline['action'] in ['unmod', 'context'] or \
474 474 nextline['action'] == line['action']:
475 475 continue
476 476 self.differ(line, nextline)
477 477 except StopIteration:
478 478 pass
479 479
480 480 return diff_container(sorted(_files, key=sorter))
481 481
482 482 def _parse_udiff(self, inline_diff=True):
483 483 raise NotImplementedError()
484 484
485 485 def _parse_lines(self, diff):
486 486 """
487 487 Parse the diff an return data for the template.
488 488 """
489 489
490 490 lineiter = iter(diff)
491 491 stats = [0, 0]
492 492 chunks = []
493 493 raw_diff = []
494 494
495 495 try:
496 496 line = lineiter.next()
497 497
498 498 while line:
499 499 raw_diff.append(line)
500 500 lines = []
501 501 chunks.append(lines)
502 502
503 503 match = self._chunk_re.match(line)
504 504
505 505 if not match:
506 506 break
507 507
508 508 gr = match.groups()
509 509 (old_line, old_end,
510 510 new_line, new_end) = [int(x or 1) for x in gr[:-1]]
511 511 old_line -= 1
512 512 new_line -= 1
513 513
514 514 context = len(gr) == 5
515 515 old_end += old_line
516 516 new_end += new_line
517 517
518 518 if context:
519 519 # skip context only if it's first line
520 520 if int(gr[0]) > 1:
521 521 lines.append({
522 522 'old_lineno': '...',
523 523 'new_lineno': '...',
524 524 'action': Action.CONTEXT,
525 525 'line': line,
526 526 })
527 527
528 528 line = lineiter.next()
529 529
530 530 while old_line < old_end or new_line < new_end:
531 531 command = ' '
532 532 if line:
533 533 command = line[0]
534 534
535 535 affects_old = affects_new = False
536 536
537 537 # ignore those if we don't expect them
538 538 if command in '#@':
539 539 continue
540 540 elif command == '+':
541 541 affects_new = True
542 542 action = Action.ADD
543 543 stats[0] += 1
544 544 elif command == '-':
545 545 affects_old = True
546 546 action = Action.DELETE
547 547 stats[1] += 1
548 548 else:
549 549 affects_old = affects_new = True
550 550 action = Action.UNMODIFIED
551 551
552 552 if not self._newline_marker.match(line):
553 553 old_line += affects_old
554 554 new_line += affects_new
555 555 lines.append({
556 556 'old_lineno': affects_old and old_line or '',
557 557 'new_lineno': affects_new and new_line or '',
558 558 'action': action,
559 559 'line': self._clean_line(line, command)
560 560 })
561 561 raw_diff.append(line)
562 562
563 563 line = lineiter.next()
564 564
565 565 if self._newline_marker.match(line):
566 566 # we need to append to lines, since this is not
567 567 # counted in the line specs of diff
568 568 lines.append({
569 569 'old_lineno': '...',
570 570 'new_lineno': '...',
571 571 'action': Action.CONTEXT,
572 572 'line': self._clean_line(line, command)
573 573 })
574 574
575 575 except StopIteration:
576 576 pass
577 577 return ''.join(raw_diff), chunks, stats
578 578
579 579 def _safe_id(self, idstring):
580 580 """Make a string safe for including in an id attribute.
581 581
582 582 The HTML spec says that id attributes 'must begin with
583 583 a letter ([A-Za-z]) and may be followed by any number
584 584 of letters, digits ([0-9]), hyphens ("-"), underscores
585 585 ("_"), colons (":"), and periods (".")'. These regexps
586 586 are slightly over-zealous, in that they remove colons
587 587 and periods unnecessarily.
588 588
589 589 Whitespace is transformed into underscores, and then
590 590 anything which is not a hyphen or a character that
591 591 matches \w (alphanumerics and underscore) is removed.
592 592
593 593 """
594 594 # Transform all whitespace to underscore
595 595 idstring = re.sub(r'\s', "_", '%s' % idstring)
596 596 # Remove everything that is not a hyphen or a member of \w
597 597 idstring = re.sub(r'(?!-)\W', "", idstring).lower()
598 598 return idstring
599 599
600 600 def prepare(self, inline_diff=True):
601 601 """
602 602 Prepare the passed udiff for HTML rendering.
603 603
604 604 :return: A list of dicts with diff information.
605 605 """
606 606 parsed = self._parser(inline_diff=inline_diff)
607 607 self.parsed = True
608 608 self.parsed_diff = parsed
609 609 return parsed
610 610
611 611 def as_raw(self, diff_lines=None):
612 612 """
613 613 Returns raw diff as a byte string
614 614 """
615 615 return self._diff.raw
616 616
617 617 def as_html(self, table_class='code-difftable', line_class='line',
618 618 old_lineno_class='lineno old', new_lineno_class='lineno new',
619 619 code_class='code', enable_comments=False, parsed_lines=None):
620 620 """
621 621 Return given diff as html table with customized css classes
622 622 """
623 623 def _link_to_if(condition, label, url):
624 624 """
625 625 Generates a link if condition is meet or just the label if not.
626 626 """
627 627
628 628 if condition:
629 return '''<a href="%(url)s" class="tooltip" title="Click to select line">%(label)s</a>''' % {
629 return '''<a href="%(url)s" class="tooltip"
630 title="%(title)s">%(label)s</a>''' % {
631 'title': _('Click to select line'),
630 632 'url': url,
631 633 'label': label
632 634 }
633 635 else:
634 636 return label
635 637 if not self.parsed:
636 638 self.prepare()
637 639
638 640 diff_lines = self.parsed_diff
639 641 if parsed_lines:
640 642 diff_lines = parsed_lines
641 643
642 644 _html_empty = True
643 645 _html = []
644 646 _html.append('''<table class="%(table_class)s">\n''' % {
645 647 'table_class': table_class
646 648 })
647 649
648 650 for diff in diff_lines:
649 651 for line in diff['chunks']:
650 652 _html_empty = False
651 653 for change in line:
652 654 _html.append('''<tr class="%(lc)s %(action)s">\n''' % {
653 655 'lc': line_class,
654 656 'action': change['action']
655 657 })
656 658 anchor_old_id = ''
657 659 anchor_new_id = ''
658 660 anchor_old = "%(filename)s_o%(oldline_no)s" % {
659 661 'filename': self._safe_id(diff['filename']),
660 662 'oldline_no': change['old_lineno']
661 663 }
662 664 anchor_new = "%(filename)s_n%(oldline_no)s" % {
663 665 'filename': self._safe_id(diff['filename']),
664 666 'oldline_no': change['new_lineno']
665 667 }
666 668 cond_old = (change['old_lineno'] != '...' and
667 669 change['old_lineno'])
668 670 cond_new = (change['new_lineno'] != '...' and
669 671 change['new_lineno'])
670 672 if cond_old:
671 673 anchor_old_id = 'id="%s"' % anchor_old
672 674 if cond_new:
673 675 anchor_new_id = 'id="%s"' % anchor_new
674 676
675 677 if change['action'] != Action.CONTEXT:
676 678 anchor_link = True
677 679 else:
678 680 anchor_link = False
679 681
680 682 ###########################################################
681 683 # COMMENT ICON
682 684 ###########################################################
683 685 _html.append('''\t<td class="add-comment-line"><span class="add-comment-content">''')
684 686
685 687 if enable_comments and change['action'] != Action.CONTEXT:
686 688 _html.append('''<a href="#"><span class="icon-comment-add"></span></a>''')
687 689
688 690 _html.append('''</span></td>\n''')
689 691
690 692 ###########################################################
691 693 # OLD LINE NUMBER
692 694 ###########################################################
693 695 _html.append('''\t<td %(a_id)s class="%(olc)s">''' % {
694 696 'a_id': anchor_old_id,
695 697 'olc': old_lineno_class
696 698 })
697 699
698 700 _html.append('''%(link)s''' % {
699 701 'link': _link_to_if(anchor_link, change['old_lineno'],
700 702 '#%s' % anchor_old)
701 703 })
702 704 _html.append('''</td>\n''')
703 705 ###########################################################
704 706 # NEW LINE NUMBER
705 707 ###########################################################
706 708
707 709 _html.append('''\t<td %(a_id)s class="%(nlc)s">''' % {
708 710 'a_id': anchor_new_id,
709 711 'nlc': new_lineno_class
710 712 })
711 713
712 714 _html.append('''%(link)s''' % {
713 715 'link': _link_to_if(anchor_link, change['new_lineno'],
714 716 '#%s' % anchor_new)
715 717 })
716 718 _html.append('''</td>\n''')
717 719 ###########################################################
718 720 # CODE
719 721 ###########################################################
720 722 code_classes = [code_class]
721 723 if (not enable_comments or
722 724 change['action'] == Action.CONTEXT):
723 725 code_classes.append('no-comment')
724 726 _html.append('\t<td class="%s">' % ' '.join(code_classes))
725 727 _html.append('''\n\t\t<pre>%(code)s</pre>\n''' % {
726 728 'code': change['line']
727 729 })
728 730
729 731 _html.append('''\t</td>''')
730 732 _html.append('''\n</tr>\n''')
731 733 _html.append('''</table>''')
732 734 if _html_empty:
733 735 return None
734 736 return ''.join(_html)
735 737
736 738 def stat(self):
737 739 """
738 740 Returns tuple of added, and removed lines for this instance
739 741 """
740 742 return self.adds, self.removes
741 743
742 744 def get_context_of_line(
743 745 self, path, diff_line=None, context_before=3, context_after=3):
744 746 """
745 747 Returns the context lines for the specified diff line.
746 748
747 749 :type diff_line: :class:`DiffLineNumber`
748 750 """
749 751 assert self.parsed, "DiffProcessor is not initialized."
750 752
751 753 if None not in diff_line:
752 754 raise ValueError(
753 755 "Cannot specify both line numbers: {}".format(diff_line))
754 756
755 757 file_diff = self._get_file_diff(path)
756 758 chunk, idx = self._find_chunk_line_index(file_diff, diff_line)
757 759
758 760 first_line_to_include = max(idx - context_before, 0)
759 761 first_line_after_context = idx + context_after + 1
760 762 context_lines = chunk[first_line_to_include:first_line_after_context]
761 763
762 764 line_contents = [
763 765 _context_line(line) for line in context_lines
764 766 if _is_diff_content(line)]
765 767 # TODO: johbo: Interim fixup, the diff chunks drop the final newline.
766 768 # Once they are fixed, we can drop this line here.
767 769 if line_contents:
768 770 line_contents[-1] = (
769 771 line_contents[-1][0], line_contents[-1][1].rstrip('\n') + '\n')
770 772 return line_contents
771 773
772 774 def find_context(self, path, context, offset=0):
773 775 """
774 776 Finds the given `context` inside of the diff.
775 777
776 778 Use the parameter `offset` to specify which offset the target line has
777 779 inside of the given `context`. This way the correct diff line will be
778 780 returned.
779 781
780 782 :param offset: Shall be used to specify the offset of the main line
781 783 within the given `context`.
782 784 """
783 785 if offset < 0 or offset >= len(context):
784 786 raise ValueError(
785 787 "Only positive values up to the length of the context "
786 788 "minus one are allowed.")
787 789
788 790 matches = []
789 791 file_diff = self._get_file_diff(path)
790 792
791 793 for chunk in file_diff['chunks']:
792 794 context_iter = iter(context)
793 795 for line_idx, line in enumerate(chunk):
794 796 try:
795 797 if _context_line(line) == context_iter.next():
796 798 continue
797 799 except StopIteration:
798 800 matches.append((line_idx, chunk))
799 801 context_iter = iter(context)
800 802
801 803 # Increment position and triger StopIteration
802 804 # if we had a match at the end
803 805 line_idx += 1
804 806 try:
805 807 context_iter.next()
806 808 except StopIteration:
807 809 matches.append((line_idx, chunk))
808 810
809 811 effective_offset = len(context) - offset
810 812 found_at_diff_lines = [
811 813 _line_to_diff_line_number(chunk[idx - effective_offset])
812 814 for idx, chunk in matches]
813 815
814 816 return found_at_diff_lines
815 817
816 818 def _get_file_diff(self, path):
817 819 for file_diff in self.parsed_diff:
818 820 if file_diff['filename'] == path:
819 821 break
820 822 else:
821 823 raise FileNotInDiffException("File {} not in diff".format(path))
822 824 return file_diff
823 825
824 826 def _find_chunk_line_index(self, file_diff, diff_line):
825 827 for chunk in file_diff['chunks']:
826 828 for idx, line in enumerate(chunk):
827 829 if line['old_lineno'] == diff_line.old:
828 830 return chunk, idx
829 831 if line['new_lineno'] == diff_line.new:
830 832 return chunk, idx
831 833 raise LineNotInDiffException(
832 834 "The line {} is not part of the diff.".format(diff_line))
833 835
834 836
835 837 def _is_diff_content(line):
836 838 return line['action'] in (
837 839 Action.UNMODIFIED, Action.ADD, Action.DELETE)
838 840
839 841
840 842 def _context_line(line):
841 843 return (line['action'], line['line'])
842 844
843 845
844 846 DiffLineNumber = collections.namedtuple('DiffLineNumber', ['old', 'new'])
845 847
846 848
847 849 def _line_to_diff_line_number(line):
848 850 new_line_no = line['new_lineno'] or None
849 851 old_line_no = line['old_lineno'] or None
850 852 return DiffLineNumber(old=old_line_no, new=new_line_no)
851 853
852 854
853 855 class FileNotInDiffException(Exception):
854 856 """
855 857 Raised when the context for a missing file is requested.
856 858
857 859 If you request the context for a line in a file which is not part of the
858 860 given diff, then this exception is raised.
859 861 """
860 862
861 863
862 864 class LineNotInDiffException(Exception):
863 865 """
864 866 Raised when the context for a missing line is requested.
865 867
866 868 If you request the context for a line in a file and this line is not
867 869 part of the given diff, then this exception is raised.
868 870 """
869 871
870 872
871 873 class DiffLimitExceeded(Exception):
872 874 pass
General Comments 0
You need to be logged in to leave comments. Login now