##// END OF EJS Templates
diffs: don't use highlite on the new ops lines
marcink -
r3082:25ff4b81 default
parent child Browse files
Show More
@@ -1,754 +1,762 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2018 RhodeCode GmbH
3 # Copyright (C) 2011-2018 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 import string
24 from itertools import groupby
23 from itertools import groupby
25
24
26 from pygments import lex
25 from pygments import lex
27 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
28 from pygments.lexers.special import TextLexer, Token
27 from pygments.lexers.special import TextLexer, Token
28 from pygments.lexers import get_lexer_by_name
29
29
30 from rhodecode.lib.helpers import (
30 from rhodecode.lib.helpers import (
31 get_lexer_for_filenode, html_escape, get_custom_lexer)
31 get_lexer_for_filenode, html_escape, get_custom_lexer)
32 from rhodecode.lib.utils2 import AttributeDict, StrictAttributeDict, safe_unicode
32 from rhodecode.lib.utils2 import AttributeDict, StrictAttributeDict, safe_unicode
33 from rhodecode.lib.vcs.nodes import FileNode
33 from rhodecode.lib.vcs.nodes import FileNode
34 from rhodecode.lib.vcs.exceptions import VCSError, NodeDoesNotExistError
34 from rhodecode.lib.vcs.exceptions import VCSError, NodeDoesNotExistError
35 from rhodecode.lib.diff_match_patch import diff_match_patch
35 from rhodecode.lib.diff_match_patch import diff_match_patch
36 from rhodecode.lib.diffs import LimitedDiffContainer, DEL_FILENODE, BIN_FILENODE
36 from rhodecode.lib.diffs import LimitedDiffContainer, DEL_FILENODE, BIN_FILENODE
37 from pygments.lexers import get_lexer_by_name
37
38
38
39 plain_text_lexer = get_lexer_by_name(
39 plain_text_lexer = get_lexer_by_name(
40 'text', stripall=False, stripnl=False, ensurenl=False)
40 'text', stripall=False, stripnl=False, ensurenl=False)
41
41
42
42
43 log = logging.getLogger(__name__)
43 log = logging.getLogger(__name__)
44
44
45
45
46 def filenode_as_lines_tokens(filenode, lexer=None):
46 def filenode_as_lines_tokens(filenode, lexer=None):
47 org_lexer = lexer
47 org_lexer = lexer
48 lexer = lexer or get_lexer_for_filenode(filenode)
48 lexer = lexer or get_lexer_for_filenode(filenode)
49 log.debug('Generating file node pygment tokens for %s, %s, org_lexer:%s',
49 log.debug('Generating file node pygment tokens for %s, %s, org_lexer:%s',
50 lexer, filenode, org_lexer)
50 lexer, filenode, org_lexer)
51 tokens = tokenize_string(filenode.content, lexer)
51 tokens = tokenize_string(filenode.content, lexer)
52 lines = split_token_stream(tokens)
52 lines = split_token_stream(tokens)
53 rv = list(lines)
53 rv = list(lines)
54 return rv
54 return rv
55
55
56
56
57 def tokenize_string(content, lexer):
57 def tokenize_string(content, lexer):
58 """
58 """
59 Use pygments to tokenize some content based on a lexer
59 Use pygments to tokenize some content based on a lexer
60 ensuring all original new lines and whitespace is preserved
60 ensuring all original new lines and whitespace is preserved
61 """
61 """
62
62
63 lexer.stripall = False
63 lexer.stripall = False
64 lexer.stripnl = False
64 lexer.stripnl = False
65 lexer.ensurenl = False
65 lexer.ensurenl = False
66
66
67 if isinstance(lexer, TextLexer):
67 if isinstance(lexer, TextLexer):
68 lexed = [(Token.Text, content)]
68 lexed = [(Token.Text, content)]
69 else:
69 else:
70 lexed = lex(content, lexer)
70 lexed = lex(content, lexer)
71
71
72 for token_type, token_text in lexed:
72 for token_type, token_text in lexed:
73 yield pygment_token_class(token_type), token_text
73 yield pygment_token_class(token_type), token_text
74
74
75
75
76 def split_token_stream(tokens):
76 def split_token_stream(tokens):
77 """
77 """
78 Take a list of (TokenType, text) tuples and split them by a string
78 Take a list of (TokenType, text) tuples and split them by a string
79
79
80 split_token_stream([(TEXT, 'some\ntext'), (TEXT, 'more\n')])
80 split_token_stream([(TEXT, 'some\ntext'), (TEXT, 'more\n')])
81 [(TEXT, 'some'), (TEXT, 'text'),
81 [(TEXT, 'some'), (TEXT, 'text'),
82 (TEXT, 'more'), (TEXT, 'text')]
82 (TEXT, 'more'), (TEXT, 'text')]
83 """
83 """
84
84
85 buffer = []
85 buffer = []
86 for token_class, token_text in tokens:
86 for token_class, token_text in tokens:
87 parts = token_text.split('\n')
87 parts = token_text.split('\n')
88 for part in parts[:-1]:
88 for part in parts[:-1]:
89 buffer.append((token_class, part))
89 buffer.append((token_class, part))
90 yield buffer
90 yield buffer
91 buffer = []
91 buffer = []
92
92
93 buffer.append((token_class, parts[-1]))
93 buffer.append((token_class, parts[-1]))
94
94
95 if buffer:
95 if buffer:
96 yield buffer
96 yield buffer
97
97
98
98
99 def filenode_as_annotated_lines_tokens(filenode):
99 def filenode_as_annotated_lines_tokens(filenode):
100 """
100 """
101 Take a file node and return a list of annotations => lines, if no annotation
101 Take a file node and return a list of annotations => lines, if no annotation
102 is found, it will be None.
102 is found, it will be None.
103
103
104 eg:
104 eg:
105
105
106 [
106 [
107 (annotation1, [
107 (annotation1, [
108 (1, line1_tokens_list),
108 (1, line1_tokens_list),
109 (2, line2_tokens_list),
109 (2, line2_tokens_list),
110 ]),
110 ]),
111 (annotation2, [
111 (annotation2, [
112 (3, line1_tokens_list),
112 (3, line1_tokens_list),
113 ]),
113 ]),
114 (None, [
114 (None, [
115 (4, line1_tokens_list),
115 (4, line1_tokens_list),
116 ]),
116 ]),
117 (annotation1, [
117 (annotation1, [
118 (5, line1_tokens_list),
118 (5, line1_tokens_list),
119 (6, line2_tokens_list),
119 (6, line2_tokens_list),
120 ])
120 ])
121 ]
121 ]
122 """
122 """
123
123
124 commit_cache = {} # cache commit_getter lookups
124 commit_cache = {} # cache commit_getter lookups
125
125
126 def _get_annotation(commit_id, commit_getter):
126 def _get_annotation(commit_id, commit_getter):
127 if commit_id not in commit_cache:
127 if commit_id not in commit_cache:
128 commit_cache[commit_id] = commit_getter()
128 commit_cache[commit_id] = commit_getter()
129 return commit_cache[commit_id]
129 return commit_cache[commit_id]
130
130
131 annotation_lookup = {
131 annotation_lookup = {
132 line_no: _get_annotation(commit_id, commit_getter)
132 line_no: _get_annotation(commit_id, commit_getter)
133 for line_no, commit_id, commit_getter, line_content
133 for line_no, commit_id, commit_getter, line_content
134 in filenode.annotate
134 in filenode.annotate
135 }
135 }
136
136
137 annotations_lines = ((annotation_lookup.get(line_no), line_no, tokens)
137 annotations_lines = ((annotation_lookup.get(line_no), line_no, tokens)
138 for line_no, tokens
138 for line_no, tokens
139 in enumerate(filenode_as_lines_tokens(filenode), 1))
139 in enumerate(filenode_as_lines_tokens(filenode), 1))
140
140
141 grouped_annotations_lines = groupby(annotations_lines, lambda x: x[0])
141 grouped_annotations_lines = groupby(annotations_lines, lambda x: x[0])
142
142
143 for annotation, group in grouped_annotations_lines:
143 for annotation, group in grouped_annotations_lines:
144 yield (
144 yield (
145 annotation, [(line_no, tokens)
145 annotation, [(line_no, tokens)
146 for (_, line_no, tokens) in group]
146 for (_, line_no, tokens) in group]
147 )
147 )
148
148
149
149
150 def render_tokenstream(tokenstream):
150 def render_tokenstream(tokenstream):
151 result = []
151 result = []
152 for token_class, token_ops_texts in rollup_tokenstream(tokenstream):
152 for token_class, token_ops_texts in rollup_tokenstream(tokenstream):
153
153
154 if token_class:
154 if token_class:
155 result.append(u'<span class="%s">' % token_class)
155 result.append(u'<span class="%s">' % token_class)
156 else:
156 else:
157 result.append(u'<span>')
157 result.append(u'<span>')
158
158
159 for op_tag, token_text in token_ops_texts:
159 for op_tag, token_text in token_ops_texts:
160
160
161 if op_tag:
161 if op_tag:
162 result.append(u'<%s>' % op_tag)
162 result.append(u'<%s>' % op_tag)
163
163
164 escaped_text = html_escape(token_text)
164 escaped_text = html_escape(token_text)
165
165
166 # TODO: dan: investigate showing hidden characters like space/nl/tab
166 # TODO: dan: investigate showing hidden characters like space/nl/tab
167 # escaped_text = escaped_text.replace(' ', '<sp> </sp>')
167 # escaped_text = escaped_text.replace(' ', '<sp> </sp>')
168 # escaped_text = escaped_text.replace('\n', '<nl>\n</nl>')
168 # escaped_text = escaped_text.replace('\n', '<nl>\n</nl>')
169 # escaped_text = escaped_text.replace('\t', '<tab>\t</tab>')
169 # escaped_text = escaped_text.replace('\t', '<tab>\t</tab>')
170
170
171 result.append(escaped_text)
171 result.append(escaped_text)
172
172
173 if op_tag:
173 if op_tag:
174 result.append(u'</%s>' % op_tag)
174 result.append(u'</%s>' % op_tag)
175
175
176 result.append(u'</span>')
176 result.append(u'</span>')
177
177
178 html = ''.join(result)
178 html = ''.join(result)
179 return html
179 return html
180
180
181
181
182 def rollup_tokenstream(tokenstream):
182 def rollup_tokenstream(tokenstream):
183 """
183 """
184 Group a token stream of the format:
184 Group a token stream of the format:
185
185
186 ('class', 'op', 'text')
186 ('class', 'op', 'text')
187 or
187 or
188 ('class', 'text')
188 ('class', 'text')
189
189
190 into
190 into
191
191
192 [('class1',
192 [('class1',
193 [('op1', 'text'),
193 [('op1', 'text'),
194 ('op2', 'text')]),
194 ('op2', 'text')]),
195 ('class2',
195 ('class2',
196 [('op3', 'text')])]
196 [('op3', 'text')])]
197
197
198 This is used to get the minimal tags necessary when
198 This is used to get the minimal tags necessary when
199 rendering to html eg for a token stream ie.
199 rendering to html eg for a token stream ie.
200
200
201 <span class="A"><ins>he</ins>llo</span>
201 <span class="A"><ins>he</ins>llo</span>
202 vs
202 vs
203 <span class="A"><ins>he</ins></span><span class="A">llo</span>
203 <span class="A"><ins>he</ins></span><span class="A">llo</span>
204
204
205 If a 2 tuple is passed in, the output op will be an empty string.
205 If a 2 tuple is passed in, the output op will be an empty string.
206
206
207 eg:
207 eg:
208
208
209 >>> rollup_tokenstream([('classA', '', 'h'),
209 >>> rollup_tokenstream([('classA', '', 'h'),
210 ('classA', 'del', 'ell'),
210 ('classA', 'del', 'ell'),
211 ('classA', '', 'o'),
211 ('classA', '', 'o'),
212 ('classB', '', ' '),
212 ('classB', '', ' '),
213 ('classA', '', 'the'),
213 ('classA', '', 'the'),
214 ('classA', '', 're'),
214 ('classA', '', 're'),
215 ])
215 ])
216
216
217 [('classA', [('', 'h'), ('del', 'ell'), ('', 'o')],
217 [('classA', [('', 'h'), ('del', 'ell'), ('', 'o')],
218 ('classB', [('', ' ')],
218 ('classB', [('', ' ')],
219 ('classA', [('', 'there')]]
219 ('classA', [('', 'there')]]
220
220
221 """
221 """
222 if tokenstream and len(tokenstream[0]) == 2:
222 if tokenstream and len(tokenstream[0]) == 2:
223 tokenstream = ((t[0], '', t[1]) for t in tokenstream)
223 tokenstream = ((t[0], '', t[1]) for t in tokenstream)
224
224
225 result = []
225 result = []
226 for token_class, op_list in groupby(tokenstream, lambda t: t[0]):
226 for token_class, op_list in groupby(tokenstream, lambda t: t[0]):
227 ops = []
227 ops = []
228 for token_op, token_text_list in groupby(op_list, lambda o: o[1]):
228 for token_op, token_text_list in groupby(op_list, lambda o: o[1]):
229 text_buffer = []
229 text_buffer = []
230 for t_class, t_op, t_text in token_text_list:
230 for t_class, t_op, t_text in token_text_list:
231 text_buffer.append(t_text)
231 text_buffer.append(t_text)
232 ops.append((token_op, ''.join(text_buffer)))
232 ops.append((token_op, ''.join(text_buffer)))
233 result.append((token_class, ops))
233 result.append((token_class, ops))
234 return result
234 return result
235
235
236
236
237 def tokens_diff(old_tokens, new_tokens, use_diff_match_patch=True):
237 def tokens_diff(old_tokens, new_tokens, use_diff_match_patch=True):
238 """
238 """
239 Converts a list of (token_class, token_text) tuples to a list of
239 Converts a list of (token_class, token_text) tuples to a list of
240 (token_class, token_op, token_text) tuples where token_op is one of
240 (token_class, token_op, token_text) tuples where token_op is one of
241 ('ins', 'del', '')
241 ('ins', 'del', '')
242
242
243 :param old_tokens: list of (token_class, token_text) tuples of old line
243 :param old_tokens: list of (token_class, token_text) tuples of old line
244 :param new_tokens: list of (token_class, token_text) tuples of new line
244 :param new_tokens: list of (token_class, token_text) tuples of new line
245 :param use_diff_match_patch: boolean, will use google's diff match patch
245 :param use_diff_match_patch: boolean, will use google's diff match patch
246 library which has options to 'smooth' out the character by character
246 library which has options to 'smooth' out the character by character
247 differences making nicer ins/del blocks
247 differences making nicer ins/del blocks
248 """
248 """
249
249
250 old_tokens_result = []
250 old_tokens_result = []
251 new_tokens_result = []
251 new_tokens_result = []
252
252
253 similarity = difflib.SequenceMatcher(None,
253 similarity = difflib.SequenceMatcher(None,
254 ''.join(token_text for token_class, token_text in old_tokens),
254 ''.join(token_text for token_class, token_text in old_tokens),
255 ''.join(token_text for token_class, token_text in new_tokens)
255 ''.join(token_text for token_class, token_text in new_tokens)
256 ).ratio()
256 ).ratio()
257
257
258 if similarity < 0.6: # return, the blocks are too different
258 if similarity < 0.6: # return, the blocks are too different
259 for token_class, token_text in old_tokens:
259 for token_class, token_text in old_tokens:
260 old_tokens_result.append((token_class, '', token_text))
260 old_tokens_result.append((token_class, '', token_text))
261 for token_class, token_text in new_tokens:
261 for token_class, token_text in new_tokens:
262 new_tokens_result.append((token_class, '', token_text))
262 new_tokens_result.append((token_class, '', token_text))
263 return old_tokens_result, new_tokens_result, similarity
263 return old_tokens_result, new_tokens_result, similarity
264
264
265 token_sequence_matcher = difflib.SequenceMatcher(None,
265 token_sequence_matcher = difflib.SequenceMatcher(None,
266 [x[1] for x in old_tokens],
266 [x[1] for x in old_tokens],
267 [x[1] for x in new_tokens])
267 [x[1] for x in new_tokens])
268
268
269 for tag, o1, o2, n1, n2 in token_sequence_matcher.get_opcodes():
269 for tag, o1, o2, n1, n2 in token_sequence_matcher.get_opcodes():
270 # check the differences by token block types first to give a more
270 # check the differences by token block types first to give a more
271 # nicer "block" level replacement vs character diffs
271 # nicer "block" level replacement vs character diffs
272
272
273 if tag == 'equal':
273 if tag == 'equal':
274 for token_class, token_text in old_tokens[o1:o2]:
274 for token_class, token_text in old_tokens[o1:o2]:
275 old_tokens_result.append((token_class, '', token_text))
275 old_tokens_result.append((token_class, '', token_text))
276 for token_class, token_text in new_tokens[n1:n2]:
276 for token_class, token_text in new_tokens[n1:n2]:
277 new_tokens_result.append((token_class, '', token_text))
277 new_tokens_result.append((token_class, '', token_text))
278 elif tag == 'delete':
278 elif tag == 'delete':
279 for token_class, token_text in old_tokens[o1:o2]:
279 for token_class, token_text in old_tokens[o1:o2]:
280 old_tokens_result.append((token_class, 'del', token_text))
280 old_tokens_result.append((token_class, 'del', token_text))
281 elif tag == 'insert':
281 elif tag == 'insert':
282 for token_class, token_text in new_tokens[n1:n2]:
282 for token_class, token_text in new_tokens[n1:n2]:
283 new_tokens_result.append((token_class, 'ins', token_text))
283 new_tokens_result.append((token_class, 'ins', token_text))
284 elif tag == 'replace':
284 elif tag == 'replace':
285 # if same type token blocks must be replaced, do a diff on the
285 # if same type token blocks must be replaced, do a diff on the
286 # characters in the token blocks to show individual changes
286 # characters in the token blocks to show individual changes
287
287
288 old_char_tokens = []
288 old_char_tokens = []
289 new_char_tokens = []
289 new_char_tokens = []
290 for token_class, token_text in old_tokens[o1:o2]:
290 for token_class, token_text in old_tokens[o1:o2]:
291 for char in token_text:
291 for char in token_text:
292 old_char_tokens.append((token_class, char))
292 old_char_tokens.append((token_class, char))
293
293
294 for token_class, token_text in new_tokens[n1:n2]:
294 for token_class, token_text in new_tokens[n1:n2]:
295 for char in token_text:
295 for char in token_text:
296 new_char_tokens.append((token_class, char))
296 new_char_tokens.append((token_class, char))
297
297
298 old_string = ''.join([token_text for
298 old_string = ''.join([token_text for
299 token_class, token_text in old_char_tokens])
299 token_class, token_text in old_char_tokens])
300 new_string = ''.join([token_text for
300 new_string = ''.join([token_text for
301 token_class, token_text in new_char_tokens])
301 token_class, token_text in new_char_tokens])
302
302
303 char_sequence = difflib.SequenceMatcher(
303 char_sequence = difflib.SequenceMatcher(
304 None, old_string, new_string)
304 None, old_string, new_string)
305 copcodes = char_sequence.get_opcodes()
305 copcodes = char_sequence.get_opcodes()
306 obuffer, nbuffer = [], []
306 obuffer, nbuffer = [], []
307
307
308 if use_diff_match_patch:
308 if use_diff_match_patch:
309 dmp = diff_match_patch()
309 dmp = diff_match_patch()
310 dmp.Diff_EditCost = 11 # TODO: dan: extract this to a setting
310 dmp.Diff_EditCost = 11 # TODO: dan: extract this to a setting
311 reps = dmp.diff_main(old_string, new_string)
311 reps = dmp.diff_main(old_string, new_string)
312 dmp.diff_cleanupEfficiency(reps)
312 dmp.diff_cleanupEfficiency(reps)
313
313
314 a, b = 0, 0
314 a, b = 0, 0
315 for op, rep in reps:
315 for op, rep in reps:
316 l = len(rep)
316 l = len(rep)
317 if op == 0:
317 if op == 0:
318 for i, c in enumerate(rep):
318 for i, c in enumerate(rep):
319 obuffer.append((old_char_tokens[a+i][0], '', c))
319 obuffer.append((old_char_tokens[a+i][0], '', c))
320 nbuffer.append((new_char_tokens[b+i][0], '', c))
320 nbuffer.append((new_char_tokens[b+i][0], '', c))
321 a += l
321 a += l
322 b += l
322 b += l
323 elif op == -1:
323 elif op == -1:
324 for i, c in enumerate(rep):
324 for i, c in enumerate(rep):
325 obuffer.append((old_char_tokens[a+i][0], 'del', c))
325 obuffer.append((old_char_tokens[a+i][0], 'del', c))
326 a += l
326 a += l
327 elif op == 1:
327 elif op == 1:
328 for i, c in enumerate(rep):
328 for i, c in enumerate(rep):
329 nbuffer.append((new_char_tokens[b+i][0], 'ins', c))
329 nbuffer.append((new_char_tokens[b+i][0], 'ins', c))
330 b += l
330 b += l
331 else:
331 else:
332 for ctag, co1, co2, cn1, cn2 in copcodes:
332 for ctag, co1, co2, cn1, cn2 in copcodes:
333 if ctag == 'equal':
333 if ctag == 'equal':
334 for token_class, token_text in old_char_tokens[co1:co2]:
334 for token_class, token_text in old_char_tokens[co1:co2]:
335 obuffer.append((token_class, '', token_text))
335 obuffer.append((token_class, '', token_text))
336 for token_class, token_text in new_char_tokens[cn1:cn2]:
336 for token_class, token_text in new_char_tokens[cn1:cn2]:
337 nbuffer.append((token_class, '', token_text))
337 nbuffer.append((token_class, '', token_text))
338 elif ctag == 'delete':
338 elif ctag == 'delete':
339 for token_class, token_text in old_char_tokens[co1:co2]:
339 for token_class, token_text in old_char_tokens[co1:co2]:
340 obuffer.append((token_class, 'del', token_text))
340 obuffer.append((token_class, 'del', token_text))
341 elif ctag == 'insert':
341 elif ctag == 'insert':
342 for token_class, token_text in new_char_tokens[cn1:cn2]:
342 for token_class, token_text in new_char_tokens[cn1:cn2]:
343 nbuffer.append((token_class, 'ins', token_text))
343 nbuffer.append((token_class, 'ins', token_text))
344 elif ctag == 'replace':
344 elif ctag == 'replace':
345 for token_class, token_text in old_char_tokens[co1:co2]:
345 for token_class, token_text in old_char_tokens[co1:co2]:
346 obuffer.append((token_class, 'del', token_text))
346 obuffer.append((token_class, 'del', token_text))
347 for token_class, token_text in new_char_tokens[cn1:cn2]:
347 for token_class, token_text in new_char_tokens[cn1:cn2]:
348 nbuffer.append((token_class, 'ins', token_text))
348 nbuffer.append((token_class, 'ins', token_text))
349
349
350 old_tokens_result.extend(obuffer)
350 old_tokens_result.extend(obuffer)
351 new_tokens_result.extend(nbuffer)
351 new_tokens_result.extend(nbuffer)
352
352
353 return old_tokens_result, new_tokens_result, similarity
353 return old_tokens_result, new_tokens_result, similarity
354
354
355
355
356 def diffset_node_getter(commit):
356 def diffset_node_getter(commit):
357 def get_node(fname):
357 def get_node(fname):
358 try:
358 try:
359 return commit.get_node(fname)
359 return commit.get_node(fname)
360 except NodeDoesNotExistError:
360 except NodeDoesNotExistError:
361 return None
361 return None
362
362
363 return get_node
363 return get_node
364
364
365
365
366 class DiffSet(object):
366 class DiffSet(object):
367 """
367 """
368 An object for parsing the diff result from diffs.DiffProcessor and
368 An object for parsing the diff result from diffs.DiffProcessor and
369 adding highlighting, side by side/unified renderings and line diffs
369 adding highlighting, side by side/unified renderings and line diffs
370 """
370 """
371
371
372 HL_REAL = 'REAL' # highlights using original file, slow
372 HL_REAL = 'REAL' # highlights using original file, slow
373 HL_FAST = 'FAST' # highlights using just the line, fast but not correct
373 HL_FAST = 'FAST' # highlights using just the line, fast but not correct
374 # in the case of multiline code
374 # in the case of multiline code
375 HL_NONE = 'NONE' # no highlighting, fastest
375 HL_NONE = 'NONE' # no highlighting, fastest
376
376
377 def __init__(self, highlight_mode=HL_REAL, repo_name=None,
377 def __init__(self, highlight_mode=HL_REAL, repo_name=None,
378 source_repo_name=None,
378 source_repo_name=None,
379 source_node_getter=lambda filename: None,
379 source_node_getter=lambda filename: None,
380 target_node_getter=lambda filename: None,
380 target_node_getter=lambda filename: None,
381 source_nodes=None, target_nodes=None,
381 source_nodes=None, target_nodes=None,
382 # files over this size will use fast highlighting
382 # files over this size will use fast highlighting
383 max_file_size_limit=150 * 1024,
383 max_file_size_limit=150 * 1024,
384 ):
384 ):
385
385
386 self.highlight_mode = highlight_mode
386 self.highlight_mode = highlight_mode
387 self.highlighted_filenodes = {}
387 self.highlighted_filenodes = {}
388 self.source_node_getter = source_node_getter
388 self.source_node_getter = source_node_getter
389 self.target_node_getter = target_node_getter
389 self.target_node_getter = target_node_getter
390 self.source_nodes = source_nodes or {}
390 self.source_nodes = source_nodes or {}
391 self.target_nodes = target_nodes or {}
391 self.target_nodes = target_nodes or {}
392 self.repo_name = repo_name
392 self.repo_name = repo_name
393 self.source_repo_name = source_repo_name or repo_name
393 self.source_repo_name = source_repo_name or repo_name
394 self.max_file_size_limit = max_file_size_limit
394 self.max_file_size_limit = max_file_size_limit
395
395
396 def render_patchset(self, patchset, source_ref=None, target_ref=None):
396 def render_patchset(self, patchset, source_ref=None, target_ref=None):
397 diffset = AttributeDict(dict(
397 diffset = AttributeDict(dict(
398 lines_added=0,
398 lines_added=0,
399 lines_deleted=0,
399 lines_deleted=0,
400 changed_files=0,
400 changed_files=0,
401 files=[],
401 files=[],
402 file_stats={},
402 file_stats={},
403 limited_diff=isinstance(patchset, LimitedDiffContainer),
403 limited_diff=isinstance(patchset, LimitedDiffContainer),
404 repo_name=self.repo_name,
404 repo_name=self.repo_name,
405 source_repo_name=self.source_repo_name,
405 source_repo_name=self.source_repo_name,
406 source_ref=source_ref,
406 source_ref=source_ref,
407 target_ref=target_ref,
407 target_ref=target_ref,
408 ))
408 ))
409 for patch in patchset:
409 for patch in patchset:
410 diffset.file_stats[patch['filename']] = patch['stats']
410 diffset.file_stats[patch['filename']] = patch['stats']
411 filediff = self.render_patch(patch)
411 filediff = self.render_patch(patch)
412 filediff.diffset = StrictAttributeDict(dict(
412 filediff.diffset = StrictAttributeDict(dict(
413 source_ref=diffset.source_ref,
413 source_ref=diffset.source_ref,
414 target_ref=diffset.target_ref,
414 target_ref=diffset.target_ref,
415 repo_name=diffset.repo_name,
415 repo_name=diffset.repo_name,
416 source_repo_name=diffset.source_repo_name,
416 source_repo_name=diffset.source_repo_name,
417 ))
417 ))
418 diffset.files.append(filediff)
418 diffset.files.append(filediff)
419 diffset.changed_files += 1
419 diffset.changed_files += 1
420 if not patch['stats']['binary']:
420 if not patch['stats']['binary']:
421 diffset.lines_added += patch['stats']['added']
421 diffset.lines_added += patch['stats']['added']
422 diffset.lines_deleted += patch['stats']['deleted']
422 diffset.lines_deleted += patch['stats']['deleted']
423
423
424 return diffset
424 return diffset
425
425
426 _lexer_cache = {}
426 _lexer_cache = {}
427
427
428 def _get_lexer_for_filename(self, filename, filenode=None):
428 def _get_lexer_for_filename(self, filename, filenode=None):
429 # cached because we might need to call it twice for source/target
429 # cached because we might need to call it twice for source/target
430 if filename not in self._lexer_cache:
430 if filename not in self._lexer_cache:
431 if filenode:
431 if filenode:
432 lexer = filenode.lexer
432 lexer = filenode.lexer
433 extension = filenode.extension
433 extension = filenode.extension
434 else:
434 else:
435 lexer = FileNode.get_lexer(filename=filename)
435 lexer = FileNode.get_lexer(filename=filename)
436 extension = filename.split('.')[-1]
436 extension = filename.split('.')[-1]
437
437
438 lexer = get_custom_lexer(extension) or lexer
438 lexer = get_custom_lexer(extension) or lexer
439 self._lexer_cache[filename] = lexer
439 self._lexer_cache[filename] = lexer
440 return self._lexer_cache[filename]
440 return self._lexer_cache[filename]
441
441
442 def render_patch(self, patch):
442 def render_patch(self, patch):
443 log.debug('rendering diff for %r', patch['filename'])
443 log.debug('rendering diff for %r', patch['filename'])
444
444
445 source_filename = patch['original_filename']
445 source_filename = patch['original_filename']
446 target_filename = patch['filename']
446 target_filename = patch['filename']
447
447
448 source_lexer = plain_text_lexer
448 source_lexer = plain_text_lexer
449 target_lexer = plain_text_lexer
449 target_lexer = plain_text_lexer
450
450
451 if not patch['stats']['binary']:
451 if not patch['stats']['binary']:
452 if self.highlight_mode == self.HL_REAL:
452 node_hl_mode = self.HL_NONE if patch['chunks'] == [] else None
453 hl_mode = node_hl_mode or self.highlight_mode
454
455 if hl_mode == self.HL_REAL:
453 if (source_filename and patch['operation'] in ('D', 'M')
456 if (source_filename and patch['operation'] in ('D', 'M')
454 and source_filename not in self.source_nodes):
457 and source_filename not in self.source_nodes):
455 self.source_nodes[source_filename] = (
458 self.source_nodes[source_filename] = (
456 self.source_node_getter(source_filename))
459 self.source_node_getter(source_filename))
457
460
458 if (target_filename and patch['operation'] in ('A', 'M')
461 if (target_filename and patch['operation'] in ('A', 'M')
459 and target_filename not in self.target_nodes):
462 and target_filename not in self.target_nodes):
460 self.target_nodes[target_filename] = (
463 self.target_nodes[target_filename] = (
461 self.target_node_getter(target_filename))
464 self.target_node_getter(target_filename))
462
465
463 elif self.highlight_mode == self.HL_FAST:
466 elif hl_mode == self.HL_FAST:
464 source_lexer = self._get_lexer_for_filename(source_filename)
467 source_lexer = self._get_lexer_for_filename(source_filename)
465 target_lexer = self._get_lexer_for_filename(target_filename)
468 target_lexer = self._get_lexer_for_filename(target_filename)
466
469
467 source_file = self.source_nodes.get(source_filename, source_filename)
470 source_file = self.source_nodes.get(source_filename, source_filename)
468 target_file = self.target_nodes.get(target_filename, target_filename)
471 target_file = self.target_nodes.get(target_filename, target_filename)
469
472
470 source_filenode, target_filenode = None, None
473 source_filenode, target_filenode = None, None
471
474
472 # TODO: dan: FileNode.lexer works on the content of the file - which
475 # TODO: dan: FileNode.lexer works on the content of the file - which
473 # can be slow - issue #4289 explains a lexer clean up - which once
476 # can be slow - issue #4289 explains a lexer clean up - which once
474 # done can allow caching a lexer for a filenode to avoid the file lookup
477 # done can allow caching a lexer for a filenode to avoid the file lookup
475 if isinstance(source_file, FileNode):
478 if isinstance(source_file, FileNode):
476 source_filenode = source_file
479 source_filenode = source_file
477 #source_lexer = source_file.lexer
480 #source_lexer = source_file.lexer
478 source_lexer = self._get_lexer_for_filename(source_filename)
481 source_lexer = self._get_lexer_for_filename(source_filename)
479 source_file.lexer = source_lexer
482 source_file.lexer = source_lexer
480
483
481 if isinstance(target_file, FileNode):
484 if isinstance(target_file, FileNode):
482 target_filenode = target_file
485 target_filenode = target_file
483 #target_lexer = target_file.lexer
486 #target_lexer = target_file.lexer
484 target_lexer = self._get_lexer_for_filename(target_filename)
487 target_lexer = self._get_lexer_for_filename(target_filename)
485 target_file.lexer = target_lexer
488 target_file.lexer = target_lexer
486
489
487 source_file_path, target_file_path = None, None
490 source_file_path, target_file_path = None, None
488
491
489 if source_filename != '/dev/null':
492 if source_filename != '/dev/null':
490 source_file_path = source_filename
493 source_file_path = source_filename
491 if target_filename != '/dev/null':
494 if target_filename != '/dev/null':
492 target_file_path = target_filename
495 target_file_path = target_filename
493
496
494 source_file_type = source_lexer.name
497 source_file_type = source_lexer.name
495 target_file_type = target_lexer.name
498 target_file_type = target_lexer.name
496
499
497 filediff = AttributeDict({
500 filediff = AttributeDict({
498 'source_file_path': source_file_path,
501 'source_file_path': source_file_path,
499 'target_file_path': target_file_path,
502 'target_file_path': target_file_path,
500 'source_filenode': source_filenode,
503 'source_filenode': source_filenode,
501 'target_filenode': target_filenode,
504 'target_filenode': target_filenode,
502 'source_file_type': target_file_type,
505 'source_file_type': target_file_type,
503 'target_file_type': source_file_type,
506 'target_file_type': source_file_type,
504 'patch': {'filename': patch['filename'], 'stats': patch['stats']},
507 'patch': {'filename': patch['filename'], 'stats': patch['stats']},
505 'operation': patch['operation'],
508 'operation': patch['operation'],
506 'source_mode': patch['stats']['old_mode'],
509 'source_mode': patch['stats']['old_mode'],
507 'target_mode': patch['stats']['new_mode'],
510 'target_mode': patch['stats']['new_mode'],
508 'limited_diff': isinstance(patch, LimitedDiffContainer),
511 'limited_diff': isinstance(patch, LimitedDiffContainer),
509 'hunks': [],
512 'hunks': [],
510 'hunk_ops': None,
513 'hunk_ops': None,
511 'diffset': self,
514 'diffset': self,
512 })
515 })
513
516 file_chunks = patch['chunks'][1:]
514 for hunk in patch['chunks'][1:]:
517 for hunk in file_chunks:
515 hunkbit = self.parse_hunk(hunk, source_file, target_file)
518 hunkbit = self.parse_hunk(hunk, source_file, target_file)
516 hunkbit.source_file_path = source_file_path
519 hunkbit.source_file_path = source_file_path
517 hunkbit.target_file_path = target_file_path
520 hunkbit.target_file_path = target_file_path
518 filediff.hunks.append(hunkbit)
521 filediff.hunks.append(hunkbit)
519
522
520 # Simulate hunk on OPS type line which doesn't really contain any diff
523 # Simulate hunk on OPS type line which doesn't really contain any diff
521 # this allows commenting on those
524 # this allows commenting on those
522 actions = []
525 if not file_chunks:
523 for op_id, op_text in filediff.patch['stats']['ops'].items():
526 actions = []
524 if op_id == DEL_FILENODE:
527 for op_id, op_text in filediff.patch['stats']['ops'].items():
525 actions.append(u'file was deleted')
528 if op_id == DEL_FILENODE:
526 elif op_id == BIN_FILENODE:
529 actions.append(u'file was deleted')
527 actions.append(u'binary diff hidden')
530 elif op_id == BIN_FILENODE:
528 else:
531 actions.append(u'binary diff hidden')
529 actions.append(safe_unicode(op_text))
532 else:
530 action_line = u'FILE WITHOUT CONTENT: ' + \
533 actions.append(safe_unicode(op_text))
531 u', '.join(map(string.upper, actions)) or u'UNDEFINED_ACTION'
534 action_line = u'NO CONTENT: ' + \
535 u', '.join(actions) or u'UNDEFINED_ACTION'
532
536
533 hunk_ops = {'source_length': 0, 'source_start': 0,
537 hunk_ops = {'source_length': 0, 'source_start': 0,
534 'lines': [
538 'lines': [
535 {'new_lineno': 0, 'old_lineno': 1,
539 {'new_lineno': 0, 'old_lineno': 1,
536 'action': 'unmod', 'line': action_line}
540 'action': 'unmod-no-hl', 'line': action_line}
537 ],
541 ],
538 'section_header': u'', 'target_start': 1, 'target_length': 1}
542 'section_header': u'', 'target_start': 1, 'target_length': 1}
539
543
540 hunkbit = self.parse_hunk(hunk_ops, source_file, target_file)
544 hunkbit = self.parse_hunk(hunk_ops, source_file, target_file)
541 hunkbit.source_file_path = source_file_path
545 hunkbit.source_file_path = source_file_path
542 hunkbit.target_file_path = target_file_path
546 hunkbit.target_file_path = target_file_path
543 filediff.hunk_ops = hunkbit
547 filediff.hunk_ops = hunkbit
544 return filediff
548 return filediff
545
549
546 def parse_hunk(self, hunk, source_file, target_file):
550 def parse_hunk(self, hunk, source_file, target_file):
547 result = AttributeDict(dict(
551 result = AttributeDict(dict(
548 source_start=hunk['source_start'],
552 source_start=hunk['source_start'],
549 source_length=hunk['source_length'],
553 source_length=hunk['source_length'],
550 target_start=hunk['target_start'],
554 target_start=hunk['target_start'],
551 target_length=hunk['target_length'],
555 target_length=hunk['target_length'],
552 section_header=hunk['section_header'],
556 section_header=hunk['section_header'],
553 lines=[],
557 lines=[],
554 ))
558 ))
555 before, after = [], []
559 before, after = [], []
556
560
557 for line in hunk['lines']:
561 for line in hunk['lines']:
558
562 if line['action'] in ['unmod', 'unmod-no-hl']:
559 if line['action'] == 'unmod':
563 no_hl = line['action'] == 'unmod-no-hl'
560 result.lines.extend(
564 result.lines.extend(
561 self.parse_lines(before, after, source_file, target_file))
565 self.parse_lines(before, after, source_file, target_file, no_hl=no_hl))
562 after.append(line)
566 after.append(line)
563 before.append(line)
567 before.append(line)
564 elif line['action'] == 'add':
568 elif line['action'] == 'add':
565 after.append(line)
569 after.append(line)
566 elif line['action'] == 'del':
570 elif line['action'] == 'del':
567 before.append(line)
571 before.append(line)
568 elif line['action'] == 'old-no-nl':
572 elif line['action'] == 'old-no-nl':
569 before.append(line)
573 before.append(line)
570 elif line['action'] == 'new-no-nl':
574 elif line['action'] == 'new-no-nl':
571 after.append(line)
575 after.append(line)
572
576
577 all_actions = [x['action'] for x in after] + [x['action'] for x in before]
578 no_hl = {x for x in all_actions} == {'unmod-no-hl'}
573 result.lines.extend(
579 result.lines.extend(
574 self.parse_lines(before, after, source_file, target_file))
580 self.parse_lines(before, after, source_file, target_file, no_hl=no_hl))
581 # NOTE(marcink): we must keep list() call here so we can cache the result...
575 result.unified = list(self.as_unified(result.lines))
582 result.unified = list(self.as_unified(result.lines))
576 result.sideside = result.lines
583 result.sideside = result.lines
577
584
578 return result
585 return result
579
586
580 def parse_lines(self, before_lines, after_lines, source_file, target_file):
587 def parse_lines(self, before_lines, after_lines, source_file, target_file,
588 no_hl=False):
581 # TODO: dan: investigate doing the diff comparison and fast highlighting
589 # TODO: dan: investigate doing the diff comparison and fast highlighting
582 # on the entire before and after buffered block lines rather than by
590 # on the entire before and after buffered block lines rather than by
583 # line, this means we can get better 'fast' highlighting if the context
591 # line, this means we can get better 'fast' highlighting if the context
584 # allows it - eg.
592 # allows it - eg.
585 # line 4: """
593 # line 4: """
586 # line 5: this gets highlighted as a string
594 # line 5: this gets highlighted as a string
587 # line 6: """
595 # line 6: """
588
596
589 lines = []
597 lines = []
590
598
591 before_newline = AttributeDict()
599 before_newline = AttributeDict()
592 after_newline = AttributeDict()
600 after_newline = AttributeDict()
593 if before_lines and before_lines[-1]['action'] == 'old-no-nl':
601 if before_lines and before_lines[-1]['action'] == 'old-no-nl':
594 before_newline_line = before_lines.pop(-1)
602 before_newline_line = before_lines.pop(-1)
595 before_newline.content = '\n {}'.format(
603 before_newline.content = '\n {}'.format(
596 render_tokenstream(
604 render_tokenstream(
597 [(x[0], '', x[1])
605 [(x[0], '', x[1])
598 for x in [('nonl', before_newline_line['line'])]]))
606 for x in [('nonl', before_newline_line['line'])]]))
599
607
600 if after_lines and after_lines[-1]['action'] == 'new-no-nl':
608 if after_lines and after_lines[-1]['action'] == 'new-no-nl':
601 after_newline_line = after_lines.pop(-1)
609 after_newline_line = after_lines.pop(-1)
602 after_newline.content = '\n {}'.format(
610 after_newline.content = '\n {}'.format(
603 render_tokenstream(
611 render_tokenstream(
604 [(x[0], '', x[1])
612 [(x[0], '', x[1])
605 for x in [('nonl', after_newline_line['line'])]]))
613 for x in [('nonl', after_newline_line['line'])]]))
606
614
607 while before_lines or after_lines:
615 while before_lines or after_lines:
608 before, after = None, None
616 before, after = None, None
609 before_tokens, after_tokens = None, None
617 before_tokens, after_tokens = None, None
610
618
611 if before_lines:
619 if before_lines:
612 before = before_lines.pop(0)
620 before = before_lines.pop(0)
613 if after_lines:
621 if after_lines:
614 after = after_lines.pop(0)
622 after = after_lines.pop(0)
615
623
616 original = AttributeDict()
624 original = AttributeDict()
617 modified = AttributeDict()
625 modified = AttributeDict()
618
626
619 if before:
627 if before:
620 if before['action'] == 'old-no-nl':
628 if before['action'] == 'old-no-nl':
621 before_tokens = [('nonl', before['line'])]
629 before_tokens = [('nonl', before['line'])]
622 else:
630 else:
623 before_tokens = self.get_line_tokens(
631 before_tokens = self.get_line_tokens(
624 line_text=before['line'],
632 line_text=before['line'], line_number=before['old_lineno'],
625 line_number=before['old_lineno'],
633 input_file=source_file, no_hl=no_hl)
626 file=source_file)
627 original.lineno = before['old_lineno']
634 original.lineno = before['old_lineno']
628 original.content = before['line']
635 original.content = before['line']
629 original.action = self.action_to_op(before['action'])
636 original.action = self.action_to_op(before['action'])
630
637
631 original.get_comment_args = (
638 original.get_comment_args = (
632 source_file, 'o', before['old_lineno'])
639 source_file, 'o', before['old_lineno'])
633
640
634 if after:
641 if after:
635 if after['action'] == 'new-no-nl':
642 if after['action'] == 'new-no-nl':
636 after_tokens = [('nonl', after['line'])]
643 after_tokens = [('nonl', after['line'])]
637 else:
644 else:
638 after_tokens = self.get_line_tokens(
645 after_tokens = self.get_line_tokens(
639 line_text=after['line'], line_number=after['new_lineno'],
646 line_text=after['line'], line_number=after['new_lineno'],
640 file=target_file)
647 input_file=target_file, no_hl=no_hl)
641 modified.lineno = after['new_lineno']
648 modified.lineno = after['new_lineno']
642 modified.content = after['line']
649 modified.content = after['line']
643 modified.action = self.action_to_op(after['action'])
650 modified.action = self.action_to_op(after['action'])
644
651
645 modified.get_comment_args = (
652 modified.get_comment_args = (target_file, 'n', after['new_lineno'])
646 target_file, 'n', after['new_lineno'])
647
653
648 # diff the lines
654 # diff the lines
649 if before_tokens and after_tokens:
655 if before_tokens and after_tokens:
650 o_tokens, m_tokens, similarity = tokens_diff(
656 o_tokens, m_tokens, similarity = tokens_diff(
651 before_tokens, after_tokens)
657 before_tokens, after_tokens)
652 original.content = render_tokenstream(o_tokens)
658 original.content = render_tokenstream(o_tokens)
653 modified.content = render_tokenstream(m_tokens)
659 modified.content = render_tokenstream(m_tokens)
654 elif before_tokens:
660 elif before_tokens:
655 original.content = render_tokenstream(
661 original.content = render_tokenstream(
656 [(x[0], '', x[1]) for x in before_tokens])
662 [(x[0], '', x[1]) for x in before_tokens])
657 elif after_tokens:
663 elif after_tokens:
658 modified.content = render_tokenstream(
664 modified.content = render_tokenstream(
659 [(x[0], '', x[1]) for x in after_tokens])
665 [(x[0], '', x[1]) for x in after_tokens])
660
666
661 if not before_lines and before_newline:
667 if not before_lines and before_newline:
662 original.content += before_newline.content
668 original.content += before_newline.content
663 before_newline = None
669 before_newline = None
664 if not after_lines and after_newline:
670 if not after_lines and after_newline:
665 modified.content += after_newline.content
671 modified.content += after_newline.content
666 after_newline = None
672 after_newline = None
667
673
668 lines.append(AttributeDict({
674 lines.append(AttributeDict({
669 'original': original,
675 'original': original,
670 'modified': modified,
676 'modified': modified,
671 }))
677 }))
672
678
673 return lines
679 return lines
674
680
675 def get_line_tokens(self, line_text, line_number, file=None):
681 def get_line_tokens(self, line_text, line_number, input_file=None, no_hl=False):
676 filenode = None
682 filenode = None
677 filename = None
683 filename = None
678
684
679 if isinstance(file, basestring):
685 if isinstance(input_file, basestring):
680 filename = file
686 filename = input_file
681 elif isinstance(file, FileNode):
687 elif isinstance(input_file, FileNode):
682 filenode = file
688 filenode = input_file
683 filename = file.unicode_path
689 filename = input_file.unicode_path
684
690
685 if self.highlight_mode == self.HL_REAL and filenode:
691 hl_mode = self.HL_NONE if no_hl else self.highlight_mode
692 if hl_mode == self.HL_REAL and filenode:
686 lexer = self._get_lexer_for_filename(filename)
693 lexer = self._get_lexer_for_filename(filename)
687 file_size_allowed = file.size < self.max_file_size_limit
694 file_size_allowed = input_file.size < self.max_file_size_limit
688 if line_number and file_size_allowed:
695 if line_number and file_size_allowed:
689 return self.get_tokenized_filenode_line(
696 return self.get_tokenized_filenode_line(
690 file, line_number, lexer)
697 input_file, line_number, lexer)
691
698
692 if self.highlight_mode in (self.HL_REAL, self.HL_FAST) and filename:
699 if hl_mode in (self.HL_REAL, self.HL_FAST) and filename:
693 lexer = self._get_lexer_for_filename(filename)
700 lexer = self._get_lexer_for_filename(filename)
694 return list(tokenize_string(line_text, lexer))
701 return list(tokenize_string(line_text, lexer))
695
702
696 return list(tokenize_string(line_text, plain_text_lexer))
703 return list(tokenize_string(line_text, plain_text_lexer))
697
704
698 def get_tokenized_filenode_line(self, filenode, line_number, lexer=None):
705 def get_tokenized_filenode_line(self, filenode, line_number, lexer=None):
699
706
700 if filenode not in self.highlighted_filenodes:
707 if filenode not in self.highlighted_filenodes:
701 tokenized_lines = filenode_as_lines_tokens(filenode, lexer)
708 tokenized_lines = filenode_as_lines_tokens(filenode, lexer)
702 self.highlighted_filenodes[filenode] = tokenized_lines
709 self.highlighted_filenodes[filenode] = tokenized_lines
703 return self.highlighted_filenodes[filenode][line_number - 1]
710 return self.highlighted_filenodes[filenode][line_number - 1]
704
711
705 def action_to_op(self, action):
712 def action_to_op(self, action):
706 return {
713 return {
707 'add': '+',
714 'add': '+',
708 'del': '-',
715 'del': '-',
709 'unmod': ' ',
716 'unmod': ' ',
717 'unmod-no-hl': ' ',
710 'old-no-nl': ' ',
718 'old-no-nl': ' ',
711 'new-no-nl': ' ',
719 'new-no-nl': ' ',
712 }.get(action, action)
720 }.get(action, action)
713
721
714 def as_unified(self, lines):
722 def as_unified(self, lines):
715 """
723 """
716 Return a generator that yields the lines of a diff in unified order
724 Return a generator that yields the lines of a diff in unified order
717 """
725 """
718 def generator():
726 def generator():
719 buf = []
727 buf = []
720 for line in lines:
728 for line in lines:
721
729
722 if buf and not line.original or line.original.action == ' ':
730 if buf and not line.original or line.original.action == ' ':
723 for b in buf:
731 for b in buf:
724 yield b
732 yield b
725 buf = []
733 buf = []
726
734
727 if line.original:
735 if line.original:
728 if line.original.action == ' ':
736 if line.original.action == ' ':
729 yield (line.original.lineno, line.modified.lineno,
737 yield (line.original.lineno, line.modified.lineno,
730 line.original.action, line.original.content,
738 line.original.action, line.original.content,
731 line.original.get_comment_args)
739 line.original.get_comment_args)
732 continue
740 continue
733
741
734 if line.original.action == '-':
742 if line.original.action == '-':
735 yield (line.original.lineno, None,
743 yield (line.original.lineno, None,
736 line.original.action, line.original.content,
744 line.original.action, line.original.content,
737 line.original.get_comment_args)
745 line.original.get_comment_args)
738
746
739 if line.modified.action == '+':
747 if line.modified.action == '+':
740 buf.append((
748 buf.append((
741 None, line.modified.lineno,
749 None, line.modified.lineno,
742 line.modified.action, line.modified.content,
750 line.modified.action, line.modified.content,
743 line.modified.get_comment_args))
751 line.modified.get_comment_args))
744 continue
752 continue
745
753
746 if line.modified:
754 if line.modified:
747 yield (None, line.modified.lineno,
755 yield (None, line.modified.lineno,
748 line.modified.action, line.modified.content,
756 line.modified.action, line.modified.content,
749 line.modified.get_comment_args)
757 line.modified.get_comment_args)
750
758
751 for b in buf:
759 for b in buf:
752 yield b
760 yield b
753
761
754 return generator()
762 return generator()
@@ -1,1228 +1,1228 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2018 RhodeCode GmbH
3 # Copyright (C) 2011-2018 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 os
26 import os
27 import re
27 import re
28 import bz2
28 import bz2
29
29
30 import collections
30 import collections
31 import difflib
31 import difflib
32 import logging
32 import logging
33 import cPickle as pickle
33 import cPickle as pickle
34 from itertools import tee, imap
34 from itertools import tee, imap
35
35
36 from rhodecode.lib.vcs.exceptions import VCSError
36 from rhodecode.lib.vcs.exceptions import VCSError
37 from rhodecode.lib.vcs.nodes import FileNode, SubModuleNode
37 from rhodecode.lib.vcs.nodes import FileNode, SubModuleNode
38 from rhodecode.lib.utils2 import safe_unicode, safe_str
38 from rhodecode.lib.utils2 import safe_unicode, safe_str
39
39
40 log = logging.getLogger(__name__)
40 log = logging.getLogger(__name__)
41
41
42 # define max context, a file with more than this numbers of lines is unusable
42 # define max context, a file with more than this numbers of lines is unusable
43 # in browser anyway
43 # in browser anyway
44 MAX_CONTEXT = 1024 * 1014
44 MAX_CONTEXT = 1024 * 1014
45
45
46
46
47 class OPS(object):
47 class OPS(object):
48 ADD = 'A'
48 ADD = 'A'
49 MOD = 'M'
49 MOD = 'M'
50 DEL = 'D'
50 DEL = 'D'
51
51
52
52
53 def get_gitdiff(filenode_old, filenode_new, ignore_whitespace=True, context=3):
53 def get_gitdiff(filenode_old, filenode_new, ignore_whitespace=True, context=3):
54 """
54 """
55 Returns git style diff between given ``filenode_old`` and ``filenode_new``.
55 Returns git style diff between given ``filenode_old`` and ``filenode_new``.
56
56
57 :param ignore_whitespace: ignore whitespaces in diff
57 :param ignore_whitespace: ignore whitespaces in diff
58 """
58 """
59 # make sure we pass in default context
59 # make sure we pass in default context
60 context = context or 3
60 context = context or 3
61 # protect against IntOverflow when passing HUGE context
61 # protect against IntOverflow when passing HUGE context
62 if context > MAX_CONTEXT:
62 if context > MAX_CONTEXT:
63 context = MAX_CONTEXT
63 context = MAX_CONTEXT
64
64
65 submodules = filter(lambda o: isinstance(o, SubModuleNode),
65 submodules = filter(lambda o: isinstance(o, SubModuleNode),
66 [filenode_new, filenode_old])
66 [filenode_new, filenode_old])
67 if submodules:
67 if submodules:
68 return ''
68 return ''
69
69
70 for filenode in (filenode_old, filenode_new):
70 for filenode in (filenode_old, filenode_new):
71 if not isinstance(filenode, FileNode):
71 if not isinstance(filenode, FileNode):
72 raise VCSError(
72 raise VCSError(
73 "Given object should be FileNode object, not %s"
73 "Given object should be FileNode object, not %s"
74 % filenode.__class__)
74 % filenode.__class__)
75
75
76 repo = filenode_new.commit.repository
76 repo = filenode_new.commit.repository
77 old_commit = filenode_old.commit or repo.EMPTY_COMMIT
77 old_commit = filenode_old.commit or repo.EMPTY_COMMIT
78 new_commit = filenode_new.commit
78 new_commit = filenode_new.commit
79
79
80 vcs_gitdiff = repo.get_diff(
80 vcs_gitdiff = repo.get_diff(
81 old_commit, new_commit, filenode_new.path,
81 old_commit, new_commit, filenode_new.path,
82 ignore_whitespace, context, path1=filenode_old.path)
82 ignore_whitespace, context, path1=filenode_old.path)
83 return vcs_gitdiff
83 return vcs_gitdiff
84
84
85 NEW_FILENODE = 1
85 NEW_FILENODE = 1
86 DEL_FILENODE = 2
86 DEL_FILENODE = 2
87 MOD_FILENODE = 3
87 MOD_FILENODE = 3
88 RENAMED_FILENODE = 4
88 RENAMED_FILENODE = 4
89 COPIED_FILENODE = 5
89 COPIED_FILENODE = 5
90 CHMOD_FILENODE = 6
90 CHMOD_FILENODE = 6
91 BIN_FILENODE = 7
91 BIN_FILENODE = 7
92
92
93
93
94 class LimitedDiffContainer(object):
94 class LimitedDiffContainer(object):
95
95
96 def __init__(self, diff_limit, cur_diff_size, diff):
96 def __init__(self, diff_limit, cur_diff_size, diff):
97 self.diff = diff
97 self.diff = diff
98 self.diff_limit = diff_limit
98 self.diff_limit = diff_limit
99 self.cur_diff_size = cur_diff_size
99 self.cur_diff_size = cur_diff_size
100
100
101 def __getitem__(self, key):
101 def __getitem__(self, key):
102 return self.diff.__getitem__(key)
102 return self.diff.__getitem__(key)
103
103
104 def __iter__(self):
104 def __iter__(self):
105 for l in self.diff:
105 for l in self.diff:
106 yield l
106 yield l
107
107
108
108
109 class Action(object):
109 class Action(object):
110 """
110 """
111 Contains constants for the action value of the lines in a parsed diff.
111 Contains constants for the action value of the lines in a parsed diff.
112 """
112 """
113
113
114 ADD = 'add'
114 ADD = 'add'
115 DELETE = 'del'
115 DELETE = 'del'
116 UNMODIFIED = 'unmod'
116 UNMODIFIED = 'unmod'
117
117
118 CONTEXT = 'context'
118 CONTEXT = 'context'
119 OLD_NO_NL = 'old-no-nl'
119 OLD_NO_NL = 'old-no-nl'
120 NEW_NO_NL = 'new-no-nl'
120 NEW_NO_NL = 'new-no-nl'
121
121
122
122
123 class DiffProcessor(object):
123 class DiffProcessor(object):
124 """
124 """
125 Give it a unified or git diff and it returns a list of the files that were
125 Give it a unified or git diff and it returns a list of the files that were
126 mentioned in the diff together with a dict of meta information that
126 mentioned in the diff together with a dict of meta information that
127 can be used to render it in a HTML template.
127 can be used to render it in a HTML template.
128
128
129 .. note:: Unicode handling
129 .. note:: Unicode handling
130
130
131 The original diffs are a byte sequence and can contain filenames
131 The original diffs are a byte sequence and can contain filenames
132 in mixed encodings. This class generally returns `unicode` objects
132 in mixed encodings. This class generally returns `unicode` objects
133 since the result is intended for presentation to the user.
133 since the result is intended for presentation to the user.
134
134
135 """
135 """
136 _chunk_re = re.compile(r'^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)')
136 _chunk_re = re.compile(r'^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)')
137 _newline_marker = re.compile(r'^\\ No newline at end of file')
137 _newline_marker = re.compile(r'^\\ No newline at end of file')
138
138
139 # used for inline highlighter word split
139 # used for inline highlighter word split
140 _token_re = re.compile(r'()(&gt;|&lt;|&amp;|\W+?)')
140 _token_re = re.compile(r'()(&gt;|&lt;|&amp;|\W+?)')
141
141
142 # collapse ranges of commits over given number
142 # collapse ranges of commits over given number
143 _collapse_commits_over = 5
143 _collapse_commits_over = 5
144
144
145 def __init__(self, diff, format='gitdiff', diff_limit=None,
145 def __init__(self, diff, format='gitdiff', diff_limit=None,
146 file_limit=None, show_full_diff=True):
146 file_limit=None, show_full_diff=True):
147 """
147 """
148 :param diff: A `Diff` object representing a diff from a vcs backend
148 :param diff: A `Diff` object representing a diff from a vcs backend
149 :param format: format of diff passed, `udiff` or `gitdiff`
149 :param format: format of diff passed, `udiff` or `gitdiff`
150 :param diff_limit: define the size of diff that is considered "big"
150 :param diff_limit: define the size of diff that is considered "big"
151 based on that parameter cut off will be triggered, set to None
151 based on that parameter cut off will be triggered, set to None
152 to show full diff
152 to show full diff
153 """
153 """
154 self._diff = diff
154 self._diff = diff
155 self._format = format
155 self._format = format
156 self.adds = 0
156 self.adds = 0
157 self.removes = 0
157 self.removes = 0
158 # calculate diff size
158 # calculate diff size
159 self.diff_limit = diff_limit
159 self.diff_limit = diff_limit
160 self.file_limit = file_limit
160 self.file_limit = file_limit
161 self.show_full_diff = show_full_diff
161 self.show_full_diff = show_full_diff
162 self.cur_diff_size = 0
162 self.cur_diff_size = 0
163 self.parsed = False
163 self.parsed = False
164 self.parsed_diff = []
164 self.parsed_diff = []
165
165
166 log.debug('Initialized DiffProcessor with %s mode', format)
166 log.debug('Initialized DiffProcessor with %s mode', format)
167 if format == 'gitdiff':
167 if format == 'gitdiff':
168 self.differ = self._highlight_line_difflib
168 self.differ = self._highlight_line_difflib
169 self._parser = self._parse_gitdiff
169 self._parser = self._parse_gitdiff
170 else:
170 else:
171 self.differ = self._highlight_line_udiff
171 self.differ = self._highlight_line_udiff
172 self._parser = self._new_parse_gitdiff
172 self._parser = self._new_parse_gitdiff
173
173
174 def _copy_iterator(self):
174 def _copy_iterator(self):
175 """
175 """
176 make a fresh copy of generator, we should not iterate thru
176 make a fresh copy of generator, we should not iterate thru
177 an original as it's needed for repeating operations on
177 an original as it's needed for repeating operations on
178 this instance of DiffProcessor
178 this instance of DiffProcessor
179 """
179 """
180 self.__udiff, iterator_copy = tee(self.__udiff)
180 self.__udiff, iterator_copy = tee(self.__udiff)
181 return iterator_copy
181 return iterator_copy
182
182
183 def _escaper(self, string):
183 def _escaper(self, string):
184 """
184 """
185 Escaper for diff escapes special chars and checks the diff limit
185 Escaper for diff escapes special chars and checks the diff limit
186
186
187 :param string:
187 :param string:
188 """
188 """
189 self.cur_diff_size += len(string)
189 self.cur_diff_size += len(string)
190
190
191 if not self.show_full_diff and (self.cur_diff_size > self.diff_limit):
191 if not self.show_full_diff and (self.cur_diff_size > self.diff_limit):
192 raise DiffLimitExceeded('Diff Limit Exceeded')
192 raise DiffLimitExceeded('Diff Limit Exceeded')
193
193
194 return string \
194 return string \
195 .replace('&', '&amp;')\
195 .replace('&', '&amp;')\
196 .replace('<', '&lt;')\
196 .replace('<', '&lt;')\
197 .replace('>', '&gt;')
197 .replace('>', '&gt;')
198
198
199 def _line_counter(self, l):
199 def _line_counter(self, l):
200 """
200 """
201 Checks each line and bumps total adds/removes for this diff
201 Checks each line and bumps total adds/removes for this diff
202
202
203 :param l:
203 :param l:
204 """
204 """
205 if l.startswith('+') and not l.startswith('+++'):
205 if l.startswith('+') and not l.startswith('+++'):
206 self.adds += 1
206 self.adds += 1
207 elif l.startswith('-') and not l.startswith('---'):
207 elif l.startswith('-') and not l.startswith('---'):
208 self.removes += 1
208 self.removes += 1
209 return safe_unicode(l)
209 return safe_unicode(l)
210
210
211 def _highlight_line_difflib(self, line, next_):
211 def _highlight_line_difflib(self, line, next_):
212 """
212 """
213 Highlight inline changes in both lines.
213 Highlight inline changes in both lines.
214 """
214 """
215
215
216 if line['action'] == Action.DELETE:
216 if line['action'] == Action.DELETE:
217 old, new = line, next_
217 old, new = line, next_
218 else:
218 else:
219 old, new = next_, line
219 old, new = next_, line
220
220
221 oldwords = self._token_re.split(old['line'])
221 oldwords = self._token_re.split(old['line'])
222 newwords = self._token_re.split(new['line'])
222 newwords = self._token_re.split(new['line'])
223 sequence = difflib.SequenceMatcher(None, oldwords, newwords)
223 sequence = difflib.SequenceMatcher(None, oldwords, newwords)
224
224
225 oldfragments, newfragments = [], []
225 oldfragments, newfragments = [], []
226 for tag, i1, i2, j1, j2 in sequence.get_opcodes():
226 for tag, i1, i2, j1, j2 in sequence.get_opcodes():
227 oldfrag = ''.join(oldwords[i1:i2])
227 oldfrag = ''.join(oldwords[i1:i2])
228 newfrag = ''.join(newwords[j1:j2])
228 newfrag = ''.join(newwords[j1:j2])
229 if tag != 'equal':
229 if tag != 'equal':
230 if oldfrag:
230 if oldfrag:
231 oldfrag = '<del>%s</del>' % oldfrag
231 oldfrag = '<del>%s</del>' % oldfrag
232 if newfrag:
232 if newfrag:
233 newfrag = '<ins>%s</ins>' % newfrag
233 newfrag = '<ins>%s</ins>' % newfrag
234 oldfragments.append(oldfrag)
234 oldfragments.append(oldfrag)
235 newfragments.append(newfrag)
235 newfragments.append(newfrag)
236
236
237 old['line'] = "".join(oldfragments)
237 old['line'] = "".join(oldfragments)
238 new['line'] = "".join(newfragments)
238 new['line'] = "".join(newfragments)
239
239
240 def _highlight_line_udiff(self, line, next_):
240 def _highlight_line_udiff(self, line, next_):
241 """
241 """
242 Highlight inline changes in both lines.
242 Highlight inline changes in both lines.
243 """
243 """
244 start = 0
244 start = 0
245 limit = min(len(line['line']), len(next_['line']))
245 limit = min(len(line['line']), len(next_['line']))
246 while start < limit and line['line'][start] == next_['line'][start]:
246 while start < limit and line['line'][start] == next_['line'][start]:
247 start += 1
247 start += 1
248 end = -1
248 end = -1
249 limit -= start
249 limit -= start
250 while -end <= limit and line['line'][end] == next_['line'][end]:
250 while -end <= limit and line['line'][end] == next_['line'][end]:
251 end -= 1
251 end -= 1
252 end += 1
252 end += 1
253 if start or end:
253 if start or end:
254 def do(l):
254 def do(l):
255 last = end + len(l['line'])
255 last = end + len(l['line'])
256 if l['action'] == Action.ADD:
256 if l['action'] == Action.ADD:
257 tag = 'ins'
257 tag = 'ins'
258 else:
258 else:
259 tag = 'del'
259 tag = 'del'
260 l['line'] = '%s<%s>%s</%s>%s' % (
260 l['line'] = '%s<%s>%s</%s>%s' % (
261 l['line'][:start],
261 l['line'][:start],
262 tag,
262 tag,
263 l['line'][start:last],
263 l['line'][start:last],
264 tag,
264 tag,
265 l['line'][last:]
265 l['line'][last:]
266 )
266 )
267 do(line)
267 do(line)
268 do(next_)
268 do(next_)
269
269
270 def _clean_line(self, line, command):
270 def _clean_line(self, line, command):
271 if command in ['+', '-', ' ']:
271 if command in ['+', '-', ' ']:
272 # only modify the line if it's actually a diff thing
272 # only modify the line if it's actually a diff thing
273 line = line[1:]
273 line = line[1:]
274 return line
274 return line
275
275
276 def _parse_gitdiff(self, inline_diff=True):
276 def _parse_gitdiff(self, inline_diff=True):
277 _files = []
277 _files = []
278 diff_container = lambda arg: arg
278 diff_container = lambda arg: arg
279
279
280 for chunk in self._diff.chunks():
280 for chunk in self._diff.chunks():
281 head = chunk.header
281 head = chunk.header
282
282
283 diff = imap(self._escaper, self.diff_splitter(chunk.diff))
283 diff = imap(self._escaper, self.diff_splitter(chunk.diff))
284 raw_diff = chunk.raw
284 raw_diff = chunk.raw
285 limited_diff = False
285 limited_diff = False
286 exceeds_limit = False
286 exceeds_limit = False
287
287
288 op = None
288 op = None
289 stats = {
289 stats = {
290 'added': 0,
290 'added': 0,
291 'deleted': 0,
291 'deleted': 0,
292 'binary': False,
292 'binary': False,
293 'ops': {},
293 'ops': {},
294 }
294 }
295
295
296 if head['deleted_file_mode']:
296 if head['deleted_file_mode']:
297 op = OPS.DEL
297 op = OPS.DEL
298 stats['binary'] = True
298 stats['binary'] = True
299 stats['ops'][DEL_FILENODE] = 'deleted file'
299 stats['ops'][DEL_FILENODE] = 'deleted file'
300
300
301 elif head['new_file_mode']:
301 elif head['new_file_mode']:
302 op = OPS.ADD
302 op = OPS.ADD
303 stats['binary'] = True
303 stats['binary'] = True
304 stats['ops'][NEW_FILENODE] = 'new file %s' % head['new_file_mode']
304 stats['ops'][NEW_FILENODE] = 'new file %s' % head['new_file_mode']
305 else: # modify operation, can be copy, rename or chmod
305 else: # modify operation, can be copy, rename or chmod
306
306
307 # CHMOD
307 # CHMOD
308 if head['new_mode'] and head['old_mode']:
308 if head['new_mode'] and head['old_mode']:
309 op = OPS.MOD
309 op = OPS.MOD
310 stats['binary'] = True
310 stats['binary'] = True
311 stats['ops'][CHMOD_FILENODE] = (
311 stats['ops'][CHMOD_FILENODE] = (
312 'modified file chmod %s => %s' % (
312 'modified file chmod %s => %s' % (
313 head['old_mode'], head['new_mode']))
313 head['old_mode'], head['new_mode']))
314 # RENAME
314 # RENAME
315 if head['rename_from'] != head['rename_to']:
315 if head['rename_from'] != head['rename_to']:
316 op = OPS.MOD
316 op = OPS.MOD
317 stats['binary'] = True
317 stats['binary'] = True
318 stats['ops'][RENAMED_FILENODE] = (
318 stats['ops'][RENAMED_FILENODE] = (
319 'file renamed from %s to %s' % (
319 'file renamed from %s to %s' % (
320 head['rename_from'], head['rename_to']))
320 head['rename_from'], head['rename_to']))
321 # COPY
321 # COPY
322 if head.get('copy_from') and head.get('copy_to'):
322 if head.get('copy_from') and head.get('copy_to'):
323 op = OPS.MOD
323 op = OPS.MOD
324 stats['binary'] = True
324 stats['binary'] = True
325 stats['ops'][COPIED_FILENODE] = (
325 stats['ops'][COPIED_FILENODE] = (
326 'file copied from %s to %s' % (
326 'file copied from %s to %s' % (
327 head['copy_from'], head['copy_to']))
327 head['copy_from'], head['copy_to']))
328
328
329 # If our new parsed headers didn't match anything fallback to
329 # If our new parsed headers didn't match anything fallback to
330 # old style detection
330 # old style detection
331 if op is None:
331 if op is None:
332 if not head['a_file'] and head['b_file']:
332 if not head['a_file'] and head['b_file']:
333 op = OPS.ADD
333 op = OPS.ADD
334 stats['binary'] = True
334 stats['binary'] = True
335 stats['ops'][NEW_FILENODE] = 'new file'
335 stats['ops'][NEW_FILENODE] = 'new file'
336
336
337 elif head['a_file'] and not head['b_file']:
337 elif head['a_file'] and not head['b_file']:
338 op = OPS.DEL
338 op = OPS.DEL
339 stats['binary'] = True
339 stats['binary'] = True
340 stats['ops'][DEL_FILENODE] = 'deleted file'
340 stats['ops'][DEL_FILENODE] = 'deleted file'
341
341
342 # it's not ADD not DELETE
342 # it's not ADD not DELETE
343 if op is None:
343 if op is None:
344 op = OPS.MOD
344 op = OPS.MOD
345 stats['binary'] = True
345 stats['binary'] = True
346 stats['ops'][MOD_FILENODE] = 'modified file'
346 stats['ops'][MOD_FILENODE] = 'modified file'
347
347
348 # a real non-binary diff
348 # a real non-binary diff
349 if head['a_file'] or head['b_file']:
349 if head['a_file'] or head['b_file']:
350 try:
350 try:
351 raw_diff, chunks, _stats = self._parse_lines(diff)
351 raw_diff, chunks, _stats = self._parse_lines(diff)
352 stats['binary'] = False
352 stats['binary'] = False
353 stats['added'] = _stats[0]
353 stats['added'] = _stats[0]
354 stats['deleted'] = _stats[1]
354 stats['deleted'] = _stats[1]
355 # explicit mark that it's a modified file
355 # explicit mark that it's a modified file
356 if op == OPS.MOD:
356 if op == OPS.MOD:
357 stats['ops'][MOD_FILENODE] = 'modified file'
357 stats['ops'][MOD_FILENODE] = 'modified file'
358 exceeds_limit = len(raw_diff) > self.file_limit
358 exceeds_limit = len(raw_diff) > self.file_limit
359
359
360 # changed from _escaper function so we validate size of
360 # changed from _escaper function so we validate size of
361 # each file instead of the whole diff
361 # each file instead of the whole diff
362 # diff will hide big files but still show small ones
362 # diff will hide big files but still show small ones
363 # from my tests, big files are fairly safe to be parsed
363 # from my tests, big files are fairly safe to be parsed
364 # but the browser is the bottleneck
364 # but the browser is the bottleneck
365 if not self.show_full_diff and exceeds_limit:
365 if not self.show_full_diff and exceeds_limit:
366 raise DiffLimitExceeded('File Limit Exceeded')
366 raise DiffLimitExceeded('File Limit Exceeded')
367
367
368 except DiffLimitExceeded:
368 except DiffLimitExceeded:
369 diff_container = lambda _diff: \
369 diff_container = lambda _diff: \
370 LimitedDiffContainer(
370 LimitedDiffContainer(
371 self.diff_limit, self.cur_diff_size, _diff)
371 self.diff_limit, self.cur_diff_size, _diff)
372
372
373 exceeds_limit = len(raw_diff) > self.file_limit
373 exceeds_limit = len(raw_diff) > self.file_limit
374 limited_diff = True
374 limited_diff = True
375 chunks = []
375 chunks = []
376
376
377 else: # GIT format binary patch, or possibly empty diff
377 else: # GIT format binary patch, or possibly empty diff
378 if head['bin_patch']:
378 if head['bin_patch']:
379 # we have operation already extracted, but we mark simply
379 # we have operation already extracted, but we mark simply
380 # it's a diff we wont show for binary files
380 # it's a diff we wont show for binary files
381 stats['ops'][BIN_FILENODE] = 'binary diff hidden'
381 stats['ops'][BIN_FILENODE] = 'binary diff hidden'
382 chunks = []
382 chunks = []
383
383
384 if chunks and not self.show_full_diff and op == OPS.DEL:
384 if chunks and not self.show_full_diff and op == OPS.DEL:
385 # if not full diff mode show deleted file contents
385 # if not full diff mode show deleted file contents
386 # TODO: anderson: if the view is not too big, there is no way
386 # TODO: anderson: if the view is not too big, there is no way
387 # to see the content of the file
387 # to see the content of the file
388 chunks = []
388 chunks = []
389
389
390 chunks.insert(0, [{
390 chunks.insert(0, [{
391 'old_lineno': '',
391 'old_lineno': '',
392 'new_lineno': '',
392 'new_lineno': '',
393 'action': Action.CONTEXT,
393 'action': Action.CONTEXT,
394 'line': msg,
394 'line': msg,
395 } for _op, msg in stats['ops'].iteritems()
395 } for _op, msg in stats['ops'].iteritems()
396 if _op not in [MOD_FILENODE]])
396 if _op not in [MOD_FILENODE]])
397
397
398 _files.append({
398 _files.append({
399 'filename': safe_unicode(head['b_path']),
399 'filename': safe_unicode(head['b_path']),
400 'old_revision': head['a_blob_id'],
400 'old_revision': head['a_blob_id'],
401 'new_revision': head['b_blob_id'],
401 'new_revision': head['b_blob_id'],
402 'chunks': chunks,
402 'chunks': chunks,
403 'raw_diff': safe_unicode(raw_diff),
403 'raw_diff': safe_unicode(raw_diff),
404 'operation': op,
404 'operation': op,
405 'stats': stats,
405 'stats': stats,
406 'exceeds_limit': exceeds_limit,
406 'exceeds_limit': exceeds_limit,
407 'is_limited_diff': limited_diff,
407 'is_limited_diff': limited_diff,
408 })
408 })
409
409
410 sorter = lambda info: {OPS.ADD: 0, OPS.MOD: 1,
410 sorter = lambda info: {OPS.ADD: 0, OPS.MOD: 1,
411 OPS.DEL: 2}.get(info['operation'])
411 OPS.DEL: 2}.get(info['operation'])
412
412
413 if not inline_diff:
413 if not inline_diff:
414 return diff_container(sorted(_files, key=sorter))
414 return diff_container(sorted(_files, key=sorter))
415
415
416 # highlight inline changes
416 # highlight inline changes
417 for diff_data in _files:
417 for diff_data in _files:
418 for chunk in diff_data['chunks']:
418 for chunk in diff_data['chunks']:
419 lineiter = iter(chunk)
419 lineiter = iter(chunk)
420 try:
420 try:
421 while 1:
421 while 1:
422 line = lineiter.next()
422 line = lineiter.next()
423 if line['action'] not in (
423 if line['action'] not in (
424 Action.UNMODIFIED, Action.CONTEXT):
424 Action.UNMODIFIED, Action.CONTEXT):
425 nextline = lineiter.next()
425 nextline = lineiter.next()
426 if nextline['action'] in ['unmod', 'context'] or \
426 if nextline['action'] in ['unmod', 'context'] or \
427 nextline['action'] == line['action']:
427 nextline['action'] == line['action']:
428 continue
428 continue
429 self.differ(line, nextline)
429 self.differ(line, nextline)
430 except StopIteration:
430 except StopIteration:
431 pass
431 pass
432
432
433 return diff_container(sorted(_files, key=sorter))
433 return diff_container(sorted(_files, key=sorter))
434
434
435 def _check_large_diff(self):
435 def _check_large_diff(self):
436 log.debug('Diff exceeds current diff_limit of %s', self.diff_limit)
436 log.debug('Diff exceeds current diff_limit of %s', self.diff_limit)
437 if not self.show_full_diff and (self.cur_diff_size > self.diff_limit):
437 if not self.show_full_diff and (self.cur_diff_size > self.diff_limit):
438 raise DiffLimitExceeded('Diff Limit `%s` Exceeded', self.diff_limit)
438 raise DiffLimitExceeded('Diff Limit `%s` Exceeded', self.diff_limit)
439
439
440 # FIXME: NEWDIFFS: dan: this replaces _parse_gitdiff
440 # FIXME: NEWDIFFS: dan: this replaces _parse_gitdiff
441 def _new_parse_gitdiff(self, inline_diff=True):
441 def _new_parse_gitdiff(self, inline_diff=True):
442 _files = []
442 _files = []
443
443
444 # this can be overriden later to a LimitedDiffContainer type
444 # this can be overriden later to a LimitedDiffContainer type
445 diff_container = lambda arg: arg
445 diff_container = lambda arg: arg
446
446
447 for chunk in self._diff.chunks():
447 for chunk in self._diff.chunks():
448 head = chunk.header
448 head = chunk.header
449 log.debug('parsing diff %r', head)
449 log.debug('parsing diff %r', head)
450
450
451 raw_diff = chunk.raw
451 raw_diff = chunk.raw
452 limited_diff = False
452 limited_diff = False
453 exceeds_limit = False
453 exceeds_limit = False
454
454
455 op = None
455 op = None
456 stats = {
456 stats = {
457 'added': 0,
457 'added': 0,
458 'deleted': 0,
458 'deleted': 0,
459 'binary': False,
459 'binary': False,
460 'old_mode': None,
460 'old_mode': None,
461 'new_mode': None,
461 'new_mode': None,
462 'ops': {},
462 'ops': {},
463 }
463 }
464 if head['old_mode']:
464 if head['old_mode']:
465 stats['old_mode'] = head['old_mode']
465 stats['old_mode'] = head['old_mode']
466 if head['new_mode']:
466 if head['new_mode']:
467 stats['new_mode'] = head['new_mode']
467 stats['new_mode'] = head['new_mode']
468 if head['b_mode']:
468 if head['b_mode']:
469 stats['new_mode'] = head['b_mode']
469 stats['new_mode'] = head['b_mode']
470
470
471 # delete file
471 # delete file
472 if head['deleted_file_mode']:
472 if head['deleted_file_mode']:
473 op = OPS.DEL
473 op = OPS.DEL
474 stats['binary'] = True
474 stats['binary'] = True
475 stats['ops'][DEL_FILENODE] = 'deleted file'
475 stats['ops'][DEL_FILENODE] = 'deleted file'
476
476
477 # new file
477 # new file
478 elif head['new_file_mode']:
478 elif head['new_file_mode']:
479 op = OPS.ADD
479 op = OPS.ADD
480 stats['binary'] = True
480 stats['binary'] = True
481 stats['old_mode'] = None
481 stats['old_mode'] = None
482 stats['new_mode'] = head['new_file_mode']
482 stats['new_mode'] = head['new_file_mode']
483 stats['ops'][NEW_FILENODE] = 'new file %s' % head['new_file_mode']
483 stats['ops'][NEW_FILENODE] = 'new file %s' % head['new_file_mode']
484
484
485 # modify operation, can be copy, rename or chmod
485 # modify operation, can be copy, rename or chmod
486 else:
486 else:
487 # CHMOD
487 # CHMOD
488 if head['new_mode'] and head['old_mode']:
488 if head['new_mode'] and head['old_mode']:
489 op = OPS.MOD
489 op = OPS.MOD
490 stats['binary'] = True
490 stats['binary'] = True
491 stats['ops'][CHMOD_FILENODE] = (
491 stats['ops'][CHMOD_FILENODE] = (
492 'modified file chmod %s => %s' % (
492 'modified file chmod %s => %s' % (
493 head['old_mode'], head['new_mode']))
493 head['old_mode'], head['new_mode']))
494
494
495 # RENAME
495 # RENAME
496 if head['rename_from'] != head['rename_to']:
496 if head['rename_from'] != head['rename_to']:
497 op = OPS.MOD
497 op = OPS.MOD
498 stats['binary'] = True
498 stats['binary'] = True
499 stats['renamed'] = (head['rename_from'], head['rename_to'])
499 stats['renamed'] = (head['rename_from'], head['rename_to'])
500 stats['ops'][RENAMED_FILENODE] = (
500 stats['ops'][RENAMED_FILENODE] = (
501 'file renamed from %s to %s' % (
501 'file renamed from %s to %s' % (
502 head['rename_from'], head['rename_to']))
502 head['rename_from'], head['rename_to']))
503 # COPY
503 # COPY
504 if head.get('copy_from') and head.get('copy_to'):
504 if head.get('copy_from') and head.get('copy_to'):
505 op = OPS.MOD
505 op = OPS.MOD
506 stats['binary'] = True
506 stats['binary'] = True
507 stats['copied'] = (head['copy_from'], head['copy_to'])
507 stats['copied'] = (head['copy_from'], head['copy_to'])
508 stats['ops'][COPIED_FILENODE] = (
508 stats['ops'][COPIED_FILENODE] = (
509 'file copied from %s to %s' % (
509 'file copied from %s to %s' % (
510 head['copy_from'], head['copy_to']))
510 head['copy_from'], head['copy_to']))
511
511
512 # If our new parsed headers didn't match anything fallback to
512 # If our new parsed headers didn't match anything fallback to
513 # old style detection
513 # old style detection
514 if op is None:
514 if op is None:
515 if not head['a_file'] and head['b_file']:
515 if not head['a_file'] and head['b_file']:
516 op = OPS.ADD
516 op = OPS.ADD
517 stats['binary'] = True
517 stats['binary'] = True
518 stats['new_file'] = True
518 stats['new_file'] = True
519 stats['ops'][NEW_FILENODE] = 'new file'
519 stats['ops'][NEW_FILENODE] = 'new file'
520
520
521 elif head['a_file'] and not head['b_file']:
521 elif head['a_file'] and not head['b_file']:
522 op = OPS.DEL
522 op = OPS.DEL
523 stats['binary'] = True
523 stats['binary'] = True
524 stats['ops'][DEL_FILENODE] = 'deleted file'
524 stats['ops'][DEL_FILENODE] = 'deleted file'
525
525
526 # it's not ADD not DELETE
526 # it's not ADD not DELETE
527 if op is None:
527 if op is None:
528 op = OPS.MOD
528 op = OPS.MOD
529 stats['binary'] = True
529 stats['binary'] = True
530 stats['ops'][MOD_FILENODE] = 'modified file'
530 stats['ops'][MOD_FILENODE] = 'modified file'
531
531
532 # a real non-binary diff
532 # a real non-binary diff
533 if head['a_file'] or head['b_file']:
533 if head['a_file'] or head['b_file']:
534 # simulate splitlines, so we keep the line end part
534 # simulate splitlines, so we keep the line end part
535 diff = self.diff_splitter(chunk.diff)
535 diff = self.diff_splitter(chunk.diff)
536
536
537 # append each file to the diff size
537 # append each file to the diff size
538 raw_chunk_size = len(raw_diff)
538 raw_chunk_size = len(raw_diff)
539
539
540 exceeds_limit = raw_chunk_size > self.file_limit
540 exceeds_limit = raw_chunk_size > self.file_limit
541 self.cur_diff_size += raw_chunk_size
541 self.cur_diff_size += raw_chunk_size
542
542
543 try:
543 try:
544 # Check each file instead of the whole diff.
544 # Check each file instead of the whole diff.
545 # Diff will hide big files but still show small ones.
545 # Diff will hide big files but still show small ones.
546 # From the tests big files are fairly safe to be parsed
546 # From the tests big files are fairly safe to be parsed
547 # but the browser is the bottleneck.
547 # but the browser is the bottleneck.
548 if not self.show_full_diff and exceeds_limit:
548 if not self.show_full_diff and exceeds_limit:
549 log.debug('File `%s` exceeds current file_limit of %s',
549 log.debug('File `%s` exceeds current file_limit of %s',
550 safe_unicode(head['b_path']), self.file_limit)
550 safe_unicode(head['b_path']), self.file_limit)
551 raise DiffLimitExceeded(
551 raise DiffLimitExceeded(
552 'File Limit %s Exceeded', self.file_limit)
552 'File Limit %s Exceeded', self.file_limit)
553
553
554 self._check_large_diff()
554 self._check_large_diff()
555
555
556 raw_diff, chunks, _stats = self._new_parse_lines(diff)
556 raw_diff, chunks, _stats = self._new_parse_lines(diff)
557 stats['binary'] = False
557 stats['binary'] = False
558 stats['added'] = _stats[0]
558 stats['added'] = _stats[0]
559 stats['deleted'] = _stats[1]
559 stats['deleted'] = _stats[1]
560 # explicit mark that it's a modified file
560 # explicit mark that it's a modified file
561 if op == OPS.MOD:
561 if op == OPS.MOD:
562 stats['ops'][MOD_FILENODE] = 'modified file'
562 stats['ops'][MOD_FILENODE] = 'modified file'
563
563
564 except DiffLimitExceeded:
564 except DiffLimitExceeded:
565 diff_container = lambda _diff: \
565 diff_container = lambda _diff: \
566 LimitedDiffContainer(
566 LimitedDiffContainer(
567 self.diff_limit, self.cur_diff_size, _diff)
567 self.diff_limit, self.cur_diff_size, _diff)
568
568
569 limited_diff = True
569 limited_diff = True
570 chunks = []
570 chunks = []
571
571
572 else: # GIT format binary patch, or possibly empty diff
572 else: # GIT format binary patch, or possibly empty diff
573 if head['bin_patch']:
573 if head['bin_patch']:
574 # we have operation already extracted, but we mark simply
574 # we have operation already extracted, but we mark simply
575 # it's a diff we wont show for binary files
575 # it's a diff we wont show for binary files
576 stats['ops'][BIN_FILENODE] = 'binary diff hidden'
576 stats['ops'][BIN_FILENODE] = 'binary diff hidden'
577 chunks = []
577 chunks = []
578
578
579 # Hide content of deleted node by setting empty chunks
579 # Hide content of deleted node by setting empty chunks
580 if chunks and not self.show_full_diff and op == OPS.DEL:
580 if chunks and not self.show_full_diff and op == OPS.DEL:
581 # if not full diff mode show deleted file contents
581 # if not full diff mode show deleted file contents
582 # TODO: anderson: if the view is not too big, there is no way
582 # TODO: anderson: if the view is not too big, there is no way
583 # to see the content of the file
583 # to see the content of the file
584 chunks = []
584 chunks = []
585
585
586 chunks.insert(
586 chunks.insert(
587 0, [{'old_lineno': '',
587 0, [{'old_lineno': '',
588 'new_lineno': '',
588 'new_lineno': '',
589 'action': Action.CONTEXT,
589 'action': Action.CONTEXT,
590 'line': msg,
590 'line': msg,
591 } for _op, msg in stats['ops'].iteritems()
591 } for _op, msg in stats['ops'].iteritems()
592 if _op not in [MOD_FILENODE]])
592 if _op not in [MOD_FILENODE]])
593
593
594 original_filename = safe_unicode(head['a_path'])
594 original_filename = safe_unicode(head['a_path'])
595 _files.append({
595 _files.append({
596 'original_filename': original_filename,
596 'original_filename': original_filename,
597 'filename': safe_unicode(head['b_path']),
597 'filename': safe_unicode(head['b_path']),
598 'old_revision': head['a_blob_id'],
598 'old_revision': head['a_blob_id'],
599 'new_revision': head['b_blob_id'],
599 'new_revision': head['b_blob_id'],
600 'chunks': chunks,
600 'chunks': chunks,
601 'raw_diff': safe_unicode(raw_diff),
601 'raw_diff': safe_unicode(raw_diff),
602 'operation': op,
602 'operation': op,
603 'stats': stats,
603 'stats': stats,
604 'exceeds_limit': exceeds_limit,
604 'exceeds_limit': exceeds_limit,
605 'is_limited_diff': limited_diff,
605 'is_limited_diff': limited_diff,
606 })
606 })
607
607
608 sorter = lambda info: {OPS.ADD: 0, OPS.MOD: 1,
608 sorter = lambda info: {OPS.ADD: 0, OPS.MOD: 1,
609 OPS.DEL: 2}.get(info['operation'])
609 OPS.DEL: 2}.get(info['operation'])
610
610
611 return diff_container(sorted(_files, key=sorter))
611 return diff_container(sorted(_files, key=sorter))
612
612
613 # FIXME: NEWDIFFS: dan: this gets replaced by _new_parse_lines
613 # FIXME: NEWDIFFS: dan: this gets replaced by _new_parse_lines
614 def _parse_lines(self, diff_iter):
614 def _parse_lines(self, diff_iter):
615 """
615 """
616 Parse the diff an return data for the template.
616 Parse the diff an return data for the template.
617 """
617 """
618
618
619 stats = [0, 0]
619 stats = [0, 0]
620 chunks = []
620 chunks = []
621 raw_diff = []
621 raw_diff = []
622
622
623 try:
623 try:
624 line = diff_iter.next()
624 line = diff_iter.next()
625
625
626 while line:
626 while line:
627 raw_diff.append(line)
627 raw_diff.append(line)
628 lines = []
628 lines = []
629 chunks.append(lines)
629 chunks.append(lines)
630
630
631 match = self._chunk_re.match(line)
631 match = self._chunk_re.match(line)
632
632
633 if not match:
633 if not match:
634 break
634 break
635
635
636 gr = match.groups()
636 gr = match.groups()
637 (old_line, old_end,
637 (old_line, old_end,
638 new_line, new_end) = [int(x or 1) for x in gr[:-1]]
638 new_line, new_end) = [int(x or 1) for x in gr[:-1]]
639 old_line -= 1
639 old_line -= 1
640 new_line -= 1
640 new_line -= 1
641
641
642 context = len(gr) == 5
642 context = len(gr) == 5
643 old_end += old_line
643 old_end += old_line
644 new_end += new_line
644 new_end += new_line
645
645
646 if context:
646 if context:
647 # skip context only if it's first line
647 # skip context only if it's first line
648 if int(gr[0]) > 1:
648 if int(gr[0]) > 1:
649 lines.append({
649 lines.append({
650 'old_lineno': '...',
650 'old_lineno': '...',
651 'new_lineno': '...',
651 'new_lineno': '...',
652 'action': Action.CONTEXT,
652 'action': Action.CONTEXT,
653 'line': line,
653 'line': line,
654 })
654 })
655
655
656 line = diff_iter.next()
656 line = diff_iter.next()
657
657
658 while old_line < old_end or new_line < new_end:
658 while old_line < old_end or new_line < new_end:
659 command = ' '
659 command = ' '
660 if line:
660 if line:
661 command = line[0]
661 command = line[0]
662
662
663 affects_old = affects_new = False
663 affects_old = affects_new = False
664
664
665 # ignore those if we don't expect them
665 # ignore those if we don't expect them
666 if command in '#@':
666 if command in '#@':
667 continue
667 continue
668 elif command == '+':
668 elif command == '+':
669 affects_new = True
669 affects_new = True
670 action = Action.ADD
670 action = Action.ADD
671 stats[0] += 1
671 stats[0] += 1
672 elif command == '-':
672 elif command == '-':
673 affects_old = True
673 affects_old = True
674 action = Action.DELETE
674 action = Action.DELETE
675 stats[1] += 1
675 stats[1] += 1
676 else:
676 else:
677 affects_old = affects_new = True
677 affects_old = affects_new = True
678 action = Action.UNMODIFIED
678 action = Action.UNMODIFIED
679
679
680 if not self._newline_marker.match(line):
680 if not self._newline_marker.match(line):
681 old_line += affects_old
681 old_line += affects_old
682 new_line += affects_new
682 new_line += affects_new
683 lines.append({
683 lines.append({
684 'old_lineno': affects_old and old_line or '',
684 'old_lineno': affects_old and old_line or '',
685 'new_lineno': affects_new and new_line or '',
685 'new_lineno': affects_new and new_line or '',
686 'action': action,
686 'action': action,
687 'line': self._clean_line(line, command)
687 'line': self._clean_line(line, command)
688 })
688 })
689 raw_diff.append(line)
689 raw_diff.append(line)
690
690
691 line = diff_iter.next()
691 line = diff_iter.next()
692
692
693 if self._newline_marker.match(line):
693 if self._newline_marker.match(line):
694 # we need to append to lines, since this is not
694 # we need to append to lines, since this is not
695 # counted in the line specs of diff
695 # counted in the line specs of diff
696 lines.append({
696 lines.append({
697 'old_lineno': '...',
697 'old_lineno': '...',
698 'new_lineno': '...',
698 'new_lineno': '...',
699 'action': Action.CONTEXT,
699 'action': Action.CONTEXT,
700 'line': self._clean_line(line, command)
700 'line': self._clean_line(line, command)
701 })
701 })
702
702
703 except StopIteration:
703 except StopIteration:
704 pass
704 pass
705 return ''.join(raw_diff), chunks, stats
705 return ''.join(raw_diff), chunks, stats
706
706
707 # FIXME: NEWDIFFS: dan: this replaces _parse_lines
707 # FIXME: NEWDIFFS: dan: this replaces _parse_lines
708 def _new_parse_lines(self, diff_iter):
708 def _new_parse_lines(self, diff_iter):
709 """
709 """
710 Parse the diff an return data for the template.
710 Parse the diff an return data for the template.
711 """
711 """
712
712
713 stats = [0, 0]
713 stats = [0, 0]
714 chunks = []
714 chunks = []
715 raw_diff = []
715 raw_diff = []
716
716
717 try:
717 try:
718 line = diff_iter.next()
718 line = diff_iter.next()
719
719
720 while line:
720 while line:
721 raw_diff.append(line)
721 raw_diff.append(line)
722 # match header e.g @@ -0,0 +1 @@\n'
722 # match header e.g @@ -0,0 +1 @@\n'
723 match = self._chunk_re.match(line)
723 match = self._chunk_re.match(line)
724
724
725 if not match:
725 if not match:
726 break
726 break
727
727
728 gr = match.groups()
728 gr = match.groups()
729 (old_line, old_end,
729 (old_line, old_end,
730 new_line, new_end) = [int(x or 1) for x in gr[:-1]]
730 new_line, new_end) = [int(x or 1) for x in gr[:-1]]
731
731
732 lines = []
732 lines = []
733 hunk = {
733 hunk = {
734 'section_header': gr[-1],
734 'section_header': gr[-1],
735 'source_start': old_line,
735 'source_start': old_line,
736 'source_length': old_end,
736 'source_length': old_end,
737 'target_start': new_line,
737 'target_start': new_line,
738 'target_length': new_end,
738 'target_length': new_end,
739 'lines': lines,
739 'lines': lines,
740 }
740 }
741 chunks.append(hunk)
741 chunks.append(hunk)
742
742
743 old_line -= 1
743 old_line -= 1
744 new_line -= 1
744 new_line -= 1
745
745
746 context = len(gr) == 5
746 context = len(gr) == 5
747 old_end += old_line
747 old_end += old_line
748 new_end += new_line
748 new_end += new_line
749
749
750 line = diff_iter.next()
750 line = diff_iter.next()
751
751
752 while old_line < old_end or new_line < new_end:
752 while old_line < old_end or new_line < new_end:
753 command = ' '
753 command = ' '
754 if line:
754 if line:
755 command = line[0]
755 command = line[0]
756
756
757 affects_old = affects_new = False
757 affects_old = affects_new = False
758
758
759 # ignore those if we don't expect them
759 # ignore those if we don't expect them
760 if command in '#@':
760 if command in '#@':
761 continue
761 continue
762 elif command == '+':
762 elif command == '+':
763 affects_new = True
763 affects_new = True
764 action = Action.ADD
764 action = Action.ADD
765 stats[0] += 1
765 stats[0] += 1
766 elif command == '-':
766 elif command == '-':
767 affects_old = True
767 affects_old = True
768 action = Action.DELETE
768 action = Action.DELETE
769 stats[1] += 1
769 stats[1] += 1
770 else:
770 else:
771 affects_old = affects_new = True
771 affects_old = affects_new = True
772 action = Action.UNMODIFIED
772 action = Action.UNMODIFIED
773
773
774 if not self._newline_marker.match(line):
774 if not self._newline_marker.match(line):
775 old_line += affects_old
775 old_line += affects_old
776 new_line += affects_new
776 new_line += affects_new
777 lines.append({
777 lines.append({
778 'old_lineno': affects_old and old_line or '',
778 'old_lineno': affects_old and old_line or '',
779 'new_lineno': affects_new and new_line or '',
779 'new_lineno': affects_new and new_line or '',
780 'action': action,
780 'action': action,
781 'line': self._clean_line(line, command)
781 'line': self._clean_line(line, command)
782 })
782 })
783 raw_diff.append(line)
783 raw_diff.append(line)
784
784
785 line = diff_iter.next()
785 line = diff_iter.next()
786
786
787 if self._newline_marker.match(line):
787 if self._newline_marker.match(line):
788 # we need to append to lines, since this is not
788 # we need to append to lines, since this is not
789 # counted in the line specs of diff
789 # counted in the line specs of diff
790 if affects_old:
790 if affects_old:
791 action = Action.OLD_NO_NL
791 action = Action.OLD_NO_NL
792 elif affects_new:
792 elif affects_new:
793 action = Action.NEW_NO_NL
793 action = Action.NEW_NO_NL
794 else:
794 else:
795 raise Exception('invalid context for no newline')
795 raise Exception('invalid context for no newline')
796
796
797 lines.append({
797 lines.append({
798 'old_lineno': None,
798 'old_lineno': None,
799 'new_lineno': None,
799 'new_lineno': None,
800 'action': action,
800 'action': action,
801 'line': self._clean_line(line, command)
801 'line': self._clean_line(line, command)
802 })
802 })
803
803
804 except StopIteration:
804 except StopIteration:
805 pass
805 pass
806
806
807 return ''.join(raw_diff), chunks, stats
807 return ''.join(raw_diff), chunks, stats
808
808
809 def _safe_id(self, idstring):
809 def _safe_id(self, idstring):
810 """Make a string safe for including in an id attribute.
810 """Make a string safe for including in an id attribute.
811
811
812 The HTML spec says that id attributes 'must begin with
812 The HTML spec says that id attributes 'must begin with
813 a letter ([A-Za-z]) and may be followed by any number
813 a letter ([A-Za-z]) and may be followed by any number
814 of letters, digits ([0-9]), hyphens ("-"), underscores
814 of letters, digits ([0-9]), hyphens ("-"), underscores
815 ("_"), colons (":"), and periods (".")'. These regexps
815 ("_"), colons (":"), and periods (".")'. These regexps
816 are slightly over-zealous, in that they remove colons
816 are slightly over-zealous, in that they remove colons
817 and periods unnecessarily.
817 and periods unnecessarily.
818
818
819 Whitespace is transformed into underscores, and then
819 Whitespace is transformed into underscores, and then
820 anything which is not a hyphen or a character that
820 anything which is not a hyphen or a character that
821 matches \w (alphanumerics and underscore) is removed.
821 matches \w (alphanumerics and underscore) is removed.
822
822
823 """
823 """
824 # Transform all whitespace to underscore
824 # Transform all whitespace to underscore
825 idstring = re.sub(r'\s', "_", '%s' % idstring)
825 idstring = re.sub(r'\s', "_", '%s' % idstring)
826 # Remove everything that is not a hyphen or a member of \w
826 # Remove everything that is not a hyphen or a member of \w
827 idstring = re.sub(r'(?!-)\W', "", idstring).lower()
827 idstring = re.sub(r'(?!-)\W', "", idstring).lower()
828 return idstring
828 return idstring
829
829
830 @classmethod
830 @classmethod
831 def diff_splitter(cls, string):
831 def diff_splitter(cls, string):
832 """
832 """
833 Diff split that emulates .splitlines() but works only on \n
833 Diff split that emulates .splitlines() but works only on \n
834 """
834 """
835 if not string:
835 if not string:
836 return
836 return
837 elif string == '\n':
837 elif string == '\n':
838 yield u'\n'
838 yield u'\n'
839 else:
839 else:
840
840
841 has_newline = string.endswith('\n')
841 has_newline = string.endswith('\n')
842 elements = string.split('\n')
842 elements = string.split('\n')
843 if has_newline:
843 if has_newline:
844 # skip last element as it's empty string from newlines
844 # skip last element as it's empty string from newlines
845 elements = elements[:-1]
845 elements = elements[:-1]
846
846
847 len_elements = len(elements)
847 len_elements = len(elements)
848
848
849 for cnt, line in enumerate(elements, start=1):
849 for cnt, line in enumerate(elements, start=1):
850 last_line = cnt == len_elements
850 last_line = cnt == len_elements
851 if last_line and not has_newline:
851 if last_line and not has_newline:
852 yield safe_unicode(line)
852 yield safe_unicode(line)
853 else:
853 else:
854 yield safe_unicode(line) + '\n'
854 yield safe_unicode(line) + '\n'
855
855
856 def prepare(self, inline_diff=True):
856 def prepare(self, inline_diff=True):
857 """
857 """
858 Prepare the passed udiff for HTML rendering.
858 Prepare the passed udiff for HTML rendering.
859
859
860 :return: A list of dicts with diff information.
860 :return: A list of dicts with diff information.
861 """
861 """
862 parsed = self._parser(inline_diff=inline_diff)
862 parsed = self._parser(inline_diff=inline_diff)
863 self.parsed = True
863 self.parsed = True
864 self.parsed_diff = parsed
864 self.parsed_diff = parsed
865 return parsed
865 return parsed
866
866
867 def as_raw(self, diff_lines=None):
867 def as_raw(self, diff_lines=None):
868 """
868 """
869 Returns raw diff as a byte string
869 Returns raw diff as a byte string
870 """
870 """
871 return self._diff.raw
871 return self._diff.raw
872
872
873 def as_html(self, table_class='code-difftable', line_class='line',
873 def as_html(self, table_class='code-difftable', line_class='line',
874 old_lineno_class='lineno old', new_lineno_class='lineno new',
874 old_lineno_class='lineno old', new_lineno_class='lineno new',
875 code_class='code', enable_comments=False, parsed_lines=None):
875 code_class='code', enable_comments=False, parsed_lines=None):
876 """
876 """
877 Return given diff as html table with customized css classes
877 Return given diff as html table with customized css classes
878 """
878 """
879 # TODO(marcink): not sure how to pass in translator
879 # TODO(marcink): not sure how to pass in translator
880 # here in an efficient way, leave the _ for proper gettext extraction
880 # here in an efficient way, leave the _ for proper gettext extraction
881 _ = lambda s: s
881 _ = lambda s: s
882
882
883 def _link_to_if(condition, label, url):
883 def _link_to_if(condition, label, url):
884 """
884 """
885 Generates a link if condition is meet or just the label if not.
885 Generates a link if condition is meet or just the label if not.
886 """
886 """
887
887
888 if condition:
888 if condition:
889 return '''<a href="%(url)s" class="tooltip"
889 return '''<a href="%(url)s" class="tooltip"
890 title="%(title)s">%(label)s</a>''' % {
890 title="%(title)s">%(label)s</a>''' % {
891 'title': _('Click to select line'),
891 'title': _('Click to select line'),
892 'url': url,
892 'url': url,
893 'label': label
893 'label': label
894 }
894 }
895 else:
895 else:
896 return label
896 return label
897 if not self.parsed:
897 if not self.parsed:
898 self.prepare()
898 self.prepare()
899
899
900 diff_lines = self.parsed_diff
900 diff_lines = self.parsed_diff
901 if parsed_lines:
901 if parsed_lines:
902 diff_lines = parsed_lines
902 diff_lines = parsed_lines
903
903
904 _html_empty = True
904 _html_empty = True
905 _html = []
905 _html = []
906 _html.append('''<table class="%(table_class)s">\n''' % {
906 _html.append('''<table class="%(table_class)s">\n''' % {
907 'table_class': table_class
907 'table_class': table_class
908 })
908 })
909
909
910 for diff in diff_lines:
910 for diff in diff_lines:
911 for line in diff['chunks']:
911 for line in diff['chunks']:
912 _html_empty = False
912 _html_empty = False
913 for change in line:
913 for change in line:
914 _html.append('''<tr class="%(lc)s %(action)s">\n''' % {
914 _html.append('''<tr class="%(lc)s %(action)s">\n''' % {
915 'lc': line_class,
915 'lc': line_class,
916 'action': change['action']
916 'action': change['action']
917 })
917 })
918 anchor_old_id = ''
918 anchor_old_id = ''
919 anchor_new_id = ''
919 anchor_new_id = ''
920 anchor_old = "%(filename)s_o%(oldline_no)s" % {
920 anchor_old = "%(filename)s_o%(oldline_no)s" % {
921 'filename': self._safe_id(diff['filename']),
921 'filename': self._safe_id(diff['filename']),
922 'oldline_no': change['old_lineno']
922 'oldline_no': change['old_lineno']
923 }
923 }
924 anchor_new = "%(filename)s_n%(oldline_no)s" % {
924 anchor_new = "%(filename)s_n%(oldline_no)s" % {
925 'filename': self._safe_id(diff['filename']),
925 'filename': self._safe_id(diff['filename']),
926 'oldline_no': change['new_lineno']
926 'oldline_no': change['new_lineno']
927 }
927 }
928 cond_old = (change['old_lineno'] != '...' and
928 cond_old = (change['old_lineno'] != '...' and
929 change['old_lineno'])
929 change['old_lineno'])
930 cond_new = (change['new_lineno'] != '...' and
930 cond_new = (change['new_lineno'] != '...' and
931 change['new_lineno'])
931 change['new_lineno'])
932 if cond_old:
932 if cond_old:
933 anchor_old_id = 'id="%s"' % anchor_old
933 anchor_old_id = 'id="%s"' % anchor_old
934 if cond_new:
934 if cond_new:
935 anchor_new_id = 'id="%s"' % anchor_new
935 anchor_new_id = 'id="%s"' % anchor_new
936
936
937 if change['action'] != Action.CONTEXT:
937 if change['action'] != Action.CONTEXT:
938 anchor_link = True
938 anchor_link = True
939 else:
939 else:
940 anchor_link = False
940 anchor_link = False
941
941
942 ###########################################################
942 ###########################################################
943 # COMMENT ICONS
943 # COMMENT ICONS
944 ###########################################################
944 ###########################################################
945 _html.append('''\t<td class="add-comment-line"><span class="add-comment-content">''')
945 _html.append('''\t<td class="add-comment-line"><span class="add-comment-content">''')
946
946
947 if enable_comments and change['action'] != Action.CONTEXT:
947 if enable_comments and change['action'] != Action.CONTEXT:
948 _html.append('''<a href="#"><span class="icon-comment-add"></span></a>''')
948 _html.append('''<a href="#"><span class="icon-comment-add"></span></a>''')
949
949
950 _html.append('''</span></td><td class="comment-toggle tooltip" title="Toggle Comment Thread"><i class="icon-comment"></i></td>\n''')
950 _html.append('''</span></td><td class="comment-toggle tooltip" title="Toggle Comment Thread"><i class="icon-comment"></i></td>\n''')
951
951
952 ###########################################################
952 ###########################################################
953 # OLD LINE NUMBER
953 # OLD LINE NUMBER
954 ###########################################################
954 ###########################################################
955 _html.append('''\t<td %(a_id)s class="%(olc)s">''' % {
955 _html.append('''\t<td %(a_id)s class="%(olc)s">''' % {
956 'a_id': anchor_old_id,
956 'a_id': anchor_old_id,
957 'olc': old_lineno_class
957 'olc': old_lineno_class
958 })
958 })
959
959
960 _html.append('''%(link)s''' % {
960 _html.append('''%(link)s''' % {
961 'link': _link_to_if(anchor_link, change['old_lineno'],
961 'link': _link_to_if(anchor_link, change['old_lineno'],
962 '#%s' % anchor_old)
962 '#%s' % anchor_old)
963 })
963 })
964 _html.append('''</td>\n''')
964 _html.append('''</td>\n''')
965 ###########################################################
965 ###########################################################
966 # NEW LINE NUMBER
966 # NEW LINE NUMBER
967 ###########################################################
967 ###########################################################
968
968
969 _html.append('''\t<td %(a_id)s class="%(nlc)s">''' % {
969 _html.append('''\t<td %(a_id)s class="%(nlc)s">''' % {
970 'a_id': anchor_new_id,
970 'a_id': anchor_new_id,
971 'nlc': new_lineno_class
971 'nlc': new_lineno_class
972 })
972 })
973
973
974 _html.append('''%(link)s''' % {
974 _html.append('''%(link)s''' % {
975 'link': _link_to_if(anchor_link, change['new_lineno'],
975 'link': _link_to_if(anchor_link, change['new_lineno'],
976 '#%s' % anchor_new)
976 '#%s' % anchor_new)
977 })
977 })
978 _html.append('''</td>\n''')
978 _html.append('''</td>\n''')
979 ###########################################################
979 ###########################################################
980 # CODE
980 # CODE
981 ###########################################################
981 ###########################################################
982 code_classes = [code_class]
982 code_classes = [code_class]
983 if (not enable_comments or
983 if (not enable_comments or
984 change['action'] == Action.CONTEXT):
984 change['action'] == Action.CONTEXT):
985 code_classes.append('no-comment')
985 code_classes.append('no-comment')
986 _html.append('\t<td class="%s">' % ' '.join(code_classes))
986 _html.append('\t<td class="%s">' % ' '.join(code_classes))
987 _html.append('''\n\t\t<pre>%(code)s</pre>\n''' % {
987 _html.append('''\n\t\t<pre>%(code)s</pre>\n''' % {
988 'code': change['line']
988 'code': change['line']
989 })
989 })
990
990
991 _html.append('''\t</td>''')
991 _html.append('''\t</td>''')
992 _html.append('''\n</tr>\n''')
992 _html.append('''\n</tr>\n''')
993 _html.append('''</table>''')
993 _html.append('''</table>''')
994 if _html_empty:
994 if _html_empty:
995 return None
995 return None
996 return ''.join(_html)
996 return ''.join(_html)
997
997
998 def stat(self):
998 def stat(self):
999 """
999 """
1000 Returns tuple of added, and removed lines for this instance
1000 Returns tuple of added, and removed lines for this instance
1001 """
1001 """
1002 return self.adds, self.removes
1002 return self.adds, self.removes
1003
1003
1004 def get_context_of_line(
1004 def get_context_of_line(
1005 self, path, diff_line=None, context_before=3, context_after=3):
1005 self, path, diff_line=None, context_before=3, context_after=3):
1006 """
1006 """
1007 Returns the context lines for the specified diff line.
1007 Returns the context lines for the specified diff line.
1008
1008
1009 :type diff_line: :class:`DiffLineNumber`
1009 :type diff_line: :class:`DiffLineNumber`
1010 """
1010 """
1011 assert self.parsed, "DiffProcessor is not initialized."
1011 assert self.parsed, "DiffProcessor is not initialized."
1012
1012
1013 if None not in diff_line:
1013 if None not in diff_line:
1014 raise ValueError(
1014 raise ValueError(
1015 "Cannot specify both line numbers: {}".format(diff_line))
1015 "Cannot specify both line numbers: {}".format(diff_line))
1016
1016
1017 file_diff = self._get_file_diff(path)
1017 file_diff = self._get_file_diff(path)
1018 chunk, idx = self._find_chunk_line_index(file_diff, diff_line)
1018 chunk, idx = self._find_chunk_line_index(file_diff, diff_line)
1019
1019
1020 first_line_to_include = max(idx - context_before, 0)
1020 first_line_to_include = max(idx - context_before, 0)
1021 first_line_after_context = idx + context_after + 1
1021 first_line_after_context = idx + context_after + 1
1022 context_lines = chunk[first_line_to_include:first_line_after_context]
1022 context_lines = chunk[first_line_to_include:first_line_after_context]
1023
1023
1024 line_contents = [
1024 line_contents = [
1025 _context_line(line) for line in context_lines
1025 _context_line(line) for line in context_lines
1026 if _is_diff_content(line)]
1026 if _is_diff_content(line)]
1027 # TODO: johbo: Interim fixup, the diff chunks drop the final newline.
1027 # TODO: johbo: Interim fixup, the diff chunks drop the final newline.
1028 # Once they are fixed, we can drop this line here.
1028 # Once they are fixed, we can drop this line here.
1029 if line_contents:
1029 if line_contents:
1030 line_contents[-1] = (
1030 line_contents[-1] = (
1031 line_contents[-1][0], line_contents[-1][1].rstrip('\n') + '\n')
1031 line_contents[-1][0], line_contents[-1][1].rstrip('\n') + '\n')
1032 return line_contents
1032 return line_contents
1033
1033
1034 def find_context(self, path, context, offset=0):
1034 def find_context(self, path, context, offset=0):
1035 """
1035 """
1036 Finds the given `context` inside of the diff.
1036 Finds the given `context` inside of the diff.
1037
1037
1038 Use the parameter `offset` to specify which offset the target line has
1038 Use the parameter `offset` to specify which offset the target line has
1039 inside of the given `context`. This way the correct diff line will be
1039 inside of the given `context`. This way the correct diff line will be
1040 returned.
1040 returned.
1041
1041
1042 :param offset: Shall be used to specify the offset of the main line
1042 :param offset: Shall be used to specify the offset of the main line
1043 within the given `context`.
1043 within the given `context`.
1044 """
1044 """
1045 if offset < 0 or offset >= len(context):
1045 if offset < 0 or offset >= len(context):
1046 raise ValueError(
1046 raise ValueError(
1047 "Only positive values up to the length of the context "
1047 "Only positive values up to the length of the context "
1048 "minus one are allowed.")
1048 "minus one are allowed.")
1049
1049
1050 matches = []
1050 matches = []
1051 file_diff = self._get_file_diff(path)
1051 file_diff = self._get_file_diff(path)
1052
1052
1053 for chunk in file_diff['chunks']:
1053 for chunk in file_diff['chunks']:
1054 context_iter = iter(context)
1054 context_iter = iter(context)
1055 for line_idx, line in enumerate(chunk):
1055 for line_idx, line in enumerate(chunk):
1056 try:
1056 try:
1057 if _context_line(line) == context_iter.next():
1057 if _context_line(line) == context_iter.next():
1058 continue
1058 continue
1059 except StopIteration:
1059 except StopIteration:
1060 matches.append((line_idx, chunk))
1060 matches.append((line_idx, chunk))
1061 context_iter = iter(context)
1061 context_iter = iter(context)
1062
1062
1063 # Increment position and triger StopIteration
1063 # Increment position and triger StopIteration
1064 # if we had a match at the end
1064 # if we had a match at the end
1065 line_idx += 1
1065 line_idx += 1
1066 try:
1066 try:
1067 context_iter.next()
1067 context_iter.next()
1068 except StopIteration:
1068 except StopIteration:
1069 matches.append((line_idx, chunk))
1069 matches.append((line_idx, chunk))
1070
1070
1071 effective_offset = len(context) - offset
1071 effective_offset = len(context) - offset
1072 found_at_diff_lines = [
1072 found_at_diff_lines = [
1073 _line_to_diff_line_number(chunk[idx - effective_offset])
1073 _line_to_diff_line_number(chunk[idx - effective_offset])
1074 for idx, chunk in matches]
1074 for idx, chunk in matches]
1075
1075
1076 return found_at_diff_lines
1076 return found_at_diff_lines
1077
1077
1078 def _get_file_diff(self, path):
1078 def _get_file_diff(self, path):
1079 for file_diff in self.parsed_diff:
1079 for file_diff in self.parsed_diff:
1080 if file_diff['filename'] == path:
1080 if file_diff['filename'] == path:
1081 break
1081 break
1082 else:
1082 else:
1083 raise FileNotInDiffException("File {} not in diff".format(path))
1083 raise FileNotInDiffException("File {} not in diff".format(path))
1084 return file_diff
1084 return file_diff
1085
1085
1086 def _find_chunk_line_index(self, file_diff, diff_line):
1086 def _find_chunk_line_index(self, file_diff, diff_line):
1087 for chunk in file_diff['chunks']:
1087 for chunk in file_diff['chunks']:
1088 for idx, line in enumerate(chunk):
1088 for idx, line in enumerate(chunk):
1089 if line['old_lineno'] == diff_line.old:
1089 if line['old_lineno'] == diff_line.old:
1090 return chunk, idx
1090 return chunk, idx
1091 if line['new_lineno'] == diff_line.new:
1091 if line['new_lineno'] == diff_line.new:
1092 return chunk, idx
1092 return chunk, idx
1093 raise LineNotInDiffException(
1093 raise LineNotInDiffException(
1094 "The line {} is not part of the diff.".format(diff_line))
1094 "The line {} is not part of the diff.".format(diff_line))
1095
1095
1096
1096
1097 def _is_diff_content(line):
1097 def _is_diff_content(line):
1098 return line['action'] in (
1098 return line['action'] in (
1099 Action.UNMODIFIED, Action.ADD, Action.DELETE)
1099 Action.UNMODIFIED, Action.ADD, Action.DELETE)
1100
1100
1101
1101
1102 def _context_line(line):
1102 def _context_line(line):
1103 return (line['action'], line['line'])
1103 return (line['action'], line['line'])
1104
1104
1105
1105
1106 DiffLineNumber = collections.namedtuple('DiffLineNumber', ['old', 'new'])
1106 DiffLineNumber = collections.namedtuple('DiffLineNumber', ['old', 'new'])
1107
1107
1108
1108
1109 def _line_to_diff_line_number(line):
1109 def _line_to_diff_line_number(line):
1110 new_line_no = line['new_lineno'] or None
1110 new_line_no = line['new_lineno'] or None
1111 old_line_no = line['old_lineno'] or None
1111 old_line_no = line['old_lineno'] or None
1112 return DiffLineNumber(old=old_line_no, new=new_line_no)
1112 return DiffLineNumber(old=old_line_no, new=new_line_no)
1113
1113
1114
1114
1115 class FileNotInDiffException(Exception):
1115 class FileNotInDiffException(Exception):
1116 """
1116 """
1117 Raised when the context for a missing file is requested.
1117 Raised when the context for a missing file is requested.
1118
1118
1119 If you request the context for a line in a file which is not part of the
1119 If you request the context for a line in a file which is not part of the
1120 given diff, then this exception is raised.
1120 given diff, then this exception is raised.
1121 """
1121 """
1122
1122
1123
1123
1124 class LineNotInDiffException(Exception):
1124 class LineNotInDiffException(Exception):
1125 """
1125 """
1126 Raised when the context for a missing line is requested.
1126 Raised when the context for a missing line is requested.
1127
1127
1128 If you request the context for a line in a file and this line is not
1128 If you request the context for a line in a file and this line is not
1129 part of the given diff, then this exception is raised.
1129 part of the given diff, then this exception is raised.
1130 """
1130 """
1131
1131
1132
1132
1133 class DiffLimitExceeded(Exception):
1133 class DiffLimitExceeded(Exception):
1134 pass
1134 pass
1135
1135
1136
1136
1137 # NOTE(marcink): if diffs.mako change, probably this
1137 # NOTE(marcink): if diffs.mako change, probably this
1138 # needs a bump to next version
1138 # needs a bump to next version
1139 CURRENT_DIFF_VERSION = 'v1'
1139 CURRENT_DIFF_VERSION = 'v2'
1140
1140
1141
1141
1142 def _cleanup_cache_file(cached_diff_file):
1142 def _cleanup_cache_file(cached_diff_file):
1143 # cleanup file to not store it "damaged"
1143 # cleanup file to not store it "damaged"
1144 try:
1144 try:
1145 os.remove(cached_diff_file)
1145 os.remove(cached_diff_file)
1146 except Exception:
1146 except Exception:
1147 log.exception('Failed to cleanup path %s', cached_diff_file)
1147 log.exception('Failed to cleanup path %s', cached_diff_file)
1148
1148
1149
1149
1150 def cache_diff(cached_diff_file, diff, commits):
1150 def cache_diff(cached_diff_file, diff, commits):
1151
1151
1152 struct = {
1152 struct = {
1153 'version': CURRENT_DIFF_VERSION,
1153 'version': CURRENT_DIFF_VERSION,
1154 'diff': diff,
1154 'diff': diff,
1155 'commits': commits
1155 'commits': commits
1156 }
1156 }
1157
1157
1158 try:
1158 try:
1159 with bz2.BZ2File(cached_diff_file, 'wb') as f:
1159 with bz2.BZ2File(cached_diff_file, 'wb') as f:
1160 pickle.dump(struct, f)
1160 pickle.dump(struct, f)
1161 log.debug('Saved diff cache under %s', cached_diff_file)
1161 log.debug('Saved diff cache under %s', cached_diff_file)
1162 except Exception:
1162 except Exception:
1163 log.warn('Failed to save cache', exc_info=True)
1163 log.warn('Failed to save cache', exc_info=True)
1164 _cleanup_cache_file(cached_diff_file)
1164 _cleanup_cache_file(cached_diff_file)
1165
1165
1166
1166
1167 def load_cached_diff(cached_diff_file):
1167 def load_cached_diff(cached_diff_file):
1168
1168
1169 default_struct = {
1169 default_struct = {
1170 'version': CURRENT_DIFF_VERSION,
1170 'version': CURRENT_DIFF_VERSION,
1171 'diff': None,
1171 'diff': None,
1172 'commits': None
1172 'commits': None
1173 }
1173 }
1174
1174
1175 has_cache = os.path.isfile(cached_diff_file)
1175 has_cache = os.path.isfile(cached_diff_file)
1176 if not has_cache:
1176 if not has_cache:
1177 return default_struct
1177 return default_struct
1178
1178
1179 data = None
1179 data = None
1180 try:
1180 try:
1181 with bz2.BZ2File(cached_diff_file, 'rb') as f:
1181 with bz2.BZ2File(cached_diff_file, 'rb') as f:
1182 data = pickle.load(f)
1182 data = pickle.load(f)
1183 log.debug('Loaded diff cache from %s', cached_diff_file)
1183 log.debug('Loaded diff cache from %s', cached_diff_file)
1184 except Exception:
1184 except Exception:
1185 log.warn('Failed to read diff cache file', exc_info=True)
1185 log.warn('Failed to read diff cache file', exc_info=True)
1186
1186
1187 if not data:
1187 if not data:
1188 data = default_struct
1188 data = default_struct
1189
1189
1190 if not isinstance(data, dict):
1190 if not isinstance(data, dict):
1191 # old version of data ?
1191 # old version of data ?
1192 data = default_struct
1192 data = default_struct
1193
1193
1194 # check version
1194 # check version
1195 if data.get('version') != CURRENT_DIFF_VERSION:
1195 if data.get('version') != CURRENT_DIFF_VERSION:
1196 # purge cache
1196 # purge cache
1197 _cleanup_cache_file(cached_diff_file)
1197 _cleanup_cache_file(cached_diff_file)
1198 return default_struct
1198 return default_struct
1199
1199
1200 return data
1200 return data
1201
1201
1202
1202
1203 def generate_diff_cache_key(*args):
1203 def generate_diff_cache_key(*args):
1204 """
1204 """
1205 Helper to generate a cache key using arguments
1205 Helper to generate a cache key using arguments
1206 """
1206 """
1207 def arg_mapper(input_param):
1207 def arg_mapper(input_param):
1208 input_param = safe_str(input_param)
1208 input_param = safe_str(input_param)
1209 # we cannot allow '/' in arguments since it would allow
1209 # we cannot allow '/' in arguments since it would allow
1210 # subdirectory usage
1210 # subdirectory usage
1211 input_param.replace('/', '_')
1211 input_param.replace('/', '_')
1212 return input_param or None # prevent empty string arguments
1212 return input_param or None # prevent empty string arguments
1213
1213
1214 return '_'.join([
1214 return '_'.join([
1215 '{}' for i in range(len(args))]).format(*map(arg_mapper, args))
1215 '{}' for i in range(len(args))]).format(*map(arg_mapper, args))
1216
1216
1217
1217
1218 def diff_cache_exist(cache_storage, *args):
1218 def diff_cache_exist(cache_storage, *args):
1219 """
1219 """
1220 Based on all generated arguments check and return a cache path
1220 Based on all generated arguments check and return a cache path
1221 """
1221 """
1222 cache_key = generate_diff_cache_key(*args)
1222 cache_key = generate_diff_cache_key(*args)
1223 cache_file_path = os.path.join(cache_storage, cache_key)
1223 cache_file_path = os.path.join(cache_storage, cache_key)
1224 # prevent path traversal attacks using some param that have e.g '../../'
1224 # prevent path traversal attacks using some param that have e.g '../../'
1225 if not os.path.abspath(cache_file_path).startswith(cache_storage):
1225 if not os.path.abspath(cache_file_path).startswith(cache_storage):
1226 raise ValueError('Final path must be within {}'.format(cache_storage))
1226 raise ValueError('Final path must be within {}'.format(cache_storage))
1227
1227
1228 return cache_file_path
1228 return cache_file_path
@@ -1,761 +1,761 b''
1 <%namespace name="commentblock" file="/changeset/changeset_file_comment.mako"/>
1 <%namespace name="commentblock" file="/changeset/changeset_file_comment.mako"/>
2
2
3 <%def name="diff_line_anchor(filename, line, type)"><%
3 <%def name="diff_line_anchor(filename, line, type)"><%
4 return '%s_%s_%i' % (h.safeid(filename), type, line)
4 return '%s_%s_%i' % (h.safeid(filename), type, line)
5 %></%def>
5 %></%def>
6
6
7 <%def name="action_class(action)">
7 <%def name="action_class(action)">
8 <%
8 <%
9 return {
9 return {
10 '-': 'cb-deletion',
10 '-': 'cb-deletion',
11 '+': 'cb-addition',
11 '+': 'cb-addition',
12 ' ': 'cb-context',
12 ' ': 'cb-context',
13 }.get(action, 'cb-empty')
13 }.get(action, 'cb-empty')
14 %>
14 %>
15 </%def>
15 </%def>
16
16
17 <%def name="op_class(op_id)">
17 <%def name="op_class(op_id)">
18 <%
18 <%
19 return {
19 return {
20 DEL_FILENODE: 'deletion', # file deleted
20 DEL_FILENODE: 'deletion', # file deleted
21 BIN_FILENODE: 'warning' # binary diff hidden
21 BIN_FILENODE: 'warning' # binary diff hidden
22 }.get(op_id, 'addition')
22 }.get(op_id, 'addition')
23 %>
23 %>
24 </%def>
24 </%def>
25
25
26
26
27
27
28 <%def name="render_diffset(diffset, commit=None,
28 <%def name="render_diffset(diffset, commit=None,
29
29
30 # collapse all file diff entries when there are more than this amount of files in the diff
30 # collapse all file diff entries when there are more than this amount of files in the diff
31 collapse_when_files_over=20,
31 collapse_when_files_over=20,
32
32
33 # collapse lines in the diff when more than this amount of lines changed in the file diff
33 # collapse lines in the diff when more than this amount of lines changed in the file diff
34 lines_changed_limit=500,
34 lines_changed_limit=500,
35
35
36 # add a ruler at to the output
36 # add a ruler at to the output
37 ruler_at_chars=0,
37 ruler_at_chars=0,
38
38
39 # show inline comments
39 # show inline comments
40 use_comments=False,
40 use_comments=False,
41
41
42 # disable new comments
42 # disable new comments
43 disable_new_comments=False,
43 disable_new_comments=False,
44
44
45 # special file-comments that were deleted in previous versions
45 # special file-comments that were deleted in previous versions
46 # it's used for showing outdated comments for deleted files in a PR
46 # it's used for showing outdated comments for deleted files in a PR
47 deleted_files_comments=None,
47 deleted_files_comments=None,
48
48
49 # for cache purpose
49 # for cache purpose
50 inline_comments=None
50 inline_comments=None
51
51
52 )">
52 )">
53 %if use_comments:
53 %if use_comments:
54 <div id="cb-comments-inline-container-template" class="js-template">
54 <div id="cb-comments-inline-container-template" class="js-template">
55 ${inline_comments_container([], inline_comments)}
55 ${inline_comments_container([], inline_comments)}
56 </div>
56 </div>
57 <div class="js-template" id="cb-comment-inline-form-template">
57 <div class="js-template" id="cb-comment-inline-form-template">
58 <div class="comment-inline-form ac">
58 <div class="comment-inline-form ac">
59
59
60 %if c.rhodecode_user.username != h.DEFAULT_USER:
60 %if c.rhodecode_user.username != h.DEFAULT_USER:
61 ## render template for inline comments
61 ## render template for inline comments
62 ${commentblock.comment_form(form_type='inline')}
62 ${commentblock.comment_form(form_type='inline')}
63 %else:
63 %else:
64 ${h.form('', class_='inline-form comment-form-login', method='get')}
64 ${h.form('', class_='inline-form comment-form-login', method='get')}
65 <div class="pull-left">
65 <div class="pull-left">
66 <div class="comment-help pull-right">
66 <div class="comment-help pull-right">
67 ${_('You need to be logged in to leave comments.')} <a href="${h.route_path('login', _query={'came_from': h.current_route_path(request)})}">${_('Login now')}</a>
67 ${_('You need to be logged in to leave comments.')} <a href="${h.route_path('login', _query={'came_from': h.current_route_path(request)})}">${_('Login now')}</a>
68 </div>
68 </div>
69 </div>
69 </div>
70 <div class="comment-button pull-right">
70 <div class="comment-button pull-right">
71 <button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
71 <button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
72 ${_('Cancel')}
72 ${_('Cancel')}
73 </button>
73 </button>
74 </div>
74 </div>
75 <div class="clearfix"></div>
75 <div class="clearfix"></div>
76 ${h.end_form()}
76 ${h.end_form()}
77 %endif
77 %endif
78 </div>
78 </div>
79 </div>
79 </div>
80
80
81 %endif
81 %endif
82 <%
82 <%
83 collapse_all = len(diffset.files) > collapse_when_files_over
83 collapse_all = len(diffset.files) > collapse_when_files_over
84 %>
84 %>
85
85
86 %if c.diffmode == 'sideside':
86 %if c.diffmode == 'sideside':
87 <style>
87 <style>
88 .wrapper {
88 .wrapper {
89 max-width: 1600px !important;
89 max-width: 1600px !important;
90 }
90 }
91 </style>
91 </style>
92 %endif
92 %endif
93
93
94 %if ruler_at_chars:
94 %if ruler_at_chars:
95 <style>
95 <style>
96 .diff table.cb .cb-content:after {
96 .diff table.cb .cb-content:after {
97 content: "";
97 content: "";
98 border-left: 1px solid blue;
98 border-left: 1px solid blue;
99 position: absolute;
99 position: absolute;
100 top: 0;
100 top: 0;
101 height: 18px;
101 height: 18px;
102 opacity: .2;
102 opacity: .2;
103 z-index: 10;
103 z-index: 10;
104 //## +5 to account for diff action (+/-)
104 //## +5 to account for diff action (+/-)
105 left: ${ruler_at_chars + 5}ch;
105 left: ${ruler_at_chars + 5}ch;
106 </style>
106 </style>
107 %endif
107 %endif
108
108
109 <div class="diffset ${disable_new_comments and 'diffset-comments-disabled'}">
109 <div class="diffset ${disable_new_comments and 'diffset-comments-disabled'}">
110 <div class="diffset-heading ${diffset.limited_diff and 'diffset-heading-warning' or ''}">
110 <div class="diffset-heading ${diffset.limited_diff and 'diffset-heading-warning' or ''}">
111 %if commit:
111 %if commit:
112 <div class="pull-right">
112 <div class="pull-right">
113 <a class="btn tooltip" title="${h.tooltip(_('Browse Files at revision {}').format(commit.raw_id))}" href="${h.route_path('repo_files',repo_name=diffset.repo_name, commit_id=commit.raw_id, f_path='')}">
113 <a class="btn tooltip" title="${h.tooltip(_('Browse Files at revision {}').format(commit.raw_id))}" href="${h.route_path('repo_files',repo_name=diffset.repo_name, commit_id=commit.raw_id, f_path='')}">
114 ${_('Browse Files')}
114 ${_('Browse Files')}
115 </a>
115 </a>
116 </div>
116 </div>
117 %endif
117 %endif
118 <h2 class="clearinner">
118 <h2 class="clearinner">
119 %if commit:
119 %if commit:
120 <a class="tooltip revision" title="${h.tooltip(commit.message)}" href="${h.route_path('repo_commit',repo_name=c.repo_name,commit_id=commit.raw_id)}">${'r%s:%s' % (commit.revision,h.short_id(commit.raw_id))}</a> -
120 <a class="tooltip revision" title="${h.tooltip(commit.message)}" href="${h.route_path('repo_commit',repo_name=c.repo_name,commit_id=commit.raw_id)}">${'r%s:%s' % (commit.revision,h.short_id(commit.raw_id))}</a> -
121 ${h.age_component(commit.date)} -
121 ${h.age_component(commit.date)} -
122 %endif
122 %endif
123
123
124 %if diffset.limited_diff:
124 %if diffset.limited_diff:
125 ${_('The requested commit is too big and content was truncated.')}
125 ${_('The requested commit is too big and content was truncated.')}
126
126
127 ${_ungettext('%(num)s file changed.', '%(num)s files changed.', diffset.changed_files) % {'num': diffset.changed_files}}
127 ${_ungettext('%(num)s file changed.', '%(num)s files changed.', diffset.changed_files) % {'num': diffset.changed_files}}
128 <a href="${h.current_route_path(request, fulldiff=1)}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a>
128 <a href="${h.current_route_path(request, fulldiff=1)}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a>
129 %else:
129 %else:
130 ${_ungettext('%(num)s file changed: %(linesadd)s inserted, ''%(linesdel)s deleted',
130 ${_ungettext('%(num)s file changed: %(linesadd)s inserted, ''%(linesdel)s deleted',
131 '%(num)s files changed: %(linesadd)s inserted, %(linesdel)s deleted', diffset.changed_files) % {'num': diffset.changed_files, 'linesadd': diffset.lines_added, 'linesdel': diffset.lines_deleted}}
131 '%(num)s files changed: %(linesadd)s inserted, %(linesdel)s deleted', diffset.changed_files) % {'num': diffset.changed_files, 'linesadd': diffset.lines_added, 'linesdel': diffset.lines_deleted}}
132 %endif
132 %endif
133
133
134 </h2>
134 </h2>
135 </div>
135 </div>
136
136
137 %if diffset.has_hidden_changes:
137 %if diffset.has_hidden_changes:
138 <p class="empty_data">${_('Some changes may be hidden')}</p>
138 <p class="empty_data">${_('Some changes may be hidden')}</p>
139 %elif not diffset.files:
139 %elif not diffset.files:
140 <p class="empty_data">${_('No files')}</p>
140 <p class="empty_data">${_('No files')}</p>
141 %endif
141 %endif
142
142
143 <div class="filediffs">
143 <div class="filediffs">
144 ## initial value could be marked as False later on
144 ## initial value could be marked as False later on
145 <% over_lines_changed_limit = False %>
145 <% over_lines_changed_limit = False %>
146 %for i, filediff in enumerate(diffset.files):
146 %for i, filediff in enumerate(diffset.files):
147
147
148 <%
148 <%
149 lines_changed = filediff.patch['stats']['added'] + filediff.patch['stats']['deleted']
149 lines_changed = filediff.patch['stats']['added'] + filediff.patch['stats']['deleted']
150 over_lines_changed_limit = lines_changed > lines_changed_limit
150 over_lines_changed_limit = lines_changed > lines_changed_limit
151 %>
151 %>
152
152
153 <input ${(collapse_all and 'checked' or '')} class="filediff-collapse-state" id="filediff-collapse-${id(filediff)}" type="checkbox">
153 <input ${(collapse_all and 'checked' or '')} class="filediff-collapse-state" id="filediff-collapse-${id(filediff)}" type="checkbox">
154 <div
154 <div
155 class="filediff"
155 class="filediff"
156 data-f-path="${filediff.patch['filename']}"
156 data-f-path="${filediff.patch['filename']}"
157 id="a_${h.FID('', filediff.patch['filename'])}"
157 id="a_${h.FID('', filediff.patch['filename'])}"
158 >
158 >
159
159
160 <label for="filediff-collapse-${id(filediff)}" class="filediff-heading">
160 <label for="filediff-collapse-${id(filediff)}" class="filediff-heading">
161 <div class="filediff-collapse-indicator"></div>
161 <div class="filediff-collapse-indicator"></div>
162 ${diff_ops(filediff)}
162 ${diff_ops(filediff)}
163 </label>
163 </label>
164 ${diff_menu(filediff, use_comments=use_comments)}
164 ${diff_menu(filediff, use_comments=use_comments)}
165 <table class="cb cb-diff-${c.diffmode} code-highlight ${(over_lines_changed_limit and 'cb-collapsed' or '')}">
165 <table class="cb cb-diff-${c.diffmode} code-highlight ${(over_lines_changed_limit and 'cb-collapsed' or '')}">
166
166
167 ## new/deleted/empty content case
167 ## new/deleted/empty content case
168 % if not filediff.hunks:
168 % if not filediff.hunks:
169 ## Comment container, on "fakes" hunk that contains all data to render comments
169 ## Comment container, on "fakes" hunk that contains all data to render comments
170 ${render_hunk_lines(c.diffmode, filediff.hunk_ops, use_comments=use_comments, inline_comments=inline_comments)}
170 ${render_hunk_lines(c.diffmode, filediff.hunk_ops, use_comments=use_comments, inline_comments=inline_comments)}
171 % endif
171 % endif
172
172
173 %if filediff.limited_diff:
173 %if filediff.limited_diff:
174 <tr class="cb-warning cb-collapser">
174 <tr class="cb-warning cb-collapser">
175 <td class="cb-text" ${(c.diffmode == 'unified' and 'colspan=4' or 'colspan=6')}>
175 <td class="cb-text" ${(c.diffmode == 'unified' and 'colspan=4' or 'colspan=6')}>
176 ${_('The requested commit is too big and content was truncated.')} <a href="${h.current_route_path(request, fulldiff=1)}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a>
176 ${_('The requested commit is too big and content was truncated.')} <a href="${h.current_route_path(request, fulldiff=1)}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a>
177 </td>
177 </td>
178 </tr>
178 </tr>
179 %else:
179 %else:
180 %if over_lines_changed_limit:
180 %if over_lines_changed_limit:
181 <tr class="cb-warning cb-collapser">
181 <tr class="cb-warning cb-collapser">
182 <td class="cb-text" ${(c.diffmode == 'unified' and 'colspan=4' or 'colspan=6')}>
182 <td class="cb-text" ${(c.diffmode == 'unified' and 'colspan=4' or 'colspan=6')}>
183 ${_('This diff has been collapsed as it changes many lines, (%i lines changed)' % lines_changed)}
183 ${_('This diff has been collapsed as it changes many lines, (%i lines changed)' % lines_changed)}
184 <a href="#" class="cb-expand"
184 <a href="#" class="cb-expand"
185 onclick="$(this).closest('table').removeClass('cb-collapsed'); return false;">${_('Show them')}
185 onclick="$(this).closest('table').removeClass('cb-collapsed'); return false;">${_('Show them')}
186 </a>
186 </a>
187 <a href="#" class="cb-collapse"
187 <a href="#" class="cb-collapse"
188 onclick="$(this).closest('table').addClass('cb-collapsed'); return false;">${_('Hide them')}
188 onclick="$(this).closest('table').addClass('cb-collapsed'); return false;">${_('Hide them')}
189 </a>
189 </a>
190 </td>
190 </td>
191 </tr>
191 </tr>
192 %endif
192 %endif
193 %endif
193 %endif
194
194
195 % for hunk in filediff.hunks:
195 % for hunk in filediff.hunks:
196 <tr class="cb-hunk">
196 <tr class="cb-hunk">
197 <td ${(c.diffmode == 'unified' and 'colspan=3' or '')}>
197 <td ${(c.diffmode == 'unified' and 'colspan=3' or '')}>
198 ## TODO: dan: add ajax loading of more context here
198 ## TODO: dan: add ajax loading of more context here
199 ## <a href="#">
199 ## <a href="#">
200 <i class="icon-more"></i>
200 <i class="icon-more"></i>
201 ## </a>
201 ## </a>
202 </td>
202 </td>
203 <td ${(c.diffmode == 'sideside' and 'colspan=5' or '')}>
203 <td ${(c.diffmode == 'sideside' and 'colspan=5' or '')}>
204 @@
204 @@
205 -${hunk.source_start},${hunk.source_length}
205 -${hunk.source_start},${hunk.source_length}
206 +${hunk.target_start},${hunk.target_length}
206 +${hunk.target_start},${hunk.target_length}
207 ${hunk.section_header}
207 ${hunk.section_header}
208 </td>
208 </td>
209 </tr>
209 </tr>
210 ${render_hunk_lines(c.diffmode, hunk, use_comments=use_comments, inline_comments=inline_comments)}
210 ${render_hunk_lines(c.diffmode, hunk, use_comments=use_comments, inline_comments=inline_comments)}
211 % endfor
211 % endfor
212
212
213 <% unmatched_comments = (inline_comments or {}).get(filediff.patch['filename'], {}) %>
213 <% unmatched_comments = (inline_comments or {}).get(filediff.patch['filename'], {}) %>
214
214
215 ## outdated comments that do not fit into currently displayed lines
215 ## outdated comments that do not fit into currently displayed lines
216 % for lineno, comments in unmatched_comments.items():
216 % for lineno, comments in unmatched_comments.items():
217
217
218 %if c.diffmode == 'unified':
218 %if c.diffmode == 'unified':
219 % if loop.index == 0:
219 % if loop.index == 0:
220 <tr class="cb-hunk">
220 <tr class="cb-hunk">
221 <td colspan="3"></td>
221 <td colspan="3"></td>
222 <td>
222 <td>
223 <div>
223 <div>
224 ${_('Unmatched inline comments below')}
224 ${_('Unmatched inline comments below')}
225 </div>
225 </div>
226 </td>
226 </td>
227 </tr>
227 </tr>
228 % endif
228 % endif
229 <tr class="cb-line">
229 <tr class="cb-line">
230 <td class="cb-data cb-context"></td>
230 <td class="cb-data cb-context"></td>
231 <td class="cb-lineno cb-context"></td>
231 <td class="cb-lineno cb-context"></td>
232 <td class="cb-lineno cb-context"></td>
232 <td class="cb-lineno cb-context"></td>
233 <td class="cb-content cb-context">
233 <td class="cb-content cb-context">
234 ${inline_comments_container(comments, inline_comments)}
234 ${inline_comments_container(comments, inline_comments)}
235 </td>
235 </td>
236 </tr>
236 </tr>
237 %elif c.diffmode == 'sideside':
237 %elif c.diffmode == 'sideside':
238 % if loop.index == 0:
238 % if loop.index == 0:
239 <tr class="cb-hunk">
239 <tr class="cb-hunk">
240 <td colspan="2"></td>
240 <td colspan="2"></td>
241 <td class="cb-line" colspan="6">
241 <td class="cb-line" colspan="6">
242 <div>
242 <div>
243 ${_('Unmatched comments below')}
243 ${_('Unmatched comments below')}
244 </div>
244 </div>
245 </td>
245 </td>
246 </tr>
246 </tr>
247 % endif
247 % endif
248 <tr class="cb-line">
248 <tr class="cb-line">
249 <td class="cb-data cb-context"></td>
249 <td class="cb-data cb-context"></td>
250 <td class="cb-lineno cb-context"></td>
250 <td class="cb-lineno cb-context"></td>
251 <td class="cb-content cb-context">
251 <td class="cb-content cb-context">
252 % if lineno.startswith('o'):
252 % if lineno.startswith('o'):
253 ${inline_comments_container(comments, inline_comments)}
253 ${inline_comments_container(comments, inline_comments)}
254 % endif
254 % endif
255 </td>
255 </td>
256
256
257 <td class="cb-data cb-context"></td>
257 <td class="cb-data cb-context"></td>
258 <td class="cb-lineno cb-context"></td>
258 <td class="cb-lineno cb-context"></td>
259 <td class="cb-content cb-context">
259 <td class="cb-content cb-context">
260 % if lineno.startswith('n'):
260 % if lineno.startswith('n'):
261 ${inline_comments_container(comments, inline_comments)}
261 ${inline_comments_container(comments, inline_comments)}
262 % endif
262 % endif
263 </td>
263 </td>
264 </tr>
264 </tr>
265 %endif
265 %endif
266
266
267 % endfor
267 % endfor
268
268
269 </table>
269 </table>
270 </div>
270 </div>
271 %endfor
271 %endfor
272
272
273 ## outdated comments that are made for a file that has been deleted
273 ## outdated comments that are made for a file that has been deleted
274 % for filename, comments_dict in (deleted_files_comments or {}).items():
274 % for filename, comments_dict in (deleted_files_comments or {}).items():
275 <%
275 <%
276 display_state = 'display: none'
276 display_state = 'display: none'
277 open_comments_in_file = [x for x in comments_dict['comments'] if x.outdated is False]
277 open_comments_in_file = [x for x in comments_dict['comments'] if x.outdated is False]
278 if open_comments_in_file:
278 if open_comments_in_file:
279 display_state = ''
279 display_state = ''
280 %>
280 %>
281 <div class="filediffs filediff-outdated" style="${display_state}">
281 <div class="filediffs filediff-outdated" style="${display_state}">
282 <input ${(collapse_all and 'checked' or '')} class="filediff-collapse-state" id="filediff-collapse-${id(filename)}" type="checkbox">
282 <input ${(collapse_all and 'checked' or '')} class="filediff-collapse-state" id="filediff-collapse-${id(filename)}" type="checkbox">
283 <div class="filediff" data-f-path="${filename}" id="a_${h.FID('', filename)}">
283 <div class="filediff" data-f-path="${filename}" id="a_${h.FID('', filename)}">
284 <label for="filediff-collapse-${id(filename)}" class="filediff-heading">
284 <label for="filediff-collapse-${id(filename)}" class="filediff-heading">
285 <div class="filediff-collapse-indicator"></div>
285 <div class="filediff-collapse-indicator"></div>
286 <span class="pill">
286 <span class="pill">
287 ## file was deleted
287 ## file was deleted
288 <strong>${filename}</strong>
288 <strong>${filename}</strong>
289 </span>
289 </span>
290 <span class="pill-group" style="float: left">
290 <span class="pill-group" style="float: left">
291 ## file op, doesn't need translation
291 ## file op, doesn't need translation
292 <span class="pill" op="removed">removed in this version</span>
292 <span class="pill" op="removed">removed in this version</span>
293 </span>
293 </span>
294 <a class="pill filediff-anchor" href="#a_${h.FID('', filename)}">ΒΆ</a>
294 <a class="pill filediff-anchor" href="#a_${h.FID('', filename)}">ΒΆ</a>
295 <span class="pill-group" style="float: right">
295 <span class="pill-group" style="float: right">
296 <span class="pill" op="deleted">-${comments_dict['stats']}</span>
296 <span class="pill" op="deleted">-${comments_dict['stats']}</span>
297 </span>
297 </span>
298 </label>
298 </label>
299
299
300 <table class="cb cb-diff-${c.diffmode} code-highlight ${over_lines_changed_limit and 'cb-collapsed' or ''}">
300 <table class="cb cb-diff-${c.diffmode} code-highlight ${over_lines_changed_limit and 'cb-collapsed' or ''}">
301 <tr>
301 <tr>
302 % if c.diffmode == 'unified':
302 % if c.diffmode == 'unified':
303 <td></td>
303 <td></td>
304 %endif
304 %endif
305
305
306 <td></td>
306 <td></td>
307 <td class="cb-text cb-${op_class(BIN_FILENODE)}" ${c.diffmode == 'unified' and 'colspan=4' or 'colspan=5'}>
307 <td class="cb-text cb-${op_class(BIN_FILENODE)}" ${c.diffmode == 'unified' and 'colspan=4' or 'colspan=5'}>
308 ${_('File was deleted in this version. There are still outdated/unresolved comments attached to it.')}
308 ${_('File was deleted in this version. There are still outdated/unresolved comments attached to it.')}
309 </td>
309 </td>
310 </tr>
310 </tr>
311 %if c.diffmode == 'unified':
311 %if c.diffmode == 'unified':
312 <tr class="cb-line">
312 <tr class="cb-line">
313 <td class="cb-data cb-context"></td>
313 <td class="cb-data cb-context"></td>
314 <td class="cb-lineno cb-context"></td>
314 <td class="cb-lineno cb-context"></td>
315 <td class="cb-lineno cb-context"></td>
315 <td class="cb-lineno cb-context"></td>
316 <td class="cb-content cb-context">
316 <td class="cb-content cb-context">
317 ${inline_comments_container(comments_dict['comments'], inline_comments)}
317 ${inline_comments_container(comments_dict['comments'], inline_comments)}
318 </td>
318 </td>
319 </tr>
319 </tr>
320 %elif c.diffmode == 'sideside':
320 %elif c.diffmode == 'sideside':
321 <tr class="cb-line">
321 <tr class="cb-line">
322 <td class="cb-data cb-context"></td>
322 <td class="cb-data cb-context"></td>
323 <td class="cb-lineno cb-context"></td>
323 <td class="cb-lineno cb-context"></td>
324 <td class="cb-content cb-context"></td>
324 <td class="cb-content cb-context"></td>
325
325
326 <td class="cb-data cb-context"></td>
326 <td class="cb-data cb-context"></td>
327 <td class="cb-lineno cb-context"></td>
327 <td class="cb-lineno cb-context"></td>
328 <td class="cb-content cb-context">
328 <td class="cb-content cb-context">
329 ${inline_comments_container(comments_dict['comments'], inline_comments)}
329 ${inline_comments_container(comments_dict['comments'], inline_comments)}
330 </td>
330 </td>
331 </tr>
331 </tr>
332 %endif
332 %endif
333 </table>
333 </table>
334 </div>
334 </div>
335 </div>
335 </div>
336 % endfor
336 % endfor
337
337
338 </div>
338 </div>
339 </div>
339 </div>
340 </%def>
340 </%def>
341
341
342 <%def name="diff_ops(filediff)">
342 <%def name="diff_ops(filediff)">
343 <%
343 <%
344 from rhodecode.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
344 from rhodecode.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
345 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE, COPIED_FILENODE
345 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE, COPIED_FILENODE
346 %>
346 %>
347 <span class="pill">
347 <span class="pill">
348 %if filediff.source_file_path and filediff.target_file_path:
348 %if filediff.source_file_path and filediff.target_file_path:
349 %if filediff.source_file_path != filediff.target_file_path:
349 %if filediff.source_file_path != filediff.target_file_path:
350 ## file was renamed, or copied
350 ## file was renamed, or copied
351 %if RENAMED_FILENODE in filediff.patch['stats']['ops']:
351 %if RENAMED_FILENODE in filediff.patch['stats']['ops']:
352 <strong>${filediff.target_file_path}</strong> β¬… <del>${filediff.source_file_path}</del>
352 <strong>${filediff.target_file_path}</strong> β¬… <del>${filediff.source_file_path}</del>
353 <% final_path = filediff.target_file_path %>
353 <% final_path = filediff.target_file_path %>
354 %elif COPIED_FILENODE in filediff.patch['stats']['ops']:
354 %elif COPIED_FILENODE in filediff.patch['stats']['ops']:
355 <strong>${filediff.target_file_path}</strong> β¬… ${filediff.source_file_path}
355 <strong>${filediff.target_file_path}</strong> β¬… ${filediff.source_file_path}
356 <% final_path = filediff.target_file_path %>
356 <% final_path = filediff.target_file_path %>
357 %endif
357 %endif
358 %else:
358 %else:
359 ## file was modified
359 ## file was modified
360 <strong>${filediff.source_file_path}</strong>
360 <strong>${filediff.source_file_path}</strong>
361 <% final_path = filediff.source_file_path %>
361 <% final_path = filediff.source_file_path %>
362 %endif
362 %endif
363 %else:
363 %else:
364 %if filediff.source_file_path:
364 %if filediff.source_file_path:
365 ## file was deleted
365 ## file was deleted
366 <strong>${filediff.source_file_path}</strong>
366 <strong>${filediff.source_file_path}</strong>
367 <% final_path = filediff.source_file_path %>
367 <% final_path = filediff.source_file_path %>
368 %else:
368 %else:
369 ## file was added
369 ## file was added
370 <strong>${filediff.target_file_path}</strong>
370 <strong>${filediff.target_file_path}</strong>
371 <% final_path = filediff.target_file_path %>
371 <% final_path = filediff.target_file_path %>
372 %endif
372 %endif
373 %endif
373 %endif
374 <i style="color: #aaa" class="tooltip icon-clipboard clipboard-action" data-clipboard-text="${final_path}" title="${_('Copy the full path')}" onclick="return false;"></i>
374 <i style="color: #aaa" class="tooltip icon-clipboard clipboard-action" data-clipboard-text="${final_path}" title="${_('Copy the full path')}" onclick="return false;"></i>
375 </span>
375 </span>
376 <span class="pill-group" style="float: left">
376 <span class="pill-group" style="float: left">
377 %if filediff.limited_diff:
377 %if filediff.limited_diff:
378 <span class="pill tooltip" op="limited" title="The stats for this diff are not complete">limited diff</span>
378 <span class="pill tooltip" op="limited" title="The stats for this diff are not complete">limited diff</span>
379 %endif
379 %endif
380
380
381 %if RENAMED_FILENODE in filediff.patch['stats']['ops']:
381 %if RENAMED_FILENODE in filediff.patch['stats']['ops']:
382 <span class="pill" op="renamed">renamed</span>
382 <span class="pill" op="renamed">renamed</span>
383 %endif
383 %endif
384
384
385 %if COPIED_FILENODE in filediff.patch['stats']['ops']:
385 %if COPIED_FILENODE in filediff.patch['stats']['ops']:
386 <span class="pill" op="copied">copied</span>
386 <span class="pill" op="copied">copied</span>
387 %endif
387 %endif
388
388
389 %if NEW_FILENODE in filediff.patch['stats']['ops']:
389 %if NEW_FILENODE in filediff.patch['stats']['ops']:
390 <span class="pill" op="created">created</span>
390 <span class="pill" op="created">created</span>
391 %if filediff['target_mode'].startswith('120'):
391 %if filediff['target_mode'].startswith('120'):
392 <span class="pill" op="symlink">symlink</span>
392 <span class="pill" op="symlink">symlink</span>
393 %else:
393 %else:
394 <span class="pill" op="mode">${nice_mode(filediff['target_mode'])}</span>
394 <span class="pill" op="mode">${nice_mode(filediff['target_mode'])}</span>
395 %endif
395 %endif
396 %endif
396 %endif
397
397
398 %if DEL_FILENODE in filediff.patch['stats']['ops']:
398 %if DEL_FILENODE in filediff.patch['stats']['ops']:
399 <span class="pill" op="removed">removed</span>
399 <span class="pill" op="removed">removed</span>
400 %endif
400 %endif
401
401
402 %if CHMOD_FILENODE in filediff.patch['stats']['ops']:
402 %if CHMOD_FILENODE in filediff.patch['stats']['ops']:
403 <span class="pill" op="mode">
403 <span class="pill" op="mode">
404 ${nice_mode(filediff['source_mode'])} ➑ ${nice_mode(filediff['target_mode'])}
404 ${nice_mode(filediff['source_mode'])} ➑ ${nice_mode(filediff['target_mode'])}
405 </span>
405 </span>
406 %endif
406 %endif
407 </span>
407 </span>
408
408
409 <a class="pill filediff-anchor" href="#a_${h.FID('', filediff.patch['filename'])}">ΒΆ</a>
409 <a class="pill filediff-anchor" href="#a_${h.FID('', filediff.patch['filename'])}">ΒΆ</a>
410
410
411 <span class="pill-group" style="float: right">
411 <span class="pill-group" style="float: right">
412 %if BIN_FILENODE in filediff.patch['stats']['ops']:
412 %if BIN_FILENODE in filediff.patch['stats']['ops']:
413 <span class="pill" op="binary">binary</span>
413 <span class="pill" op="binary">binary</span>
414 %if MOD_FILENODE in filediff.patch['stats']['ops']:
414 %if MOD_FILENODE in filediff.patch['stats']['ops']:
415 <span class="pill" op="modified">modified</span>
415 <span class="pill" op="modified">modified</span>
416 %endif
416 %endif
417 %endif
417 %endif
418 %if filediff.patch['stats']['added']:
418 %if filediff.patch['stats']['added']:
419 <span class="pill" op="added">+${filediff.patch['stats']['added']}</span>
419 <span class="pill" op="added">+${filediff.patch['stats']['added']}</span>
420 %endif
420 %endif
421 %if filediff.patch['stats']['deleted']:
421 %if filediff.patch['stats']['deleted']:
422 <span class="pill" op="deleted">-${filediff.patch['stats']['deleted']}</span>
422 <span class="pill" op="deleted">-${filediff.patch['stats']['deleted']}</span>
423 %endif
423 %endif
424 </span>
424 </span>
425
425
426 </%def>
426 </%def>
427
427
428 <%def name="nice_mode(filemode)">
428 <%def name="nice_mode(filemode)">
429 ${filemode.startswith('100') and filemode[3:] or filemode}
429 ${filemode.startswith('100') and filemode[3:] or filemode}
430 </%def>
430 </%def>
431
431
432 <%def name="diff_menu(filediff, use_comments=False)">
432 <%def name="diff_menu(filediff, use_comments=False)">
433 <div class="filediff-menu">
433 <div class="filediff-menu">
434 %if filediff.diffset.source_ref:
434 %if filediff.diffset.source_ref:
435 %if filediff.operation in ['D', 'M']:
435 %if filediff.operation in ['D', 'M']:
436 <a
436 <a
437 class="tooltip"
437 class="tooltip"
438 href="${h.route_path('repo_files',repo_name=filediff.diffset.repo_name,commit_id=filediff.diffset.source_ref,f_path=filediff.source_file_path)}"
438 href="${h.route_path('repo_files',repo_name=filediff.diffset.repo_name,commit_id=filediff.diffset.source_ref,f_path=filediff.source_file_path)}"
439 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
439 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
440 >
440 >
441 ${_('Show file before')}
441 ${_('Show file before')}
442 </a> |
442 </a> |
443 %else:
443 %else:
444 <span
444 <span
445 class="tooltip"
445 class="tooltip"
446 title="${h.tooltip(_('File no longer present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
446 title="${h.tooltip(_('File no longer present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
447 >
447 >
448 ${_('Show file before')}
448 ${_('Show file before')}
449 </span> |
449 </span> |
450 %endif
450 %endif
451 %if filediff.operation in ['A', 'M']:
451 %if filediff.operation in ['A', 'M']:
452 <a
452 <a
453 class="tooltip"
453 class="tooltip"
454 href="${h.route_path('repo_files',repo_name=filediff.diffset.source_repo_name,commit_id=filediff.diffset.target_ref,f_path=filediff.target_file_path)}"
454 href="${h.route_path('repo_files',repo_name=filediff.diffset.source_repo_name,commit_id=filediff.diffset.target_ref,f_path=filediff.target_file_path)}"
455 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
455 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
456 >
456 >
457 ${_('Show file after')}
457 ${_('Show file after')}
458 </a> |
458 </a> |
459 %else:
459 %else:
460 <span
460 <span
461 class="tooltip"
461 class="tooltip"
462 title="${h.tooltip(_('File no longer present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
462 title="${h.tooltip(_('File no longer present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
463 >
463 >
464 ${_('Show file after')}
464 ${_('Show file after')}
465 </span> |
465 </span> |
466 %endif
466 %endif
467 <a
467 <a
468 class="tooltip"
468 class="tooltip"
469 title="${h.tooltip(_('Raw diff'))}"
469 title="${h.tooltip(_('Raw diff'))}"
470 href="${h.route_path('repo_files_diff',repo_name=filediff.diffset.repo_name,f_path=filediff.target_file_path, _query=dict(diff2=filediff.diffset.target_ref,diff1=filediff.diffset.source_ref,diff='raw'))}"
470 href="${h.route_path('repo_files_diff',repo_name=filediff.diffset.repo_name,f_path=filediff.target_file_path, _query=dict(diff2=filediff.diffset.target_ref,diff1=filediff.diffset.source_ref,diff='raw'))}"
471 >
471 >
472 ${_('Raw diff')}
472 ${_('Raw diff')}
473 </a> |
473 </a> |
474 <a
474 <a
475 class="tooltip"
475 class="tooltip"
476 title="${h.tooltip(_('Download diff'))}"
476 title="${h.tooltip(_('Download diff'))}"
477 href="${h.route_path('repo_files_diff',repo_name=filediff.diffset.repo_name,f_path=filediff.target_file_path, _query=dict(diff2=filediff.diffset.target_ref,diff1=filediff.diffset.source_ref,diff='download'))}"
477 href="${h.route_path('repo_files_diff',repo_name=filediff.diffset.repo_name,f_path=filediff.target_file_path, _query=dict(diff2=filediff.diffset.target_ref,diff1=filediff.diffset.source_ref,diff='download'))}"
478 >
478 >
479 ${_('Download diff')}
479 ${_('Download diff')}
480 </a>
480 </a>
481 % if use_comments:
481 % if use_comments:
482 |
482 |
483 % endif
483 % endif
484
484
485 ## TODO: dan: refactor ignorews_url and context_url into the diff renderer same as diffmode=unified/sideside. Also use ajax to load more context (by clicking hunks)
485 ## TODO: dan: refactor ignorews_url and context_url into the diff renderer same as diffmode=unified/sideside. Also use ajax to load more context (by clicking hunks)
486 %if hasattr(c, 'ignorews_url'):
486 %if hasattr(c, 'ignorews_url'):
487 ${c.ignorews_url(request, h.FID('', filediff.patch['filename']))}
487 ${c.ignorews_url(request, h.FID('', filediff.patch['filename']))}
488 %endif
488 %endif
489 %if hasattr(c, 'context_url'):
489 %if hasattr(c, 'context_url'):
490 ${c.context_url(request, h.FID('', filediff.patch['filename']))}
490 ${c.context_url(request, h.FID('', filediff.patch['filename']))}
491 %endif
491 %endif
492
492
493 %if use_comments:
493 %if use_comments:
494 <a href="#" onclick="return Rhodecode.comments.toggleComments(this);">
494 <a href="#" onclick="return Rhodecode.comments.toggleComments(this);">
495 <span class="show-comment-button">${_('Show comments')}</span><span class="hide-comment-button">${_('Hide comments')}</span>
495 <span class="show-comment-button">${_('Show comments')}</span><span class="hide-comment-button">${_('Hide comments')}</span>
496 </a>
496 </a>
497 %endif
497 %endif
498 %endif
498 %endif
499 </div>
499 </div>
500 </%def>
500 </%def>
501
501
502
502
503 <%def name="inline_comments_container(comments, inline_comments)">
503 <%def name="inline_comments_container(comments, inline_comments)">
504 <div class="inline-comments">
504 <div class="inline-comments">
505 %for comment in comments:
505 %for comment in comments:
506 ${commentblock.comment_block(comment, inline=True)}
506 ${commentblock.comment_block(comment, inline=True)}
507 %endfor
507 %endfor
508 % if comments and comments[-1].outdated:
508 % if comments and comments[-1].outdated:
509 <span class="btn btn-secondary cb-comment-add-button comment-outdated}"
509 <span class="btn btn-secondary cb-comment-add-button comment-outdated}"
510 style="display: none;}">
510 style="display: none;}">
511 ${_('Add another comment')}
511 ${_('Add another comment')}
512 </span>
512 </span>
513 % else:
513 % else:
514 <span onclick="return Rhodecode.comments.createComment(this)"
514 <span onclick="return Rhodecode.comments.createComment(this)"
515 class="btn btn-secondary cb-comment-add-button">
515 class="btn btn-secondary cb-comment-add-button">
516 ${_('Add another comment')}
516 ${_('Add another comment')}
517 </span>
517 </span>
518 % endif
518 % endif
519
519
520 </div>
520 </div>
521 </%def>
521 </%def>
522
522
523 <%!
523 <%!
524 def get_comments_for(diff_type, comments, filename, line_version, line_number):
524 def get_comments_for(diff_type, comments, filename, line_version, line_number):
525 if hasattr(filename, 'unicode_path'):
525 if hasattr(filename, 'unicode_path'):
526 filename = filename.unicode_path
526 filename = filename.unicode_path
527
527
528 if not isinstance(filename, basestring):
528 if not isinstance(filename, basestring):
529 return None
529 return None
530
530
531 line_key = '{}{}'.format(line_version, line_number) ## e.g o37, n12
531 line_key = '{}{}'.format(line_version, line_number) ## e.g o37, n12
532
532
533 if comments and filename in comments:
533 if comments and filename in comments:
534 file_comments = comments[filename]
534 file_comments = comments[filename]
535 if line_key in file_comments:
535 if line_key in file_comments:
536 data = file_comments.pop(line_key)
536 data = file_comments.pop(line_key)
537 return data
537 return data
538 %>
538 %>
539
539
540 <%def name="render_hunk_lines_sideside(hunk, use_comments=False, inline_comments=None)">
540 <%def name="render_hunk_lines_sideside(hunk, use_comments=False, inline_comments=None)">
541
541
542 %for i, line in enumerate(hunk.sideside):
542 %for i, line in enumerate(hunk.sideside):
543 <%
543 <%
544 old_line_anchor, new_line_anchor = None, None
544 old_line_anchor, new_line_anchor = None, None
545 if line.original.lineno:
545 if line.original.lineno:
546 old_line_anchor = diff_line_anchor(hunk.source_file_path, line.original.lineno, 'o')
546 old_line_anchor = diff_line_anchor(hunk.source_file_path, line.original.lineno, 'o')
547 if line.modified.lineno:
547 if line.modified.lineno:
548 new_line_anchor = diff_line_anchor(hunk.target_file_path, line.modified.lineno, 'n')
548 new_line_anchor = diff_line_anchor(hunk.target_file_path, line.modified.lineno, 'n')
549 %>
549 %>
550
550
551 <tr class="cb-line">
551 <tr class="cb-line">
552 <td class="cb-data ${action_class(line.original.action)}"
552 <td class="cb-data ${action_class(line.original.action)}"
553 data-line-no="${line.original.lineno}"
553 data-line-no="${line.original.lineno}"
554 >
554 >
555 <div>
555 <div>
556
556
557 <% line_old_comments = None %>
557 <% line_old_comments = None %>
558 %if line.original.get_comment_args:
558 %if line.original.get_comment_args:
559 <% line_old_comments = get_comments_for('side-by-side', inline_comments, *line.original.get_comment_args) %>
559 <% line_old_comments = get_comments_for('side-by-side', inline_comments, *line.original.get_comment_args) %>
560 %endif
560 %endif
561 %if line_old_comments:
561 %if line_old_comments:
562 <% has_outdated = any([x.outdated for x in line_old_comments]) %>
562 <% has_outdated = any([x.outdated for x in line_old_comments]) %>
563 % if has_outdated:
563 % if has_outdated:
564 <i title="${_('comments including outdated')}:${len(line_old_comments)}" class="icon-comment_toggle" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
564 <i title="${_('comments including outdated')}:${len(line_old_comments)}" class="icon-comment_toggle" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
565 % else:
565 % else:
566 <i title="${_('comments')}: ${len(line_old_comments)}" class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
566 <i title="${_('comments')}: ${len(line_old_comments)}" class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
567 % endif
567 % endif
568 %endif
568 %endif
569 </div>
569 </div>
570 </td>
570 </td>
571 <td class="cb-lineno ${action_class(line.original.action)}"
571 <td class="cb-lineno ${action_class(line.original.action)}"
572 data-line-no="${line.original.lineno}"
572 data-line-no="${line.original.lineno}"
573 %if old_line_anchor:
573 %if old_line_anchor:
574 id="${old_line_anchor}"
574 id="${old_line_anchor}"
575 %endif
575 %endif
576 >
576 >
577 %if line.original.lineno:
577 %if line.original.lineno:
578 <a name="${old_line_anchor}" href="#${old_line_anchor}">${line.original.lineno}</a>
578 <a name="${old_line_anchor}" href="#${old_line_anchor}">${line.original.lineno}</a>
579 %endif
579 %endif
580 </td>
580 </td>
581 <td class="cb-content ${action_class(line.original.action)}"
581 <td class="cb-content ${action_class(line.original.action)}"
582 data-line-no="o${line.original.lineno}"
582 data-line-no="o${line.original.lineno}"
583 >
583 >
584 %if use_comments and line.original.lineno:
584 %if use_comments and line.original.lineno:
585 ${render_add_comment_button()}
585 ${render_add_comment_button()}
586 %endif
586 %endif
587 <span class="cb-code">${line.original.action} ${line.original.content or '' | n}</span>
587 <span class="cb-code">${line.original.action} ${line.original.content or '' | n}</span>
588
588
589 %if use_comments and line.original.lineno and line_old_comments:
589 %if use_comments and line.original.lineno and line_old_comments:
590 ${inline_comments_container(line_old_comments, inline_comments)}
590 ${inline_comments_container(line_old_comments, inline_comments)}
591 %endif
591 %endif
592
592
593 </td>
593 </td>
594 <td class="cb-data ${action_class(line.modified.action)}"
594 <td class="cb-data ${action_class(line.modified.action)}"
595 data-line-no="${line.modified.lineno}"
595 data-line-no="${line.modified.lineno}"
596 >
596 >
597 <div>
597 <div>
598
598
599 %if line.modified.get_comment_args:
599 %if line.modified.get_comment_args:
600 <% line_new_comments = get_comments_for('side-by-side', inline_comments, *line.modified.get_comment_args) %>
600 <% line_new_comments = get_comments_for('side-by-side', inline_comments, *line.modified.get_comment_args) %>
601 %else:
601 %else:
602 <% line_new_comments = None%>
602 <% line_new_comments = None%>
603 %endif
603 %endif
604 %if line_new_comments:
604 %if line_new_comments:
605 <% has_outdated = any([x.outdated for x in line_new_comments]) %>
605 <% has_outdated = any([x.outdated for x in line_new_comments]) %>
606 % if has_outdated:
606 % if has_outdated:
607 <i title="${_('comments including outdated')}:${len(line_new_comments)}" class="icon-comment_toggle" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
607 <i title="${_('comments including outdated')}:${len(line_new_comments)}" class="icon-comment_toggle" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
608 % else:
608 % else:
609 <i title="${_('comments')}: ${len(line_new_comments)}" class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
609 <i title="${_('comments')}: ${len(line_new_comments)}" class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
610 % endif
610 % endif
611 %endif
611 %endif
612 </div>
612 </div>
613 </td>
613 </td>
614 <td class="cb-lineno ${action_class(line.modified.action)}"
614 <td class="cb-lineno ${action_class(line.modified.action)}"
615 data-line-no="${line.modified.lineno}"
615 data-line-no="${line.modified.lineno}"
616 %if new_line_anchor:
616 %if new_line_anchor:
617 id="${new_line_anchor}"
617 id="${new_line_anchor}"
618 %endif
618 %endif
619 >
619 >
620 %if line.modified.lineno:
620 %if line.modified.lineno:
621 <a name="${new_line_anchor}" href="#${new_line_anchor}">${line.modified.lineno}</a>
621 <a name="${new_line_anchor}" href="#${new_line_anchor}">${line.modified.lineno}</a>
622 %endif
622 %endif
623 </td>
623 </td>
624 <td class="cb-content ${action_class(line.modified.action)}"
624 <td class="cb-content ${action_class(line.modified.action)}"
625 data-line-no="n${line.modified.lineno}"
625 data-line-no="n${line.modified.lineno}"
626 >
626 >
627 %if use_comments and line.modified.lineno:
627 %if use_comments and line.modified.lineno:
628 ${render_add_comment_button()}
628 ${render_add_comment_button()}
629 %endif
629 %endif
630 <span class="cb-code">${line.modified.action} ${line.modified.content or '' | n}</span>
630 <span class="cb-code">${line.modified.action} ${line.modified.content or '' | n}</span>
631 %if use_comments and line.modified.lineno and line_new_comments:
631 %if use_comments and line.modified.lineno and line_new_comments:
632 ${inline_comments_container(line_new_comments, inline_comments)}
632 ${inline_comments_container(line_new_comments, inline_comments)}
633 %endif
633 %endif
634 </td>
634 </td>
635 </tr>
635 </tr>
636 %endfor
636 %endfor
637 </%def>
637 </%def>
638
638
639
639
640 <%def name="render_hunk_lines_unified(hunk, use_comments=False, inline_comments=None)">
640 <%def name="render_hunk_lines_unified(hunk, use_comments=False, inline_comments=None)">
641 %for old_line_no, new_line_no, action, content, comments_args in hunk.unified:
641 %for old_line_no, new_line_no, action, content, comments_args in hunk.unified:
642 <%
642 <%
643 old_line_anchor, new_line_anchor = None, None
643 old_line_anchor, new_line_anchor = None, None
644 if old_line_no:
644 if old_line_no:
645 old_line_anchor = diff_line_anchor(hunk.source_file_path, old_line_no, 'o')
645 old_line_anchor = diff_line_anchor(hunk.source_file_path, old_line_no, 'o')
646 if new_line_no:
646 if new_line_no:
647 new_line_anchor = diff_line_anchor(hunk.target_file_path, new_line_no, 'n')
647 new_line_anchor = diff_line_anchor(hunk.target_file_path, new_line_no, 'n')
648 %>
648 %>
649 <tr class="cb-line">
649 <tr class="cb-line">
650 <td class="cb-data ${action_class(action)}">
650 <td class="cb-data ${action_class(action)}">
651 <div>
651 <div>
652
652
653 %if comments_args:
653 %if comments_args:
654 <% comments = get_comments_for('unified', inline_comments, *comments_args) %>
654 <% comments = get_comments_for('unified', inline_comments, *comments_args) %>
655 %else:
655 %else:
656 <% comments = None%>
656 <% comments = None %>
657 %endif
657 %endif
658
658
659 % if comments:
659 % if comments:
660 <% has_outdated = any([x.outdated for x in comments]) %>
660 <% has_outdated = any([x.outdated for x in comments]) %>
661 % if has_outdated:
661 % if has_outdated:
662 <i title="${_('comments including outdated')}:${len(comments)}" class="icon-comment_toggle" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
662 <i title="${_('comments including outdated')}:${len(comments)}" class="icon-comment_toggle" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
663 % else:
663 % else:
664 <i title="${_('comments')}: ${len(comments)}" class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
664 <i title="${_('comments')}: ${len(comments)}" class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
665 % endif
665 % endif
666 % endif
666 % endif
667 </div>
667 </div>
668 </td>
668 </td>
669 <td class="cb-lineno ${action_class(action)}"
669 <td class="cb-lineno ${action_class(action)}"
670 data-line-no="${old_line_no}"
670 data-line-no="${old_line_no}"
671 %if old_line_anchor:
671 %if old_line_anchor:
672 id="${old_line_anchor}"
672 id="${old_line_anchor}"
673 %endif
673 %endif
674 >
674 >
675 %if old_line_anchor:
675 %if old_line_anchor:
676 <a name="${old_line_anchor}" href="#${old_line_anchor}">${old_line_no}</a>
676 <a name="${old_line_anchor}" href="#${old_line_anchor}">${old_line_no}</a>
677 %endif
677 %endif
678 </td>
678 </td>
679 <td class="cb-lineno ${action_class(action)}"
679 <td class="cb-lineno ${action_class(action)}"
680 data-line-no="${new_line_no}"
680 data-line-no="${new_line_no}"
681 %if new_line_anchor:
681 %if new_line_anchor:
682 id="${new_line_anchor}"
682 id="${new_line_anchor}"
683 %endif
683 %endif
684 >
684 >
685 %if new_line_anchor:
685 %if new_line_anchor:
686 <a name="${new_line_anchor}" href="#${new_line_anchor}">${new_line_no}</a>
686 <a name="${new_line_anchor}" href="#${new_line_anchor}">${new_line_no}</a>
687 %endif
687 %endif
688 </td>
688 </td>
689 <td class="cb-content ${action_class(action)}"
689 <td class="cb-content ${action_class(action)}"
690 data-line-no="${new_line_no and 'n' or 'o'}${new_line_no or old_line_no}"
690 data-line-no="${new_line_no and 'n' or 'o'}${new_line_no or old_line_no}"
691 >
691 >
692 %if use_comments:
692 %if use_comments:
693 ${render_add_comment_button()}
693 ${render_add_comment_button()}
694 %endif
694 %endif
695 <span class="cb-code">${action} ${content or '' | n}</span>
695 <span class="cb-code">${action} ${content or '' | n}</span>
696 %if use_comments and comments:
696 %if use_comments and comments:
697 ${inline_comments_container(comments, inline_comments)}
697 ${inline_comments_container(comments, inline_comments)}
698 %endif
698 %endif
699 </td>
699 </td>
700 </tr>
700 </tr>
701 %endfor
701 %endfor
702 </%def>
702 </%def>
703
703
704
704
705 <%def name="render_hunk_lines(diff_mode, hunk, use_comments, inline_comments)">
705 <%def name="render_hunk_lines(diff_mode, hunk, use_comments, inline_comments)">
706 % if diff_mode == 'unified':
706 % if diff_mode == 'unified':
707 ${render_hunk_lines_unified(hunk, use_comments=use_comments, inline_comments=inline_comments)}
707 ${render_hunk_lines_unified(hunk, use_comments=use_comments, inline_comments=inline_comments)}
708 % elif diff_mode == 'sideside':
708 % elif diff_mode == 'sideside':
709 ${render_hunk_lines_sideside(hunk, use_comments=use_comments, inline_comments=inline_comments)}
709 ${render_hunk_lines_sideside(hunk, use_comments=use_comments, inline_comments=inline_comments)}
710 % else:
710 % else:
711 <tr class="cb-line">
711 <tr class="cb-line">
712 <td>unknown diff mode</td>
712 <td>unknown diff mode</td>
713 </tr>
713 </tr>
714 % endif
714 % endif
715 </%def>
715 </%def>
716
716
717
717
718 <%def name="render_add_comment_button()">
718 <%def name="render_add_comment_button()">
719 <button class="btn btn-small btn-primary cb-comment-box-opener" onclick="return Rhodecode.comments.createComment(this)">
719 <button class="btn btn-small btn-primary cb-comment-box-opener" onclick="return Rhodecode.comments.createComment(this)">
720 <span><i class="icon-comment"></i></span>
720 <span><i class="icon-comment"></i></span>
721 </button>
721 </button>
722 </%def>
722 </%def>
723
723
724 <%def name="render_diffset_menu()">
724 <%def name="render_diffset_menu()">
725
725
726 <div class="diffset-menu clearinner">
726 <div class="diffset-menu clearinner">
727 <div class="pull-right">
727 <div class="pull-right">
728 <div class="btn-group">
728 <div class="btn-group">
729
729
730 <a
730 <a
731 class="btn ${c.diffmode == 'sideside' and 'btn-primary'} tooltip"
731 class="btn ${c.diffmode == 'sideside' and 'btn-primary'} tooltip"
732 title="${h.tooltip(_('View side by side'))}"
732 title="${h.tooltip(_('View side by side'))}"
733 href="${h.current_route_path(request, diffmode='sideside')}">
733 href="${h.current_route_path(request, diffmode='sideside')}">
734 <span>${_('Side by Side')}</span>
734 <span>${_('Side by Side')}</span>
735 </a>
735 </a>
736 <a
736 <a
737 class="btn ${c.diffmode == 'unified' and 'btn-primary'} tooltip"
737 class="btn ${c.diffmode == 'unified' and 'btn-primary'} tooltip"
738 title="${h.tooltip(_('View unified'))}" href="${h.current_route_path(request, diffmode='unified')}">
738 title="${h.tooltip(_('View unified'))}" href="${h.current_route_path(request, diffmode='unified')}">
739 <span>${_('Unified')}</span>
739 <span>${_('Unified')}</span>
740 </a>
740 </a>
741 </div>
741 </div>
742 </div>
742 </div>
743
743
744 <div class="pull-left">
744 <div class="pull-left">
745 <div class="btn-group">
745 <div class="btn-group">
746 <a
746 <a
747 class="btn"
747 class="btn"
748 href="#"
748 href="#"
749 onclick="$('input[class=filediff-collapse-state]').prop('checked', false); return false">${_('Expand All Files')}</a>
749 onclick="$('input[class=filediff-collapse-state]').prop('checked', false); return false">${_('Expand All Files')}</a>
750 <a
750 <a
751 class="btn"
751 class="btn"
752 href="#"
752 href="#"
753 onclick="$('input[class=filediff-collapse-state]').prop('checked', true); return false">${_('Collapse All Files')}</a>
753 onclick="$('input[class=filediff-collapse-state]').prop('checked', true); return false">${_('Collapse All Files')}</a>
754 <a
754 <a
755 class="btn"
755 class="btn"
756 href="#"
756 href="#"
757 onclick="return Rhodecode.comments.toggleWideMode(this)">${_('Wide Mode Diff')}</a>
757 onclick="return Rhodecode.comments.toggleWideMode(this)">${_('Wide Mode Diff')}</a>
758 </div>
758 </div>
759 </div>
759 </div>
760 </div>
760 </div>
761 </%def>
761 </%def>
General Comments 0
You need to be logged in to leave comments. Login now