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