##// END OF EJS Templates
comments: ensure we ALWAYS display unmatched comments.
marcink -
r3080:4caa8a84 default
parent child Browse files
Show More
@@ -1,745 +1,728 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 from itertools import groupby
23 from itertools import groupby
24
24
25 from pygments import lex
25 from pygments import lex
26 from pygments.formatters.html import _get_ttype_class as pygment_token_class
26 from pygments.formatters.html import _get_ttype_class as pygment_token_class
27 from pygments.lexers.special import TextLexer, Token
27 from pygments.lexers.special import TextLexer, Token
28
28
29 from rhodecode.lib.helpers import (
29 from rhodecode.lib.helpers import (
30 get_lexer_for_filenode, html_escape, get_custom_lexer)
30 get_lexer_for_filenode, html_escape, get_custom_lexer)
31 from rhodecode.lib.utils2 import AttributeDict, StrictAttributeDict
31 from rhodecode.lib.utils2 import AttributeDict, StrictAttributeDict
32 from rhodecode.lib.vcs.nodes import FileNode
32 from rhodecode.lib.vcs.nodes import FileNode
33 from rhodecode.lib.vcs.exceptions import VCSError, NodeDoesNotExistError
33 from rhodecode.lib.vcs.exceptions import VCSError, NodeDoesNotExistError
34 from rhodecode.lib.diff_match_patch import diff_match_patch
34 from rhodecode.lib.diff_match_patch import diff_match_patch
35 from rhodecode.lib.diffs import LimitedDiffContainer
35 from rhodecode.lib.diffs import LimitedDiffContainer
36 from pygments.lexers import get_lexer_by_name
36 from pygments.lexers import get_lexer_by_name
37
37
38 plain_text_lexer = get_lexer_by_name(
38 plain_text_lexer = get_lexer_by_name(
39 'text', stripall=False, stripnl=False, ensurenl=False)
39 'text', stripall=False, stripnl=False, ensurenl=False)
40
40
41
41
42 log = logging.getLogger(__name__)
42 log = logging.getLogger(__name__)
43
43
44
44
45 def filenode_as_lines_tokens(filenode, lexer=None):
45 def filenode_as_lines_tokens(filenode, lexer=None):
46 org_lexer = lexer
46 org_lexer = lexer
47 lexer = lexer or get_lexer_for_filenode(filenode)
47 lexer = lexer or get_lexer_for_filenode(filenode)
48 log.debug('Generating file node pygment tokens for %s, %s, org_lexer:%s',
48 log.debug('Generating file node pygment tokens for %s, %s, org_lexer:%s',
49 lexer, filenode, org_lexer)
49 lexer, filenode, org_lexer)
50 tokens = tokenize_string(filenode.content, lexer)
50 tokens = tokenize_string(filenode.content, lexer)
51 lines = split_token_stream(tokens)
51 lines = split_token_stream(tokens)
52 rv = list(lines)
52 rv = list(lines)
53 return rv
53 return rv
54
54
55
55
56 def tokenize_string(content, lexer):
56 def tokenize_string(content, lexer):
57 """
57 """
58 Use pygments to tokenize some content based on a lexer
58 Use pygments to tokenize some content based on a lexer
59 ensuring all original new lines and whitespace is preserved
59 ensuring all original new lines and whitespace is preserved
60 """
60 """
61
61
62 lexer.stripall = False
62 lexer.stripall = False
63 lexer.stripnl = False
63 lexer.stripnl = False
64 lexer.ensurenl = False
64 lexer.ensurenl = False
65
65
66 if isinstance(lexer, TextLexer):
66 if isinstance(lexer, TextLexer):
67 lexed = [(Token.Text, content)]
67 lexed = [(Token.Text, content)]
68 else:
68 else:
69 lexed = lex(content, lexer)
69 lexed = lex(content, lexer)
70
70
71 for token_type, token_text in lexed:
71 for token_type, token_text in lexed:
72 yield pygment_token_class(token_type), token_text
72 yield pygment_token_class(token_type), token_text
73
73
74
74
75 def split_token_stream(tokens):
75 def split_token_stream(tokens):
76 """
76 """
77 Take a list of (TokenType, text) tuples and split them by a string
77 Take a list of (TokenType, text) tuples and split them by a string
78
78
79 split_token_stream([(TEXT, 'some\ntext'), (TEXT, 'more\n')])
79 split_token_stream([(TEXT, 'some\ntext'), (TEXT, 'more\n')])
80 [(TEXT, 'some'), (TEXT, 'text'),
80 [(TEXT, 'some'), (TEXT, 'text'),
81 (TEXT, 'more'), (TEXT, 'text')]
81 (TEXT, 'more'), (TEXT, 'text')]
82 """
82 """
83
83
84 buffer = []
84 buffer = []
85 for token_class, token_text in tokens:
85 for token_class, token_text in tokens:
86 parts = token_text.split('\n')
86 parts = token_text.split('\n')
87 for part in parts[:-1]:
87 for part in parts[:-1]:
88 buffer.append((token_class, part))
88 buffer.append((token_class, part))
89 yield buffer
89 yield buffer
90 buffer = []
90 buffer = []
91
91
92 buffer.append((token_class, parts[-1]))
92 buffer.append((token_class, parts[-1]))
93
93
94 if buffer:
94 if buffer:
95 yield buffer
95 yield buffer
96
96
97
97
98 def filenode_as_annotated_lines_tokens(filenode):
98 def filenode_as_annotated_lines_tokens(filenode):
99 """
99 """
100 Take a file node and return a list of annotations => lines, if no annotation
100 Take a file node and return a list of annotations => lines, if no annotation
101 is found, it will be None.
101 is found, it will be None.
102
102
103 eg:
103 eg:
104
104
105 [
105 [
106 (annotation1, [
106 (annotation1, [
107 (1, line1_tokens_list),
107 (1, line1_tokens_list),
108 (2, line2_tokens_list),
108 (2, line2_tokens_list),
109 ]),
109 ]),
110 (annotation2, [
110 (annotation2, [
111 (3, line1_tokens_list),
111 (3, line1_tokens_list),
112 ]),
112 ]),
113 (None, [
113 (None, [
114 (4, line1_tokens_list),
114 (4, line1_tokens_list),
115 ]),
115 ]),
116 (annotation1, [
116 (annotation1, [
117 (5, line1_tokens_list),
117 (5, line1_tokens_list),
118 (6, line2_tokens_list),
118 (6, line2_tokens_list),
119 ])
119 ])
120 ]
120 ]
121 """
121 """
122
122
123 commit_cache = {} # cache commit_getter lookups
123 commit_cache = {} # cache commit_getter lookups
124
124
125 def _get_annotation(commit_id, commit_getter):
125 def _get_annotation(commit_id, commit_getter):
126 if commit_id not in commit_cache:
126 if commit_id not in commit_cache:
127 commit_cache[commit_id] = commit_getter()
127 commit_cache[commit_id] = commit_getter()
128 return commit_cache[commit_id]
128 return commit_cache[commit_id]
129
129
130 annotation_lookup = {
130 annotation_lookup = {
131 line_no: _get_annotation(commit_id, commit_getter)
131 line_no: _get_annotation(commit_id, commit_getter)
132 for line_no, commit_id, commit_getter, line_content
132 for line_no, commit_id, commit_getter, line_content
133 in filenode.annotate
133 in filenode.annotate
134 }
134 }
135
135
136 annotations_lines = ((annotation_lookup.get(line_no), line_no, tokens)
136 annotations_lines = ((annotation_lookup.get(line_no), line_no, tokens)
137 for line_no, tokens
137 for line_no, tokens
138 in enumerate(filenode_as_lines_tokens(filenode), 1))
138 in enumerate(filenode_as_lines_tokens(filenode), 1))
139
139
140 grouped_annotations_lines = groupby(annotations_lines, lambda x: x[0])
140 grouped_annotations_lines = groupby(annotations_lines, lambda x: x[0])
141
141
142 for annotation, group in grouped_annotations_lines:
142 for annotation, group in grouped_annotations_lines:
143 yield (
143 yield (
144 annotation, [(line_no, tokens)
144 annotation, [(line_no, tokens)
145 for (_, line_no, tokens) in group]
145 for (_, line_no, tokens) in group]
146 )
146 )
147
147
148
148
149 def render_tokenstream(tokenstream):
149 def render_tokenstream(tokenstream):
150 result = []
150 result = []
151 for token_class, token_ops_texts in rollup_tokenstream(tokenstream):
151 for token_class, token_ops_texts in rollup_tokenstream(tokenstream):
152
152
153 if token_class:
153 if token_class:
154 result.append(u'<span class="%s">' % token_class)
154 result.append(u'<span class="%s">' % token_class)
155 else:
155 else:
156 result.append(u'<span>')
156 result.append(u'<span>')
157
157
158 for op_tag, token_text in token_ops_texts:
158 for op_tag, token_text in token_ops_texts:
159
159
160 if op_tag:
160 if op_tag:
161 result.append(u'<%s>' % op_tag)
161 result.append(u'<%s>' % op_tag)
162
162
163 escaped_text = html_escape(token_text)
163 escaped_text = html_escape(token_text)
164
164
165 # TODO: dan: investigate showing hidden characters like space/nl/tab
165 # TODO: dan: investigate showing hidden characters like space/nl/tab
166 # escaped_text = escaped_text.replace(' ', '<sp> </sp>')
166 # escaped_text = escaped_text.replace(' ', '<sp> </sp>')
167 # escaped_text = escaped_text.replace('\n', '<nl>\n</nl>')
167 # escaped_text = escaped_text.replace('\n', '<nl>\n</nl>')
168 # escaped_text = escaped_text.replace('\t', '<tab>\t</tab>')
168 # escaped_text = escaped_text.replace('\t', '<tab>\t</tab>')
169
169
170 result.append(escaped_text)
170 result.append(escaped_text)
171
171
172 if op_tag:
172 if op_tag:
173 result.append(u'</%s>' % op_tag)
173 result.append(u'</%s>' % op_tag)
174
174
175 result.append(u'</span>')
175 result.append(u'</span>')
176
176
177 html = ''.join(result)
177 html = ''.join(result)
178 return html
178 return html
179
179
180
180
181 def rollup_tokenstream(tokenstream):
181 def rollup_tokenstream(tokenstream):
182 """
182 """
183 Group a token stream of the format:
183 Group a token stream of the format:
184
184
185 ('class', 'op', 'text')
185 ('class', 'op', 'text')
186 or
186 or
187 ('class', 'text')
187 ('class', 'text')
188
188
189 into
189 into
190
190
191 [('class1',
191 [('class1',
192 [('op1', 'text'),
192 [('op1', 'text'),
193 ('op2', 'text')]),
193 ('op2', 'text')]),
194 ('class2',
194 ('class2',
195 [('op3', 'text')])]
195 [('op3', 'text')])]
196
196
197 This is used to get the minimal tags necessary when
197 This is used to get the minimal tags necessary when
198 rendering to html eg for a token stream ie.
198 rendering to html eg for a token stream ie.
199
199
200 <span class="A"><ins>he</ins>llo</span>
200 <span class="A"><ins>he</ins>llo</span>
201 vs
201 vs
202 <span class="A"><ins>he</ins></span><span class="A">llo</span>
202 <span class="A"><ins>he</ins></span><span class="A">llo</span>
203
203
204 If a 2 tuple is passed in, the output op will be an empty string.
204 If a 2 tuple is passed in, the output op will be an empty string.
205
205
206 eg:
206 eg:
207
207
208 >>> rollup_tokenstream([('classA', '', 'h'),
208 >>> rollup_tokenstream([('classA', '', 'h'),
209 ('classA', 'del', 'ell'),
209 ('classA', 'del', 'ell'),
210 ('classA', '', 'o'),
210 ('classA', '', 'o'),
211 ('classB', '', ' '),
211 ('classB', '', ' '),
212 ('classA', '', 'the'),
212 ('classA', '', 'the'),
213 ('classA', '', 're'),
213 ('classA', '', 're'),
214 ])
214 ])
215
215
216 [('classA', [('', 'h'), ('del', 'ell'), ('', 'o')],
216 [('classA', [('', 'h'), ('del', 'ell'), ('', 'o')],
217 ('classB', [('', ' ')],
217 ('classB', [('', ' ')],
218 ('classA', [('', 'there')]]
218 ('classA', [('', 'there')]]
219
219
220 """
220 """
221 if tokenstream and len(tokenstream[0]) == 2:
221 if tokenstream and len(tokenstream[0]) == 2:
222 tokenstream = ((t[0], '', t[1]) for t in tokenstream)
222 tokenstream = ((t[0], '', t[1]) for t in tokenstream)
223
223
224 result = []
224 result = []
225 for token_class, op_list in groupby(tokenstream, lambda t: t[0]):
225 for token_class, op_list in groupby(tokenstream, lambda t: t[0]):
226 ops = []
226 ops = []
227 for token_op, token_text_list in groupby(op_list, lambda o: o[1]):
227 for token_op, token_text_list in groupby(op_list, lambda o: o[1]):
228 text_buffer = []
228 text_buffer = []
229 for t_class, t_op, t_text in token_text_list:
229 for t_class, t_op, t_text in token_text_list:
230 text_buffer.append(t_text)
230 text_buffer.append(t_text)
231 ops.append((token_op, ''.join(text_buffer)))
231 ops.append((token_op, ''.join(text_buffer)))
232 result.append((token_class, ops))
232 result.append((token_class, ops))
233 return result
233 return result
234
234
235
235
236 def tokens_diff(old_tokens, new_tokens, use_diff_match_patch=True):
236 def tokens_diff(old_tokens, new_tokens, use_diff_match_patch=True):
237 """
237 """
238 Converts a list of (token_class, token_text) tuples to a list of
238 Converts a list of (token_class, token_text) tuples to a list of
239 (token_class, token_op, token_text) tuples where token_op is one of
239 (token_class, token_op, token_text) tuples where token_op is one of
240 ('ins', 'del', '')
240 ('ins', 'del', '')
241
241
242 :param old_tokens: list of (token_class, token_text) tuples of old line
242 :param old_tokens: list of (token_class, token_text) tuples of old line
243 :param new_tokens: list of (token_class, token_text) tuples of new line
243 :param new_tokens: list of (token_class, token_text) tuples of new line
244 :param use_diff_match_patch: boolean, will use google's diff match patch
244 :param use_diff_match_patch: boolean, will use google's diff match patch
245 library which has options to 'smooth' out the character by character
245 library which has options to 'smooth' out the character by character
246 differences making nicer ins/del blocks
246 differences making nicer ins/del blocks
247 """
247 """
248
248
249 old_tokens_result = []
249 old_tokens_result = []
250 new_tokens_result = []
250 new_tokens_result = []
251
251
252 similarity = difflib.SequenceMatcher(None,
252 similarity = difflib.SequenceMatcher(None,
253 ''.join(token_text for token_class, token_text in old_tokens),
253 ''.join(token_text for token_class, token_text in old_tokens),
254 ''.join(token_text for token_class, token_text in new_tokens)
254 ''.join(token_text for token_class, token_text in new_tokens)
255 ).ratio()
255 ).ratio()
256
256
257 if similarity < 0.6: # return, the blocks are too different
257 if similarity < 0.6: # return, the blocks are too different
258 for token_class, token_text in old_tokens:
258 for token_class, token_text in old_tokens:
259 old_tokens_result.append((token_class, '', token_text))
259 old_tokens_result.append((token_class, '', token_text))
260 for token_class, token_text in new_tokens:
260 for token_class, token_text in new_tokens:
261 new_tokens_result.append((token_class, '', token_text))
261 new_tokens_result.append((token_class, '', token_text))
262 return old_tokens_result, new_tokens_result, similarity
262 return old_tokens_result, new_tokens_result, similarity
263
263
264 token_sequence_matcher = difflib.SequenceMatcher(None,
264 token_sequence_matcher = difflib.SequenceMatcher(None,
265 [x[1] for x in old_tokens],
265 [x[1] for x in old_tokens],
266 [x[1] for x in new_tokens])
266 [x[1] for x in new_tokens])
267
267
268 for tag, o1, o2, n1, n2 in token_sequence_matcher.get_opcodes():
268 for tag, o1, o2, n1, n2 in token_sequence_matcher.get_opcodes():
269 # check the differences by token block types first to give a more
269 # check the differences by token block types first to give a more
270 # nicer "block" level replacement vs character diffs
270 # nicer "block" level replacement vs character diffs
271
271
272 if tag == 'equal':
272 if tag == 'equal':
273 for token_class, token_text in old_tokens[o1:o2]:
273 for token_class, token_text in old_tokens[o1:o2]:
274 old_tokens_result.append((token_class, '', token_text))
274 old_tokens_result.append((token_class, '', token_text))
275 for token_class, token_text in new_tokens[n1:n2]:
275 for token_class, token_text in new_tokens[n1:n2]:
276 new_tokens_result.append((token_class, '', token_text))
276 new_tokens_result.append((token_class, '', token_text))
277 elif tag == 'delete':
277 elif tag == 'delete':
278 for token_class, token_text in old_tokens[o1:o2]:
278 for token_class, token_text in old_tokens[o1:o2]:
279 old_tokens_result.append((token_class, 'del', token_text))
279 old_tokens_result.append((token_class, 'del', token_text))
280 elif tag == 'insert':
280 elif tag == 'insert':
281 for token_class, token_text in new_tokens[n1:n2]:
281 for token_class, token_text in new_tokens[n1:n2]:
282 new_tokens_result.append((token_class, 'ins', token_text))
282 new_tokens_result.append((token_class, 'ins', token_text))
283 elif tag == 'replace':
283 elif tag == 'replace':
284 # if same type token blocks must be replaced, do a diff on the
284 # if same type token blocks must be replaced, do a diff on the
285 # characters in the token blocks to show individual changes
285 # characters in the token blocks to show individual changes
286
286
287 old_char_tokens = []
287 old_char_tokens = []
288 new_char_tokens = []
288 new_char_tokens = []
289 for token_class, token_text in old_tokens[o1:o2]:
289 for token_class, token_text in old_tokens[o1:o2]:
290 for char in token_text:
290 for char in token_text:
291 old_char_tokens.append((token_class, char))
291 old_char_tokens.append((token_class, char))
292
292
293 for token_class, token_text in new_tokens[n1:n2]:
293 for token_class, token_text in new_tokens[n1:n2]:
294 for char in token_text:
294 for char in token_text:
295 new_char_tokens.append((token_class, char))
295 new_char_tokens.append((token_class, char))
296
296
297 old_string = ''.join([token_text for
297 old_string = ''.join([token_text for
298 token_class, token_text in old_char_tokens])
298 token_class, token_text in old_char_tokens])
299 new_string = ''.join([token_text for
299 new_string = ''.join([token_text for
300 token_class, token_text in new_char_tokens])
300 token_class, token_text in new_char_tokens])
301
301
302 char_sequence = difflib.SequenceMatcher(
302 char_sequence = difflib.SequenceMatcher(
303 None, old_string, new_string)
303 None, old_string, new_string)
304 copcodes = char_sequence.get_opcodes()
304 copcodes = char_sequence.get_opcodes()
305 obuffer, nbuffer = [], []
305 obuffer, nbuffer = [], []
306
306
307 if use_diff_match_patch:
307 if use_diff_match_patch:
308 dmp = diff_match_patch()
308 dmp = diff_match_patch()
309 dmp.Diff_EditCost = 11 # TODO: dan: extract this to a setting
309 dmp.Diff_EditCost = 11 # TODO: dan: extract this to a setting
310 reps = dmp.diff_main(old_string, new_string)
310 reps = dmp.diff_main(old_string, new_string)
311 dmp.diff_cleanupEfficiency(reps)
311 dmp.diff_cleanupEfficiency(reps)
312
312
313 a, b = 0, 0
313 a, b = 0, 0
314 for op, rep in reps:
314 for op, rep in reps:
315 l = len(rep)
315 l = len(rep)
316 if op == 0:
316 if op == 0:
317 for i, c in enumerate(rep):
317 for i, c in enumerate(rep):
318 obuffer.append((old_char_tokens[a+i][0], '', c))
318 obuffer.append((old_char_tokens[a+i][0], '', c))
319 nbuffer.append((new_char_tokens[b+i][0], '', c))
319 nbuffer.append((new_char_tokens[b+i][0], '', c))
320 a += l
320 a += l
321 b += l
321 b += l
322 elif op == -1:
322 elif op == -1:
323 for i, c in enumerate(rep):
323 for i, c in enumerate(rep):
324 obuffer.append((old_char_tokens[a+i][0], 'del', c))
324 obuffer.append((old_char_tokens[a+i][0], 'del', c))
325 a += l
325 a += l
326 elif op == 1:
326 elif op == 1:
327 for i, c in enumerate(rep):
327 for i, c in enumerate(rep):
328 nbuffer.append((new_char_tokens[b+i][0], 'ins', c))
328 nbuffer.append((new_char_tokens[b+i][0], 'ins', c))
329 b += l
329 b += l
330 else:
330 else:
331 for ctag, co1, co2, cn1, cn2 in copcodes:
331 for ctag, co1, co2, cn1, cn2 in copcodes:
332 if ctag == 'equal':
332 if ctag == 'equal':
333 for token_class, token_text in old_char_tokens[co1:co2]:
333 for token_class, token_text in old_char_tokens[co1:co2]:
334 obuffer.append((token_class, '', token_text))
334 obuffer.append((token_class, '', token_text))
335 for token_class, token_text in new_char_tokens[cn1:cn2]:
335 for token_class, token_text in new_char_tokens[cn1:cn2]:
336 nbuffer.append((token_class, '', token_text))
336 nbuffer.append((token_class, '', token_text))
337 elif ctag == 'delete':
337 elif ctag == 'delete':
338 for token_class, token_text in old_char_tokens[co1:co2]:
338 for token_class, token_text in old_char_tokens[co1:co2]:
339 obuffer.append((token_class, 'del', token_text))
339 obuffer.append((token_class, 'del', token_text))
340 elif ctag == 'insert':
340 elif ctag == 'insert':
341 for token_class, token_text in new_char_tokens[cn1:cn2]:
341 for token_class, token_text in new_char_tokens[cn1:cn2]:
342 nbuffer.append((token_class, 'ins', token_text))
342 nbuffer.append((token_class, 'ins', token_text))
343 elif ctag == 'replace':
343 elif ctag == 'replace':
344 for token_class, token_text in old_char_tokens[co1:co2]:
344 for token_class, token_text in old_char_tokens[co1:co2]:
345 obuffer.append((token_class, 'del', token_text))
345 obuffer.append((token_class, 'del', token_text))
346 for token_class, token_text in new_char_tokens[cn1:cn2]:
346 for token_class, token_text in new_char_tokens[cn1:cn2]:
347 nbuffer.append((token_class, 'ins', token_text))
347 nbuffer.append((token_class, 'ins', token_text))
348
348
349 old_tokens_result.extend(obuffer)
349 old_tokens_result.extend(obuffer)
350 new_tokens_result.extend(nbuffer)
350 new_tokens_result.extend(nbuffer)
351
351
352 return old_tokens_result, new_tokens_result, similarity
352 return old_tokens_result, new_tokens_result, similarity
353
353
354
354
355 def diffset_node_getter(commit):
355 def diffset_node_getter(commit):
356 def get_node(fname):
356 def get_node(fname):
357 try:
357 try:
358 return commit.get_node(fname)
358 return commit.get_node(fname)
359 except NodeDoesNotExistError:
359 except NodeDoesNotExistError:
360 return None
360 return None
361
361
362 return get_node
362 return get_node
363
363
364
364
365 class DiffSet(object):
365 class DiffSet(object):
366 """
366 """
367 An object for parsing the diff result from diffs.DiffProcessor and
367 An object for parsing the diff result from diffs.DiffProcessor and
368 adding highlighting, side by side/unified renderings and line diffs
368 adding highlighting, side by side/unified renderings and line diffs
369 """
369 """
370
370
371 HL_REAL = 'REAL' # highlights using original file, slow
371 HL_REAL = 'REAL' # highlights using original file, slow
372 HL_FAST = 'FAST' # highlights using just the line, fast but not correct
372 HL_FAST = 'FAST' # highlights using just the line, fast but not correct
373 # in the case of multiline code
373 # in the case of multiline code
374 HL_NONE = 'NONE' # no highlighting, fastest
374 HL_NONE = 'NONE' # no highlighting, fastest
375
375
376 def __init__(self, highlight_mode=HL_REAL, repo_name=None,
376 def __init__(self, highlight_mode=HL_REAL, repo_name=None,
377 source_repo_name=None,
377 source_repo_name=None,
378 source_node_getter=lambda filename: None,
378 source_node_getter=lambda filename: None,
379 target_node_getter=lambda filename: None,
379 target_node_getter=lambda filename: None,
380 source_nodes=None, target_nodes=None,
380 source_nodes=None, target_nodes=None,
381 max_file_size_limit=150 * 1024, # files over this size will
381 # files over this size will use fast highlighting
382 # use fast highlighting
382 max_file_size_limit=150 * 1024,
383 comments=None,
384 ):
383 ):
385
384
386 self.highlight_mode = highlight_mode
385 self.highlight_mode = highlight_mode
387 self.highlighted_filenodes = {}
386 self.highlighted_filenodes = {}
388 self.source_node_getter = source_node_getter
387 self.source_node_getter = source_node_getter
389 self.target_node_getter = target_node_getter
388 self.target_node_getter = target_node_getter
390 self.source_nodes = source_nodes or {}
389 self.source_nodes = source_nodes or {}
391 self.target_nodes = target_nodes or {}
390 self.target_nodes = target_nodes or {}
392 self.repo_name = repo_name
391 self.repo_name = repo_name
393 self.source_repo_name = source_repo_name or repo_name
392 self.source_repo_name = source_repo_name or repo_name
394 self.comments = comments or {}
395 self.comments_store = self.comments.copy()
396 self.max_file_size_limit = max_file_size_limit
393 self.max_file_size_limit = max_file_size_limit
397
394
398 def render_patchset(self, patchset, source_ref=None, target_ref=None):
395 def render_patchset(self, patchset, source_ref=None, target_ref=None):
399 diffset = AttributeDict(dict(
396 diffset = AttributeDict(dict(
400 lines_added=0,
397 lines_added=0,
401 lines_deleted=0,
398 lines_deleted=0,
402 changed_files=0,
399 changed_files=0,
403 files=[],
400 files=[],
404 file_stats={},
401 file_stats={},
405 limited_diff=isinstance(patchset, LimitedDiffContainer),
402 limited_diff=isinstance(patchset, LimitedDiffContainer),
406 repo_name=self.repo_name,
403 repo_name=self.repo_name,
407 source_repo_name=self.source_repo_name,
404 source_repo_name=self.source_repo_name,
408 source_ref=source_ref,
405 source_ref=source_ref,
409 target_ref=target_ref,
406 target_ref=target_ref,
410 ))
407 ))
411 for patch in patchset:
408 for patch in patchset:
412 diffset.file_stats[patch['filename']] = patch['stats']
409 diffset.file_stats[patch['filename']] = patch['stats']
413 filediff = self.render_patch(patch)
410 filediff = self.render_patch(patch)
414 filediff.diffset = StrictAttributeDict(dict(
411 filediff.diffset = StrictAttributeDict(dict(
415 source_ref=diffset.source_ref,
412 source_ref=diffset.source_ref,
416 target_ref=diffset.target_ref,
413 target_ref=diffset.target_ref,
417 repo_name=diffset.repo_name,
414 repo_name=diffset.repo_name,
418 source_repo_name=diffset.source_repo_name,
415 source_repo_name=diffset.source_repo_name,
419 ))
416 ))
420 diffset.files.append(filediff)
417 diffset.files.append(filediff)
421 diffset.changed_files += 1
418 diffset.changed_files += 1
422 if not patch['stats']['binary']:
419 if not patch['stats']['binary']:
423 diffset.lines_added += patch['stats']['added']
420 diffset.lines_added += patch['stats']['added']
424 diffset.lines_deleted += patch['stats']['deleted']
421 diffset.lines_deleted += patch['stats']['deleted']
425
422
426 return diffset
423 return diffset
427
424
428 _lexer_cache = {}
425 _lexer_cache = {}
429
426
430 def _get_lexer_for_filename(self, filename, filenode=None):
427 def _get_lexer_for_filename(self, filename, filenode=None):
431 # cached because we might need to call it twice for source/target
428 # cached because we might need to call it twice for source/target
432 if filename not in self._lexer_cache:
429 if filename not in self._lexer_cache:
433 if filenode:
430 if filenode:
434 lexer = filenode.lexer
431 lexer = filenode.lexer
435 extension = filenode.extension
432 extension = filenode.extension
436 else:
433 else:
437 lexer = FileNode.get_lexer(filename=filename)
434 lexer = FileNode.get_lexer(filename=filename)
438 extension = filename.split('.')[-1]
435 extension = filename.split('.')[-1]
439
436
440 lexer = get_custom_lexer(extension) or lexer
437 lexer = get_custom_lexer(extension) or lexer
441 self._lexer_cache[filename] = lexer
438 self._lexer_cache[filename] = lexer
442 return self._lexer_cache[filename]
439 return self._lexer_cache[filename]
443
440
444 def render_patch(self, patch):
441 def render_patch(self, patch):
445 log.debug('rendering diff for %r', patch['filename'])
442 log.debug('rendering diff for %r', patch['filename'])
446
443
447 source_filename = patch['original_filename']
444 source_filename = patch['original_filename']
448 target_filename = patch['filename']
445 target_filename = patch['filename']
449
446
450 source_lexer = plain_text_lexer
447 source_lexer = plain_text_lexer
451 target_lexer = plain_text_lexer
448 target_lexer = plain_text_lexer
452
449
453 if not patch['stats']['binary']:
450 if not patch['stats']['binary']:
454 if self.highlight_mode == self.HL_REAL:
451 if self.highlight_mode == self.HL_REAL:
455 if (source_filename and patch['operation'] in ('D', 'M')
452 if (source_filename and patch['operation'] in ('D', 'M')
456 and source_filename not in self.source_nodes):
453 and source_filename not in self.source_nodes):
457 self.source_nodes[source_filename] = (
454 self.source_nodes[source_filename] = (
458 self.source_node_getter(source_filename))
455 self.source_node_getter(source_filename))
459
456
460 if (target_filename and patch['operation'] in ('A', 'M')
457 if (target_filename and patch['operation'] in ('A', 'M')
461 and target_filename not in self.target_nodes):
458 and target_filename not in self.target_nodes):
462 self.target_nodes[target_filename] = (
459 self.target_nodes[target_filename] = (
463 self.target_node_getter(target_filename))
460 self.target_node_getter(target_filename))
464
461
465 elif self.highlight_mode == self.HL_FAST:
462 elif self.highlight_mode == self.HL_FAST:
466 source_lexer = self._get_lexer_for_filename(source_filename)
463 source_lexer = self._get_lexer_for_filename(source_filename)
467 target_lexer = self._get_lexer_for_filename(target_filename)
464 target_lexer = self._get_lexer_for_filename(target_filename)
468
465
469 source_file = self.source_nodes.get(source_filename, source_filename)
466 source_file = self.source_nodes.get(source_filename, source_filename)
470 target_file = self.target_nodes.get(target_filename, target_filename)
467 target_file = self.target_nodes.get(target_filename, target_filename)
471
468
472 source_filenode, target_filenode = None, None
469 source_filenode, target_filenode = None, None
473
470
474 # TODO: dan: FileNode.lexer works on the content of the file - which
471 # TODO: dan: FileNode.lexer works on the content of the file - which
475 # can be slow - issue #4289 explains a lexer clean up - which once
472 # can be slow - issue #4289 explains a lexer clean up - which once
476 # done can allow caching a lexer for a filenode to avoid the file lookup
473 # done can allow caching a lexer for a filenode to avoid the file lookup
477 if isinstance(source_file, FileNode):
474 if isinstance(source_file, FileNode):
478 source_filenode = source_file
475 source_filenode = source_file
479 #source_lexer = source_file.lexer
476 #source_lexer = source_file.lexer
480 source_lexer = self._get_lexer_for_filename(source_filename)
477 source_lexer = self._get_lexer_for_filename(source_filename)
481 source_file.lexer = source_lexer
478 source_file.lexer = source_lexer
482
479
483 if isinstance(target_file, FileNode):
480 if isinstance(target_file, FileNode):
484 target_filenode = target_file
481 target_filenode = target_file
485 #target_lexer = target_file.lexer
482 #target_lexer = target_file.lexer
486 target_lexer = self._get_lexer_for_filename(target_filename)
483 target_lexer = self._get_lexer_for_filename(target_filename)
487 target_file.lexer = target_lexer
484 target_file.lexer = target_lexer
488
485
489 source_file_path, target_file_path = None, None
486 source_file_path, target_file_path = None, None
490
487
491 if source_filename != '/dev/null':
488 if source_filename != '/dev/null':
492 source_file_path = source_filename
489 source_file_path = source_filename
493 if target_filename != '/dev/null':
490 if target_filename != '/dev/null':
494 target_file_path = target_filename
491 target_file_path = target_filename
495
492
496 source_file_type = source_lexer.name
493 source_file_type = source_lexer.name
497 target_file_type = target_lexer.name
494 target_file_type = target_lexer.name
498
495
499 filediff = AttributeDict({
496 filediff = AttributeDict({
500 'source_file_path': source_file_path,
497 'source_file_path': source_file_path,
501 'target_file_path': target_file_path,
498 'target_file_path': target_file_path,
502 'source_filenode': source_filenode,
499 'source_filenode': source_filenode,
503 'target_filenode': target_filenode,
500 'target_filenode': target_filenode,
504 'source_file_type': target_file_type,
501 'source_file_type': target_file_type,
505 'target_file_type': source_file_type,
502 'target_file_type': source_file_type,
506 'patch': {'filename': patch['filename'], 'stats': patch['stats']},
503 'patch': {'filename': patch['filename'], 'stats': patch['stats']},
507 'operation': patch['operation'],
504 'operation': patch['operation'],
508 'source_mode': patch['stats']['old_mode'],
505 'source_mode': patch['stats']['old_mode'],
509 'target_mode': patch['stats']['new_mode'],
506 'target_mode': patch['stats']['new_mode'],
510 'limited_diff': isinstance(patch, LimitedDiffContainer),
507 'limited_diff': isinstance(patch, LimitedDiffContainer),
511 'hunks': [],
508 'hunks': [],
512 'diffset': self,
509 'diffset': self,
513 })
510 })
514
511
515 for hunk in patch['chunks'][1:]:
512 for hunk in patch['chunks'][1:]:
516 hunkbit = self.parse_hunk(hunk, source_file, target_file)
513 hunkbit = self.parse_hunk(hunk, source_file, target_file)
517 hunkbit.source_file_path = source_file_path
514 hunkbit.source_file_path = source_file_path
518 hunkbit.target_file_path = target_file_path
515 hunkbit.target_file_path = target_file_path
519 filediff.hunks.append(hunkbit)
516 filediff.hunks.append(hunkbit)
520
517
521 left_comments = {}
522 if source_file_path in self.comments_store:
523 for lineno, comments in self.comments_store[source_file_path].items():
524 left_comments[lineno] = comments
525
526 if target_file_path in self.comments_store:
527 for lineno, comments in self.comments_store[target_file_path].items():
528 left_comments[lineno] = comments
529
530 # left comments are one that we couldn't place in diff lines.
531 # could be outdated, or the diff changed and this line is no
532 # longer available
533 filediff.left_comments = left_comments
534
535 return filediff
518 return filediff
536
519
537 def parse_hunk(self, hunk, source_file, target_file):
520 def parse_hunk(self, hunk, source_file, target_file):
538 result = AttributeDict(dict(
521 result = AttributeDict(dict(
539 source_start=hunk['source_start'],
522 source_start=hunk['source_start'],
540 source_length=hunk['source_length'],
523 source_length=hunk['source_length'],
541 target_start=hunk['target_start'],
524 target_start=hunk['target_start'],
542 target_length=hunk['target_length'],
525 target_length=hunk['target_length'],
543 section_header=hunk['section_header'],
526 section_header=hunk['section_header'],
544 lines=[],
527 lines=[],
545 ))
528 ))
546 before, after = [], []
529 before, after = [], []
547
530
548 for line in hunk['lines']:
531 for line in hunk['lines']:
549
532
550 if line['action'] == 'unmod':
533 if line['action'] == 'unmod':
551 result.lines.extend(
534 result.lines.extend(
552 self.parse_lines(before, after, source_file, target_file))
535 self.parse_lines(before, after, source_file, target_file))
553 after.append(line)
536 after.append(line)
554 before.append(line)
537 before.append(line)
555 elif line['action'] == 'add':
538 elif line['action'] == 'add':
556 after.append(line)
539 after.append(line)
557 elif line['action'] == 'del':
540 elif line['action'] == 'del':
558 before.append(line)
541 before.append(line)
559 elif line['action'] == 'old-no-nl':
542 elif line['action'] == 'old-no-nl':
560 before.append(line)
543 before.append(line)
561 elif line['action'] == 'new-no-nl':
544 elif line['action'] == 'new-no-nl':
562 after.append(line)
545 after.append(line)
563
546
564 result.lines.extend(
547 result.lines.extend(
565 self.parse_lines(before, after, source_file, target_file))
548 self.parse_lines(before, after, source_file, target_file))
566 result.unified = list(self.as_unified(result.lines))
549 result.unified = list(self.as_unified(result.lines))
567 result.sideside = result.lines
550 result.sideside = result.lines
568
551
569 return result
552 return result
570
553
571 def parse_lines(self, before_lines, after_lines, source_file, target_file):
554 def parse_lines(self, before_lines, after_lines, source_file, target_file):
572 # TODO: dan: investigate doing the diff comparison and fast highlighting
555 # TODO: dan: investigate doing the diff comparison and fast highlighting
573 # on the entire before and after buffered block lines rather than by
556 # on the entire before and after buffered block lines rather than by
574 # line, this means we can get better 'fast' highlighting if the context
557 # line, this means we can get better 'fast' highlighting if the context
575 # allows it - eg.
558 # allows it - eg.
576 # line 4: """
559 # line 4: """
577 # line 5: this gets highlighted as a string
560 # line 5: this gets highlighted as a string
578 # line 6: """
561 # line 6: """
579
562
580 lines = []
563 lines = []
581
564
582 before_newline = AttributeDict()
565 before_newline = AttributeDict()
583 after_newline = AttributeDict()
566 after_newline = AttributeDict()
584 if before_lines and before_lines[-1]['action'] == 'old-no-nl':
567 if before_lines and before_lines[-1]['action'] == 'old-no-nl':
585 before_newline_line = before_lines.pop(-1)
568 before_newline_line = before_lines.pop(-1)
586 before_newline.content = '\n {}'.format(
569 before_newline.content = '\n {}'.format(
587 render_tokenstream(
570 render_tokenstream(
588 [(x[0], '', x[1])
571 [(x[0], '', x[1])
589 for x in [('nonl', before_newline_line['line'])]]))
572 for x in [('nonl', before_newline_line['line'])]]))
590
573
591 if after_lines and after_lines[-1]['action'] == 'new-no-nl':
574 if after_lines and after_lines[-1]['action'] == 'new-no-nl':
592 after_newline_line = after_lines.pop(-1)
575 after_newline_line = after_lines.pop(-1)
593 after_newline.content = '\n {}'.format(
576 after_newline.content = '\n {}'.format(
594 render_tokenstream(
577 render_tokenstream(
595 [(x[0], '', x[1])
578 [(x[0], '', x[1])
596 for x in [('nonl', after_newline_line['line'])]]))
579 for x in [('nonl', after_newline_line['line'])]]))
597
580
598 while before_lines or after_lines:
581 while before_lines or after_lines:
599 before, after = None, None
582 before, after = None, None
600 before_tokens, after_tokens = None, None
583 before_tokens, after_tokens = None, None
601
584
602 if before_lines:
585 if before_lines:
603 before = before_lines.pop(0)
586 before = before_lines.pop(0)
604 if after_lines:
587 if after_lines:
605 after = after_lines.pop(0)
588 after = after_lines.pop(0)
606
589
607 original = AttributeDict()
590 original = AttributeDict()
608 modified = AttributeDict()
591 modified = AttributeDict()
609
592
610 if before:
593 if before:
611 if before['action'] == 'old-no-nl':
594 if before['action'] == 'old-no-nl':
612 before_tokens = [('nonl', before['line'])]
595 before_tokens = [('nonl', before['line'])]
613 else:
596 else:
614 before_tokens = self.get_line_tokens(
597 before_tokens = self.get_line_tokens(
615 line_text=before['line'],
598 line_text=before['line'],
616 line_number=before['old_lineno'],
599 line_number=before['old_lineno'],
617 file=source_file)
600 file=source_file)
618 original.lineno = before['old_lineno']
601 original.lineno = before['old_lineno']
619 original.content = before['line']
602 original.content = before['line']
620 original.action = self.action_to_op(before['action'])
603 original.action = self.action_to_op(before['action'])
621
604
622 original.get_comment_args = (
605 original.get_comment_args = (
623 source_file, 'o', before['old_lineno'])
606 source_file, 'o', before['old_lineno'])
624
607
625 if after:
608 if after:
626 if after['action'] == 'new-no-nl':
609 if after['action'] == 'new-no-nl':
627 after_tokens = [('nonl', after['line'])]
610 after_tokens = [('nonl', after['line'])]
628 else:
611 else:
629 after_tokens = self.get_line_tokens(
612 after_tokens = self.get_line_tokens(
630 line_text=after['line'], line_number=after['new_lineno'],
613 line_text=after['line'], line_number=after['new_lineno'],
631 file=target_file)
614 file=target_file)
632 modified.lineno = after['new_lineno']
615 modified.lineno = after['new_lineno']
633 modified.content = after['line']
616 modified.content = after['line']
634 modified.action = self.action_to_op(after['action'])
617 modified.action = self.action_to_op(after['action'])
635
618
636 modified.get_comment_args = (
619 modified.get_comment_args = (
637 target_file, 'n', after['new_lineno'])
620 target_file, 'n', after['new_lineno'])
638
621
639 # diff the lines
622 # diff the lines
640 if before_tokens and after_tokens:
623 if before_tokens and after_tokens:
641 o_tokens, m_tokens, similarity = tokens_diff(
624 o_tokens, m_tokens, similarity = tokens_diff(
642 before_tokens, after_tokens)
625 before_tokens, after_tokens)
643 original.content = render_tokenstream(o_tokens)
626 original.content = render_tokenstream(o_tokens)
644 modified.content = render_tokenstream(m_tokens)
627 modified.content = render_tokenstream(m_tokens)
645 elif before_tokens:
628 elif before_tokens:
646 original.content = render_tokenstream(
629 original.content = render_tokenstream(
647 [(x[0], '', x[1]) for x in before_tokens])
630 [(x[0], '', x[1]) for x in before_tokens])
648 elif after_tokens:
631 elif after_tokens:
649 modified.content = render_tokenstream(
632 modified.content = render_tokenstream(
650 [(x[0], '', x[1]) for x in after_tokens])
633 [(x[0], '', x[1]) for x in after_tokens])
651
634
652 if not before_lines and before_newline:
635 if not before_lines and before_newline:
653 original.content += before_newline.content
636 original.content += before_newline.content
654 before_newline = None
637 before_newline = None
655 if not after_lines and after_newline:
638 if not after_lines and after_newline:
656 modified.content += after_newline.content
639 modified.content += after_newline.content
657 after_newline = None
640 after_newline = None
658
641
659 lines.append(AttributeDict({
642 lines.append(AttributeDict({
660 'original': original,
643 'original': original,
661 'modified': modified,
644 'modified': modified,
662 }))
645 }))
663
646
664 return lines
647 return lines
665
648
666 def get_line_tokens(self, line_text, line_number, file=None):
649 def get_line_tokens(self, line_text, line_number, file=None):
667 filenode = None
650 filenode = None
668 filename = None
651 filename = None
669
652
670 if isinstance(file, basestring):
653 if isinstance(file, basestring):
671 filename = file
654 filename = file
672 elif isinstance(file, FileNode):
655 elif isinstance(file, FileNode):
673 filenode = file
656 filenode = file
674 filename = file.unicode_path
657 filename = file.unicode_path
675
658
676 if self.highlight_mode == self.HL_REAL and filenode:
659 if self.highlight_mode == self.HL_REAL and filenode:
677 lexer = self._get_lexer_for_filename(filename)
660 lexer = self._get_lexer_for_filename(filename)
678 file_size_allowed = file.size < self.max_file_size_limit
661 file_size_allowed = file.size < self.max_file_size_limit
679 if line_number and file_size_allowed:
662 if line_number and file_size_allowed:
680 return self.get_tokenized_filenode_line(
663 return self.get_tokenized_filenode_line(
681 file, line_number, lexer)
664 file, line_number, lexer)
682
665
683 if self.highlight_mode in (self.HL_REAL, self.HL_FAST) and filename:
666 if self.highlight_mode in (self.HL_REAL, self.HL_FAST) and filename:
684 lexer = self._get_lexer_for_filename(filename)
667 lexer = self._get_lexer_for_filename(filename)
685 return list(tokenize_string(line_text, lexer))
668 return list(tokenize_string(line_text, lexer))
686
669
687 return list(tokenize_string(line_text, plain_text_lexer))
670 return list(tokenize_string(line_text, plain_text_lexer))
688
671
689 def get_tokenized_filenode_line(self, filenode, line_number, lexer=None):
672 def get_tokenized_filenode_line(self, filenode, line_number, lexer=None):
690
673
691 if filenode not in self.highlighted_filenodes:
674 if filenode not in self.highlighted_filenodes:
692 tokenized_lines = filenode_as_lines_tokens(filenode, lexer)
675 tokenized_lines = filenode_as_lines_tokens(filenode, lexer)
693 self.highlighted_filenodes[filenode] = tokenized_lines
676 self.highlighted_filenodes[filenode] = tokenized_lines
694 return self.highlighted_filenodes[filenode][line_number - 1]
677 return self.highlighted_filenodes[filenode][line_number - 1]
695
678
696 def action_to_op(self, action):
679 def action_to_op(self, action):
697 return {
680 return {
698 'add': '+',
681 'add': '+',
699 'del': '-',
682 'del': '-',
700 'unmod': ' ',
683 'unmod': ' ',
701 'old-no-nl': ' ',
684 'old-no-nl': ' ',
702 'new-no-nl': ' ',
685 'new-no-nl': ' ',
703 }.get(action, action)
686 }.get(action, action)
704
687
705 def as_unified(self, lines):
688 def as_unified(self, lines):
706 """
689 """
707 Return a generator that yields the lines of a diff in unified order
690 Return a generator that yields the lines of a diff in unified order
708 """
691 """
709 def generator():
692 def generator():
710 buf = []
693 buf = []
711 for line in lines:
694 for line in lines:
712
695
713 if buf and not line.original or line.original.action == ' ':
696 if buf and not line.original or line.original.action == ' ':
714 for b in buf:
697 for b in buf:
715 yield b
698 yield b
716 buf = []
699 buf = []
717
700
718 if line.original:
701 if line.original:
719 if line.original.action == ' ':
702 if line.original.action == ' ':
720 yield (line.original.lineno, line.modified.lineno,
703 yield (line.original.lineno, line.modified.lineno,
721 line.original.action, line.original.content,
704 line.original.action, line.original.content,
722 line.original.get_comment_args)
705 line.original.get_comment_args)
723 continue
706 continue
724
707
725 if line.original.action == '-':
708 if line.original.action == '-':
726 yield (line.original.lineno, None,
709 yield (line.original.lineno, None,
727 line.original.action, line.original.content,
710 line.original.action, line.original.content,
728 line.original.get_comment_args)
711 line.original.get_comment_args)
729
712
730 if line.modified.action == '+':
713 if line.modified.action == '+':
731 buf.append((
714 buf.append((
732 None, line.modified.lineno,
715 None, line.modified.lineno,
733 line.modified.action, line.modified.content,
716 line.modified.action, line.modified.content,
734 line.modified.get_comment_args))
717 line.modified.get_comment_args))
735 continue
718 continue
736
719
737 if line.modified:
720 if line.modified:
738 yield (None, line.modified.lineno,
721 yield (None, line.modified.lineno,
739 line.modified.action, line.modified.content,
722 line.modified.action, line.modified.content,
740 line.modified.get_comment_args)
723 line.modified.get_comment_args)
741
724
742 for b in buf:
725 for b in buf:
743 yield b
726 yield b
744
727
745 return generator()
728 return generator()
@@ -1,407 +1,407 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 ## usage:
2 ## usage:
3 ## <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
3 ## <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
4 ## ${comment.comment_block(comment)}
4 ## ${comment.comment_block(comment)}
5 ##
5 ##
6 <%namespace name="base" file="/base/base.mako"/>
6 <%namespace name="base" file="/base/base.mako"/>
7
7
8 <%def name="comment_block(comment, inline=False)">
8 <%def name="comment_block(comment, inline=False)">
9 <% pr_index_ver = comment.get_index_version(getattr(c, 'versions', [])) %>
9 <% pr_index_ver = comment.get_index_version(getattr(c, 'versions', [])) %>
10 <% latest_ver = len(getattr(c, 'versions', [])) %>
10 <% latest_ver = len(getattr(c, 'versions', [])) %>
11 % if inline:
11 % if inline:
12 <% outdated_at_ver = comment.outdated_at_version(getattr(c, 'at_version_num', None)) %>
12 <% outdated_at_ver = comment.outdated_at_version(getattr(c, 'at_version_num', None)) %>
13 % else:
13 % else:
14 <% outdated_at_ver = comment.older_than_version(getattr(c, 'at_version_num', None)) %>
14 <% outdated_at_ver = comment.older_than_version(getattr(c, 'at_version_num', None)) %>
15 % endif
15 % endif
16
16
17
17
18 <div class="comment
18 <div class="comment
19 ${'comment-inline' if inline else 'comment-general'}
19 ${'comment-inline' if inline else 'comment-general'}
20 ${'comment-outdated' if outdated_at_ver else 'comment-current'}"
20 ${'comment-outdated' if outdated_at_ver else 'comment-current'}"
21 id="comment-${comment.comment_id}"
21 id="comment-${comment.comment_id}"
22 line="${comment.line_no}"
22 line="${comment.line_no}"
23 data-comment-id="${comment.comment_id}"
23 data-comment-id="${comment.comment_id}"
24 data-comment-type="${comment.comment_type}"
24 data-comment-type="${comment.comment_type}"
25 data-comment-line-no="${comment.line_no}"
25 data-comment-line-no="${comment.line_no}"
26 data-comment-inline=${h.json.dumps(inline)}
26 data-comment-inline=${h.json.dumps(inline)}
27 style="${'display: none;' if outdated_at_ver else ''}">
27 style="${'display: none;' if outdated_at_ver else ''}">
28
28
29 <div class="meta">
29 <div class="meta">
30 <div class="comment-type-label">
30 <div class="comment-type-label">
31 <div class="comment-label ${comment.comment_type or 'note'}" id="comment-label-${comment.comment_id}">
31 <div class="comment-label ${comment.comment_type or 'note'}" id="comment-label-${comment.comment_id}" title="line: ${comment.line_no}">
32 % if comment.comment_type == 'todo':
32 % if comment.comment_type == 'todo':
33 % if comment.resolved:
33 % if comment.resolved:
34 <div class="resolved tooltip" title="${_('Resolved by comment #{}').format(comment.resolved.comment_id)}">
34 <div class="resolved tooltip" title="${_('Resolved by comment #{}').format(comment.resolved.comment_id)}">
35 <a href="#comment-${comment.resolved.comment_id}">${comment.comment_type}</a>
35 <a href="#comment-${comment.resolved.comment_id}">${comment.comment_type}</a>
36 </div>
36 </div>
37 % else:
37 % else:
38 <div class="resolved tooltip" style="display: none">
38 <div class="resolved tooltip" style="display: none">
39 <span>${comment.comment_type}</span>
39 <span>${comment.comment_type}</span>
40 </div>
40 </div>
41 <div class="resolve tooltip" onclick="return Rhodecode.comments.createResolutionComment(${comment.comment_id});" title="${_('Click to resolve this comment')}">
41 <div class="resolve tooltip" onclick="return Rhodecode.comments.createResolutionComment(${comment.comment_id});" title="${_('Click to resolve this comment')}">
42 ${comment.comment_type}
42 ${comment.comment_type}
43 </div>
43 </div>
44 % endif
44 % endif
45 % else:
45 % else:
46 % if comment.resolved_comment:
46 % if comment.resolved_comment:
47 fix
47 fix
48 % else:
48 % else:
49 ${comment.comment_type or 'note'}
49 ${comment.comment_type or 'note'}
50 % endif
50 % endif
51 % endif
51 % endif
52 </div>
52 </div>
53 </div>
53 </div>
54
54
55 <div class="author ${'author-inline' if inline else 'author-general'}">
55 <div class="author ${'author-inline' if inline else 'author-general'}">
56 ${base.gravatar_with_user(comment.author.email, 16)}
56 ${base.gravatar_with_user(comment.author.email, 16)}
57 </div>
57 </div>
58 <div class="date">
58 <div class="date">
59 ${h.age_component(comment.modified_at, time_is_local=True)}
59 ${h.age_component(comment.modified_at, time_is_local=True)}
60 </div>
60 </div>
61 % if inline:
61 % if inline:
62 <span></span>
62 <span></span>
63 % else:
63 % else:
64 <div class="status-change">
64 <div class="status-change">
65 % if comment.pull_request:
65 % if comment.pull_request:
66 <a href="${h.route_path('pullrequest_show',repo_name=comment.pull_request.target_repo.repo_name,pull_request_id=comment.pull_request.pull_request_id)}">
66 <a href="${h.route_path('pullrequest_show',repo_name=comment.pull_request.target_repo.repo_name,pull_request_id=comment.pull_request.pull_request_id)}">
67 % if comment.status_change:
67 % if comment.status_change:
68 ${_('pull request #%s') % comment.pull_request.pull_request_id}:
68 ${_('pull request #%s') % comment.pull_request.pull_request_id}:
69 % else:
69 % else:
70 ${_('pull request #%s') % comment.pull_request.pull_request_id}
70 ${_('pull request #%s') % comment.pull_request.pull_request_id}
71 % endif
71 % endif
72 </a>
72 </a>
73 % else:
73 % else:
74 % if comment.status_change:
74 % if comment.status_change:
75 ${_('Status change on commit')}:
75 ${_('Status change on commit')}:
76 % endif
76 % endif
77 % endif
77 % endif
78 </div>
78 </div>
79 % endif
79 % endif
80
80
81 % if comment.status_change:
81 % if comment.status_change:
82 <div class="${'flag_status %s' % comment.status_change[0].status}"></div>
82 <div class="${'flag_status %s' % comment.status_change[0].status}"></div>
83 <div title="${_('Commit status')}" class="changeset-status-lbl">
83 <div title="${_('Commit status')}" class="changeset-status-lbl">
84 ${comment.status_change[0].status_lbl}
84 ${comment.status_change[0].status_lbl}
85 </div>
85 </div>
86 % endif
86 % endif
87
87
88 % if comment.resolved_comment:
88 % if comment.resolved_comment:
89 <a class="has-spacer-before" href="#comment-${comment.resolved_comment.comment_id}" onclick="Rhodecode.comments.scrollToComment($('#comment-${comment.resolved_comment.comment_id}'), 0, ${h.json.dumps(comment.resolved_comment.outdated)})">
89 <a class="has-spacer-before" href="#comment-${comment.resolved_comment.comment_id}" onclick="Rhodecode.comments.scrollToComment($('#comment-${comment.resolved_comment.comment_id}'), 0, ${h.json.dumps(comment.resolved_comment.outdated)})">
90 ${_('resolves comment #{}').format(comment.resolved_comment.comment_id)}
90 ${_('resolves comment #{}').format(comment.resolved_comment.comment_id)}
91 </a>
91 </a>
92 % endif
92 % endif
93
93
94 <a class="permalink" href="#comment-${comment.comment_id}"> &para;</a>
94 <a class="permalink" href="#comment-${comment.comment_id}"> &para;</a>
95
95
96 <div class="comment-links-block">
96 <div class="comment-links-block">
97 % if comment.pull_request and comment.pull_request.author.user_id == comment.author.user_id:
97 % if comment.pull_request and comment.pull_request.author.user_id == comment.author.user_id:
98 <span class="tag authortag tooltip" title="${_('Pull request author')}">
98 <span class="tag authortag tooltip" title="${_('Pull request author')}">
99 ${_('author')}
99 ${_('author')}
100 </span>
100 </span>
101 |
101 |
102 % endif
102 % endif
103 % if inline:
103 % if inline:
104 <div class="pr-version-inline">
104 <div class="pr-version-inline">
105 <a href="${request.current_route_path(_query=dict(version=comment.pull_request_version_id), _anchor='comment-{}'.format(comment.comment_id))}">
105 <a href="${request.current_route_path(_query=dict(version=comment.pull_request_version_id), _anchor='comment-{}'.format(comment.comment_id))}">
106 % if outdated_at_ver:
106 % if outdated_at_ver:
107 <code class="pr-version-num" title="${_('Outdated comment from pull request version v{0}, latest v{1}').format(pr_index_ver, latest_ver)}">
107 <code class="pr-version-num" title="${_('Outdated comment from pull request version v{0}, latest v{1}').format(pr_index_ver, latest_ver)}">
108 outdated ${'v{}'.format(pr_index_ver)} |
108 outdated ${'v{}'.format(pr_index_ver)} |
109 </code>
109 </code>
110 % elif pr_index_ver:
110 % elif pr_index_ver:
111 <code class="pr-version-num" title="${_('Comment from pull request version v{0}, latest v{1}').format(pr_index_ver, latest_ver)}">
111 <code class="pr-version-num" title="${_('Comment from pull request version v{0}, latest v{1}').format(pr_index_ver, latest_ver)}">
112 ${'v{}'.format(pr_index_ver)} |
112 ${'v{}'.format(pr_index_ver)} |
113 </code>
113 </code>
114 % endif
114 % endif
115 </a>
115 </a>
116 </div>
116 </div>
117 % else:
117 % else:
118 % if comment.pull_request_version_id and pr_index_ver:
118 % if comment.pull_request_version_id and pr_index_ver:
119 |
119 |
120 <div class="pr-version">
120 <div class="pr-version">
121 % if comment.outdated:
121 % if comment.outdated:
122 <a href="?version=${comment.pull_request_version_id}#comment-${comment.comment_id}">
122 <a href="?version=${comment.pull_request_version_id}#comment-${comment.comment_id}">
123 ${_('Outdated comment from pull request version v{0}, latest v{1}').format(pr_index_ver, latest_ver)}
123 ${_('Outdated comment from pull request version v{0}, latest v{1}').format(pr_index_ver, latest_ver)}
124 </a>
124 </a>
125 % else:
125 % else:
126 <div title="${_('Comment from pull request version v{0}, latest v{1}').format(pr_index_ver, latest_ver)}">
126 <div title="${_('Comment from pull request version v{0}, latest v{1}').format(pr_index_ver, latest_ver)}">
127 <a href="${h.route_path('pullrequest_show',repo_name=comment.pull_request.target_repo.repo_name,pull_request_id=comment.pull_request.pull_request_id, version=comment.pull_request_version_id)}">
127 <a href="${h.route_path('pullrequest_show',repo_name=comment.pull_request.target_repo.repo_name,pull_request_id=comment.pull_request.pull_request_id, version=comment.pull_request_version_id)}">
128 <code class="pr-version-num">
128 <code class="pr-version-num">
129 ${'v{}'.format(pr_index_ver)}
129 ${'v{}'.format(pr_index_ver)}
130 </code>
130 </code>
131 </a>
131 </a>
132 </div>
132 </div>
133 % endif
133 % endif
134 </div>
134 </div>
135 % endif
135 % endif
136 % endif
136 % endif
137
137
138 ## show delete comment if it's not a PR (regular comments) or it's PR that is not closed
138 ## show delete comment if it's not a PR (regular comments) or it's PR that is not closed
139 ## only super-admin, repo admin OR comment owner can delete, also hide delete if currently viewed comment is outdated
139 ## only super-admin, repo admin OR comment owner can delete, also hide delete if currently viewed comment is outdated
140 %if not outdated_at_ver and (not comment.pull_request or (comment.pull_request and not comment.pull_request.is_closed())):
140 %if not outdated_at_ver and (not comment.pull_request or (comment.pull_request and not comment.pull_request.is_closed())):
141 ## permissions to delete
141 ## permissions to delete
142 %if h.HasPermissionAny('hg.admin')() or h.HasRepoPermissionAny('repository.admin')(c.repo_name) or comment.author.user_id == c.rhodecode_user.user_id:
142 %if h.HasPermissionAny('hg.admin')() or h.HasRepoPermissionAny('repository.admin')(c.repo_name) or comment.author.user_id == c.rhodecode_user.user_id:
143 ## TODO: dan: add edit comment here
143 ## TODO: dan: add edit comment here
144 <a onclick="return Rhodecode.comments.deleteComment(this);" class="delete-comment"> ${_('Delete')}</a>
144 <a onclick="return Rhodecode.comments.deleteComment(this);" class="delete-comment"> ${_('Delete')}</a>
145 %else:
145 %else:
146 <button class="btn-link" disabled="disabled"> ${_('Delete')}</button>
146 <button class="btn-link" disabled="disabled"> ${_('Delete')}</button>
147 %endif
147 %endif
148 %else:
148 %else:
149 <button class="btn-link" disabled="disabled"> ${_('Delete')}</button>
149 <button class="btn-link" disabled="disabled"> ${_('Delete')}</button>
150 %endif
150 %endif
151
151
152 % if outdated_at_ver:
152 % if outdated_at_ver:
153 | <a onclick="return Rhodecode.comments.prevOutdatedComment(this);" class="prev-comment"> ${_('Prev')}</a>
153 | <a onclick="return Rhodecode.comments.prevOutdatedComment(this);" class="prev-comment"> ${_('Prev')}</a>
154 | <a onclick="return Rhodecode.comments.nextOutdatedComment(this);" class="next-comment"> ${_('Next')}</a>
154 | <a onclick="return Rhodecode.comments.nextOutdatedComment(this);" class="next-comment"> ${_('Next')}</a>
155 % else:
155 % else:
156 | <a onclick="return Rhodecode.comments.prevComment(this);" class="prev-comment"> ${_('Prev')}</a>
156 | <a onclick="return Rhodecode.comments.prevComment(this);" class="prev-comment"> ${_('Prev')}</a>
157 | <a onclick="return Rhodecode.comments.nextComment(this);" class="next-comment"> ${_('Next')}</a>
157 | <a onclick="return Rhodecode.comments.nextComment(this);" class="next-comment"> ${_('Next')}</a>
158 % endif
158 % endif
159
159
160 </div>
160 </div>
161 </div>
161 </div>
162 <div class="text">
162 <div class="text">
163 ${h.render(comment.text, renderer=comment.renderer, mentions=True)}
163 ${h.render(comment.text, renderer=comment.renderer, mentions=True)}
164 </div>
164 </div>
165
165
166 </div>
166 </div>
167 </%def>
167 </%def>
168
168
169 ## generate main comments
169 ## generate main comments
170 <%def name="generate_comments(comments, include_pull_request=False, is_pull_request=False)">
170 <%def name="generate_comments(comments, include_pull_request=False, is_pull_request=False)">
171 <div class="general-comments" id="comments">
171 <div class="general-comments" id="comments">
172 %for comment in comments:
172 %for comment in comments:
173 <div id="comment-tr-${comment.comment_id}">
173 <div id="comment-tr-${comment.comment_id}">
174 ## only render comments that are not from pull request, or from
174 ## only render comments that are not from pull request, or from
175 ## pull request and a status change
175 ## pull request and a status change
176 %if not comment.pull_request or (comment.pull_request and comment.status_change) or include_pull_request:
176 %if not comment.pull_request or (comment.pull_request and comment.status_change) or include_pull_request:
177 ${comment_block(comment)}
177 ${comment_block(comment)}
178 %endif
178 %endif
179 </div>
179 </div>
180 %endfor
180 %endfor
181 ## to anchor ajax comments
181 ## to anchor ajax comments
182 <div id="injected_page_comments"></div>
182 <div id="injected_page_comments"></div>
183 </div>
183 </div>
184 </%def>
184 </%def>
185
185
186
186
187 <%def name="comments(post_url, cur_status, is_pull_request=False, is_compare=False, change_status=True, form_extras=None)">
187 <%def name="comments(post_url, cur_status, is_pull_request=False, is_compare=False, change_status=True, form_extras=None)">
188
188
189 <div class="comments">
189 <div class="comments">
190 <%
190 <%
191 if is_pull_request:
191 if is_pull_request:
192 placeholder = _('Leave a comment on this Pull Request.')
192 placeholder = _('Leave a comment on this Pull Request.')
193 elif is_compare:
193 elif is_compare:
194 placeholder = _('Leave a comment on {} commits in this range.').format(len(form_extras))
194 placeholder = _('Leave a comment on {} commits in this range.').format(len(form_extras))
195 else:
195 else:
196 placeholder = _('Leave a comment on this Commit.')
196 placeholder = _('Leave a comment on this Commit.')
197 %>
197 %>
198
198
199 % if c.rhodecode_user.username != h.DEFAULT_USER:
199 % if c.rhodecode_user.username != h.DEFAULT_USER:
200 <div class="js-template" id="cb-comment-general-form-template">
200 <div class="js-template" id="cb-comment-general-form-template">
201 ## template generated for injection
201 ## template generated for injection
202 ${comment_form(form_type='general', review_statuses=c.commit_statuses, form_extras=form_extras)}
202 ${comment_form(form_type='general', review_statuses=c.commit_statuses, form_extras=form_extras)}
203 </div>
203 </div>
204
204
205 <div id="cb-comment-general-form-placeholder" class="comment-form ac">
205 <div id="cb-comment-general-form-placeholder" class="comment-form ac">
206 ## inject form here
206 ## inject form here
207 </div>
207 </div>
208 <script type="text/javascript">
208 <script type="text/javascript">
209 var lineNo = 'general';
209 var lineNo = 'general';
210 var resolvesCommentId = null;
210 var resolvesCommentId = null;
211 var generalCommentForm = Rhodecode.comments.createGeneralComment(
211 var generalCommentForm = Rhodecode.comments.createGeneralComment(
212 lineNo, "${placeholder}", resolvesCommentId);
212 lineNo, "${placeholder}", resolvesCommentId);
213
213
214 // set custom success callback on rangeCommit
214 // set custom success callback on rangeCommit
215 % if is_compare:
215 % if is_compare:
216 generalCommentForm.setHandleFormSubmit(function(o) {
216 generalCommentForm.setHandleFormSubmit(function(o) {
217 var self = generalCommentForm;
217 var self = generalCommentForm;
218
218
219 var text = self.cm.getValue();
219 var text = self.cm.getValue();
220 var status = self.getCommentStatus();
220 var status = self.getCommentStatus();
221 var commentType = self.getCommentType();
221 var commentType = self.getCommentType();
222
222
223 if (text === "" && !status) {
223 if (text === "" && !status) {
224 return;
224 return;
225 }
225 }
226
226
227 // we can pick which commits we want to make the comment by
227 // we can pick which commits we want to make the comment by
228 // selecting them via click on preview pane, this will alter the hidden inputs
228 // selecting them via click on preview pane, this will alter the hidden inputs
229 var cherryPicked = $('#changeset_compare_view_content .compare_select.hl').length > 0;
229 var cherryPicked = $('#changeset_compare_view_content .compare_select.hl').length > 0;
230
230
231 var commitIds = [];
231 var commitIds = [];
232 $('#changeset_compare_view_content .compare_select').each(function(el) {
232 $('#changeset_compare_view_content .compare_select').each(function(el) {
233 var commitId = this.id.replace('row-', '');
233 var commitId = this.id.replace('row-', '');
234 if ($(this).hasClass('hl') || !cherryPicked) {
234 if ($(this).hasClass('hl') || !cherryPicked) {
235 $("input[data-commit-id='{0}']".format(commitId)).val(commitId);
235 $("input[data-commit-id='{0}']".format(commitId)).val(commitId);
236 commitIds.push(commitId);
236 commitIds.push(commitId);
237 } else {
237 } else {
238 $("input[data-commit-id='{0}']".format(commitId)).val('')
238 $("input[data-commit-id='{0}']".format(commitId)).val('')
239 }
239 }
240 });
240 });
241
241
242 self.setActionButtonsDisabled(true);
242 self.setActionButtonsDisabled(true);
243 self.cm.setOption("readOnly", true);
243 self.cm.setOption("readOnly", true);
244 var postData = {
244 var postData = {
245 'text': text,
245 'text': text,
246 'changeset_status': status,
246 'changeset_status': status,
247 'comment_type': commentType,
247 'comment_type': commentType,
248 'commit_ids': commitIds,
248 'commit_ids': commitIds,
249 'csrf_token': CSRF_TOKEN
249 'csrf_token': CSRF_TOKEN
250 };
250 };
251
251
252 var submitSuccessCallback = function(o) {
252 var submitSuccessCallback = function(o) {
253 location.reload(true);
253 location.reload(true);
254 };
254 };
255 var submitFailCallback = function(){
255 var submitFailCallback = function(){
256 self.resetCommentFormState(text)
256 self.resetCommentFormState(text)
257 };
257 };
258 self.submitAjaxPOST(
258 self.submitAjaxPOST(
259 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
259 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
260 });
260 });
261 % endif
261 % endif
262
262
263
263
264 </script>
264 </script>
265 % else:
265 % else:
266 ## form state when not logged in
266 ## form state when not logged in
267 <div class="comment-form ac">
267 <div class="comment-form ac">
268
268
269 <div class="comment-area">
269 <div class="comment-area">
270 <div class="comment-area-header">
270 <div class="comment-area-header">
271 <ul class="nav-links clearfix">
271 <ul class="nav-links clearfix">
272 <li class="active">
272 <li class="active">
273 <a class="disabled" href="#edit-btn" disabled="disabled" onclick="return false">${_('Write')}</a>
273 <a class="disabled" href="#edit-btn" disabled="disabled" onclick="return false">${_('Write')}</a>
274 </li>
274 </li>
275 <li class="">
275 <li class="">
276 <a class="disabled" href="#preview-btn" disabled="disabled" onclick="return false">${_('Preview')}</a>
276 <a class="disabled" href="#preview-btn" disabled="disabled" onclick="return false">${_('Preview')}</a>
277 </li>
277 </li>
278 </ul>
278 </ul>
279 </div>
279 </div>
280
280
281 <div class="comment-area-write" style="display: block;">
281 <div class="comment-area-write" style="display: block;">
282 <div id="edit-container">
282 <div id="edit-container">
283 <div style="padding: 40px 0">
283 <div style="padding: 40px 0">
284 ${_('You need to be logged in to leave comments.')}
284 ${_('You need to be logged in to leave comments.')}
285 <a href="${h.route_path('login', _query={'came_from': h.current_route_path(request)})}">${_('Login now')}</a>
285 <a href="${h.route_path('login', _query={'came_from': h.current_route_path(request)})}">${_('Login now')}</a>
286 </div>
286 </div>
287 </div>
287 </div>
288 <div id="preview-container" class="clearfix" style="display: none;">
288 <div id="preview-container" class="clearfix" style="display: none;">
289 <div id="preview-box" class="preview-box"></div>
289 <div id="preview-box" class="preview-box"></div>
290 </div>
290 </div>
291 </div>
291 </div>
292
292
293 <div class="comment-area-footer">
293 <div class="comment-area-footer">
294 <div class="toolbar">
294 <div class="toolbar">
295 <div class="toolbar-text">
295 <div class="toolbar-text">
296 </div>
296 </div>
297 </div>
297 </div>
298 </div>
298 </div>
299 </div>
299 </div>
300
300
301 <div class="comment-footer">
301 <div class="comment-footer">
302 </div>
302 </div>
303
303
304 </div>
304 </div>
305 % endif
305 % endif
306
306
307 <script type="text/javascript">
307 <script type="text/javascript">
308 bindToggleButtons();
308 bindToggleButtons();
309 </script>
309 </script>
310 </div>
310 </div>
311 </%def>
311 </%def>
312
312
313
313
314 <%def name="comment_form(form_type, form_id='', lineno_id='{1}', review_statuses=None, form_extras=None)">
314 <%def name="comment_form(form_type, form_id='', lineno_id='{1}', review_statuses=None, form_extras=None)">
315 ## comment injected based on assumption that user is logged in
315 ## comment injected based on assumption that user is logged in
316
316
317 <form ${'id="{}"'.format(form_id) if form_id else '' |n} action="#" method="GET">
317 <form ${'id="{}"'.format(form_id) if form_id else '' |n} action="#" method="GET">
318
318
319 <div class="comment-area">
319 <div class="comment-area">
320 <div class="comment-area-header">
320 <div class="comment-area-header">
321 <ul class="nav-links clearfix">
321 <ul class="nav-links clearfix">
322 <li class="active">
322 <li class="active">
323 <a href="#edit-btn" tabindex="-1" id="edit-btn_${lineno_id}">${_('Write')}</a>
323 <a href="#edit-btn" tabindex="-1" id="edit-btn_${lineno_id}">${_('Write')}</a>
324 </li>
324 </li>
325 <li class="">
325 <li class="">
326 <a href="#preview-btn" tabindex="-1" id="preview-btn_${lineno_id}">${_('Preview')}</a>
326 <a href="#preview-btn" tabindex="-1" id="preview-btn_${lineno_id}">${_('Preview')}</a>
327 </li>
327 </li>
328 <li class="pull-right">
328 <li class="pull-right">
329 <select class="comment-type" id="comment_type_${lineno_id}" name="comment_type">
329 <select class="comment-type" id="comment_type_${lineno_id}" name="comment_type">
330 % for val in c.visual.comment_types:
330 % for val in c.visual.comment_types:
331 <option value="${val}">${val.upper()}</option>
331 <option value="${val}">${val.upper()}</option>
332 % endfor
332 % endfor
333 </select>
333 </select>
334 </li>
334 </li>
335 </ul>
335 </ul>
336 </div>
336 </div>
337
337
338 <div class="comment-area-write" style="display: block;">
338 <div class="comment-area-write" style="display: block;">
339 <div id="edit-container_${lineno_id}">
339 <div id="edit-container_${lineno_id}">
340 <textarea id="text_${lineno_id}" name="text" class="comment-block-ta ac-input"></textarea>
340 <textarea id="text_${lineno_id}" name="text" class="comment-block-ta ac-input"></textarea>
341 </div>
341 </div>
342 <div id="preview-container_${lineno_id}" class="clearfix" style="display: none;">
342 <div id="preview-container_${lineno_id}" class="clearfix" style="display: none;">
343 <div id="preview-box_${lineno_id}" class="preview-box"></div>
343 <div id="preview-box_${lineno_id}" class="preview-box"></div>
344 </div>
344 </div>
345 </div>
345 </div>
346
346
347 <div class="comment-area-footer">
347 <div class="comment-area-footer">
348 <div class="toolbar">
348 <div class="toolbar">
349 <div class="toolbar-text">
349 <div class="toolbar-text">
350 ${(_('Comments parsed using %s syntax with %s, and %s actions support.') % (
350 ${(_('Comments parsed using %s syntax with %s, and %s actions support.') % (
351 ('<a href="%s">%s</a>' % (h.route_url('%s_help' % c.visual.default_renderer), c.visual.default_renderer.upper())),
351 ('<a href="%s">%s</a>' % (h.route_url('%s_help' % c.visual.default_renderer), c.visual.default_renderer.upper())),
352 ('<span class="tooltip" title="%s">@mention</span>' % _('Use @username inside this text to send notification to this RhodeCode user')),
352 ('<span class="tooltip" title="%s">@mention</span>' % _('Use @username inside this text to send notification to this RhodeCode user')),
353 ('<span class="tooltip" title="%s">`/`</span>' % _('Start typing with / for certain actions to be triggered via text box.'))
353 ('<span class="tooltip" title="%s">`/`</span>' % _('Start typing with / for certain actions to be triggered via text box.'))
354 )
354 )
355 )|n}
355 )|n}
356 </div>
356 </div>
357 </div>
357 </div>
358 </div>
358 </div>
359 </div>
359 </div>
360
360
361 <div class="comment-footer">
361 <div class="comment-footer">
362
362
363 % if review_statuses:
363 % if review_statuses:
364 <div class="status_box">
364 <div class="status_box">
365 <select id="change_status_${lineno_id}" name="changeset_status">
365 <select id="change_status_${lineno_id}" name="changeset_status">
366 <option></option> ## Placeholder
366 <option></option> ## Placeholder
367 % for status, lbl in review_statuses:
367 % for status, lbl in review_statuses:
368 <option value="${status}" data-status="${status}">${lbl}</option>
368 <option value="${status}" data-status="${status}">${lbl}</option>
369 %if is_pull_request and change_status and status in ('approved', 'rejected'):
369 %if is_pull_request and change_status and status in ('approved', 'rejected'):
370 <option value="${status}_closed" data-status="${status}">${lbl} & ${_('Closed')}</option>
370 <option value="${status}_closed" data-status="${status}">${lbl} & ${_('Closed')}</option>
371 %endif
371 %endif
372 % endfor
372 % endfor
373 </select>
373 </select>
374 </div>
374 </div>
375 % endif
375 % endif
376
376
377 ## inject extra inputs into the form
377 ## inject extra inputs into the form
378 % if form_extras and isinstance(form_extras, (list, tuple)):
378 % if form_extras and isinstance(form_extras, (list, tuple)):
379 <div id="comment_form_extras">
379 <div id="comment_form_extras">
380 % for form_ex_el in form_extras:
380 % for form_ex_el in form_extras:
381 ${form_ex_el|n}
381 ${form_ex_el|n}
382 % endfor
382 % endfor
383 </div>
383 </div>
384 % endif
384 % endif
385
385
386 <div class="action-buttons">
386 <div class="action-buttons">
387 ## inline for has a file, and line-number together with cancel hide button.
387 ## inline for has a file, and line-number together with cancel hide button.
388 % if form_type == 'inline':
388 % if form_type == 'inline':
389 <input type="hidden" name="f_path" value="{0}">
389 <input type="hidden" name="f_path" value="{0}">
390 <input type="hidden" name="line" value="${lineno_id}">
390 <input type="hidden" name="line" value="${lineno_id}">
391 <button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
391 <button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
392 ${_('Cancel')}
392 ${_('Cancel')}
393 </button>
393 </button>
394 % endif
394 % endif
395
395
396 % if form_type != 'inline':
396 % if form_type != 'inline':
397 <div class="action-buttons-extra"></div>
397 <div class="action-buttons-extra"></div>
398 % endif
398 % endif
399
399
400 ${h.submit('save', _('Comment'), class_='btn btn-success comment-button-input')}
400 ${h.submit('save', _('Comment'), class_='btn btn-success comment-button-input')}
401
401
402 </div>
402 </div>
403 </div>
403 </div>
404
404
405 </form>
405 </form>
406
406
407 </%def> No newline at end of file
407 </%def>
@@ -1,735 +1,760 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 <input ${collapse_all and 'checked' or ''} class="filediff-collapse-state" id="filediff-collapse-${id(filediff)}" type="checkbox">
152 <input ${collapse_all and 'checked' or ''} class="filediff-collapse-state" id="filediff-collapse-${id(filediff)}" type="checkbox">
153 <div
153 <div
154 class="filediff"
154 class="filediff"
155 data-f-path="${filediff.patch['filename']}"
155 data-f-path="${filediff.patch['filename']}"
156 id="a_${h.FID('', filediff.patch['filename'])}">
156 id="a_${h.FID('', filediff.patch['filename'])}">
157 <label for="filediff-collapse-${id(filediff)}" class="filediff-heading">
157 <label for="filediff-collapse-${id(filediff)}" class="filediff-heading">
158 <div class="filediff-collapse-indicator"></div>
158 <div class="filediff-collapse-indicator"></div>
159 ${diff_ops(filediff)}
159 ${diff_ops(filediff)}
160 </label>
160 </label>
161 ${diff_menu(filediff, use_comments=use_comments)}
161 ${diff_menu(filediff, use_comments=use_comments)}
162 <table class="cb cb-diff-${c.diffmode} code-highlight ${over_lines_changed_limit and 'cb-collapsed' or ''}">
162 <table class="cb cb-diff-${c.diffmode} code-highlight ${over_lines_changed_limit and 'cb-collapsed' or ''}">
163 %if not filediff.hunks:
163 %if not filediff.hunks:
164 %for op_id, op_text in filediff.patch['stats']['ops'].items():
164 %for op_id, op_text in filediff.patch['stats']['ops'].items():
165 <tr>
165 <tr>
166 <td class="cb-text cb-${op_class(op_id)}" ${c.diffmode == 'unified' and 'colspan=4' or 'colspan=6'}>
166 <td class="cb-text cb-${op_class(op_id)}" ${c.diffmode == 'unified' and 'colspan=4' or 'colspan=6'}>
167 %if op_id == DEL_FILENODE:
167 %if op_id == DEL_FILENODE:
168 ${_('File was deleted')}
168 ${_('File was deleted')}
169 %elif op_id == BIN_FILENODE:
169 %elif op_id == BIN_FILENODE:
170 ${_('Binary file hidden')}
170 ${_('Binary file hidden')}
171 %else:
171 %else:
172 ${op_text}
172 ${op_text}
173 %endif
173 %endif
174 </td>
174 </td>
175 </tr>
175 </tr>
176 %endfor
176 %endfor
177 %endif
177 %endif
178 %if filediff.limited_diff:
178 %if filediff.limited_diff:
179 <tr class="cb-warning cb-collapser">
179 <tr class="cb-warning cb-collapser">
180 <td class="cb-text" ${c.diffmode == 'unified' and 'colspan=4' or 'colspan=6'}>
180 <td class="cb-text" ${c.diffmode == 'unified' and 'colspan=4' or 'colspan=6'}>
181 ${_('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>
181 ${_('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>
182 </td>
182 </td>
183 </tr>
183 </tr>
184 %else:
184 %else:
185 %if over_lines_changed_limit:
185 %if over_lines_changed_limit:
186 <tr class="cb-warning cb-collapser">
186 <tr class="cb-warning cb-collapser">
187 <td class="cb-text" ${c.diffmode == 'unified' and 'colspan=4' or 'colspan=6'}>
187 <td class="cb-text" ${c.diffmode == 'unified' and 'colspan=4' or 'colspan=6'}>
188 ${_('This diff has been collapsed as it changes many lines, (%i lines changed)' % lines_changed)}
188 ${_('This diff has been collapsed as it changes many lines, (%i lines changed)' % lines_changed)}
189 <a href="#" class="cb-expand"
189 <a href="#" class="cb-expand"
190 onclick="$(this).closest('table').removeClass('cb-collapsed'); return false;">${_('Show them')}
190 onclick="$(this).closest('table').removeClass('cb-collapsed'); return false;">${_('Show them')}
191 </a>
191 </a>
192 <a href="#" class="cb-collapse"
192 <a href="#" class="cb-collapse"
193 onclick="$(this).closest('table').addClass('cb-collapsed'); return false;">${_('Hide them')}
193 onclick="$(this).closest('table').addClass('cb-collapsed'); return false;">${_('Hide them')}
194 </a>
194 </a>
195 </td>
195 </td>
196 </tr>
196 </tr>
197 %endif
197 %endif
198 %endif
198 %endif
199
199
200 %for hunk in filediff.hunks:
200 %for hunk in filediff.hunks:
201 <tr class="cb-hunk">
201 <tr class="cb-hunk">
202 <td ${c.diffmode == 'unified' and 'colspan=3' or ''}>
202 <td ${c.diffmode == 'unified' and 'colspan=3' or ''}>
203 ## TODO: dan: add ajax loading of more context here
203 ## TODO: dan: add ajax loading of more context here
204 ## <a href="#">
204 ## <a href="#">
205 <i class="icon-more"></i>
205 <i class="icon-more"></i>
206 ## </a>
206 ## </a>
207 </td>
207 </td>
208 <td ${c.diffmode == 'sideside' and 'colspan=5' or ''}>
208 <td ${c.diffmode == 'sideside' and 'colspan=5' or ''}>
209 @@
209 @@
210 -${hunk.source_start},${hunk.source_length}
210 -${hunk.source_start},${hunk.source_length}
211 +${hunk.target_start},${hunk.target_length}
211 +${hunk.target_start},${hunk.target_length}
212 ${hunk.section_header}
212 ${hunk.section_header}
213 </td>
213 </td>
214 </tr>
214 </tr>
215 %if c.diffmode == 'unified':
215 %if c.diffmode == 'unified':
216 ${render_hunk_lines_unified(hunk, use_comments=use_comments, inline_comments=inline_comments)}
216 ${render_hunk_lines_unified(hunk, use_comments=use_comments, inline_comments=inline_comments)}
217 %elif c.diffmode == 'sideside':
217 %elif c.diffmode == 'sideside':
218 ${render_hunk_lines_sideside(hunk, use_comments=use_comments, inline_comments=inline_comments)}
218 ${render_hunk_lines_sideside(hunk, use_comments=use_comments, inline_comments=inline_comments)}
219 %else:
219 %else:
220 <tr class="cb-line">
220 <tr class="cb-line">
221 <td>unknown diff mode</td>
221 <td>unknown diff mode</td>
222 </tr>
222 </tr>
223 %endif
223 %endif
224 %endfor
224 %endfor
225
225
226 <% unmatched_comments = (inline_comments or {}).get(filediff.patch['filename'], {}) %>
227
226 ## outdated comments that do not fit into currently displayed lines
228 ## outdated comments that do not fit into currently displayed lines
227 % for lineno, comments in filediff.left_comments.items():
229 % for lineno, comments in unmatched_comments.items():
228
230
229 %if c.diffmode == 'unified':
231 %if c.diffmode == 'unified':
230 <tr class="cb-line">
232 % if loop.index == 0:
231 <td class="cb-data cb-context"></td>
233 <tr class="cb-hunk">
232 <td class="cb-lineno cb-context"></td>
234 <td colspan="3"></td>
233 <td class="cb-lineno cb-context"></td>
235 <td>
234 <td class="cb-content cb-context">
236 <div>
235 ${inline_comments_container(comments, inline_comments)}
237 ${_('Unmatched inline comments below')}
236 </td>
238 </div>
237 </tr>
239 </td>
238 %elif c.diffmode == 'sideside':
240 </tr>
239 <tr class="cb-line">
241 % endif
240 <td class="cb-data cb-context"></td>
242 <tr class="cb-line">
241 <td class="cb-lineno cb-context"></td>
243 <td class="cb-data cb-context"></td>
242 <td class="cb-content cb-context">
244 <td class="cb-lineno cb-context"></td>
243 % if lineno.startswith('o'):
245 <td class="cb-lineno cb-context"></td>
246 <td class="cb-content cb-context">
244 ${inline_comments_container(comments, inline_comments)}
247 ${inline_comments_container(comments, inline_comments)}
245 % endif
248 </td>
246 </td>
249 </tr>
250 %elif c.diffmode == 'sideside':
251 % if loop.index == 0:
252 <tr class="cb-hunk">
253 <td colspan="2"></td>
254 <td class="cb-line" colspan="6">
255 <div>
256 ${_('Unmatched comments below')}
257 </div>
258 </td>
259 </tr>
260 % endif
261 <tr class="cb-line">
262 <td class="cb-data cb-context"></td>
263 <td class="cb-lineno cb-context"></td>
264 <td class="cb-content cb-context">
265 % if lineno.startswith('o'):
266 ${inline_comments_container(comments, inline_comments)}
267 % endif
268 </td>
247
269
248 <td class="cb-data cb-context"></td>
270 <td class="cb-data cb-context"></td>
249 <td class="cb-lineno cb-context"></td>
271 <td class="cb-lineno cb-context"></td>
250 <td class="cb-content cb-context">
272 <td class="cb-content cb-context">
251 % if lineno.startswith('n'):
273 % if lineno.startswith('n'):
252 ${inline_comments_container(comments, inline_comments)}
274 ${inline_comments_container(comments, inline_comments)}
253 % endif
275 % endif
254 </td>
276 </td>
255 </tr>
277 </tr>
256 %endif
278 %endif
257
279
258 % endfor
280 % endfor
259
281
260 </table>
282 </table>
261 </div>
283 </div>
262 %endfor
284 %endfor
263
285
264 ## outdated comments that are made for a file that has been deleted
286 ## outdated comments that are made for a file that has been deleted
265 % for filename, comments_dict in (deleted_files_comments or {}).items():
287 % for filename, comments_dict in (deleted_files_comments or {}).items():
266 <%
288 <%
267 display_state = 'display: none'
289 display_state = 'display: none'
268 open_comments_in_file = [x for x in comments_dict['comments'] if x.outdated is False]
290 open_comments_in_file = [x for x in comments_dict['comments'] if x.outdated is False]
269 if open_comments_in_file:
291 if open_comments_in_file:
270 display_state = ''
292 display_state = ''
271 %>
293 %>
272 <div class="filediffs filediff-outdated" style="${display_state}">
294 <div class="filediffs filediff-outdated" style="${display_state}">
273 <input ${collapse_all and 'checked' or ''} class="filediff-collapse-state" id="filediff-collapse-${id(filename)}" type="checkbox">
295 <input ${collapse_all and 'checked' or ''} class="filediff-collapse-state" id="filediff-collapse-${id(filename)}" type="checkbox">
274 <div class="filediff" data-f-path="${filename}" id="a_${h.FID('', filename)}">
296 <div class="filediff" data-f-path="${filename}" id="a_${h.FID('', filename)}">
275 <label for="filediff-collapse-${id(filename)}" class="filediff-heading">
297 <label for="filediff-collapse-${id(filename)}" class="filediff-heading">
276 <div class="filediff-collapse-indicator"></div>
298 <div class="filediff-collapse-indicator"></div>
277 <span class="pill">
299 <span class="pill">
278 ## file was deleted
300 ## file was deleted
279 <strong>${filename}</strong>
301 <strong>${filename}</strong>
280 </span>
302 </span>
281 <span class="pill-group" style="float: left">
303 <span class="pill-group" style="float: left">
282 ## file op, doesn't need translation
304 ## file op, doesn't need translation
283 <span class="pill" op="removed">removed in this version</span>
305 <span class="pill" op="removed">removed in this version</span>
284 </span>
306 </span>
285 <a class="pill filediff-anchor" href="#a_${h.FID('', filename)}">ΒΆ</a>
307 <a class="pill filediff-anchor" href="#a_${h.FID('', filename)}">ΒΆ</a>
286 <span class="pill-group" style="float: right">
308 <span class="pill-group" style="float: right">
287 <span class="pill" op="deleted">-${comments_dict['stats']}</span>
309 <span class="pill" op="deleted">-${comments_dict['stats']}</span>
288 </span>
310 </span>
289 </label>
311 </label>
290
312
291 <table class="cb cb-diff-${c.diffmode} code-highlight ${over_lines_changed_limit and 'cb-collapsed' or ''}">
313 <table class="cb cb-diff-${c.diffmode} code-highlight ${over_lines_changed_limit and 'cb-collapsed' or ''}">
292 <tr>
314 <tr>
293 % if c.diffmode == 'unified':
315 % if c.diffmode == 'unified':
294 <td></td>
316 <td></td>
295 %endif
317 %endif
296
318
297 <td></td>
319 <td></td>
298 <td class="cb-text cb-${op_class(BIN_FILENODE)}" ${c.diffmode == 'unified' and 'colspan=4' or 'colspan=5'}>
320 <td class="cb-text cb-${op_class(BIN_FILENODE)}" ${c.diffmode == 'unified' and 'colspan=4' or 'colspan=5'}>
299 ${_('File was deleted in this version. There are still outdated/unresolved comments attached to it.')}
321 ${_('File was deleted in this version. There are still outdated/unresolved comments attached to it.')}
300 </td>
322 </td>
301 </tr>
323 </tr>
302 %if c.diffmode == 'unified':
324 %if c.diffmode == 'unified':
303 <tr class="cb-line">
325 <tr class="cb-line">
304 <td class="cb-data cb-context"></td>
326 <td class="cb-data cb-context"></td>
305 <td class="cb-lineno cb-context"></td>
327 <td class="cb-lineno cb-context"></td>
306 <td class="cb-lineno cb-context"></td>
328 <td class="cb-lineno cb-context"></td>
307 <td class="cb-content cb-context">
329 <td class="cb-content cb-context">
308 ${inline_comments_container(comments_dict['comments'], inline_comments)}
330 ${inline_comments_container(comments_dict['comments'], inline_comments)}
309 </td>
331 </td>
310 </tr>
332 </tr>
311 %elif c.diffmode == 'sideside':
333 %elif c.diffmode == 'sideside':
312 <tr class="cb-line">
334 <tr class="cb-line">
313 <td class="cb-data cb-context"></td>
335 <td class="cb-data cb-context"></td>
314 <td class="cb-lineno cb-context"></td>
336 <td class="cb-lineno cb-context"></td>
315 <td class="cb-content cb-context"></td>
337 <td class="cb-content cb-context"></td>
316
338
317 <td class="cb-data cb-context"></td>
339 <td class="cb-data cb-context"></td>
318 <td class="cb-lineno cb-context"></td>
340 <td class="cb-lineno cb-context"></td>
319 <td class="cb-content cb-context">
341 <td class="cb-content cb-context">
320 ${inline_comments_container(comments_dict['comments'], inline_comments)}
342 ${inline_comments_container(comments_dict['comments'], inline_comments)}
321 </td>
343 </td>
322 </tr>
344 </tr>
323 %endif
345 %endif
324 </table>
346 </table>
325 </div>
347 </div>
326 </div>
348 </div>
327 % endfor
349 % endfor
328
350
329 </div>
351 </div>
330 </div>
352 </div>
331 </%def>
353 </%def>
332
354
333 <%def name="diff_ops(filediff)">
355 <%def name="diff_ops(filediff)">
334 <%
356 <%
335 from rhodecode.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
357 from rhodecode.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
336 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE, COPIED_FILENODE
358 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE, COPIED_FILENODE
337 %>
359 %>
338 <span class="pill">
360 <span class="pill">
339 %if filediff.source_file_path and filediff.target_file_path:
361 %if filediff.source_file_path and filediff.target_file_path:
340 %if filediff.source_file_path != filediff.target_file_path:
362 %if filediff.source_file_path != filediff.target_file_path:
341 ## file was renamed, or copied
363 ## file was renamed, or copied
342 %if RENAMED_FILENODE in filediff.patch['stats']['ops']:
364 %if RENAMED_FILENODE in filediff.patch['stats']['ops']:
343 <strong>${filediff.target_file_path}</strong> β¬… <del>${filediff.source_file_path}</del>
365 <strong>${filediff.target_file_path}</strong> β¬… <del>${filediff.source_file_path}</del>
344 <% final_path = filediff.target_file_path %>
366 <% final_path = filediff.target_file_path %>
345 %elif COPIED_FILENODE in filediff.patch['stats']['ops']:
367 %elif COPIED_FILENODE in filediff.patch['stats']['ops']:
346 <strong>${filediff.target_file_path}</strong> β¬… ${filediff.source_file_path}
368 <strong>${filediff.target_file_path}</strong> β¬… ${filediff.source_file_path}
347 <% final_path = filediff.target_file_path %>
369 <% final_path = filediff.target_file_path %>
348 %endif
370 %endif
349 %else:
371 %else:
350 ## file was modified
372 ## file was modified
351 <strong>${filediff.source_file_path}</strong>
373 <strong>${filediff.source_file_path}</strong>
352 <% final_path = filediff.source_file_path %>
374 <% final_path = filediff.source_file_path %>
353 %endif
375 %endif
354 %else:
376 %else:
355 %if filediff.source_file_path:
377 %if filediff.source_file_path:
356 ## file was deleted
378 ## file was deleted
357 <strong>${filediff.source_file_path}</strong>
379 <strong>${filediff.source_file_path}</strong>
358 <% final_path = filediff.source_file_path %>
380 <% final_path = filediff.source_file_path %>
359 %else:
381 %else:
360 ## file was added
382 ## file was added
361 <strong>${filediff.target_file_path}</strong>
383 <strong>${filediff.target_file_path}</strong>
362 <% final_path = filediff.target_file_path %>
384 <% final_path = filediff.target_file_path %>
363 %endif
385 %endif
364 %endif
386 %endif
365 <i style="color: #aaa" class="tooltip icon-clipboard clipboard-action" data-clipboard-text="${final_path}" title="${_('Copy the full path')}" onclick="return false;"></i>
387 <i style="color: #aaa" class="tooltip icon-clipboard clipboard-action" data-clipboard-text="${final_path}" title="${_('Copy the full path')}" onclick="return false;"></i>
366 </span>
388 </span>
367 <span class="pill-group" style="float: left">
389 <span class="pill-group" style="float: left">
368 %if filediff.limited_diff:
390 %if filediff.limited_diff:
369 <span class="pill tooltip" op="limited" title="The stats for this diff are not complete">limited diff</span>
391 <span class="pill tooltip" op="limited" title="The stats for this diff are not complete">limited diff</span>
370 %endif
392 %endif
371
393
372 %if RENAMED_FILENODE in filediff.patch['stats']['ops']:
394 %if RENAMED_FILENODE in filediff.patch['stats']['ops']:
373 <span class="pill" op="renamed">renamed</span>
395 <span class="pill" op="renamed">renamed</span>
374 %endif
396 %endif
375
397
376 %if COPIED_FILENODE in filediff.patch['stats']['ops']:
398 %if COPIED_FILENODE in filediff.patch['stats']['ops']:
377 <span class="pill" op="copied">copied</span>
399 <span class="pill" op="copied">copied</span>
378 %endif
400 %endif
379
401
380 %if NEW_FILENODE in filediff.patch['stats']['ops']:
402 %if NEW_FILENODE in filediff.patch['stats']['ops']:
381 <span class="pill" op="created">created</span>
403 <span class="pill" op="created">created</span>
382 %if filediff['target_mode'].startswith('120'):
404 %if filediff['target_mode'].startswith('120'):
383 <span class="pill" op="symlink">symlink</span>
405 <span class="pill" op="symlink">symlink</span>
384 %else:
406 %else:
385 <span class="pill" op="mode">${nice_mode(filediff['target_mode'])}</span>
407 <span class="pill" op="mode">${nice_mode(filediff['target_mode'])}</span>
386 %endif
408 %endif
387 %endif
409 %endif
388
410
389 %if DEL_FILENODE in filediff.patch['stats']['ops']:
411 %if DEL_FILENODE in filediff.patch['stats']['ops']:
390 <span class="pill" op="removed">removed</span>
412 <span class="pill" op="removed">removed</span>
391 %endif
413 %endif
392
414
393 %if CHMOD_FILENODE in filediff.patch['stats']['ops']:
415 %if CHMOD_FILENODE in filediff.patch['stats']['ops']:
394 <span class="pill" op="mode">
416 <span class="pill" op="mode">
395 ${nice_mode(filediff['source_mode'])} ➑ ${nice_mode(filediff['target_mode'])}
417 ${nice_mode(filediff['source_mode'])} ➑ ${nice_mode(filediff['target_mode'])}
396 </span>
418 </span>
397 %endif
419 %endif
398 </span>
420 </span>
399
421
400 <a class="pill filediff-anchor" href="#a_${h.FID('', filediff.patch['filename'])}">ΒΆ</a>
422 <a class="pill filediff-anchor" href="#a_${h.FID('', filediff.patch['filename'])}">ΒΆ</a>
401
423
402 <span class="pill-group" style="float: right">
424 <span class="pill-group" style="float: right">
403 %if BIN_FILENODE in filediff.patch['stats']['ops']:
425 %if BIN_FILENODE in filediff.patch['stats']['ops']:
404 <span class="pill" op="binary">binary</span>
426 <span class="pill" op="binary">binary</span>
405 %if MOD_FILENODE in filediff.patch['stats']['ops']:
427 %if MOD_FILENODE in filediff.patch['stats']['ops']:
406 <span class="pill" op="modified">modified</span>
428 <span class="pill" op="modified">modified</span>
407 %endif
429 %endif
408 %endif
430 %endif
409 %if filediff.patch['stats']['added']:
431 %if filediff.patch['stats']['added']:
410 <span class="pill" op="added">+${filediff.patch['stats']['added']}</span>
432 <span class="pill" op="added">+${filediff.patch['stats']['added']}</span>
411 %endif
433 %endif
412 %if filediff.patch['stats']['deleted']:
434 %if filediff.patch['stats']['deleted']:
413 <span class="pill" op="deleted">-${filediff.patch['stats']['deleted']}</span>
435 <span class="pill" op="deleted">-${filediff.patch['stats']['deleted']}</span>
414 %endif
436 %endif
415 </span>
437 </span>
416
438
417 </%def>
439 </%def>
418
440
419 <%def name="nice_mode(filemode)">
441 <%def name="nice_mode(filemode)">
420 ${filemode.startswith('100') and filemode[3:] or filemode}
442 ${filemode.startswith('100') and filemode[3:] or filemode}
421 </%def>
443 </%def>
422
444
423 <%def name="diff_menu(filediff, use_comments=False)">
445 <%def name="diff_menu(filediff, use_comments=False)">
424 <div class="filediff-menu">
446 <div class="filediff-menu">
425 %if filediff.diffset.source_ref:
447 %if filediff.diffset.source_ref:
426 %if filediff.operation in ['D', 'M']:
448 %if filediff.operation in ['D', 'M']:
427 <a
449 <a
428 class="tooltip"
450 class="tooltip"
429 href="${h.route_path('repo_files',repo_name=filediff.diffset.repo_name,commit_id=filediff.diffset.source_ref,f_path=filediff.source_file_path)}"
451 href="${h.route_path('repo_files',repo_name=filediff.diffset.repo_name,commit_id=filediff.diffset.source_ref,f_path=filediff.source_file_path)}"
430 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
452 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
431 >
453 >
432 ${_('Show file before')}
454 ${_('Show file before')}
433 </a> |
455 </a> |
434 %else:
456 %else:
435 <span
457 <span
436 class="tooltip"
458 class="tooltip"
437 title="${h.tooltip(_('File no longer present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
459 title="${h.tooltip(_('File no longer present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
438 >
460 >
439 ${_('Show file before')}
461 ${_('Show file before')}
440 </span> |
462 </span> |
441 %endif
463 %endif
442 %if filediff.operation in ['A', 'M']:
464 %if filediff.operation in ['A', 'M']:
443 <a
465 <a
444 class="tooltip"
466 class="tooltip"
445 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)}"
467 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)}"
446 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
468 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
447 >
469 >
448 ${_('Show file after')}
470 ${_('Show file after')}
449 </a> |
471 </a> |
450 %else:
472 %else:
451 <span
473 <span
452 class="tooltip"
474 class="tooltip"
453 title="${h.tooltip(_('File no longer present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
475 title="${h.tooltip(_('File no longer present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
454 >
476 >
455 ${_('Show file after')}
477 ${_('Show file after')}
456 </span> |
478 </span> |
457 %endif
479 %endif
458 <a
480 <a
459 class="tooltip"
481 class="tooltip"
460 title="${h.tooltip(_('Raw diff'))}"
482 title="${h.tooltip(_('Raw diff'))}"
461 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'))}"
483 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'))}"
462 >
484 >
463 ${_('Raw diff')}
485 ${_('Raw diff')}
464 </a> |
486 </a> |
465 <a
487 <a
466 class="tooltip"
488 class="tooltip"
467 title="${h.tooltip(_('Download diff'))}"
489 title="${h.tooltip(_('Download diff'))}"
468 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'))}"
490 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'))}"
469 >
491 >
470 ${_('Download diff')}
492 ${_('Download diff')}
471 </a>
493 </a>
472 % if use_comments:
494 % if use_comments:
473 |
495 |
474 % endif
496 % endif
475
497
476 ## 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)
498 ## 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)
477 %if hasattr(c, 'ignorews_url'):
499 %if hasattr(c, 'ignorews_url'):
478 ${c.ignorews_url(request, h.FID('', filediff.patch['filename']))}
500 ${c.ignorews_url(request, h.FID('', filediff.patch['filename']))}
479 %endif
501 %endif
480 %if hasattr(c, 'context_url'):
502 %if hasattr(c, 'context_url'):
481 ${c.context_url(request, h.FID('', filediff.patch['filename']))}
503 ${c.context_url(request, h.FID('', filediff.patch['filename']))}
482 %endif
504 %endif
483
505
484 %if use_comments:
506 %if use_comments:
485 <a href="#" onclick="return Rhodecode.comments.toggleComments(this);">
507 <a href="#" onclick="return Rhodecode.comments.toggleComments(this);">
486 <span class="show-comment-button">${_('Show comments')}</span><span class="hide-comment-button">${_('Hide comments')}</span>
508 <span class="show-comment-button">${_('Show comments')}</span><span class="hide-comment-button">${_('Hide comments')}</span>
487 </a>
509 </a>
488 %endif
510 %endif
489 %endif
511 %endif
490 </div>
512 </div>
491 </%def>
513 </%def>
492
514
493
515
494 <%def name="inline_comments_container(comments, inline_comments)">
516 <%def name="inline_comments_container(comments, inline_comments)">
495 <div class="inline-comments">
517 <div class="inline-comments">
496 %for comment in comments:
518 %for comment in comments:
497 ${commentblock.comment_block(comment, inline=True)}
519 ${commentblock.comment_block(comment, inline=True)}
498 %endfor
520 %endfor
499 % if comments and comments[-1].outdated:
521 % if comments and comments[-1].outdated:
500 <span class="btn btn-secondary cb-comment-add-button comment-outdated}"
522 <span class="btn btn-secondary cb-comment-add-button comment-outdated}"
501 style="display: none;}">
523 style="display: none;}">
502 ${_('Add another comment')}
524 ${_('Add another comment')}
503 </span>
525 </span>
504 % else:
526 % else:
505 <span onclick="return Rhodecode.comments.createComment(this)"
527 <span onclick="return Rhodecode.comments.createComment(this)"
506 class="btn btn-secondary cb-comment-add-button">
528 class="btn btn-secondary cb-comment-add-button">
507 ${_('Add another comment')}
529 ${_('Add another comment')}
508 </span>
530 </span>
509 % endif
531 % endif
510
532
511 </div>
533 </div>
512 </%def>
534 </%def>
513
535
514 <%!
536 <%!
515 def get_comments_for(comments, filename, line_version, line_number):
537 def get_comments_for(diff_type, comments, filename, line_version, line_number):
516 if hasattr(filename, 'unicode_path'):
538 if hasattr(filename, 'unicode_path'):
517 filename = filename.unicode_path
539 filename = filename.unicode_path
518
540
519 if not isinstance(filename, basestring):
541 if not isinstance(filename, basestring):
520 return None
542 return None
521
543
522 line_key = '{}{}'.format(line_version, line_number)
544 line_key = '{}{}'.format(line_version, line_number) ## e.g o37, n12
545
523 if comments and filename in comments:
546 if comments and filename in comments:
524 file_comments = comments[filename]
547 file_comments = comments[filename]
525 if line_key in file_comments:
548 if line_key in file_comments:
526 return file_comments[line_key]
549 data = file_comments.pop(line_key)
550 return data
527 %>
551 %>
528
552
529 <%def name="render_hunk_lines_sideside(hunk, use_comments=False, inline_comments=None)">
553 <%def name="render_hunk_lines_sideside(hunk, use_comments=False, inline_comments=None)">
530
554
531 %for i, line in enumerate(hunk.sideside):
555 %for i, line in enumerate(hunk.sideside):
532 <%
556 <%
533 old_line_anchor, new_line_anchor = None, None
557 old_line_anchor, new_line_anchor = None, None
534 if line.original.lineno:
558 if line.original.lineno:
535 old_line_anchor = diff_line_anchor(hunk.source_file_path, line.original.lineno, 'o')
559 old_line_anchor = diff_line_anchor(hunk.source_file_path, line.original.lineno, 'o')
536 if line.modified.lineno:
560 if line.modified.lineno:
537 new_line_anchor = diff_line_anchor(hunk.target_file_path, line.modified.lineno, 'n')
561 new_line_anchor = diff_line_anchor(hunk.target_file_path, line.modified.lineno, 'n')
538 %>
562 %>
539
563
540 <tr class="cb-line">
564 <tr class="cb-line">
541 <td class="cb-data ${action_class(line.original.action)}"
565 <td class="cb-data ${action_class(line.original.action)}"
542 data-line-no="${line.original.lineno}"
566 data-line-no="${line.original.lineno}"
543 >
567 >
544 <div>
568 <div>
545 <% loc = None %>
569
570 <% line_old_comments = None %>
546 %if line.original.get_comment_args:
571 %if line.original.get_comment_args:
547 <% loc = get_comments_for(inline_comments, *line.original.get_comment_args) %>
572 <% line_old_comments = get_comments_for('side-by-side', inline_comments, *line.original.get_comment_args) %>
548 %endif
573 %endif
549 %if loc:
574 %if line_old_comments:
550 <% has_outdated = any([x.outdated for x in loc]) %>
575 <% has_outdated = any([x.outdated for x in line_old_comments]) %>
551 % if has_outdated:
576 % if has_outdated:
552 <i title="${_('comments including outdated')}:${len(loc)}" class="icon-comment_toggle" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
577 <i title="${_('comments including outdated')}:${len(line_old_comments)}" class="icon-comment_toggle" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
553 % else:
578 % else:
554 <i title="${_('comments')}: ${len(loc)}" class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
579 <i title="${_('comments')}: ${len(line_old_comments)}" class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
555 % endif
580 % endif
556 %endif
581 %endif
557 </div>
582 </div>
558 </td>
583 </td>
559 <td class="cb-lineno ${action_class(line.original.action)}"
584 <td class="cb-lineno ${action_class(line.original.action)}"
560 data-line-no="${line.original.lineno}"
585 data-line-no="${line.original.lineno}"
561 %if old_line_anchor:
586 %if old_line_anchor:
562 id="${old_line_anchor}"
587 id="${old_line_anchor}"
563 %endif
588 %endif
564 >
589 >
565 %if line.original.lineno:
590 %if line.original.lineno:
566 <a name="${old_line_anchor}" href="#${old_line_anchor}">${line.original.lineno}</a>
591 <a name="${old_line_anchor}" href="#${old_line_anchor}">${line.original.lineno}</a>
567 %endif
592 %endif
568 </td>
593 </td>
569 <td class="cb-content ${action_class(line.original.action)}"
594 <td class="cb-content ${action_class(line.original.action)}"
570 data-line-no="o${line.original.lineno}"
595 data-line-no="o${line.original.lineno}"
571 >
596 >
572 %if use_comments and line.original.lineno:
597 %if use_comments and line.original.lineno:
573 ${render_add_comment_button()}
598 ${render_add_comment_button()}
574 %endif
599 %endif
575 <span class="cb-code">${line.original.action} ${line.original.content or '' | n}</span>
600 <span class="cb-code">${line.original.action} ${line.original.content or '' | n}</span>
576
601
577 %if use_comments and line.original.lineno and loc:
602 %if use_comments and line.original.lineno and line_old_comments:
578 ${inline_comments_container(loc, inline_comments)}
603 ${inline_comments_container(line_old_comments, inline_comments)}
579 %endif
604 %endif
580
605
581 </td>
606 </td>
582 <td class="cb-data ${action_class(line.modified.action)}"
607 <td class="cb-data ${action_class(line.modified.action)}"
583 data-line-no="${line.modified.lineno}"
608 data-line-no="${line.modified.lineno}"
584 >
609 >
585 <div>
610 <div>
586
611
587 %if line.modified.get_comment_args:
612 %if line.modified.get_comment_args:
588 <% lmc = get_comments_for(inline_comments, *line.modified.get_comment_args) %>
613 <% line_new_comments = get_comments_for('side-by-side', inline_comments, *line.modified.get_comment_args) %>
589 %else:
614 %else:
590 <% lmc = None%>
615 <% line_new_comments = None%>
591 %endif
616 %endif
592 %if lmc:
617 %if line_new_comments:
593 <% has_outdated = any([x.outdated for x in lmc]) %>
618 <% has_outdated = any([x.outdated for x in line_new_comments]) %>
594 % if has_outdated:
619 % if has_outdated:
595 <i title="${_('comments including outdated')}:${len(lmc)}" class="icon-comment_toggle" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
620 <i title="${_('comments including outdated')}:${len(line_new_comments)}" class="icon-comment_toggle" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
596 % else:
621 % else:
597 <i title="${_('comments')}: ${len(lmc)}" class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
622 <i title="${_('comments')}: ${len(line_new_comments)}" class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
598 % endif
623 % endif
599 %endif
624 %endif
600 </div>
625 </div>
601 </td>
626 </td>
602 <td class="cb-lineno ${action_class(line.modified.action)}"
627 <td class="cb-lineno ${action_class(line.modified.action)}"
603 data-line-no="${line.modified.lineno}"
628 data-line-no="${line.modified.lineno}"
604 %if new_line_anchor:
629 %if new_line_anchor:
605 id="${new_line_anchor}"
630 id="${new_line_anchor}"
606 %endif
631 %endif
607 >
632 >
608 %if line.modified.lineno:
633 %if line.modified.lineno:
609 <a name="${new_line_anchor}" href="#${new_line_anchor}">${line.modified.lineno}</a>
634 <a name="${new_line_anchor}" href="#${new_line_anchor}">${line.modified.lineno}</a>
610 %endif
635 %endif
611 </td>
636 </td>
612 <td class="cb-content ${action_class(line.modified.action)}"
637 <td class="cb-content ${action_class(line.modified.action)}"
613 data-line-no="n${line.modified.lineno}"
638 data-line-no="n${line.modified.lineno}"
614 >
639 >
615 %if use_comments and line.modified.lineno:
640 %if use_comments and line.modified.lineno:
616 ${render_add_comment_button()}
641 ${render_add_comment_button()}
617 %endif
642 %endif
618 <span class="cb-code">${line.modified.action} ${line.modified.content or '' | n}</span>
643 <span class="cb-code">${line.modified.action} ${line.modified.content or '' | n}</span>
619 %if use_comments and line.modified.lineno and lmc:
644 %if use_comments and line.modified.lineno and line_new_comments:
620 ${inline_comments_container(lmc, inline_comments)}
645 ${inline_comments_container(line_new_comments, inline_comments)}
621 %endif
646 %endif
622 </td>
647 </td>
623 </tr>
648 </tr>
624 %endfor
649 %endfor
625 </%def>
650 </%def>
626
651
627
652
628 <%def name="render_hunk_lines_unified(hunk, use_comments=False, inline_comments=None)">
653 <%def name="render_hunk_lines_unified(hunk, use_comments=False, inline_comments=None)">
629 %for old_line_no, new_line_no, action, content, comments_args in hunk.unified:
654 %for old_line_no, new_line_no, action, content, comments_args in hunk.unified:
630 <%
655 <%
631 old_line_anchor, new_line_anchor = None, None
656 old_line_anchor, new_line_anchor = None, None
632 if old_line_no:
657 if old_line_no:
633 old_line_anchor = diff_line_anchor(hunk.source_file_path, old_line_no, 'o')
658 old_line_anchor = diff_line_anchor(hunk.source_file_path, old_line_no, 'o')
634 if new_line_no:
659 if new_line_no:
635 new_line_anchor = diff_line_anchor(hunk.target_file_path, new_line_no, 'n')
660 new_line_anchor = diff_line_anchor(hunk.target_file_path, new_line_no, 'n')
636 %>
661 %>
637 <tr class="cb-line">
662 <tr class="cb-line">
638 <td class="cb-data ${action_class(action)}">
663 <td class="cb-data ${action_class(action)}">
639 <div>
664 <div>
640
665
641 %if comments_args:
666 %if comments_args:
642 <% comments = get_comments_for(inline_comments, *comments_args) %>
667 <% comments = get_comments_for('unified', inline_comments, *comments_args) %>
643 %else:
668 %else:
644 <% comments = None%>
669 <% comments = None%>
645 %endif
670 %endif
646
671
647 % if comments:
672 % if comments:
648 <% has_outdated = any([x.outdated for x in comments]) %>
673 <% has_outdated = any([x.outdated for x in comments]) %>
649 % if has_outdated:
674 % if has_outdated:
650 <i title="${_('comments including outdated')}:${len(comments)}" class="icon-comment_toggle" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
675 <i title="${_('comments including outdated')}:${len(comments)}" class="icon-comment_toggle" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
651 % else:
676 % else:
652 <i title="${_('comments')}: ${len(comments)}" class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
677 <i title="${_('comments')}: ${len(comments)}" class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
653 % endif
678 % endif
654 % endif
679 % endif
655 </div>
680 </div>
656 </td>
681 </td>
657 <td class="cb-lineno ${action_class(action)}"
682 <td class="cb-lineno ${action_class(action)}"
658 data-line-no="${old_line_no}"
683 data-line-no="${old_line_no}"
659 %if old_line_anchor:
684 %if old_line_anchor:
660 id="${old_line_anchor}"
685 id="${old_line_anchor}"
661 %endif
686 %endif
662 >
687 >
663 %if old_line_anchor:
688 %if old_line_anchor:
664 <a name="${old_line_anchor}" href="#${old_line_anchor}">${old_line_no}</a>
689 <a name="${old_line_anchor}" href="#${old_line_anchor}">${old_line_no}</a>
665 %endif
690 %endif
666 </td>
691 </td>
667 <td class="cb-lineno ${action_class(action)}"
692 <td class="cb-lineno ${action_class(action)}"
668 data-line-no="${new_line_no}"
693 data-line-no="${new_line_no}"
669 %if new_line_anchor:
694 %if new_line_anchor:
670 id="${new_line_anchor}"
695 id="${new_line_anchor}"
671 %endif
696 %endif
672 >
697 >
673 %if new_line_anchor:
698 %if new_line_anchor:
674 <a name="${new_line_anchor}" href="#${new_line_anchor}">${new_line_no}</a>
699 <a name="${new_line_anchor}" href="#${new_line_anchor}">${new_line_no}</a>
675 %endif
700 %endif
676 </td>
701 </td>
677 <td class="cb-content ${action_class(action)}"
702 <td class="cb-content ${action_class(action)}"
678 data-line-no="${new_line_no and 'n' or 'o'}${new_line_no or old_line_no}"
703 data-line-no="${new_line_no and 'n' or 'o'}${new_line_no or old_line_no}"
679 >
704 >
680 %if use_comments:
705 %if use_comments:
681 ${render_add_comment_button()}
706 ${render_add_comment_button()}
682 %endif
707 %endif
683 <span class="cb-code">${action} ${content or '' | n}</span>
708 <span class="cb-code">${action} ${content or '' | n}</span>
684 %if use_comments and comments:
709 %if use_comments and comments:
685 ${inline_comments_container(comments, inline_comments)}
710 ${inline_comments_container(comments, inline_comments)}
686 %endif
711 %endif
687 </td>
712 </td>
688 </tr>
713 </tr>
689 %endfor
714 %endfor
690 </%def>
715 </%def>
691
716
692 <%def name="render_add_comment_button()">
717 <%def name="render_add_comment_button()">
693 <button class="btn btn-small btn-primary cb-comment-box-opener" onclick="return Rhodecode.comments.createComment(this)">
718 <button class="btn btn-small btn-primary cb-comment-box-opener" onclick="return Rhodecode.comments.createComment(this)">
694 <span><i class="icon-comment"></i></span>
719 <span><i class="icon-comment"></i></span>
695 </button>
720 </button>
696 </%def>
721 </%def>
697
722
698 <%def name="render_diffset_menu()">
723 <%def name="render_diffset_menu()">
699
724
700 <div class="diffset-menu clearinner">
725 <div class="diffset-menu clearinner">
701 <div class="pull-right">
726 <div class="pull-right">
702 <div class="btn-group">
727 <div class="btn-group">
703
728
704 <a
729 <a
705 class="btn ${c.diffmode == 'sideside' and 'btn-primary'} tooltip"
730 class="btn ${c.diffmode == 'sideside' and 'btn-primary'} tooltip"
706 title="${h.tooltip(_('View side by side'))}"
731 title="${h.tooltip(_('View side by side'))}"
707 href="${h.current_route_path(request, diffmode='sideside')}">
732 href="${h.current_route_path(request, diffmode='sideside')}">
708 <span>${_('Side by Side')}</span>
733 <span>${_('Side by Side')}</span>
709 </a>
734 </a>
710 <a
735 <a
711 class="btn ${c.diffmode == 'unified' and 'btn-primary'} tooltip"
736 class="btn ${c.diffmode == 'unified' and 'btn-primary'} tooltip"
712 title="${h.tooltip(_('View unified'))}" href="${h.current_route_path(request, diffmode='unified')}">
737 title="${h.tooltip(_('View unified'))}" href="${h.current_route_path(request, diffmode='unified')}">
713 <span>${_('Unified')}</span>
738 <span>${_('Unified')}</span>
714 </a>
739 </a>
715 </div>
740 </div>
716 </div>
741 </div>
717
742
718 <div class="pull-left">
743 <div class="pull-left">
719 <div class="btn-group">
744 <div class="btn-group">
720 <a
745 <a
721 class="btn"
746 class="btn"
722 href="#"
747 href="#"
723 onclick="$('input[class=filediff-collapse-state]').prop('checked', false); return false">${_('Expand All Files')}</a>
748 onclick="$('input[class=filediff-collapse-state]').prop('checked', false); return false">${_('Expand All Files')}</a>
724 <a
749 <a
725 class="btn"
750 class="btn"
726 href="#"
751 href="#"
727 onclick="$('input[class=filediff-collapse-state]').prop('checked', true); return false">${_('Collapse All Files')}</a>
752 onclick="$('input[class=filediff-collapse-state]').prop('checked', true); return false">${_('Collapse All Files')}</a>
728 <a
753 <a
729 class="btn"
754 class="btn"
730 href="#"
755 href="#"
731 onclick="return Rhodecode.comments.toggleWideMode(this)">${_('Wide Mode Diff')}</a>
756 onclick="return Rhodecode.comments.toggleWideMode(this)">${_('Wide Mode Diff')}</a>
732 </div>
757 </div>
733 </div>
758 </div>
734 </div>
759 </div>
735 </%def>
760 </%def>
General Comments 0
You need to be logged in to leave comments. Login now