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