##// END OF EJS Templates
diffs: fixed problem with rendering no newline at the end of file markers....
marcink -
r2252:dc922ddb stable
parent child Browse files
Show More
@@ -0,0 +1,10 b''
1 diff --git a/server.properties b/server.properties
2 --- a/server.properties
3 +++ b/server.properties
4 @@ -1,2 +1,3 @@
5 property=value
6 -anotherProperty=value
7 \ No newline at end of file
8 +anotherProperty=value
9 +newProperty=super_important_value
10 \ No newline at end of file No newline at end of file
@@ -1,711 +1,735 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2017 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 logging
22 22 import difflib
23 23 from itertools import groupby
24 24
25 25 from pygments import lex
26 26 from pygments.formatters.html import _get_ttype_class as pygment_token_class
27 27 from rhodecode.lib.helpers import (
28 28 get_lexer_for_filenode, html_escape, get_custom_lexer)
29 29 from rhodecode.lib.utils2 import AttributeDict
30 30 from rhodecode.lib.vcs.nodes import FileNode
31 31 from rhodecode.lib.diff_match_patch import diff_match_patch
32 32 from rhodecode.lib.diffs import LimitedDiffContainer
33 33 from pygments.lexers import get_lexer_by_name
34 34
35 35 plain_text_lexer = get_lexer_by_name(
36 36 'text', stripall=False, stripnl=False, ensurenl=False)
37 37
38 38
39 39 log = logging.getLogger()
40 40
41 41
42 42 def filenode_as_lines_tokens(filenode, lexer=None):
43 43 org_lexer = lexer
44 44 lexer = lexer or get_lexer_for_filenode(filenode)
45 45 log.debug('Generating file node pygment tokens for %s, %s, org_lexer:%s',
46 46 lexer, filenode, org_lexer)
47 47 tokens = tokenize_string(filenode.content, lexer)
48 48 lines = split_token_stream(tokens, split_string='\n')
49 49 rv = list(lines)
50 50 return rv
51 51
52 52
53 53 def tokenize_string(content, lexer):
54 54 """
55 55 Use pygments to tokenize some content based on a lexer
56 56 ensuring all original new lines and whitespace is preserved
57 57 """
58 58
59 59 lexer.stripall = False
60 60 lexer.stripnl = False
61 61 lexer.ensurenl = False
62 62 for token_type, token_text in lex(content, lexer):
63 63 yield pygment_token_class(token_type), token_text
64 64
65 65
66 66 def split_token_stream(tokens, split_string=u'\n'):
67 67 """
68 68 Take a list of (TokenType, text) tuples and split them by a string
69 69
70 70 >>> split_token_stream([(TEXT, 'some\ntext'), (TEXT, 'more\n')])
71 71 [(TEXT, 'some'), (TEXT, 'text'),
72 72 (TEXT, 'more'), (TEXT, 'text')]
73 73 """
74 74
75 75 buffer = []
76 76 for token_class, token_text in tokens:
77 77 parts = token_text.split(split_string)
78 78 for part in parts[:-1]:
79 79 buffer.append((token_class, part))
80 80 yield buffer
81 81 buffer = []
82 82
83 83 buffer.append((token_class, parts[-1]))
84 84
85 85 if buffer:
86 86 yield buffer
87 87
88 88
89 89 def filenode_as_annotated_lines_tokens(filenode):
90 90 """
91 91 Take a file node and return a list of annotations => lines, if no annotation
92 92 is found, it will be None.
93 93
94 94 eg:
95 95
96 96 [
97 97 (annotation1, [
98 98 (1, line1_tokens_list),
99 99 (2, line2_tokens_list),
100 100 ]),
101 101 (annotation2, [
102 102 (3, line1_tokens_list),
103 103 ]),
104 104 (None, [
105 105 (4, line1_tokens_list),
106 106 ]),
107 107 (annotation1, [
108 108 (5, line1_tokens_list),
109 109 (6, line2_tokens_list),
110 110 ])
111 111 ]
112 112 """
113 113
114 114 commit_cache = {} # cache commit_getter lookups
115 115
116 116 def _get_annotation(commit_id, commit_getter):
117 117 if commit_id not in commit_cache:
118 118 commit_cache[commit_id] = commit_getter()
119 119 return commit_cache[commit_id]
120 120
121 121 annotation_lookup = {
122 122 line_no: _get_annotation(commit_id, commit_getter)
123 123 for line_no, commit_id, commit_getter, line_content
124 124 in filenode.annotate
125 125 }
126 126
127 127 annotations_lines = ((annotation_lookup.get(line_no), line_no, tokens)
128 128 for line_no, tokens
129 129 in enumerate(filenode_as_lines_tokens(filenode), 1))
130 130
131 131 grouped_annotations_lines = groupby(annotations_lines, lambda x: x[0])
132 132
133 133 for annotation, group in grouped_annotations_lines:
134 134 yield (
135 135 annotation, [(line_no, tokens)
136 136 for (_, line_no, tokens) in group]
137 137 )
138 138
139 139
140 140 def render_tokenstream(tokenstream):
141 141 result = []
142 142 for token_class, token_ops_texts in rollup_tokenstream(tokenstream):
143 143
144 144 if token_class:
145 145 result.append(u'<span class="%s">' % token_class)
146 146 else:
147 147 result.append(u'<span>')
148 148
149 149 for op_tag, token_text in token_ops_texts:
150 150
151 151 if op_tag:
152 152 result.append(u'<%s>' % op_tag)
153 153
154 154 escaped_text = html_escape(token_text)
155 155
156 156 # TODO: dan: investigate showing hidden characters like space/nl/tab
157 157 # escaped_text = escaped_text.replace(' ', '<sp> </sp>')
158 158 # escaped_text = escaped_text.replace('\n', '<nl>\n</nl>')
159 159 # escaped_text = escaped_text.replace('\t', '<tab>\t</tab>')
160 160
161 161 result.append(escaped_text)
162 162
163 163 if op_tag:
164 164 result.append(u'</%s>' % op_tag)
165 165
166 166 result.append(u'</span>')
167 167
168 168 html = ''.join(result)
169 169 return html
170 170
171 171
172 172 def rollup_tokenstream(tokenstream):
173 173 """
174 174 Group a token stream of the format:
175 175
176 176 ('class', 'op', 'text')
177 177 or
178 178 ('class', 'text')
179 179
180 180 into
181 181
182 182 [('class1',
183 183 [('op1', 'text'),
184 184 ('op2', 'text')]),
185 185 ('class2',
186 186 [('op3', 'text')])]
187 187
188 188 This is used to get the minimal tags necessary when
189 189 rendering to html eg for a token stream ie.
190 190
191 191 <span class="A"><ins>he</ins>llo</span>
192 192 vs
193 193 <span class="A"><ins>he</ins></span><span class="A">llo</span>
194 194
195 195 If a 2 tuple is passed in, the output op will be an empty string.
196 196
197 197 eg:
198 198
199 199 >>> rollup_tokenstream([('classA', '', 'h'),
200 200 ('classA', 'del', 'ell'),
201 201 ('classA', '', 'o'),
202 202 ('classB', '', ' '),
203 203 ('classA', '', 'the'),
204 204 ('classA', '', 're'),
205 205 ])
206 206
207 207 [('classA', [('', 'h'), ('del', 'ell'), ('', 'o')],
208 208 ('classB', [('', ' ')],
209 209 ('classA', [('', 'there')]]
210 210
211 211 """
212 212 if tokenstream and len(tokenstream[0]) == 2:
213 213 tokenstream = ((t[0], '', t[1]) for t in tokenstream)
214 214
215 215 result = []
216 216 for token_class, op_list in groupby(tokenstream, lambda t: t[0]):
217 217 ops = []
218 218 for token_op, token_text_list in groupby(op_list, lambda o: o[1]):
219 219 text_buffer = []
220 220 for t_class, t_op, t_text in token_text_list:
221 221 text_buffer.append(t_text)
222 222 ops.append((token_op, ''.join(text_buffer)))
223 223 result.append((token_class, ops))
224 224 return result
225 225
226 226
227 227 def tokens_diff(old_tokens, new_tokens, use_diff_match_patch=True):
228 228 """
229 229 Converts a list of (token_class, token_text) tuples to a list of
230 230 (token_class, token_op, token_text) tuples where token_op is one of
231 231 ('ins', 'del', '')
232 232
233 233 :param old_tokens: list of (token_class, token_text) tuples of old line
234 234 :param new_tokens: list of (token_class, token_text) tuples of new line
235 235 :param use_diff_match_patch: boolean, will use google's diff match patch
236 236 library which has options to 'smooth' out the character by character
237 237 differences making nicer ins/del blocks
238 238 """
239 239
240 240 old_tokens_result = []
241 241 new_tokens_result = []
242 242
243 243 similarity = difflib.SequenceMatcher(None,
244 244 ''.join(token_text for token_class, token_text in old_tokens),
245 245 ''.join(token_text for token_class, token_text in new_tokens)
246 246 ).ratio()
247 247
248 248 if similarity < 0.6: # return, the blocks are too different
249 249 for token_class, token_text in old_tokens:
250 250 old_tokens_result.append((token_class, '', token_text))
251 251 for token_class, token_text in new_tokens:
252 252 new_tokens_result.append((token_class, '', token_text))
253 253 return old_tokens_result, new_tokens_result, similarity
254 254
255 255 token_sequence_matcher = difflib.SequenceMatcher(None,
256 256 [x[1] for x in old_tokens],
257 257 [x[1] for x in new_tokens])
258 258
259 259 for tag, o1, o2, n1, n2 in token_sequence_matcher.get_opcodes():
260 260 # check the differences by token block types first to give a more
261 261 # nicer "block" level replacement vs character diffs
262 262
263 263 if tag == 'equal':
264 264 for token_class, token_text in old_tokens[o1:o2]:
265 265 old_tokens_result.append((token_class, '', token_text))
266 266 for token_class, token_text in new_tokens[n1:n2]:
267 267 new_tokens_result.append((token_class, '', token_text))
268 268 elif tag == 'delete':
269 269 for token_class, token_text in old_tokens[o1:o2]:
270 270 old_tokens_result.append((token_class, 'del', token_text))
271 271 elif tag == 'insert':
272 272 for token_class, token_text in new_tokens[n1:n2]:
273 273 new_tokens_result.append((token_class, 'ins', token_text))
274 274 elif tag == 'replace':
275 275 # if same type token blocks must be replaced, do a diff on the
276 276 # characters in the token blocks to show individual changes
277 277
278 278 old_char_tokens = []
279 279 new_char_tokens = []
280 280 for token_class, token_text in old_tokens[o1:o2]:
281 281 for char in token_text:
282 282 old_char_tokens.append((token_class, char))
283 283
284 284 for token_class, token_text in new_tokens[n1:n2]:
285 285 for char in token_text:
286 286 new_char_tokens.append((token_class, char))
287 287
288 288 old_string = ''.join([token_text for
289 289 token_class, token_text in old_char_tokens])
290 290 new_string = ''.join([token_text for
291 291 token_class, token_text in new_char_tokens])
292 292
293 293 char_sequence = difflib.SequenceMatcher(
294 294 None, old_string, new_string)
295 295 copcodes = char_sequence.get_opcodes()
296 296 obuffer, nbuffer = [], []
297 297
298 298 if use_diff_match_patch:
299 299 dmp = diff_match_patch()
300 300 dmp.Diff_EditCost = 11 # TODO: dan: extract this to a setting
301 301 reps = dmp.diff_main(old_string, new_string)
302 302 dmp.diff_cleanupEfficiency(reps)
303 303
304 304 a, b = 0, 0
305 305 for op, rep in reps:
306 306 l = len(rep)
307 307 if op == 0:
308 308 for i, c in enumerate(rep):
309 309 obuffer.append((old_char_tokens[a+i][0], '', c))
310 310 nbuffer.append((new_char_tokens[b+i][0], '', c))
311 311 a += l
312 312 b += l
313 313 elif op == -1:
314 314 for i, c in enumerate(rep):
315 315 obuffer.append((old_char_tokens[a+i][0], 'del', c))
316 316 a += l
317 317 elif op == 1:
318 318 for i, c in enumerate(rep):
319 319 nbuffer.append((new_char_tokens[b+i][0], 'ins', c))
320 320 b += l
321 321 else:
322 322 for ctag, co1, co2, cn1, cn2 in copcodes:
323 323 if ctag == 'equal':
324 324 for token_class, token_text in old_char_tokens[co1:co2]:
325 325 obuffer.append((token_class, '', token_text))
326 326 for token_class, token_text in new_char_tokens[cn1:cn2]:
327 327 nbuffer.append((token_class, '', token_text))
328 328 elif ctag == 'delete':
329 329 for token_class, token_text in old_char_tokens[co1:co2]:
330 330 obuffer.append((token_class, 'del', token_text))
331 331 elif ctag == 'insert':
332 332 for token_class, token_text in new_char_tokens[cn1:cn2]:
333 333 nbuffer.append((token_class, 'ins', token_text))
334 334 elif ctag == 'replace':
335 335 for token_class, token_text in old_char_tokens[co1:co2]:
336 336 obuffer.append((token_class, 'del', token_text))
337 337 for token_class, token_text in new_char_tokens[cn1:cn2]:
338 338 nbuffer.append((token_class, 'ins', token_text))
339 339
340 340 old_tokens_result.extend(obuffer)
341 341 new_tokens_result.extend(nbuffer)
342 342
343 343 return old_tokens_result, new_tokens_result, similarity
344 344
345 345
346 346 class DiffSet(object):
347 347 """
348 348 An object for parsing the diff result from diffs.DiffProcessor and
349 349 adding highlighting, side by side/unified renderings and line diffs
350 350 """
351 351
352 352 HL_REAL = 'REAL' # highlights using original file, slow
353 353 HL_FAST = 'FAST' # highlights using just the line, fast but not correct
354 354 # in the case of multiline code
355 355 HL_NONE = 'NONE' # no highlighting, fastest
356 356
357 357 def __init__(self, highlight_mode=HL_REAL, repo_name=None,
358 358 source_repo_name=None,
359 359 source_node_getter=lambda filename: None,
360 360 target_node_getter=lambda filename: None,
361 361 source_nodes=None, target_nodes=None,
362 362 max_file_size_limit=150 * 1024, # files over this size will
363 363 # use fast highlighting
364 364 comments=None,
365 365 ):
366 366
367 367 self.highlight_mode = highlight_mode
368 368 self.highlighted_filenodes = {}
369 369 self.source_node_getter = source_node_getter
370 370 self.target_node_getter = target_node_getter
371 371 self.source_nodes = source_nodes or {}
372 372 self.target_nodes = target_nodes or {}
373 373 self.repo_name = repo_name
374 374 self.source_repo_name = source_repo_name or repo_name
375 375 self.comments = comments or {}
376 376 self.comments_store = self.comments.copy()
377 377 self.max_file_size_limit = max_file_size_limit
378 378
379 379 def render_patchset(self, patchset, source_ref=None, target_ref=None):
380 380 diffset = AttributeDict(dict(
381 381 lines_added=0,
382 382 lines_deleted=0,
383 383 changed_files=0,
384 384 files=[],
385 385 file_stats={},
386 386 limited_diff=isinstance(patchset, LimitedDiffContainer),
387 387 repo_name=self.repo_name,
388 388 source_repo_name=self.source_repo_name,
389 389 source_ref=source_ref,
390 390 target_ref=target_ref,
391 391 ))
392 392 for patch in patchset:
393 393 diffset.file_stats[patch['filename']] = patch['stats']
394 394 filediff = self.render_patch(patch)
395 395 filediff.diffset = diffset
396 396 diffset.files.append(filediff)
397 397 diffset.changed_files += 1
398 398 if not patch['stats']['binary']:
399 399 diffset.lines_added += patch['stats']['added']
400 400 diffset.lines_deleted += patch['stats']['deleted']
401 401
402 402 return diffset
403 403
404 404 _lexer_cache = {}
405 405
406 406 def _get_lexer_for_filename(self, filename, filenode=None):
407 407 # cached because we might need to call it twice for source/target
408 408 if filename not in self._lexer_cache:
409 409 if filenode:
410 410 lexer = filenode.lexer
411 411 extension = filenode.extension
412 412 else:
413 413 lexer = FileNode.get_lexer(filename=filename)
414 414 extension = filename.split('.')[-1]
415 415
416 416 lexer = get_custom_lexer(extension) or lexer
417 417 self._lexer_cache[filename] = lexer
418 418 return self._lexer_cache[filename]
419 419
420 420 def render_patch(self, patch):
421 421 log.debug('rendering diff for %r' % patch['filename'])
422 422
423 423 source_filename = patch['original_filename']
424 424 target_filename = patch['filename']
425 425
426 426 source_lexer = plain_text_lexer
427 427 target_lexer = plain_text_lexer
428 428
429 429 if not patch['stats']['binary']:
430 430 if self.highlight_mode == self.HL_REAL:
431 431 if (source_filename and patch['operation'] in ('D', 'M')
432 432 and source_filename not in self.source_nodes):
433 433 self.source_nodes[source_filename] = (
434 434 self.source_node_getter(source_filename))
435 435
436 436 if (target_filename and patch['operation'] in ('A', 'M')
437 437 and target_filename not in self.target_nodes):
438 438 self.target_nodes[target_filename] = (
439 439 self.target_node_getter(target_filename))
440 440
441 441 elif self.highlight_mode == self.HL_FAST:
442 442 source_lexer = self._get_lexer_for_filename(source_filename)
443 443 target_lexer = self._get_lexer_for_filename(target_filename)
444 444
445 445 source_file = self.source_nodes.get(source_filename, source_filename)
446 446 target_file = self.target_nodes.get(target_filename, target_filename)
447 447
448 448 source_filenode, target_filenode = None, None
449 449
450 450 # TODO: dan: FileNode.lexer works on the content of the file - which
451 451 # can be slow - issue #4289 explains a lexer clean up - which once
452 452 # done can allow caching a lexer for a filenode to avoid the file lookup
453 453 if isinstance(source_file, FileNode):
454 454 source_filenode = source_file
455 455 #source_lexer = source_file.lexer
456 456 source_lexer = self._get_lexer_for_filename(source_filename)
457 457 source_file.lexer = source_lexer
458 458
459 459 if isinstance(target_file, FileNode):
460 460 target_filenode = target_file
461 461 #target_lexer = target_file.lexer
462 462 target_lexer = self._get_lexer_for_filename(target_filename)
463 463 target_file.lexer = target_lexer
464 464
465 465 source_file_path, target_file_path = None, None
466 466
467 467 if source_filename != '/dev/null':
468 468 source_file_path = source_filename
469 469 if target_filename != '/dev/null':
470 470 target_file_path = target_filename
471 471
472 472 source_file_type = source_lexer.name
473 473 target_file_type = target_lexer.name
474 474
475 475 filediff = AttributeDict({
476 476 'source_file_path': source_file_path,
477 477 'target_file_path': target_file_path,
478 478 'source_filenode': source_filenode,
479 479 'target_filenode': target_filenode,
480 480 'source_file_type': target_file_type,
481 481 'target_file_type': source_file_type,
482 482 'patch': {'filename': patch['filename'], 'stats': patch['stats']},
483 483 'operation': patch['operation'],
484 484 'source_mode': patch['stats']['old_mode'],
485 485 'target_mode': patch['stats']['new_mode'],
486 486 'limited_diff': isinstance(patch, LimitedDiffContainer),
487 487 'hunks': [],
488 488 'diffset': self,
489 489 })
490 490
491 491 for hunk in patch['chunks'][1:]:
492 492 hunkbit = self.parse_hunk(hunk, source_file, target_file)
493 493 hunkbit.source_file_path = source_file_path
494 494 hunkbit.target_file_path = target_file_path
495 495 filediff.hunks.append(hunkbit)
496 496
497 497 left_comments = {}
498 498 if source_file_path in self.comments_store:
499 499 for lineno, comments in self.comments_store[source_file_path].items():
500 500 left_comments[lineno] = comments
501 501
502 502 if target_file_path in self.comments_store:
503 503 for lineno, comments in self.comments_store[target_file_path].items():
504 504 left_comments[lineno] = comments
505 505 # left comments are one that we couldn't place in diff lines.
506 506 # could be outdated, or the diff changed and this line is no
507 507 # longer available
508 508 filediff.left_comments = left_comments
509 509
510 510 return filediff
511 511
512 512 def parse_hunk(self, hunk, source_file, target_file):
513 513 result = AttributeDict(dict(
514 514 source_start=hunk['source_start'],
515 515 source_length=hunk['source_length'],
516 516 target_start=hunk['target_start'],
517 517 target_length=hunk['target_length'],
518 518 section_header=hunk['section_header'],
519 519 lines=[],
520 520 ))
521 521 before, after = [], []
522 522
523 523 for line in hunk['lines']:
524 524
525 525 if line['action'] == 'unmod':
526 526 result.lines.extend(
527 527 self.parse_lines(before, after, source_file, target_file))
528 528 after.append(line)
529 529 before.append(line)
530 530 elif line['action'] == 'add':
531 531 after.append(line)
532 532 elif line['action'] == 'del':
533 533 before.append(line)
534 534 elif line['action'] == 'old-no-nl':
535 535 before.append(line)
536 536 elif line['action'] == 'new-no-nl':
537 537 after.append(line)
538 538
539 539 result.lines.extend(
540 540 self.parse_lines(before, after, source_file, target_file))
541 541 result.unified = self.as_unified(result.lines)
542 542 result.sideside = result.lines
543 543
544 544 return result
545 545
546 546 def parse_lines(self, before_lines, after_lines, source_file, target_file):
547 547 # TODO: dan: investigate doing the diff comparison and fast highlighting
548 548 # on the entire before and after buffered block lines rather than by
549 549 # line, this means we can get better 'fast' highlighting if the context
550 550 # allows it - eg.
551 551 # line 4: """
552 552 # line 5: this gets highlighted as a string
553 553 # line 6: """
554 554
555 555 lines = []
556
557 before_newline = AttributeDict()
558 after_newline = AttributeDict()
559 if before_lines and before_lines[-1]['action'] == 'old-no-nl':
560 before_newline_line = before_lines.pop(-1)
561 before_newline.content = '\n {}'.format(
562 render_tokenstream(
563 [(x[0], '', x[1])
564 for x in [('nonl', before_newline_line['line'])]]))
565
566 if after_lines and after_lines[-1]['action'] == 'new-no-nl':
567 after_newline_line = after_lines.pop(-1)
568 after_newline.content = '\n {}'.format(
569 render_tokenstream(
570 [(x[0], '', x[1])
571 for x in [('nonl', after_newline_line['line'])]]))
572
556 573 while before_lines or after_lines:
557 574 before, after = None, None
558 575 before_tokens, after_tokens = None, None
559 576
560 577 if before_lines:
561 578 before = before_lines.pop(0)
562 579 if after_lines:
563 580 after = after_lines.pop(0)
564 581
565 582 original = AttributeDict()
566 583 modified = AttributeDict()
567 584
568 585 if before:
569 586 if before['action'] == 'old-no-nl':
570 587 before_tokens = [('nonl', before['line'])]
571 588 else:
572 589 before_tokens = self.get_line_tokens(
573 590 line_text=before['line'],
574 591 line_number=before['old_lineno'],
575 592 file=source_file)
576 593 original.lineno = before['old_lineno']
577 594 original.content = before['line']
578 595 original.action = self.action_to_op(before['action'])
579 596 original.comments = self.get_comments_for('old',
580 597 source_file, before['old_lineno'])
581 598
582 599 if after:
583 600 if after['action'] == 'new-no-nl':
584 601 after_tokens = [('nonl', after['line'])]
585 602 else:
586 603 after_tokens = self.get_line_tokens(
587 604 line_text=after['line'], line_number=after['new_lineno'],
588 605 file=target_file)
589 606 modified.lineno = after['new_lineno']
590 607 modified.content = after['line']
591 608 modified.action = self.action_to_op(after['action'])
592 609 modified.comments = self.get_comments_for('new',
593 610 target_file, after['new_lineno'])
594 611
595 612 # diff the lines
596 613 if before_tokens and after_tokens:
597 614 o_tokens, m_tokens, similarity = tokens_diff(
598 615 before_tokens, after_tokens)
599 616 original.content = render_tokenstream(o_tokens)
600 617 modified.content = render_tokenstream(m_tokens)
601 618 elif before_tokens:
602 619 original.content = render_tokenstream(
603 620 [(x[0], '', x[1]) for x in before_tokens])
604 621 elif after_tokens:
605 622 modified.content = render_tokenstream(
606 623 [(x[0], '', x[1]) for x in after_tokens])
607 624
625 if not before_lines and before_newline:
626 original.content += before_newline.content
627 before_newline = None
628 if not after_lines and after_newline:
629 modified.content += after_newline.content
630 after_newline = None
631
608 632 lines.append(AttributeDict({
609 633 'original': original,
610 634 'modified': modified,
611 635 }))
612 636
613 637 return lines
614 638
615 639 def get_comments_for(self, version, filename, line_number):
616 640 if hasattr(filename, 'unicode_path'):
617 641 filename = filename.unicode_path
618 642
619 643 if not isinstance(filename, basestring):
620 644 return None
621 645
622 646 line_key = {
623 647 'old': 'o',
624 648 'new': 'n',
625 649 }[version] + str(line_number)
626 650
627 651 if filename in self.comments_store:
628 652 file_comments = self.comments_store[filename]
629 653 if line_key in file_comments:
630 654 return file_comments.pop(line_key)
631 655
632 656 def get_line_tokens(self, line_text, line_number, file=None):
633 657 filenode = None
634 658 filename = None
635 659
636 660 if isinstance(file, basestring):
637 661 filename = file
638 662 elif isinstance(file, FileNode):
639 663 filenode = file
640 664 filename = file.unicode_path
641 665
642 666 if self.highlight_mode == self.HL_REAL and filenode:
643 667 lexer = self._get_lexer_for_filename(filename)
644 668 file_size_allowed = file.size < self.max_file_size_limit
645 669 if line_number and file_size_allowed:
646 670 return self.get_tokenized_filenode_line(
647 671 file, line_number, lexer)
648 672
649 673 if self.highlight_mode in (self.HL_REAL, self.HL_FAST) and filename:
650 674 lexer = self._get_lexer_for_filename(filename)
651 675 return list(tokenize_string(line_text, lexer))
652 676
653 677 return list(tokenize_string(line_text, plain_text_lexer))
654 678
655 679 def get_tokenized_filenode_line(self, filenode, line_number, lexer=None):
656 680
657 681 if filenode not in self.highlighted_filenodes:
658 682 tokenized_lines = filenode_as_lines_tokens(filenode, lexer)
659 683 self.highlighted_filenodes[filenode] = tokenized_lines
660 684 return self.highlighted_filenodes[filenode][line_number - 1]
661 685
662 686 def action_to_op(self, action):
663 687 return {
664 688 'add': '+',
665 689 'del': '-',
666 690 'unmod': ' ',
667 691 'old-no-nl': ' ',
668 692 'new-no-nl': ' ',
669 693 }.get(action, action)
670 694
671 695 def as_unified(self, lines):
672 696 """
673 697 Return a generator that yields the lines of a diff in unified order
674 698 """
675 699 def generator():
676 700 buf = []
677 701 for line in lines:
678 702
679 703 if buf and not line.original or line.original.action == ' ':
680 704 for b in buf:
681 705 yield b
682 706 buf = []
683 707
684 708 if line.original:
685 709 if line.original.action == ' ':
686 710 yield (line.original.lineno, line.modified.lineno,
687 711 line.original.action, line.original.content,
688 712 line.original.comments)
689 713 continue
690 714
691 715 if line.original.action == '-':
692 716 yield (line.original.lineno, None,
693 717 line.original.action, line.original.content,
694 718 line.original.comments)
695 719
696 720 if line.modified.action == '+':
697 721 buf.append((
698 722 None, line.modified.lineno,
699 723 line.modified.action, line.modified.content,
700 724 line.modified.comments))
701 725 continue
702 726
703 727 if line.modified:
704 728 yield (None, line.modified.lineno,
705 729 line.modified.action, line.modified.content,
706 730 line.modified.comments)
707 731
708 732 for b in buf:
709 733 yield b
710 734
711 735 return generator()
@@ -1,1170 +1,1170 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2017 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 OLD_NO_NL = 'old-no-nl'
184 184 NEW_NO_NL = 'new-no-nl'
185 185
186 186
187 187 class DiffProcessor(object):
188 188 """
189 189 Give it a unified or git diff and it returns a list of the files that were
190 190 mentioned in the diff together with a dict of meta information that
191 191 can be used to render it in a HTML template.
192 192
193 193 .. note:: Unicode handling
194 194
195 195 The original diffs are a byte sequence and can contain filenames
196 196 in mixed encodings. This class generally returns `unicode` objects
197 197 since the result is intended for presentation to the user.
198 198
199 199 """
200 200 _chunk_re = re.compile(r'^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)')
201 201 _newline_marker = re.compile(r'^\\ No newline at end of file')
202 202
203 203 # used for inline highlighter word split
204 204 _token_re = re.compile(r'()(&gt;|&lt;|&amp;|\W+?)')
205 205
206 206 # collapse ranges of commits over given number
207 207 _collapse_commits_over = 5
208 208
209 209 def __init__(self, diff, format='gitdiff', diff_limit=None,
210 210 file_limit=None, show_full_diff=True):
211 211 """
212 212 :param diff: A `Diff` object representing a diff from a vcs backend
213 213 :param format: format of diff passed, `udiff` or `gitdiff`
214 214 :param diff_limit: define the size of diff that is considered "big"
215 215 based on that parameter cut off will be triggered, set to None
216 216 to show full diff
217 217 """
218 218 self._diff = diff
219 219 self._format = format
220 220 self.adds = 0
221 221 self.removes = 0
222 222 # calculate diff size
223 223 self.diff_limit = diff_limit
224 224 self.file_limit = file_limit
225 225 self.show_full_diff = show_full_diff
226 226 self.cur_diff_size = 0
227 227 self.parsed = False
228 228 self.parsed_diff = []
229 229
230 230 log.debug('Initialized DiffProcessor with %s mode', format)
231 231 if format == 'gitdiff':
232 232 self.differ = self._highlight_line_difflib
233 233 self._parser = self._parse_gitdiff
234 234 else:
235 235 self.differ = self._highlight_line_udiff
236 236 self._parser = self._new_parse_gitdiff
237 237
238 238 def _copy_iterator(self):
239 239 """
240 240 make a fresh copy of generator, we should not iterate thru
241 241 an original as it's needed for repeating operations on
242 242 this instance of DiffProcessor
243 243 """
244 244 self.__udiff, iterator_copy = tee(self.__udiff)
245 245 return iterator_copy
246 246
247 247 def _escaper(self, string):
248 248 """
249 249 Escaper for diff escapes special chars and checks the diff limit
250 250
251 251 :param string:
252 252 """
253 253
254 254 self.cur_diff_size += len(string)
255 255
256 256 if not self.show_full_diff and (self.cur_diff_size > self.diff_limit):
257 257 raise DiffLimitExceeded('Diff Limit Exceeded')
258 258
259 259 return safe_unicode(string)\
260 260 .replace('&', '&amp;')\
261 261 .replace('<', '&lt;')\
262 262 .replace('>', '&gt;')
263 263
264 264 def _line_counter(self, l):
265 265 """
266 266 Checks each line and bumps total adds/removes for this diff
267 267
268 268 :param l:
269 269 """
270 270 if l.startswith('+') and not l.startswith('+++'):
271 271 self.adds += 1
272 272 elif l.startswith('-') and not l.startswith('---'):
273 273 self.removes += 1
274 274 return safe_unicode(l)
275 275
276 276 def _highlight_line_difflib(self, line, next_):
277 277 """
278 278 Highlight inline changes in both lines.
279 279 """
280 280
281 281 if line['action'] == Action.DELETE:
282 282 old, new = line, next_
283 283 else:
284 284 old, new = next_, line
285 285
286 286 oldwords = self._token_re.split(old['line'])
287 287 newwords = self._token_re.split(new['line'])
288 288 sequence = difflib.SequenceMatcher(None, oldwords, newwords)
289 289
290 290 oldfragments, newfragments = [], []
291 291 for tag, i1, i2, j1, j2 in sequence.get_opcodes():
292 292 oldfrag = ''.join(oldwords[i1:i2])
293 293 newfrag = ''.join(newwords[j1:j2])
294 294 if tag != 'equal':
295 295 if oldfrag:
296 296 oldfrag = '<del>%s</del>' % oldfrag
297 297 if newfrag:
298 298 newfrag = '<ins>%s</ins>' % newfrag
299 299 oldfragments.append(oldfrag)
300 300 newfragments.append(newfrag)
301 301
302 302 old['line'] = "".join(oldfragments)
303 303 new['line'] = "".join(newfragments)
304 304
305 305 def _highlight_line_udiff(self, line, next_):
306 306 """
307 307 Highlight inline changes in both lines.
308 308 """
309 309 start = 0
310 310 limit = min(len(line['line']), len(next_['line']))
311 311 while start < limit and line['line'][start] == next_['line'][start]:
312 312 start += 1
313 313 end = -1
314 314 limit -= start
315 315 while -end <= limit and line['line'][end] == next_['line'][end]:
316 316 end -= 1
317 317 end += 1
318 318 if start or end:
319 319 def do(l):
320 320 last = end + len(l['line'])
321 321 if l['action'] == Action.ADD:
322 322 tag = 'ins'
323 323 else:
324 324 tag = 'del'
325 325 l['line'] = '%s<%s>%s</%s>%s' % (
326 326 l['line'][:start],
327 327 tag,
328 328 l['line'][start:last],
329 329 tag,
330 330 l['line'][last:]
331 331 )
332 332 do(line)
333 333 do(next_)
334 334
335 335 def _clean_line(self, line, command):
336 336 if command in ['+', '-', ' ']:
337 337 # only modify the line if it's actually a diff thing
338 338 line = line[1:]
339 339 return line
340 340
341 341 def _parse_gitdiff(self, inline_diff=True):
342 342 _files = []
343 343 diff_container = lambda arg: arg
344 344
345 345 for chunk in self._diff.chunks():
346 346 head = chunk.header
347 347
348 348 diff = imap(self._escaper, chunk.diff.splitlines(1))
349 349 raw_diff = chunk.raw
350 350 limited_diff = False
351 351 exceeds_limit = False
352 352
353 353 op = None
354 354 stats = {
355 355 'added': 0,
356 356 'deleted': 0,
357 357 'binary': False,
358 358 'ops': {},
359 359 }
360 360
361 361 if head['deleted_file_mode']:
362 362 op = OPS.DEL
363 363 stats['binary'] = True
364 364 stats['ops'][DEL_FILENODE] = 'deleted file'
365 365
366 366 elif head['new_file_mode']:
367 367 op = OPS.ADD
368 368 stats['binary'] = True
369 369 stats['ops'][NEW_FILENODE] = 'new file %s' % head['new_file_mode']
370 370 else: # modify operation, can be copy, rename or chmod
371 371
372 372 # CHMOD
373 373 if head['new_mode'] and head['old_mode']:
374 374 op = OPS.MOD
375 375 stats['binary'] = True
376 376 stats['ops'][CHMOD_FILENODE] = (
377 377 'modified file chmod %s => %s' % (
378 378 head['old_mode'], head['new_mode']))
379 379 # RENAME
380 380 if head['rename_from'] != head['rename_to']:
381 381 op = OPS.MOD
382 382 stats['binary'] = True
383 383 stats['ops'][RENAMED_FILENODE] = (
384 384 'file renamed from %s to %s' % (
385 385 head['rename_from'], head['rename_to']))
386 386 # COPY
387 387 if head.get('copy_from') and head.get('copy_to'):
388 388 op = OPS.MOD
389 389 stats['binary'] = True
390 390 stats['ops'][COPIED_FILENODE] = (
391 391 'file copied from %s to %s' % (
392 392 head['copy_from'], head['copy_to']))
393 393
394 394 # If our new parsed headers didn't match anything fallback to
395 395 # old style detection
396 396 if op is None:
397 397 if not head['a_file'] and head['b_file']:
398 398 op = OPS.ADD
399 399 stats['binary'] = True
400 400 stats['ops'][NEW_FILENODE] = 'new file'
401 401
402 402 elif head['a_file'] and not head['b_file']:
403 403 op = OPS.DEL
404 404 stats['binary'] = True
405 405 stats['ops'][DEL_FILENODE] = 'deleted file'
406 406
407 407 # it's not ADD not DELETE
408 408 if op is None:
409 409 op = OPS.MOD
410 410 stats['binary'] = True
411 411 stats['ops'][MOD_FILENODE] = 'modified file'
412 412
413 413 # a real non-binary diff
414 414 if head['a_file'] or head['b_file']:
415 415 try:
416 416 raw_diff, chunks, _stats = self._parse_lines(diff)
417 417 stats['binary'] = False
418 418 stats['added'] = _stats[0]
419 419 stats['deleted'] = _stats[1]
420 420 # explicit mark that it's a modified file
421 421 if op == OPS.MOD:
422 422 stats['ops'][MOD_FILENODE] = 'modified file'
423 423 exceeds_limit = len(raw_diff) > self.file_limit
424 424
425 425 # changed from _escaper function so we validate size of
426 426 # each file instead of the whole diff
427 427 # diff will hide big files but still show small ones
428 428 # from my tests, big files are fairly safe to be parsed
429 429 # but the browser is the bottleneck
430 430 if not self.show_full_diff and exceeds_limit:
431 431 raise DiffLimitExceeded('File Limit Exceeded')
432 432
433 433 except DiffLimitExceeded:
434 434 diff_container = lambda _diff: \
435 435 LimitedDiffContainer(
436 436 self.diff_limit, self.cur_diff_size, _diff)
437 437
438 438 exceeds_limit = len(raw_diff) > self.file_limit
439 439 limited_diff = True
440 440 chunks = []
441 441
442 442 else: # GIT format binary patch, or possibly empty diff
443 443 if head['bin_patch']:
444 444 # we have operation already extracted, but we mark simply
445 445 # it's a diff we wont show for binary files
446 446 stats['ops'][BIN_FILENODE] = 'binary diff hidden'
447 447 chunks = []
448 448
449 449 if chunks and not self.show_full_diff and op == OPS.DEL:
450 450 # if not full diff mode show deleted file contents
451 451 # TODO: anderson: if the view is not too big, there is no way
452 452 # to see the content of the file
453 453 chunks = []
454 454
455 455 chunks.insert(0, [{
456 456 'old_lineno': '',
457 457 'new_lineno': '',
458 458 'action': Action.CONTEXT,
459 459 'line': msg,
460 460 } for _op, msg in stats['ops'].iteritems()
461 461 if _op not in [MOD_FILENODE]])
462 462
463 463 _files.append({
464 464 'filename': safe_unicode(head['b_path']),
465 465 'old_revision': head['a_blob_id'],
466 466 'new_revision': head['b_blob_id'],
467 467 'chunks': chunks,
468 468 'raw_diff': safe_unicode(raw_diff),
469 469 'operation': op,
470 470 'stats': stats,
471 471 'exceeds_limit': exceeds_limit,
472 472 'is_limited_diff': limited_diff,
473 473 })
474 474
475 475 sorter = lambda info: {OPS.ADD: 0, OPS.MOD: 1,
476 476 OPS.DEL: 2}.get(info['operation'])
477 477
478 478 if not inline_diff:
479 479 return diff_container(sorted(_files, key=sorter))
480 480
481 481 # highlight inline changes
482 482 for diff_data in _files:
483 483 for chunk in diff_data['chunks']:
484 484 lineiter = iter(chunk)
485 485 try:
486 486 while 1:
487 487 line = lineiter.next()
488 488 if line['action'] not in (
489 489 Action.UNMODIFIED, Action.CONTEXT):
490 490 nextline = lineiter.next()
491 491 if nextline['action'] in ['unmod', 'context'] or \
492 492 nextline['action'] == line['action']:
493 493 continue
494 494 self.differ(line, nextline)
495 495 except StopIteration:
496 496 pass
497 497
498 498 return diff_container(sorted(_files, key=sorter))
499 499
500 500 def _check_large_diff(self):
501 501 log.debug('Diff exceeds current diff_limit of %s', self.diff_limit)
502 502 if not self.show_full_diff and (self.cur_diff_size > self.diff_limit):
503 503 raise DiffLimitExceeded('Diff Limit `%s` Exceeded', self.diff_limit)
504 504
505 505 # FIXME: NEWDIFFS: dan: this replaces _parse_gitdiff
506 506 def _new_parse_gitdiff(self, inline_diff=True):
507 507 _files = []
508 508
509 509 # this can be overriden later to a LimitedDiffContainer type
510 510 diff_container = lambda arg: arg
511 511
512 512 for chunk in self._diff.chunks():
513 513 head = chunk.header
514 514 log.debug('parsing diff %r' % head)
515 515
516 516 raw_diff = chunk.raw
517 517 limited_diff = False
518 518 exceeds_limit = False
519 519
520 520 op = None
521 521 stats = {
522 522 'added': 0,
523 523 'deleted': 0,
524 524 'binary': False,
525 525 'old_mode': None,
526 526 'new_mode': None,
527 527 'ops': {},
528 528 }
529 529 if head['old_mode']:
530 530 stats['old_mode'] = head['old_mode']
531 531 if head['new_mode']:
532 532 stats['new_mode'] = head['new_mode']
533 533 if head['b_mode']:
534 534 stats['new_mode'] = head['b_mode']
535 535
536 536 # delete file
537 537 if head['deleted_file_mode']:
538 538 op = OPS.DEL
539 539 stats['binary'] = True
540 540 stats['ops'][DEL_FILENODE] = 'deleted file'
541 541
542 542 # new file
543 543 elif head['new_file_mode']:
544 544 op = OPS.ADD
545 545 stats['binary'] = True
546 546 stats['old_mode'] = None
547 547 stats['new_mode'] = head['new_file_mode']
548 548 stats['ops'][NEW_FILENODE] = 'new file %s' % head['new_file_mode']
549 549
550 550 # modify operation, can be copy, rename or chmod
551 551 else:
552 552 # CHMOD
553 553 if head['new_mode'] and head['old_mode']:
554 554 op = OPS.MOD
555 555 stats['binary'] = True
556 556 stats['ops'][CHMOD_FILENODE] = (
557 557 'modified file chmod %s => %s' % (
558 558 head['old_mode'], head['new_mode']))
559 559
560 560 # RENAME
561 561 if head['rename_from'] != head['rename_to']:
562 562 op = OPS.MOD
563 563 stats['binary'] = True
564 564 stats['renamed'] = (head['rename_from'], head['rename_to'])
565 565 stats['ops'][RENAMED_FILENODE] = (
566 566 'file renamed from %s to %s' % (
567 567 head['rename_from'], head['rename_to']))
568 568 # COPY
569 569 if head.get('copy_from') and head.get('copy_to'):
570 570 op = OPS.MOD
571 571 stats['binary'] = True
572 572 stats['copied'] = (head['copy_from'], head['copy_to'])
573 573 stats['ops'][COPIED_FILENODE] = (
574 574 'file copied from %s to %s' % (
575 575 head['copy_from'], head['copy_to']))
576 576
577 577 # If our new parsed headers didn't match anything fallback to
578 578 # old style detection
579 579 if op is None:
580 580 if not head['a_file'] and head['b_file']:
581 581 op = OPS.ADD
582 582 stats['binary'] = True
583 583 stats['new_file'] = True
584 584 stats['ops'][NEW_FILENODE] = 'new file'
585 585
586 586 elif head['a_file'] and not head['b_file']:
587 587 op = OPS.DEL
588 588 stats['binary'] = True
589 589 stats['ops'][DEL_FILENODE] = 'deleted file'
590 590
591 591 # it's not ADD not DELETE
592 592 if op is None:
593 593 op = OPS.MOD
594 594 stats['binary'] = True
595 595 stats['ops'][MOD_FILENODE] = 'modified file'
596 596
597 597 # a real non-binary diff
598 598 if head['a_file'] or head['b_file']:
599 599 diff = iter(chunk.diff.splitlines(1))
600 600
601 601 # append each file to the diff size
602 602 raw_chunk_size = len(raw_diff)
603 603
604 604 exceeds_limit = raw_chunk_size > self.file_limit
605 605 self.cur_diff_size += raw_chunk_size
606 606
607 607 try:
608 608 # Check each file instead of the whole diff.
609 609 # Diff will hide big files but still show small ones.
610 610 # From the tests big files are fairly safe to be parsed
611 611 # but the browser is the bottleneck.
612 612 if not self.show_full_diff and exceeds_limit:
613 613 log.debug('File `%s` exceeds current file_limit of %s',
614 614 safe_unicode(head['b_path']), self.file_limit)
615 615 raise DiffLimitExceeded(
616 616 'File Limit %s Exceeded', self.file_limit)
617 617
618 618 self._check_large_diff()
619 619
620 620 raw_diff, chunks, _stats = self._new_parse_lines(diff)
621 621 stats['binary'] = False
622 622 stats['added'] = _stats[0]
623 623 stats['deleted'] = _stats[1]
624 624 # explicit mark that it's a modified file
625 625 if op == OPS.MOD:
626 626 stats['ops'][MOD_FILENODE] = 'modified file'
627 627
628 628 except DiffLimitExceeded:
629 629 diff_container = lambda _diff: \
630 630 LimitedDiffContainer(
631 631 self.diff_limit, self.cur_diff_size, _diff)
632 632
633 633 limited_diff = True
634 634 chunks = []
635 635
636 636 else: # GIT format binary patch, or possibly empty diff
637 637 if head['bin_patch']:
638 638 # we have operation already extracted, but we mark simply
639 639 # it's a diff we wont show for binary files
640 640 stats['ops'][BIN_FILENODE] = 'binary diff hidden'
641 641 chunks = []
642 642
643 643 # Hide content of deleted node by setting empty chunks
644 644 if chunks and not self.show_full_diff and op == OPS.DEL:
645 645 # if not full diff mode show deleted file contents
646 646 # TODO: anderson: if the view is not too big, there is no way
647 647 # to see the content of the file
648 648 chunks = []
649 649
650 650 chunks.insert(
651 651 0, [{'old_lineno': '',
652 652 'new_lineno': '',
653 653 'action': Action.CONTEXT,
654 654 'line': msg,
655 655 } for _op, msg in stats['ops'].iteritems()
656 656 if _op not in [MOD_FILENODE]])
657 657
658 658 original_filename = safe_unicode(head['a_path'])
659 659 _files.append({
660 660 'original_filename': original_filename,
661 661 'filename': safe_unicode(head['b_path']),
662 662 'old_revision': head['a_blob_id'],
663 663 'new_revision': head['b_blob_id'],
664 664 'chunks': chunks,
665 665 'raw_diff': safe_unicode(raw_diff),
666 666 'operation': op,
667 667 'stats': stats,
668 668 'exceeds_limit': exceeds_limit,
669 669 'is_limited_diff': limited_diff,
670 670 })
671 671
672 672 sorter = lambda info: {OPS.ADD: 0, OPS.MOD: 1,
673 673 OPS.DEL: 2}.get(info['operation'])
674 674
675 675 return diff_container(sorted(_files, key=sorter))
676 676
677 677 # FIXME: NEWDIFFS: dan: this gets replaced by _new_parse_lines
678 678 def _parse_lines(self, diff):
679 679 """
680 680 Parse the diff an return data for the template.
681 681 """
682 682
683 683 lineiter = iter(diff)
684 684 stats = [0, 0]
685 685 chunks = []
686 686 raw_diff = []
687 687
688 688 try:
689 689 line = lineiter.next()
690 690
691 691 while line:
692 692 raw_diff.append(line)
693 693 lines = []
694 694 chunks.append(lines)
695 695
696 696 match = self._chunk_re.match(line)
697 697
698 698 if not match:
699 699 break
700 700
701 701 gr = match.groups()
702 702 (old_line, old_end,
703 703 new_line, new_end) = [int(x or 1) for x in gr[:-1]]
704 704 old_line -= 1
705 705 new_line -= 1
706 706
707 707 context = len(gr) == 5
708 708 old_end += old_line
709 709 new_end += new_line
710 710
711 711 if context:
712 712 # skip context only if it's first line
713 713 if int(gr[0]) > 1:
714 714 lines.append({
715 715 'old_lineno': '...',
716 716 'new_lineno': '...',
717 717 'action': Action.CONTEXT,
718 718 'line': line,
719 719 })
720 720
721 721 line = lineiter.next()
722 722
723 723 while old_line < old_end or new_line < new_end:
724 724 command = ' '
725 725 if line:
726 726 command = line[0]
727 727
728 728 affects_old = affects_new = False
729 729
730 730 # ignore those if we don't expect them
731 731 if command in '#@':
732 732 continue
733 733 elif command == '+':
734 734 affects_new = True
735 735 action = Action.ADD
736 736 stats[0] += 1
737 737 elif command == '-':
738 738 affects_old = True
739 739 action = Action.DELETE
740 740 stats[1] += 1
741 741 else:
742 742 affects_old = affects_new = True
743 743 action = Action.UNMODIFIED
744 744
745 745 if not self._newline_marker.match(line):
746 746 old_line += affects_old
747 747 new_line += affects_new
748 748 lines.append({
749 749 'old_lineno': affects_old and old_line or '',
750 750 'new_lineno': affects_new and new_line or '',
751 751 'action': action,
752 752 'line': self._clean_line(line, command)
753 753 })
754 754 raw_diff.append(line)
755 755
756 756 line = lineiter.next()
757 757
758 758 if self._newline_marker.match(line):
759 759 # we need to append to lines, since this is not
760 760 # counted in the line specs of diff
761 761 lines.append({
762 762 'old_lineno': '...',
763 763 'new_lineno': '...',
764 764 'action': Action.CONTEXT,
765 765 'line': self._clean_line(line, command)
766 766 })
767 767
768 768 except StopIteration:
769 769 pass
770 770 return ''.join(raw_diff), chunks, stats
771 771
772 772 # FIXME: NEWDIFFS: dan: this replaces _parse_lines
773 773 def _new_parse_lines(self, diff_iter):
774 774 """
775 775 Parse the diff an return data for the template.
776 776 """
777 777
778 778 stats = [0, 0]
779 779 chunks = []
780 780 raw_diff = []
781 781
782 782 diff_iter = imap(lambda s: safe_unicode(s), diff_iter)
783 783
784 784 try:
785 785 line = diff_iter.next()
786 786
787 787 while line:
788 788 raw_diff.append(line)
789 789 match = self._chunk_re.match(line)
790 790
791 791 if not match:
792 792 break
793 793
794 794 gr = match.groups()
795 795 (old_line, old_end,
796 796 new_line, new_end) = [int(x or 1) for x in gr[:-1]]
797 797
798 798 lines = []
799 799 hunk = {
800 800 'section_header': gr[-1],
801 801 'source_start': old_line,
802 802 'source_length': old_end,
803 803 'target_start': new_line,
804 804 'target_length': new_end,
805 805 'lines': lines,
806 806 }
807 807 chunks.append(hunk)
808 808
809 809 old_line -= 1
810 810 new_line -= 1
811 811
812 812 context = len(gr) == 5
813 813 old_end += old_line
814 814 new_end += new_line
815 815
816 816 line = diff_iter.next()
817 817
818 818 while old_line < old_end or new_line < new_end:
819 819 command = ' '
820 820 if line:
821 821 command = line[0]
822 822
823 823 affects_old = affects_new = False
824 824
825 825 # ignore those if we don't expect them
826 826 if command in '#@':
827 827 continue
828 828 elif command == '+':
829 829 affects_new = True
830 830 action = Action.ADD
831 831 stats[0] += 1
832 832 elif command == '-':
833 833 affects_old = True
834 834 action = Action.DELETE
835 835 stats[1] += 1
836 836 else:
837 837 affects_old = affects_new = True
838 838 action = Action.UNMODIFIED
839 839
840 840 if not self._newline_marker.match(line):
841 841 old_line += affects_old
842 842 new_line += affects_new
843 843 lines.append({
844 844 'old_lineno': affects_old and old_line or '',
845 845 'new_lineno': affects_new and new_line or '',
846 846 'action': action,
847 847 'line': self._clean_line(line, command)
848 848 })
849 raw_diff.append(line)
849 raw_diff.append(line)
850 850
851 851 line = diff_iter.next()
852 852
853 853 if self._newline_marker.match(line):
854 854 # we need to append to lines, since this is not
855 855 # counted in the line specs of diff
856 856 if affects_old:
857 857 action = Action.OLD_NO_NL
858 858 elif affects_new:
859 859 action = Action.NEW_NO_NL
860 860 else:
861 861 raise Exception('invalid context for no newline')
862 862
863 863 lines.append({
864 864 'old_lineno': None,
865 865 'new_lineno': None,
866 866 'action': action,
867 867 'line': self._clean_line(line, command)
868 868 })
869 869
870 870 except StopIteration:
871 871 pass
872 872
873 873 return ''.join(raw_diff), chunks, stats
874 874
875 875 def _safe_id(self, idstring):
876 876 """Make a string safe for including in an id attribute.
877 877
878 878 The HTML spec says that id attributes 'must begin with
879 879 a letter ([A-Za-z]) and may be followed by any number
880 880 of letters, digits ([0-9]), hyphens ("-"), underscores
881 881 ("_"), colons (":"), and periods (".")'. These regexps
882 882 are slightly over-zealous, in that they remove colons
883 883 and periods unnecessarily.
884 884
885 885 Whitespace is transformed into underscores, and then
886 886 anything which is not a hyphen or a character that
887 887 matches \w (alphanumerics and underscore) is removed.
888 888
889 889 """
890 890 # Transform all whitespace to underscore
891 891 idstring = re.sub(r'\s', "_", '%s' % idstring)
892 892 # Remove everything that is not a hyphen or a member of \w
893 893 idstring = re.sub(r'(?!-)\W', "", idstring).lower()
894 894 return idstring
895 895
896 896 def prepare(self, inline_diff=True):
897 897 """
898 898 Prepare the passed udiff for HTML rendering.
899 899
900 900 :return: A list of dicts with diff information.
901 901 """
902 902 parsed = self._parser(inline_diff=inline_diff)
903 903 self.parsed = True
904 904 self.parsed_diff = parsed
905 905 return parsed
906 906
907 907 def as_raw(self, diff_lines=None):
908 908 """
909 909 Returns raw diff as a byte string
910 910 """
911 911 return self._diff.raw
912 912
913 913 def as_html(self, table_class='code-difftable', line_class='line',
914 914 old_lineno_class='lineno old', new_lineno_class='lineno new',
915 915 code_class='code', enable_comments=False, parsed_lines=None):
916 916 """
917 917 Return given diff as html table with customized css classes
918 918 """
919 919 def _link_to_if(condition, label, url):
920 920 """
921 921 Generates a link if condition is meet or just the label if not.
922 922 """
923 923
924 924 if condition:
925 925 return '''<a href="%(url)s" class="tooltip"
926 926 title="%(title)s">%(label)s</a>''' % {
927 927 'title': _('Click to select line'),
928 928 'url': url,
929 929 'label': label
930 930 }
931 931 else:
932 932 return label
933 933 if not self.parsed:
934 934 self.prepare()
935 935
936 936 diff_lines = self.parsed_diff
937 937 if parsed_lines:
938 938 diff_lines = parsed_lines
939 939
940 940 _html_empty = True
941 941 _html = []
942 942 _html.append('''<table class="%(table_class)s">\n''' % {
943 943 'table_class': table_class
944 944 })
945 945
946 946 for diff in diff_lines:
947 947 for line in diff['chunks']:
948 948 _html_empty = False
949 949 for change in line:
950 950 _html.append('''<tr class="%(lc)s %(action)s">\n''' % {
951 951 'lc': line_class,
952 952 'action': change['action']
953 953 })
954 954 anchor_old_id = ''
955 955 anchor_new_id = ''
956 956 anchor_old = "%(filename)s_o%(oldline_no)s" % {
957 957 'filename': self._safe_id(diff['filename']),
958 958 'oldline_no': change['old_lineno']
959 959 }
960 960 anchor_new = "%(filename)s_n%(oldline_no)s" % {
961 961 'filename': self._safe_id(diff['filename']),
962 962 'oldline_no': change['new_lineno']
963 963 }
964 964 cond_old = (change['old_lineno'] != '...' and
965 965 change['old_lineno'])
966 966 cond_new = (change['new_lineno'] != '...' and
967 967 change['new_lineno'])
968 968 if cond_old:
969 969 anchor_old_id = 'id="%s"' % anchor_old
970 970 if cond_new:
971 971 anchor_new_id = 'id="%s"' % anchor_new
972 972
973 973 if change['action'] != Action.CONTEXT:
974 974 anchor_link = True
975 975 else:
976 976 anchor_link = False
977 977
978 978 ###########################################################
979 979 # COMMENT ICONS
980 980 ###########################################################
981 981 _html.append('''\t<td class="add-comment-line"><span class="add-comment-content">''')
982 982
983 983 if enable_comments and change['action'] != Action.CONTEXT:
984 984 _html.append('''<a href="#"><span class="icon-comment-add"></span></a>''')
985 985
986 986 _html.append('''</span></td><td class="comment-toggle tooltip" title="Toggle Comment Thread"><i class="icon-comment"></i></td>\n''')
987 987
988 988 ###########################################################
989 989 # OLD LINE NUMBER
990 990 ###########################################################
991 991 _html.append('''\t<td %(a_id)s class="%(olc)s">''' % {
992 992 'a_id': anchor_old_id,
993 993 'olc': old_lineno_class
994 994 })
995 995
996 996 _html.append('''%(link)s''' % {
997 997 'link': _link_to_if(anchor_link, change['old_lineno'],
998 998 '#%s' % anchor_old)
999 999 })
1000 1000 _html.append('''</td>\n''')
1001 1001 ###########################################################
1002 1002 # NEW LINE NUMBER
1003 1003 ###########################################################
1004 1004
1005 1005 _html.append('''\t<td %(a_id)s class="%(nlc)s">''' % {
1006 1006 'a_id': anchor_new_id,
1007 1007 'nlc': new_lineno_class
1008 1008 })
1009 1009
1010 1010 _html.append('''%(link)s''' % {
1011 1011 'link': _link_to_if(anchor_link, change['new_lineno'],
1012 1012 '#%s' % anchor_new)
1013 1013 })
1014 1014 _html.append('''</td>\n''')
1015 1015 ###########################################################
1016 1016 # CODE
1017 1017 ###########################################################
1018 1018 code_classes = [code_class]
1019 1019 if (not enable_comments or
1020 1020 change['action'] == Action.CONTEXT):
1021 1021 code_classes.append('no-comment')
1022 1022 _html.append('\t<td class="%s">' % ' '.join(code_classes))
1023 1023 _html.append('''\n\t\t<pre>%(code)s</pre>\n''' % {
1024 1024 'code': change['line']
1025 1025 })
1026 1026
1027 1027 _html.append('''\t</td>''')
1028 1028 _html.append('''\n</tr>\n''')
1029 1029 _html.append('''</table>''')
1030 1030 if _html_empty:
1031 1031 return None
1032 1032 return ''.join(_html)
1033 1033
1034 1034 def stat(self):
1035 1035 """
1036 1036 Returns tuple of added, and removed lines for this instance
1037 1037 """
1038 1038 return self.adds, self.removes
1039 1039
1040 1040 def get_context_of_line(
1041 1041 self, path, diff_line=None, context_before=3, context_after=3):
1042 1042 """
1043 1043 Returns the context lines for the specified diff line.
1044 1044
1045 1045 :type diff_line: :class:`DiffLineNumber`
1046 1046 """
1047 1047 assert self.parsed, "DiffProcessor is not initialized."
1048 1048
1049 1049 if None not in diff_line:
1050 1050 raise ValueError(
1051 1051 "Cannot specify both line numbers: {}".format(diff_line))
1052 1052
1053 1053 file_diff = self._get_file_diff(path)
1054 1054 chunk, idx = self._find_chunk_line_index(file_diff, diff_line)
1055 1055
1056 1056 first_line_to_include = max(idx - context_before, 0)
1057 1057 first_line_after_context = idx + context_after + 1
1058 1058 context_lines = chunk[first_line_to_include:first_line_after_context]
1059 1059
1060 1060 line_contents = [
1061 1061 _context_line(line) for line in context_lines
1062 1062 if _is_diff_content(line)]
1063 1063 # TODO: johbo: Interim fixup, the diff chunks drop the final newline.
1064 1064 # Once they are fixed, we can drop this line here.
1065 1065 if line_contents:
1066 1066 line_contents[-1] = (
1067 1067 line_contents[-1][0], line_contents[-1][1].rstrip('\n') + '\n')
1068 1068 return line_contents
1069 1069
1070 1070 def find_context(self, path, context, offset=0):
1071 1071 """
1072 1072 Finds the given `context` inside of the diff.
1073 1073
1074 1074 Use the parameter `offset` to specify which offset the target line has
1075 1075 inside of the given `context`. This way the correct diff line will be
1076 1076 returned.
1077 1077
1078 1078 :param offset: Shall be used to specify the offset of the main line
1079 1079 within the given `context`.
1080 1080 """
1081 1081 if offset < 0 or offset >= len(context):
1082 1082 raise ValueError(
1083 1083 "Only positive values up to the length of the context "
1084 1084 "minus one are allowed.")
1085 1085
1086 1086 matches = []
1087 1087 file_diff = self._get_file_diff(path)
1088 1088
1089 1089 for chunk in file_diff['chunks']:
1090 1090 context_iter = iter(context)
1091 1091 for line_idx, line in enumerate(chunk):
1092 1092 try:
1093 1093 if _context_line(line) == context_iter.next():
1094 1094 continue
1095 1095 except StopIteration:
1096 1096 matches.append((line_idx, chunk))
1097 1097 context_iter = iter(context)
1098 1098
1099 1099 # Increment position and triger StopIteration
1100 1100 # if we had a match at the end
1101 1101 line_idx += 1
1102 1102 try:
1103 1103 context_iter.next()
1104 1104 except StopIteration:
1105 1105 matches.append((line_idx, chunk))
1106 1106
1107 1107 effective_offset = len(context) - offset
1108 1108 found_at_diff_lines = [
1109 1109 _line_to_diff_line_number(chunk[idx - effective_offset])
1110 1110 for idx, chunk in matches]
1111 1111
1112 1112 return found_at_diff_lines
1113 1113
1114 1114 def _get_file_diff(self, path):
1115 1115 for file_diff in self.parsed_diff:
1116 1116 if file_diff['filename'] == path:
1117 1117 break
1118 1118 else:
1119 1119 raise FileNotInDiffException("File {} not in diff".format(path))
1120 1120 return file_diff
1121 1121
1122 1122 def _find_chunk_line_index(self, file_diff, diff_line):
1123 1123 for chunk in file_diff['chunks']:
1124 1124 for idx, line in enumerate(chunk):
1125 1125 if line['old_lineno'] == diff_line.old:
1126 1126 return chunk, idx
1127 1127 if line['new_lineno'] == diff_line.new:
1128 1128 return chunk, idx
1129 1129 raise LineNotInDiffException(
1130 1130 "The line {} is not part of the diff.".format(diff_line))
1131 1131
1132 1132
1133 1133 def _is_diff_content(line):
1134 1134 return line['action'] in (
1135 1135 Action.UNMODIFIED, Action.ADD, Action.DELETE)
1136 1136
1137 1137
1138 1138 def _context_line(line):
1139 1139 return (line['action'], line['line'])
1140 1140
1141 1141
1142 1142 DiffLineNumber = collections.namedtuple('DiffLineNumber', ['old', 'new'])
1143 1143
1144 1144
1145 1145 def _line_to_diff_line_number(line):
1146 1146 new_line_no = line['new_lineno'] or None
1147 1147 old_line_no = line['old_lineno'] or None
1148 1148 return DiffLineNumber(old=old_line_no, new=new_line_no)
1149 1149
1150 1150
1151 1151 class FileNotInDiffException(Exception):
1152 1152 """
1153 1153 Raised when the context for a missing file is requested.
1154 1154
1155 1155 If you request the context for a line in a file which is not part of the
1156 1156 given diff, then this exception is raised.
1157 1157 """
1158 1158
1159 1159
1160 1160 class LineNotInDiffException(Exception):
1161 1161 """
1162 1162 Raised when the context for a missing line is requested.
1163 1163
1164 1164 If you request the context for a line in a file and this line is not
1165 1165 part of the given diff, then this exception is raised.
1166 1166 """
1167 1167
1168 1168
1169 1169 class DiffLimitExceeded(Exception):
1170 1170 pass
@@ -1,822 +1,830 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 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 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 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 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 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 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 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 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 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 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 'hg_diff_no_newline.diff',
247 [('server.properties', 'M',
248 {'added': 2,
249 'deleted': 1,
250 'binary': False,
251 'ops': {MOD_FILENODE: 'modified file'}}),
252 ]),
253 ('hg',
246 254 'hg_diff_mod_file_and_rename.diff',
247 255 [('README.rst', 'M',
248 256 {'added': 3,
249 257 'deleted': 0,
250 258 'binary': False,
251 259 'ops': {MOD_FILENODE: 'modified file',
252 260 RENAMED_FILENODE: 'file renamed from README to README.rst'}}),
253 261 ]),
254 262 ('hg',
255 263 'hg_diff_del_single_binary_file.diff',
256 264 [('US Warszawa.jpg', 'D',
257 265 {'added': 0,
258 266 'deleted': 0,
259 267 'binary': True,
260 268 'ops': {DEL_FILENODE: 'deleted file',
261 269 BIN_FILENODE: 'binary diff hidden'}}),
262 270 ]),
263 271 ('hg',
264 272 'hg_diff_chmod_and_mod_single_binary_file.diff',
265 273 [('gravatar.png', 'M',
266 274 {'added': 0,
267 275 'deleted': 0,
268 276 'binary': True,
269 277 'ops': {CHMOD_FILENODE: 'modified file chmod 100644 => 100755',
270 278 BIN_FILENODE: 'binary diff hidden'}}),
271 279 ]),
272 280 ('hg',
273 281 'hg_diff_chmod.diff',
274 282 [('file', 'M',
275 283 {'added': 0,
276 284 'deleted': 0,
277 285 'binary': True,
278 286 'ops': {CHMOD_FILENODE: 'modified file chmod 100755 => 100644'}}),
279 287 ]),
280 288 ('hg',
281 289 'hg_diff_rename_file.diff',
282 290 [('file_renamed', 'M',
283 291 {'added': 0,
284 292 'deleted': 0,
285 293 'binary': True,
286 294 'ops': {RENAMED_FILENODE: 'file renamed from file to file_renamed'}}),
287 295 ]),
288 296 ('hg',
289 297 'hg_diff_rename_and_chmod_file.diff',
290 298 [('README', 'M',
291 299 {'added': 0,
292 300 'deleted': 0,
293 301 'binary': True,
294 302 'ops': {CHMOD_FILENODE: 'modified file chmod 100644 => 100755',
295 303 RENAMED_FILENODE: 'file renamed from README.rst to README'}}),
296 304 ]),
297 305 ('hg',
298 306 'hg_diff_binary_and_normal.diff',
299 307 [('img/baseline-10px.png', 'A',
300 308 {'added': 0,
301 309 'deleted': 0,
302 310 'binary': True,
303 311 'ops': {NEW_FILENODE: 'new file 100644',
304 312 BIN_FILENODE: 'binary diff hidden'}}),
305 313 ('js/jquery/hashgrid.js', 'A',
306 314 {'added': 340,
307 315 'deleted': 0,
308 316 'binary': False,
309 317 'ops': {NEW_FILENODE: 'new file 100755'}}),
310 318 ('index.html', 'M',
311 319 {'added': 3,
312 320 'deleted': 2,
313 321 'binary': False,
314 322 'ops': {MOD_FILENODE: 'modified file'}}),
315 323 ('less/docs.less', 'M',
316 324 {'added': 34,
317 325 'deleted': 0,
318 326 'binary': False,
319 327 'ops': {MOD_FILENODE: 'modified file'}}),
320 328 ('less/scaffolding.less', 'M',
321 329 {'added': 1,
322 330 'deleted': 3,
323 331 'binary': False,
324 332 'ops': {MOD_FILENODE: 'modified file'}}),
325 333 ('readme.markdown', 'M',
326 334 {'added': 1,
327 335 'deleted': 10,
328 336 'binary': False,
329 337 'ops': {MOD_FILENODE: 'modified file'}}),
330 338 ('img/baseline-20px.png', 'D',
331 339 {'added': 0,
332 340 'deleted': 0,
333 341 'binary': True,
334 342 'ops': {DEL_FILENODE: 'deleted file',
335 343 BIN_FILENODE: 'binary diff hidden'}}),
336 344 ('js/global.js', 'D',
337 345 {'added': 0,
338 346 'deleted': 75,
339 347 'binary': False,
340 348 'ops': {DEL_FILENODE: 'deleted file'}})
341 349 ]),
342 350 ('git',
343 351 'git_diff_chmod.diff',
344 352 [('work-horus.xls', 'M',
345 353 {'added': 0,
346 354 'deleted': 0,
347 355 'binary': True,
348 356 'ops': {CHMOD_FILENODE: 'modified file chmod 100644 => 100755'}})
349 357 ]),
350 358 ('git',
351 359 'git_diff_rename_file.diff',
352 360 [('file.xls', 'M',
353 361 {'added': 0,
354 362 'deleted': 0,
355 363 'binary': True,
356 364 'ops': {
357 365 RENAMED_FILENODE: 'file renamed from work-horus.xls to file.xls'}})
358 366 ]),
359 367 ('git',
360 368 'git_diff_mod_single_binary_file.diff',
361 369 [('US Warszawa.jpg', 'M',
362 370 {'added': 0,
363 371 'deleted': 0,
364 372 'binary': True,
365 373 'ops': {MOD_FILENODE: 'modified file',
366 374 BIN_FILENODE: 'binary diff hidden'}})
367 375 ]),
368 376 ('git',
369 377 'git_diff_binary_and_normal.diff',
370 378 [('img/baseline-10px.png', 'A',
371 379 {'added': 0,
372 380 'deleted': 0,
373 381 'binary': True,
374 382 'ops': {NEW_FILENODE: 'new file 100644',
375 383 BIN_FILENODE: 'binary diff hidden'}}),
376 384 ('js/jquery/hashgrid.js', 'A',
377 385 {'added': 340,
378 386 'deleted': 0,
379 387 'binary': False,
380 388 'ops': {NEW_FILENODE: 'new file 100755'}}),
381 389 ('index.html', 'M',
382 390 {'added': 3,
383 391 'deleted': 2,
384 392 'binary': False,
385 393 'ops': {MOD_FILENODE: 'modified file'}}),
386 394 ('less/docs.less', 'M',
387 395 {'added': 34,
388 396 'deleted': 0,
389 397 'binary': False,
390 398 'ops': {MOD_FILENODE: 'modified file'}}),
391 399 ('less/scaffolding.less', 'M',
392 400 {'added': 1,
393 401 'deleted': 3,
394 402 'binary': False,
395 403 'ops': {MOD_FILENODE: 'modified file'}}),
396 404 ('readme.markdown', 'M',
397 405 {'added': 1,
398 406 'deleted': 10,
399 407 'binary': False,
400 408 'ops': {MOD_FILENODE: 'modified file'}}),
401 409 ('img/baseline-20px.png', 'D',
402 410 {'added': 0,
403 411 'deleted': 0,
404 412 'binary': True,
405 413 'ops': {DEL_FILENODE: 'deleted file',
406 414 BIN_FILENODE: 'binary diff hidden'}}),
407 415 ('js/global.js', 'D',
408 416 {'added': 0,
409 417 'deleted': 75,
410 418 'binary': False,
411 419 'ops': {DEL_FILENODE: 'deleted file'}}),
412 420 ]),
413 421 ('hg',
414 422 'diff_with_diff_data.diff',
415 423 [('vcs/backends/base.py', 'M',
416 424 {'added': 18,
417 425 'deleted': 2,
418 426 'binary': False,
419 427 'ops': {MOD_FILENODE: 'modified file'}}),
420 428 ('vcs/backends/git/repository.py', 'M',
421 429 {'added': 46,
422 430 'deleted': 15,
423 431 'binary': False,
424 432 'ops': {MOD_FILENODE: 'modified file'}}),
425 433 ('vcs/backends/hg.py', 'M',
426 434 {'added': 22,
427 435 'deleted': 3,
428 436 'binary': False,
429 437 'ops': {MOD_FILENODE: 'modified file'}}),
430 438 ('vcs/tests/test_git.py', 'M',
431 439 {'added': 5,
432 440 'deleted': 5,
433 441 'binary': False,
434 442 'ops': {MOD_FILENODE: 'modified file'}}),
435 443 ('vcs/tests/test_repository.py', 'M',
436 444 {'added': 174,
437 445 'deleted': 2,
438 446 'binary': False,
439 447 'ops': {MOD_FILENODE: 'modified file'}}),
440 448 ]),
441 449 ('hg',
442 450 'hg_diff_copy_file.diff',
443 451 [('file2', 'M',
444 452 {'added': 0,
445 453 'deleted': 0,
446 454 'binary': True,
447 455 'ops': {COPIED_FILENODE: 'file copied from file1 to file2'}}),
448 456 ]),
449 457 ('hg',
450 458 'hg_diff_copy_and_modify_file.diff',
451 459 [('file3', 'M',
452 460 {'added': 1,
453 461 'deleted': 0,
454 462 'binary': False,
455 463 'ops': {COPIED_FILENODE: 'file copied from file2 to file3',
456 464 MOD_FILENODE: 'modified file'}}),
457 465 ]),
458 466 ('hg',
459 467 'hg_diff_copy_and_chmod_file.diff',
460 468 [('file4', 'M',
461 469 {'added': 0,
462 470 'deleted': 0,
463 471 'binary': True,
464 472 'ops': {COPIED_FILENODE: 'file copied from file3 to file4',
465 473 CHMOD_FILENODE: 'modified file chmod 100644 => 100755'}}),
466 474 ]),
467 475 ('hg',
468 476 'hg_diff_copy_chmod_and_edit_file.diff',
469 477 [('file5', 'M',
470 478 {'added': 2,
471 479 'deleted': 1,
472 480 'binary': False,
473 481 'ops': {COPIED_FILENODE: 'file copied from file4 to file5',
474 482 CHMOD_FILENODE: 'modified file chmod 100755 => 100644',
475 483 MOD_FILENODE: 'modified file'}})]),
476 484
477 485 # Diffs to validate rename and copy file with space in its name
478 486 ('git',
479 487 'git_diff_rename_file_with_spaces.diff',
480 488 [('file_with_ two spaces.txt', 'M',
481 489 {'added': 0,
482 490 'deleted': 0,
483 491 'binary': True,
484 492 'ops': {
485 493 RENAMED_FILENODE: (
486 494 'file renamed from file_with_ spaces.txt to file_with_ '
487 495 ' two spaces.txt')}
488 496 }), ]),
489 497 ('hg',
490 498 'hg_diff_rename_file_with_spaces.diff',
491 499 [('file_changed _.txt', 'M',
492 500 {'added': 0,
493 501 'deleted': 0,
494 502 'binary': True,
495 503 'ops': {
496 504 RENAMED_FILENODE: (
497 505 'file renamed from file_ with update.txt to file_changed'
498 506 ' _.txt')}
499 507 }), ]),
500 508 ('hg',
501 509 'hg_diff_copy_file_with_spaces.diff',
502 510 [('file_copied_ with spaces.txt', 'M',
503 511 {'added': 0,
504 512 'deleted': 0,
505 513 'binary': True,
506 514 'ops': {
507 515 COPIED_FILENODE: (
508 516 'file copied from file_changed_without_spaces.txt to'
509 517 ' file_copied_ with spaces.txt')}
510 518 }),
511 519 ]),
512 520
513 521 # special signs from git
514 522 ('git',
515 523 'git_diff_binary_special_files.diff',
516 524 [('css/_Icon\\r', 'A',
517 525 {'added': 0,
518 526 'deleted': 0,
519 527 'binary': True,
520 528 'ops': {NEW_FILENODE: 'new file 100644',
521 529 BIN_FILENODE: 'binary diff hidden'}
522 530 }),
523 531 ]),
524 532 ('git',
525 533 'git_diff_binary_special_files_2.diff',
526 534 [('css/Icon\\r', 'A',
527 535 {'added': 0,
528 536 'deleted': 0,
529 537 'binary': True,
530 538 'ops': {NEW_FILENODE: 'new file 100644', }
531 539 }),
532 540 ]),
533 541
534 542 ('svn',
535 543 'svn_diff_binary_add_file.diff',
536 544 [('intl.dll', 'A',
537 545 {'added': 0,
538 546 'deleted': 0,
539 547 'binary': False,
540 548 'ops': {NEW_FILENODE: 'new file 10644',
541 549 #TODO(Marcink): depends on binary detection on svn patches
542 550 # BIN_FILENODE: 'binary diff hidden'
543 551 }
544 552 }),
545 553 ]),
546 554
547 555 ('svn',
548 556 'svn_diff_multiple_changes.diff',
549 557 [('trunk/doc/images/SettingsOverlay.png', 'M',
550 558 {'added': 0,
551 559 'deleted': 0,
552 560 'binary': False,
553 561 'ops': {MOD_FILENODE: 'modified file',
554 562 #TODO(Marcink): depends on binary detection on svn patches
555 563 # BIN_FILENODE: 'binary diff hidden'
556 564 }
557 565 }),
558 566 ('trunk/doc/source/de/tsvn_ch04.xml', 'M',
559 567 {'added': 89,
560 568 'deleted': 34,
561 569 'binary': False,
562 570 'ops': {MOD_FILENODE: 'modified file'}
563 571 }),
564 572 ('trunk/doc/source/en/tsvn_ch04.xml', 'M',
565 573 {'added': 66,
566 574 'deleted': 21,
567 575 'binary': False,
568 576 'ops': {MOD_FILENODE: 'modified file'}
569 577 }),
570 578 ('trunk/src/Changelog.txt', 'M',
571 579 {'added': 2,
572 580 'deleted': 0,
573 581 'binary': False,
574 582 'ops': {MOD_FILENODE: 'modified file'}
575 583 }),
576 584 ('trunk/src/Resources/TortoiseProcENG.rc', 'M',
577 585 {'added': 19,
578 586 'deleted': 13,
579 587 'binary': False,
580 588 'ops': {MOD_FILENODE: 'modified file'}
581 589 }),
582 590 ('trunk/src/TortoiseProc/SetOverlayPage.cpp', 'M',
583 591 {'added': 16,
584 592 'deleted': 1,
585 593 'binary': False,
586 594 'ops': {MOD_FILENODE: 'modified file'}
587 595 }),
588 596 ('trunk/src/TortoiseProc/SetOverlayPage.h', 'M',
589 597 {'added': 3,
590 598 'deleted': 0,
591 599 'binary': False,
592 600 'ops': {MOD_FILENODE: 'modified file'}
593 601 }),
594 602 ('trunk/src/TortoiseProc/resource.h', 'M',
595 603 {'added': 2,
596 604 'deleted': 0,
597 605 'binary': False,
598 606 'ops': {MOD_FILENODE: 'modified file'}
599 607 }),
600 608 ('trunk/src/TortoiseShell/ShellCache.h', 'M',
601 609 {'added': 50,
602 610 'deleted': 1,
603 611 'binary': False,
604 612 'ops': {MOD_FILENODE: 'modified file'}
605 613 }),
606 614 ]),
607 615
608 616
609 617 # TODO: mikhail: do we still need this?
610 618 # (
611 619 # 'hg',
612 620 # 'large_diff.diff',
613 621 # [
614 622 # ('.hgignore', 'A', {
615 623 # 'deleted': 0, 'binary': False, 'added': 3, 'ops': {
616 624 # 1: 'new file 100644'}}),
617 625 # (
618 626 # 'MANIFEST.in', 'A',
619 627 # {'deleted': 0, 'binary': False, 'added': 3, 'ops': {
620 628 # 1: 'new file 100644'}}),
621 629 # (
622 630 # 'README.txt', 'A',
623 631 # {'deleted': 0, 'binary': False, 'added': 19, 'ops': {
624 632 # 1: 'new file 100644'}}),
625 633 # (
626 634 # 'development.ini', 'A', {
627 635 # 'deleted': 0, 'binary': False, 'added': 116, 'ops': {
628 636 # 1: 'new file 100644'}}),
629 637 # (
630 638 # 'docs/index.txt', 'A', {
631 639 # 'deleted': 0, 'binary': False, 'added': 19, 'ops': {
632 640 # 1: 'new file 100644'}}),
633 641 # (
634 642 # 'ez_setup.py', 'A', {
635 643 # 'deleted': 0, 'binary': False, 'added': 276, 'ops': {
636 644 # 1: 'new file 100644'}}),
637 645 # (
638 646 # 'hgapp.py', 'A', {
639 647 # 'deleted': 0, 'binary': False, 'added': 26, 'ops': {
640 648 # 1: 'new file 100644'}}),
641 649 # (
642 650 # 'hgwebdir.config', 'A', {
643 651 # 'deleted': 0, 'binary': False, 'added': 21, 'ops': {
644 652 # 1: 'new file 100644'}}),
645 653 # (
646 654 # 'pylons_app.egg-info/PKG-INFO', 'A', {
647 655 # 'deleted': 0, 'binary': False, 'added': 10, 'ops': {
648 656 # 1: 'new file 100644'}}),
649 657 # (
650 658 # 'pylons_app.egg-info/SOURCES.txt', 'A', {
651 659 # 'deleted': 0, 'binary': False, 'added': 33, 'ops': {
652 660 # 1: 'new file 100644'}}),
653 661 # (
654 662 # 'pylons_app.egg-info/dependency_links.txt', 'A', {
655 663 # 'deleted': 0, 'binary': False, 'added': 1, 'ops': {
656 664 # 1: 'new file 100644'}}),
657 665 # ]
658 666 # ),
659 667 ]
660 668
661 669 DIFF_FIXTURES_WITH_CONTENT = [
662 670 (
663 671 'hg', 'hg_diff_single_file_change_newline.diff',
664 672 [
665 673 (
666 674 'file_b', # filename
667 675 'A', # change
668 676 { # stats
669 677 'added': 1,
670 678 'deleted': 0,
671 679 'binary': False,
672 680 'ops': {NEW_FILENODE: 'new file 100644', }
673 681 },
674 682 '@@ -0,0 +1 @@\n+test_content b\n' # diff
675 683 ),
676 684 ],
677 685 ),
678 686 (
679 687 'hg', 'hg_diff_double_file_change_newline.diff',
680 688 [
681 689 (
682 690 'file_b', # filename
683 691 'A', # change
684 692 { # stats
685 693 'added': 1,
686 694 'deleted': 0,
687 695 'binary': False,
688 696 'ops': {NEW_FILENODE: 'new file 100644', }
689 697 },
690 698 '@@ -0,0 +1 @@\n+test_content b\n' # diff
691 699 ),
692 700 (
693 701 'file_c', # filename
694 702 'A', # change
695 703 { # stats
696 704 'added': 1,
697 705 'deleted': 0,
698 706 'binary': False,
699 707 'ops': {NEW_FILENODE: 'new file 100644', }
700 708 },
701 709 '@@ -0,0 +1 @@\n+test_content c\n' # diff
702 710 ),
703 711 ],
704 712 ),
705 713 (
706 714 'hg', 'hg_diff_double_file_change_double_newline.diff',
707 715 [
708 716 (
709 717 'file_b', # filename
710 718 'A', # change
711 719 { # stats
712 720 'added': 1,
713 721 'deleted': 0,
714 722 'binary': False,
715 723 'ops': {NEW_FILENODE: 'new file 100644', }
716 724 },
717 725 '@@ -0,0 +1 @@\n+test_content b\n\n' # diff
718 726 ),
719 727 (
720 728 'file_c', # filename
721 729 'A', # change
722 730 { # stats
723 731 'added': 1,
724 732 'deleted': 0,
725 733 'binary': False,
726 734 'ops': {NEW_FILENODE: 'new file 100644', }
727 735 },
728 736 '@@ -0,0 +1 @@\n+test_content c\n' # diff
729 737 ),
730 738 ],
731 739 ),
732 740 (
733 741 'hg', 'hg_diff_four_file_change_newline.diff',
734 742 [
735 743 (
736 744 'file', # filename
737 745 'A', # change
738 746 { # stats
739 747 'added': 1,
740 748 'deleted': 0,
741 749 'binary': False,
742 750 'ops': {NEW_FILENODE: 'new file 100644', }
743 751 },
744 752 '@@ -0,0 +1,1 @@\n+file\n' # diff
745 753 ),
746 754 (
747 755 'file2', # filename
748 756 'A', # change
749 757 { # stats
750 758 'added': 1,
751 759 'deleted': 0,
752 760 'binary': False,
753 761 'ops': {NEW_FILENODE: 'new file 100644', }
754 762 },
755 763 '@@ -0,0 +1,1 @@\n+another line\n' # diff
756 764 ),
757 765 (
758 766 'file3', # filename
759 767 'A', # change
760 768 { # stats
761 769 'added': 1,
762 770 'deleted': 0,
763 771 'binary': False,
764 772 'ops': {NEW_FILENODE: 'new file 100644', }
765 773 },
766 774 '@@ -0,0 +1,1 @@\n+newline\n' # diff
767 775 ),
768 776 (
769 777 'file4', # filename
770 778 'A', # change
771 779 { # stats
772 780 'added': 1,
773 781 'deleted': 0,
774 782 'binary': False,
775 783 'ops': {NEW_FILENODE: 'new file 100644', }
776 784 },
777 785 '@@ -0,0 +1,1 @@\n+fil4\n\\ No newline at end of file' # diff
778 786 ),
779 787 ],
780 788 ),
781 789
782 790 ]
783 791
784 792
785 793 diff_class = {
786 794 'git': GitDiff,
787 795 'hg': MercurialDiff,
788 796 'svn': SubversionDiff,
789 797 }
790 798
791 799
792 800 @pytest.fixture(params=DIFF_FIXTURES)
793 801 def diff_fixture(request):
794 802 vcs, diff_fixture, expected = request.param
795 803 diff_txt = fixture.load_resource(diff_fixture)
796 804 diff = diff_class[vcs](diff_txt)
797 805 return diff, expected
798 806
799 807
800 808 def test_diff_lib(diff_fixture):
801 809 diff, expected_data = diff_fixture
802 810 diff_proc = DiffProcessor(diff)
803 811 diff_proc_d = diff_proc.prepare()
804 812 data = [(x['filename'], x['operation'], x['stats']) for x in diff_proc_d]
805 813 assert expected_data == data
806 814
807 815
808 816 @pytest.fixture(params=DIFF_FIXTURES_WITH_CONTENT)
809 817 def diff_fixture_w_content(request):
810 818 vcs, diff_fixture, expected = request.param
811 819 diff_txt = fixture.load_resource(diff_fixture)
812 820 diff = diff_class[vcs](diff_txt)
813 821 return diff, expected
814 822
815 823
816 824 def test_diff_lib_newlines(diff_fixture_w_content):
817 825 diff, expected_data = diff_fixture_w_content
818 826 diff_proc = DiffProcessor(diff)
819 827 diff_proc_d = diff_proc.prepare()
820 828 data = [(x['filename'], x['operation'], x['stats'], x['raw_diff'])
821 829 for x in diff_proc_d]
822 830 assert expected_data == data
General Comments 0
You need to be logged in to leave comments. Login now