##// 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,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