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